Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions lib/CppInterOp/CppInterOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,33 @@
};

static void DefaultProcessCrashHandler(void*);

/// Set by UseExternalInterpreter to suppress llvm_shutdown at process exit
/// -- the client owns LLVM in that case.
static bool SkipShutDown = false;

/// RAII guard whose dtor calls llvm_shutdown for the owned-interpreter case.
/// Constructed as a function-local static AFTER sInterpreters, so its dtor
/// fires FIRST (reverse-of-construction); llvm_shutdown then drains the
/// ManagedStatic registry, including sInterpreters, deterministically.
/// The llvm_shutdown call itself is gated on LLVM 23+, where
/// Platform::lookupResolvedInitSymbols (llvm/llvm-project#196874) makes
/// ~Interpreter's JIT deinit skip lazy materialization. On older LLVM
/// the same chain SEGFAULTs in cleanUp against destroyed function-local
/// statics, so the dtor is a no-op and sInterpreters leaks instead.
struct InterpreterShutdown {
~InterpreterShutdown() {
if (!SkipShutDown) {
#if LLVM_VERSION_MAJOR > 22
llvm::llvm_shutdown();
#endif
}
}
};

// Function-static storage for interpreters
static std::deque<InterpreterInfo>&
GetInterpreters(bool SetCrashHandler = true) {
// static int FakeArgc = 1;
// static const std::string VersionStr = GetVersion();
// static const char* ArgvBuffer[] = {VersionStr.c_str(), nullptr};
// static const char** FakeArgv = ArgvBuffer;
// static llvm::InitLLVM X(FakeArgc, FakeArgv);
// Cannot be a llvm::ManagedStatic because X will call shutdown which will
// trigger destruction on llvm::ManagedStatics and the destruction of the
// InterpreterInfos require to have llvm around.
// FIXME: Currently we never call llvm::llvm_shutdown and sInterpreters leaks.
static llvm::ManagedStatic<std::deque<InterpreterInfo>> sInterpreters;
static std::once_flag ProcessInitialized;
std::call_once(ProcessInitialized, [SetCrashHandler]() {
Expand All @@ -215,13 +230,21 @@
llvm::InitializeAllAsmParsers();
llvm::InitializeAllAsmPrinters();

if (SetCrashHandler)
// Pipe / OOM / crash handlers replicate what InitLLVM did before it was
// dropped. Skipped when the host owns LLVM -- they're its decision.
if (SetCrashHandler) {
llvm::sys::SetOneShotPipeSignalFunction(
llvm::sys::DefaultOneShotPipeSignalHandler);
llvm::sys::AddSignalHandler(DefaultProcessCrashHandler,
/*Cookie=*/nullptr);

// std::atexit(llvm::llvm_shutdown);
llvm::install_out_of_memory_new_handler();
}
});

// Constructed after sInterpreters above, so its dtor fires first at
// process exit; see InterpreterShutdown.
static InterpreterShutdown Shutdown;

return *sInterpreters;
}

Expand Down Expand Up @@ -299,6 +322,7 @@
void UseExternalInterpreter(TInterp_t I) {
INTEROP_TRACE(I);
assert(GetInterpreters(false).empty() && "sInterpreter already in use!");
SkipShutDown = true;

Check warning on line 325 in lib/CppInterOp/CppInterOp.cpp

View check run for this annotation

Codecov / codecov/patch

lib/CppInterOp/CppInterOp.cpp#L325

Added line #L325 was not covered by tests
RegisterInterpreter(static_cast<compat::Interpreter*>(I), /*Owned=*/false);
return INTEROP_VOID_RETURN();
}
Expand Down
69 changes: 69 additions & 0 deletions unittests/CppInterOp/InterpreterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

#include <algorithm>
#include <csignal>
#include <cstdlib>

using ::testing::StartsWith;

Expand Down Expand Up @@ -149,6 +150,74 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DeleteInterpreter) {
EXPECT_EQ(I2, Cpp::GetInterpreter()) << "I2 is not active";
}

TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_StaticDtorsRunOnDelete) {
#ifdef EMSCRIPTEN
GTEST_SKIP() << "Test fails for Emscripten builds";
#endif
if (TypeParam::isOutOfProcess)
GTEST_SKIP() << "Test fails for OOP JIT builds";

auto* I = TestFixture::CreateInterpreter();
ASSERT_NE(I, nullptr);

// Host-side flag the JIT writes to; survives the interpreter deletion
// that frees JIT memory. Reset explicitly for --gtest_repeat.
static int HostFlag;
HostFlag = 0;
std::string Inject = "extern \"C\" void* dtor_sink = (void*)" +
std::to_string(reinterpret_cast<uintptr_t>(&HostFlag)) +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use reinterpret_cast [cppcoreguidelines-pro-type-reinterpret-cast]

                       std::to_string(reinterpret_cast<uintptr_t>(&HostFlag)) +
                                      ^

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "std::to_string" is directly included [misc-include-cleaner]

                       std::to_string(reinterpret_cast<uintptr_t>(&HostFlag)) +
                            ^

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "uintptr_t" is directly included [misc-include-cleaner]

                       std::to_string(reinterpret_cast<uintptr_t>(&HostFlag)) +
                                                       ^

";";
Cpp::Declare(Inject.c_str(), I);
Cpp::Declare(R"(
struct DtorNotify { ~DtorNotify() { *static_cast<int*>(dtor_sink) = 1; } };
)",
I);
Cpp::Process("static DtorNotify notify;");

EXPECT_EQ(HostFlag, 0);
Cpp::DeleteInterpreter(I);
EXPECT_EQ(HostFlag, 1) << "JIT static dtor did not run on DeleteInterpreter";
}

// Mirrors clang/unittests/Interpreter/InterpreterTest.cpp's
// ShutdownDoesNotMaterializeAgainstDestroyedGlobals at the CppInterOp
// wrapper level: two interpreters each register a static with a
// non-trivial dtor, then std::exit drives the C++ static-dtor phase.
// On LLVM 23+ our InterpreterShutdown calls llvm_shutdown which fires
// ~InterpreterInfo -> ~Interpreter -> JIT deinit; the deinit lookup
// skips lazy materialization (Platform::lookupResolvedInitSymbols,
// llvm/llvm-project#196874) and the process exits cleanly.
//
// Skipped on older LLVM: InterpreterShutdown's llvm_shutdown call is
// gated out (we leak instead of risking the JIT cleanUp crash), and
// driving the chain manually from the test deadlocks rather than
// crashing cleanly through the compat layer. The crash contract is
// pinned upstream by clang's
// ShutdownDoesNotMaterializeAgainstDestroyedGlobals test.
TYPED_TEST(CPPINTEROP_TEST_MODE,
Interpreter_ShutdownDoesNotMaterializeAgainstDestroyedGlobals) {
#ifdef EMSCRIPTEN
GTEST_SKIP() << "Test fails for Emscripten builds";
#endif
if (TypeParam::isOutOfProcess)
GTEST_SKIP() << "Test fails for OOP JIT builds";
#if LLVM_VERSION_MAJOR <= 22
GTEST_SKIP() << "Requires LLVM 23+ (lookupResolvedInitSymbols)";
#else
EXPECT_EXIT(
{
auto* I1 = TestFixture::CreateInterpreter();
auto* I2 = TestFixture::CreateInterpreter();
Cpp::ActivateInterpreter(I1);
Cpp::Process("struct S1 { S1() {} ~S1() {} }; static S1 s1;");
Cpp::ActivateInterpreter(I2);
Cpp::Process("struct S2 { S2() {} ~S2() {} }; static S2 s2;");
std::exit(0);
},
::testing::ExitedWithCode(0), "");
#endif
}

TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_ActivateInterpreter) {
#ifdef EMSCRIPTEN_STATIC_LIBRARY
GTEST_SKIP() << "Test fails for Emscipten static library build";
Expand Down
Loading