From 227181f7913e8b5ec9bf34f2a72d9fc578a2486c Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 28 Sep 2026 15:05:56 +0200 Subject: [PATCH] [cppinterop] Upgrade to upstream 261bebbf --- interpreter/CppInterOp/.bazelrc | 4 + interpreter/CppInterOp/.gitignore | 4 +- interpreter/CppInterOp/BUILD.bazel | 37 +- .../check_no_clangCppInterOp_link.sh | 0 interpreter/CppInterOp/cppinterop-version.tag | 2 +- .../include/CppInterOp/CXCppInterOp.h | 4 + .../include/CppInterOp/CppInterOpTypes.h | 21 +- .../CppInterOp/lib/CppInterOp/CMakeLists.txt | 47 +- .../lib/CppInterOp/CXCppInterOp.cpp | 5 + .../CppInterOp/lib/CppInterOp/Compatibility.h | 214 ++++++- .../lib/CppInterOp/CompatibilityGLIBC.h | 247 ++++++++ .../CppInterOp/lib/CppInterOp/CppInterOp.cpp | 197 +++++-- .../CppInterOp/lib/CppInterOp/CppInterOp.td | 45 +- .../lib/CppInterOp/CppInterOpAPI.td | 3 + .../lib/CppInterOp/CppInterOpInterpreter.h | 148 ++++- .../lib/CppInterOp/DynamicLibraryManager.cpp | 14 +- .../lib/CppInterOp/DynamicLibraryManager.h | 5 +- .../CppInterOp/APINotes/TestAttributeMerge.h | 14 + .../CppInterOp/APINotes/TestHeader.apinotes | 14 + .../CppInterOp/APINotes/TestHeader.h | 7 + .../unittests/CppInterOp/CAPITest.cpp | 6 + .../unittests/CppInterOp/CMakeLists.txt | 13 +- .../unittests/CppInterOp/DispatchInit.cpp | 19 +- .../CppInterOp/DispatchSmokeTest.cpp | 15 +- .../unittests/CppInterOp/DispatchTest.cpp | 9 +- .../CppInterOp/DynamicLibraryManagerTest.cpp | 534 ++++++++++++++++++ .../CppInterOp/FunctionReflectionTest.cpp | 437 +++++++++++++- .../unittests/CppInterOp/InterpreterTest.cpp | 284 ++++++++++ .../CppInterOp/ScopeReflectionTest.cpp | 18 + .../unittests/CppInterOp/TestPaths.h | 40 ++ .../TestSharedLib/TestSharedLib.cpp | 19 + .../CppInterOp/TestSharedLib/TestSharedLib.h | 47 ++ .../unittests/CppInterOp/TracingTests.cpp | 23 +- 33 files changed, 2363 insertions(+), 133 deletions(-) mode change 100644 => 100755 interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh create mode 100644 interpreter/CppInterOp/lib/CppInterOp/CompatibilityGLIBC.h create mode 100644 interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestAttributeMerge.h create mode 100644 interpreter/CppInterOp/unittests/CppInterOp/TestPaths.h diff --git a/interpreter/CppInterOp/.bazelrc b/interpreter/CppInterOp/.bazelrc index b63aa0dff520f..c4b6b70b60ac5 100644 --- a/interpreter/CppInterOp/.bazelrc +++ b/interpreter/CppInterOp/.bazelrc @@ -4,3 +4,7 @@ test --test_output=errors # The C++ toolchain (clang from the LLVM tree) and all ABI-critical flags are # centralized in the cppyy_bazel module, which registers @llvm//:cc_toolchain. # No per-repo compiler wiring needed. + +# The dispatch tests dlopen /lib/libclangCppInterOp.so. +# In the standalone build the runfiles root (the test cwd) is that prefix. +test --test_env=CPPINTEROP_BIN_DIR=. diff --git a/interpreter/CppInterOp/.gitignore b/interpreter/CppInterOp/.gitignore index 69733a901df51..4272b5da76d8e 100644 --- a/interpreter/CppInterOp/.gitignore +++ b/interpreter/CppInterOp/.gitignore @@ -28,8 +28,8 @@ *.app # Directories -build -install +build* +install* # CLion files .idea diff --git a/interpreter/CppInterOp/BUILD.bazel b/interpreter/CppInterOp/BUILD.bazel index dd26bb5a1d9c3..3c6810d662c0f 100644 --- a/interpreter/CppInterOp/BUILD.bazel +++ b/interpreter/CppInterOp/BUILD.bazel @@ -311,6 +311,18 @@ cc_library( includes = ["include"], ) +# Tests resolve repo files against CPPINTEROP_SRC_DIR / CPPINTEROP_BIN_DIR, +# which here is the runfiles root (the test's cwd). repo_rloc self-corrects +# between the standalone build (main repo -> ".") and use as an external dep +# (-> "../"). This BUILD is always @cppinterop, so is_main_repo() marks +# the self-reference. +_CPPINTEROP_RUNFILES_DIR = repo_rloc( + "@cppinterop", + is_main_repo(repository_name()), +) + +_CPPINTEROP_SRC_DIR_COPT = "-DCPPINTEROP_SRC_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"" + # CppInterOpTests: the main gtest suite. Source list = CMake's # add_cppinterop_unittest(CppInterOpTests ...) plus the non-EMSCRIPTEN # target_sources additions (CUDATest, VTableOverlayTest) plus main.cpp. @@ -335,8 +347,13 @@ cppinterop_cc_test( "unittests/CppInterOp/VariableReflectionTest.cpp", "unittests/CppInterOp/main.cpp", ], + # The APINotes check builds an -I into the checkout, so ship that directory. + data = glob(["unittests/CppInterOp/APINotes/**"]), # CMake sets -Wno-pedantic on VariableReflectionTest.cpp; applied suite-wide. - extra_copts = ["-Wno-pedantic"], + extra_copts = [ + "-Wno-pedantic", + _CPPINTEROP_SRC_DIR_COPT, + ], extra_deps = [":CAPITestC"], ) @@ -352,20 +369,11 @@ cppinterop_cc_test( ], ) -# TracingTests reads CppInterOp.h, the generated .inc, and lib/*.cpp at runtime, -# relative to CPPINTEROP_DIR / CPPINTEROP_BINARY_DIR (the repo's runfiles root, -# which is the test's cwd). repo_rloc self-corrects between the standalone build -# (cppinterop is the main repo -> ".") and being an external dep (-> "../"). -# This BUILD is always @cppinterop, so is_main_repo() marks the self-reference. -# No test-source changes needed. -_CPPINTEROP_RUNFILES_DIR = repo_rloc( - "@cppinterop", - is_main_repo(repository_name()), -) - +# TracingTests reads CppInterOp.h, the generated .inc, and lib/*.cpp at runtime. cppinterop_cc_test( name = "TracingTests", srcs = [ + "unittests/CppInterOp/TestPaths.h", "unittests/CppInterOp/TracingTests.cpp", "unittests/CppInterOp/Utils.cpp", "unittests/CppInterOp/Utils.h", @@ -373,8 +381,8 @@ cppinterop_cc_test( ], data = glob(["lib/CppInterOp/*.cpp"]), extra_copts = [ - "-DCPPINTEROP_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"", - "-DCPPINTEROP_BINARY_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"", + _CPPINTEROP_SRC_DIR_COPT, + "-DCPPINTEROP_BIN_DIR=\\\"" + _CPPINTEROP_RUNFILES_DIR + "\\\"", ], ) @@ -385,6 +393,7 @@ cppinterop_dispatch_cc_test( "unittests/CppInterOp/DispatchInit.cpp", "unittests/CppInterOp/DispatchSmokeTest.cpp", "unittests/CppInterOp/DispatchTest.cpp", + "unittests/CppInterOp/TestPaths.h", "unittests/CppInterOp/main.cpp", ], ) diff --git a/interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh b/interpreter/CppInterOp/bazel-support/check_no_clangCppInterOp_link.sh old mode 100644 new mode 100755 diff --git a/interpreter/CppInterOp/cppinterop-version.tag b/interpreter/CppInterOp/cppinterop-version.tag index 85a7dace6dfa8..9cd89f6911ecf 100644 --- a/interpreter/CppInterOp/cppinterop-version.tag +++ b/interpreter/CppInterOp/cppinterop-version.tag @@ -1 +1 @@ -7cb23ca494cdaf3d5e6cc57318c4f561abcf1d84 +261bebbf25c8f709d2e2490fe6e3f876f6ec5920 diff --git a/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h b/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h index 95f025f53dbf7..baa23b701b220 100644 --- a/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h +++ b/interpreter/CppInterOp/include/CppInterOp/CXCppInterOp.h @@ -63,6 +63,10 @@ CPPINTEROP_API intptr_t cppinterop_Evaluate(const char* code, bool* HadError); CPPINTEROP_API CppInterOpArray cppinterop_GetClassTemplatedMethods(const char* name, CppConstDeclRef parent); +/// C-ABI bridge for \c Cpp::LoadLibrary. The optional \c std::string* +/// reason argument is C++-only, so the generated wrapper is suppressed. +CPPINTEROP_API bool cppinterop_LoadLibrary(const char* lib_stem, bool lookup); + #ifdef __cplusplus } // extern "C" #pragma clang diagnostic pop diff --git a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h index 4a2f72e61e9d8..6bc5724325695 100644 --- a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h +++ b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h @@ -411,11 +411,10 @@ enum class AllocType : unsigned char { New, NewArr, Malloc, - Unknown, - CustomAlloc, - Null, OperatorNew, - OperatorNewArr + OperatorNewArr, + Null, + Unknown }; enum class DeallocType : unsigned char { @@ -432,6 +431,20 @@ inline QualKind operator|(QualKind a, QualKind b) { static_cast(b)); } +enum class OwnershipBehaviour : unsigned char { + Unknown = 0, // If function does not have any ownership attribute, it is + // assumed to be Unknown, not None + OwnershipReturns = 1 << 0, + OwnershipHolds = 1 << 1, + OwnershipTakes = 1 << 2 +}; + +inline OwnershipBehaviour operator|(OwnershipBehaviour A, + OwnershipBehaviour B) { + return static_cast(static_cast(A) | + static_cast(B)); +} + enum class ValueKind : std::uint8_t { None, LValue, diff --git a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt index f098b417e43cf..68391bab9a54b 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt @@ -67,6 +67,17 @@ if (LLVM_LINK_LLVM_DYLIB) ) set(new_libs ${link_libs}) set(libs ${new_libs}) + # LLVM component libraries behind libLLVM.so that static clang archives + # reference without declaring them in their link interfaces (the dylib + # umbrella covered them). Substituted for the LLVM shared target below. + # Do NOT expand to every LLVM library: some (e.g. LLVMInterpreter) require + # optional dependencies (libffi) that may not be installed. + set(llvm_component_libs LLVMFrontendDriver) + foreach(component_lib ${llvm_component_libs}) + if(NOT TARGET ${component_lib}) + list(REMOVE_ITEM llvm_component_libs ${component_lib}) + endif() + endforeach() while(NOT "${new_libs}" STREQUAL "") foreach(lib ${new_libs}) if(TARGET ${lib}) @@ -75,22 +86,36 @@ if (LLVM_LINK_LLVM_DYLIB) continue() endif() foreach(transitive_lib ${transitive_libs}) - if(NOT TARGET ${transitive_lib}) - continue() + # Replace the LLVM shared library (libLLVM.so) that packages like + # Debian's declare as the link interface of the static clang archives + # with the static LLVM component libraries: some LLVM libs are only + # reachable through that umbrella (e.g. LLVMFrontendDriver for + # llvm::driver::createTLII in clangCodeGen), so dropping it outright + # produces unresolved symbols at link time. + # Everything else must be kept verbatim: plain items (rt, dl, m) and + # non-LLVM targets (e.g. ZLIB::ZLIB) are LLVM's system dependencies + # that the static archives still need. + if(TARGET ${transitive_lib} + AND "${transitive_lib}" MATCHES "^LLVM") + get_target_property(lib_type ${transitive_lib} TYPE) + if("${lib_type}" STREQUAL "SHARED_LIBRARY" + OR "${lib_type}" STREQUAL "MODULE_LIBRARY") + foreach(component_lib ${llvm_component_libs}) + if(TARGET ${component_lib} AND NOT ${component_lib} IN_LIST static_transitive_libs) + list(APPEND static_transitive_libs ${component_lib}) + endif() + endforeach() + continue() + endif() endif() - get_target_property(lib_type ${transitive_lib} TYPE) - if("${lib_type}" STREQUAL "STATIC_LIBRARY") - list(APPEND static_transitive_libs ${transitive_lib}) - else() - # Filter our libLLVM.so and friends. - continue() - endif() - if(NOT ${transitive_lib} IN_LIST libs) + list(APPEND static_transitive_libs ${transitive_lib}) + if(TARGET ${transitive_lib} AND NOT ${transitive_lib} IN_LIST libs) list(APPEND newer_libs ${transitive_lib}) list(APPEND libs ${transitive_lib}) endif() endforeach(transitive_lib) - # Update the target properties with the list of only static libraries. + # Update the target properties with the filtered dependency list + # (shared LLVM umbrella replaced by static component libraries). set_target_properties(${lib} PROPERTIES INTERFACE_LINK_LIBRARIES "${static_transitive_libs}") set(static_transitive_libs "") endif() diff --git a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp index 09c32cfa74964..c2347d4bea139 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CXCppInterOp.cpp @@ -61,6 +61,11 @@ CPPINTEROP_API intptr_t cppinterop_Evaluate(const char* code, bool* HadError) { return Cpp::Evaluate(code, HadError); } +// The std::string* reason argument of Cpp::LoadLibrary is C++-only. +CPPINTEROP_API bool cppinterop_LoadLibrary(const char* lib_stem, bool lookup) { + return Cpp::LoadLibrary(lib_stem, lookup); +} + // GetClassTemplatedMethods returns bool AND fills a vector out-param. // The C wrapper drops the bool (caller checks arr.size > 0 instead). CPPINTEROP_API Cpp::CppInterOpArray diff --git a/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h b/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h index a62623f42da02..c5b96fa09088a 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h +++ b/interpreter/CppInterOp/lib/CppInterOp/Compatibility.h @@ -30,8 +30,17 @@ #include "clang/Sema/Sema.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/DerivedTypes.h" +#include "llvm/IR/GlobalValue.h" +#include "llvm/IR/GlobalVariable.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Value.h" #include "llvm/Support/FileSystem.h" +#include "llvm/TargetParser/Triple.h" #include "CppInterOp/Box.h" @@ -243,10 +252,13 @@ inline void codeComplete(std::vector& Results, #endif #if LLVM_VERSION_MAJOR > 21 && !defined(_WIN32) -#include #include #endif +#ifndef _WIN32 +#include +#endif + #include namespace compat { @@ -593,6 +605,186 @@ inline void maybeMangleDeclName(const clang::GlobalDecl& GD, RawStr.flush(); } +#ifndef _WIN32 +// =========================================================================== +// Bind weak globals the process already defines (in-process JIT). +// +// WORKAROUND: this compensates for a deficiency in clang's in-process JIT +// (clang-repl / ORC in clang::Interpreter), which does not demote weak +// definitions the surrounding process already exports and so ends up with a +// second copy. The demotion policy belongs in clang's Interpreter, where it +// covers every embedder rather than only CppInterOp; that is tracked upstream +// by llvm/llvm-project#211786. This pass and its guarded call sites are a +// stopgap and should be deleted once the upstream fix lands and the minimum +// supported clang carries it -- at which point the toggle below becomes a +// `CLANG_VERSION_MAJOR` guard that compiles the workaround out, and then it +// is removed entirely. +// +// A singleton *defined* in a header -- a function-local static in an inline +// function (Meyers) or a C++17 inline static data member -- compiles to a +// weak/linkonce_odr global variable. When jitted code includes such a header, +// the ORC JIT materializes its own copy: JITDylib definitions win over the +// process-symbol generator, which is only consulted for symbols the JITDylib +// lacks. The result is two instances of one singleton (and a double +// destruction at teardown) where a dynamic linker would have unified them. +// +// Mitigation: before a module reaches the JIT, demote every mutable weak +// global-variable definition whose symbol the dynamic linker already +// resolves to an external declaration; the JIT then binds the process copy. +// A dynamically initialized static's guard variable (_ZGV<...>) moves with +// its data, and only when the process exports both: sharing the data but not +// the guard would re-run initialization on the process copy, sharing the +// guard but not the data would leave the jitted copy uninitialized. +// +// Only mutable variables are demoted: duplicated constants are harmless +// under ODR (and folding them keeps jitted code fast); duplicated mutable +// state is the singleton bug. thread_locals are excluded (the JIT's +// emulated-TLS storage regime is separate), and functions are never demoted +// -- jitted code may legitimately carry its own copies of inline functions. +// ELF and in-process only (the probe is the process's own dlsym). +// =========================================================================== +// +// Single toggle for the whole workaround (definition + call sites). +// TODO: remove once ORC uniques globals across the process (ETA LLVM 25). +#if LLVM_VERSION_MAJOR >= 25 +#error "ORC uniques globals now: remove bindProcessWeakGlobals" +#endif +#define CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS 1 +#if CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS +inline bool bindProcessWeakGlobals(llvm::Module& M) { + if (!llvm::Triple(M.getTargetTriple()).isOSBinFormatELF()) + return false; + + auto ProcessHas = [](llvm::StringRef Name) { + return ::dlsym(RTLD_DEFAULT, Name.str().c_str()) != nullptr; + }; + + llvm::SmallVector Demoted; + for (llvm::GlobalVariable& GV : M.globals()) { + if (GV.isDeclaration() || !GV.isWeakForLinker() || GV.isThreadLocal() || + GV.isConstant()) + continue; + + llvm::StringRef Name = GV.getName(); + // Guards are only ever demoted together with their variable, below. + if (Name.starts_with("_ZGV")) + continue; + if (!ProcessHas(Name)) + continue; + + llvm::GlobalVariable* Guard = nullptr; + if (Name.starts_with("_Z")) { + llvm::GlobalVariable* G = + M.getNamedGlobal(("_ZGV" + Name.drop_front(2)).str()); + if (G && !G->isDeclaration()) { + if (!ProcessHas(G->getName())) + continue; + Guard = G; + } + } + + Demoted.push_back(&GV); + if (Guard) + Demoted.push_back(Guard); + } + if (Demoted.empty()) + return false; + + for (llvm::GlobalVariable* GV : Demoted) { + GV->setInitializer(nullptr); + GV->setLinkage(llvm::GlobalValue::ExternalLinkage); + GV->setComdat(nullptr); + GV->setVisibility(llvm::GlobalValue::DefaultVisibility); + GV->setDSOLocal(false); + } + + // Declarations may not appear in the used lists; drop demoted entries. + for (const char* ListName : {"llvm.used", "llvm.compiler.used"}) { + llvm::GlobalVariable* Used = M.getNamedGlobal(ListName); + if (!Used || !Used->hasInitializer()) + continue; + auto* Init = llvm::cast(Used->getInitializer()); + llvm::SmallVector Kept; + for (llvm::Value* Op : Init->operand_values()) { + auto* C = llvm::cast(Op); + if (!llvm::is_contained(Demoted, C->stripPointerCasts())) + Kept.push_back(C); + } + if (Kept.size() == Init->getNumOperands()) + continue; + Used->eraseFromParent(); + if (!Kept.empty()) { + auto* ATy = llvm::ArrayType::get(Kept.front()->getType(), Kept.size()); + auto* NewUsed = new llvm::GlobalVariable( + M, ATy, /*isConstant=*/false, llvm::GlobalValue::AppendingLinkage, + llvm::ConstantArray::get(ATy, Kept), ListName); + NewUsed->setSection("llvm.metadata"); + } + } + return true; +} +#endif // CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS +#endif // !_WIN32 + +// =========================================================================== +// Workaround for an LLVM ORC emulated-TLS discard crash (in-process JIT). +// +// Fixed upstream by llvm/llvm-project#208413 (reland of #207161, which was +// reverted in #207775 over a Darwin test failure): IRMaterializationUnit's ctor +// registered a thread_local's emulated-TLS companion (`__emutls_t.` / +// `__emutls_v.`) in SymbolFlags but not SymbolToDefinition, so discarding +// a *duplicated* weak thread_local dereferenced end() -- an assertion in +// +Asserts builds, heap corruption otherwise. The trigger is two incremental +// modules that each define the same weak/linkonce_odr thread_local (an inline +// function or template static odr-used from two PTUs, as MakeFunctionCallable +// and repeated process() do). +// +// Mitigation: before a module reaches the JIT, demote every *duplicate* weak +// thread_local definition to available_externally. The IRMaterializationUnit +// ctor skips available_externally globals, so the duplicate contributes no +// symbol and the buggy discard is never reached; the first module stays the +// sole definition and later modules resolve to it within the JITDylib. +// +// Every weak thread_local is covered regardless of initializer: a thread_local +// with a non-trivial ctor (e.g. one holding a std::unique_ptr) is a +// zeroinitializer in IR -- its real init runs in a __tls_init function -- yet +// still emits the companion that trips discard, so the initializer must not be +// used to filter. Only weak thread_locals are demoted; demoting other weak +// globals or functions mis-resolves references/calls at runtime in the +// incremental JIT. +// +// ELF only: that is where the interpreter's emulated TLS makes the buggy +// discard reachable. On COFF the demotion orphans the comdat group's +// associated members ("Associative COMDAT symbol ... is not a key for its +// COMDAT"), and on Mach-O (native TLS, no comdats) it breaks the paired +// _ZTH/_ZTW thread_local init emission -- and on both, the JIT fails to +// resolve __emutls_get_address before the discard bug is even reachable. +// +// DEPRECATION: exists only for clang < 24 -- the upstream fix +// (llvm/llvm-project#208413) ships in clang 24, and every use site carries the +// same guard. Delete the workaround and its guarded call sites once clang-23 +// support is dropped from CppInterOp. +// =========================================================================== +#if CLANG_VERSION_MAJOR < 24 +inline void dedupeWeakEmulatedTLS(llvm::Module& M, llvm::StringSet<>& Defined) { + if (!llvm::Triple(M.getTargetTriple()).isOSBinFormatELF()) + return; + + for (llvm::GlobalVariable& GV : M.globals()) { + if (!GV.isThreadLocal() || GV.isDeclaration() || !GV.isWeakForLinker()) + continue; + + // First definer wins; later duplicates become references, mirroring what + // discard() would have done (available_externally + no comdat) but without + // hitting the crashing code path. + if (!Defined.insert(GV.getName()).second) { + GV.setLinkage(llvm::GlobalValue::AvailableExternallyLinkage); + GV.setComdat(nullptr); + } + } +} +#endif + // Clang 18 - Add new Interpreter methods: CodeComplete inline llvm::orc::LLJIT* getExecutionEngine(clang::Interpreter& I) { @@ -690,6 +882,7 @@ class SynthesizingCodeRAII { SynthesizingCodeRAII(Interpreter* i) : m_Interpreter(i) {} // ~SynthesizingCodeRAII() {} // TODO: implement }; + } // namespace compat #endif // CPPINTEROP_USE_REPL @@ -767,6 +960,25 @@ inline void InstantiateClassTemplateSpecialization( /*Complain=*/true, /*PrimaryHasMatchedPackOnParmToNonPackOnArg=*/false); } + +class DiagnosticsEngineRAII { +private: + clang::DiagnosticsEngine& diags; + +public: + bool reset_condition; // additional condition to reset diagnostics + + DiagnosticsEngineRAII(clang::DiagnosticsEngine& d, bool c = true) + : diags(d), reset_condition(c) {} + ~DiagnosticsEngineRAII() { + if (diags.hasErrorOccurred() && reset_condition) { + // instantiation failed, need to reset DiagnosticsEngine + diags.Reset(/*soft=*/true); + diags.getClient()->clear(); + } + } +}; + } // namespace compat #endif // CPPINTEROP_COMPATIBILITY_H diff --git a/interpreter/CppInterOp/lib/CppInterOp/CompatibilityGLIBC.h b/interpreter/CppInterOp/lib/CppInterOp/CompatibilityGLIBC.h new file mode 100644 index 0000000000000..f5cffc08a55af --- /dev/null +++ b/interpreter/CppInterOp/lib/CppInterOp/CompatibilityGLIBC.h @@ -0,0 +1,247 @@ +//--------------------------------------------------------------------*- C++ -*- +// CppInterOp Compatibility - glibc +//------------------------------------------------------------------------------ +#ifndef CPPINTEROP_COMPATIBILITY_GLIBC_H +#define CPPINTEROP_COMPATIBILITY_GLIBC_H + +// Declares at_quick_exit, and defines __GLIBC__ before the test below. +#include + +// =========================================================================== +// Resolve glibc's libc_nonshared.a symbols for the in-process JIT. +// +// glibc defines at_quick_exit, atexit, pthread_atfork, and +// __stack_chk_fail_local in libc_nonshared.a rather than libc.so: every ELF +// module links a private, non-exported copy, dlsym sees none of them, and a +// jitted reference fails with "Symbols not found: [ at_quick_exit ]". +// +// __stack_chk_fail_local is pure code and resolves to this library's copy +// (GlibcNonsharedSymbolGenerator). at_quick_exit and pthread_atfork are +// registration functions, and a jitted handler must not outlive the JIT +// that owns it, so they resolve to JIT-side shims (addGlibcNonsharedShims) +// backed by the host-side registry below: quick-exit handlers run once, at +// the earlier of a real quick_exit or the owning interpreter's teardown; +// atfork entries fire on fork while the interpreter lives and are dropped +// at teardown. atexit needs no entry -- LLJIT's platform support defines a +// JIT-aware atexit per JITDylib and runs its handlers on deinitialize. +// Other libc_nonshared.a members vary by glibc build and are omitted. +// =========================================================================== +#ifdef __GLIBC__ +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Config/llvm-config.h" +#include "llvm/ExecutionEngine/Orc/LLJIT.h" +#include "llvm/ExecutionEngine/Orc/ThreadSafeModule.h" +#include "llvm/IR/BasicBlock.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/IR/DerivedTypes.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/GlobalValue.h" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Type.h" +#include "llvm/IR/Value.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace compat { + +// The reserved name is the point: this is glibc's own symbol. +// NOLINTNEXTLINE(bugprone-reserved-identifier, readability-identifier-naming) +extern "C" void __stack_chk_fail_local(); + +inline const llvm::StringMap& glibcNonsharedSymbols() { + static const llvm::StringMap Symbols = { + {"__stack_chk_fail_local", + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(&__stack_chk_fail_local)}, + }; + return Symbols; +} + +/// Host-side registry for handlers jitted code passes to the shimmed +/// at_quick_exit/pthread_atfork. glibc only ever sees this library's own +/// hooks; entries are tagged with the owning interpreter and flushed before +/// its JIT is torn down, so no jitted pointer outlives the code it targets. +class JitGlibcHandlerRegistry { +public: + using Handler = void (*)(); + + static JitGlibcHandlerRegistry& instance() { + static JitGlibcHandlerRegistry R; + return R; + } + + int addQuickExit(void* Owner, Handler Fn) { + std::lock_guard L(M); + if (!QuickExitHookInstalled) { + // Host-resident hook: live handlers still run on a real quick_exit. + if (::at_quick_exit(&runAllQuickExitHandlers) != 0) + return -1; + QuickExitHookInstalled = true; + } + QuickExit.push_back({Owner, Fn}); + return 0; + } + + int addAtfork(void* Owner, Handler Prepare, Handler Parent, Handler Child) { + std::lock_guard L(M); + if (!AtforkHookInstalled) { + if (int Err = ::pthread_atfork(&runAtforkPrepare, &runAtforkParent, + &runAtforkChild)) + return Err; + AtforkHookInstalled = true; + } + Atfork.push_back({Owner, Prepare, Parent, Child}); + return 0; + } + + /// Runs the owner's quick-exit handlers (LIFO, exactly once) and drops its + /// atfork entries. Call while the owner's JIT can still execute code. + void flushOwner(void* Owner) { + while (Handler Fn = takeLastQuickExit(Owner)) + Fn(); + std::lock_guard L(M); + Atfork.erase( + std::remove_if(Atfork.begin(), Atfork.end(), + [&](const AtforkEntry& E) { return E.Owner == Owner; }), + Atfork.end()); + } + + /// The hook registered with ::at_quick_exit: drains every live handler, + /// LIFO. Public so tests can drive the real quick_exit path in-process. + static void runAllQuickExitHandlers() { + while (Handler Fn = instance().takeLastQuickExit(/*Owner=*/nullptr)) + Fn(); + } + +private: + struct QuickExitEntry { + void* Owner; + Handler Fn; + }; + struct AtforkEntry { + void* Owner; + Handler Prepare; + Handler Parent; + Handler Child; + }; + + // Handlers run outside the lock; they may legally re-register. + Handler takeLastQuickExit(void* Owner) { + std::lock_guard L(M); + for (auto It = QuickExit.rbegin(); It != QuickExit.rend(); ++It) { + if (Owner && It->Owner != Owner) + continue; + Handler Fn = It->Fn; + QuickExit.erase(std::next(It).base()); + return Fn; + } + return nullptr; + } + + // The prepare hook snapshots under the lock; the parent/child hooks run + // lock-free off the snapshot (another thread could hold the mutex across + // fork and the child would inherit it locked). + static void runAtforkPrepare() { + JitGlibcHandlerRegistry& R = instance(); + { + std::lock_guard L(R.M); + R.ForkSnapshot = R.Atfork; + } + for (auto It = R.ForkSnapshot.rbegin(); It != R.ForkSnapshot.rend(); ++It) + if (It->Prepare) + It->Prepare(); + } + static void runAtforkParent() { + for (const AtforkEntry& E : instance().ForkSnapshot) + if (E.Parent) + E.Parent(); + } + static void runAtforkChild() { + for (const AtforkEntry& E : instance().ForkSnapshot) + if (E.Child) + E.Child(); + } + + std::mutex M; + std::vector QuickExit; + std::vector Atfork; + std::vector ForkSnapshot; + bool QuickExitHookInstalled = false; + bool AtforkHookInstalled = false; +}; + +/// C-signature entry points the JIT-side shims call; bound by name via +/// absoluteSymbols in installGlibcNonsharedSupport. +inline int jitAtQuickExit(void* Owner, void (*Fn)()) { + return JitGlibcHandlerRegistry::instance().addQuickExit(Owner, Fn); +} +inline int jitPthreadAtfork(void* Owner, void (*Prepare)(), void (*Parent)(), + void (*Child)()) { + return JitGlibcHandlerRegistry::instance().addAtfork(Owner, Prepare, Parent, + Child); +} + +/// Adds an IR module to the main JITDylib defining at_quick_exit and +/// pthread_atfork shims that forward, with the owning interpreter baked in, +/// to the registry entry points above -- the pattern LLJIT's generic +/// platform uses for atexit. An IR module rather than interpreted source so +/// Undo cannot drop it and no PTU bookkeeping is disturbed. +inline llvm::Error addGlibcNonsharedShims(llvm::orc::LLJIT& J, void* Owner) { + auto TSCtx = std::make_unique(); + auto M = + std::make_unique("", *TSCtx); + M->setDataLayout(J.getDataLayout()); +#if LLVM_VERSION_MAJOR < 21 + M->setTargetTriple(J.getTargetTriple().str()); +#else + M->setTargetTriple(J.getTargetTriple()); +#endif + + llvm::Type* IntTy = llvm::Type::getInt32Ty(*TSCtx); + llvm::PointerType* PtrTy = llvm::PointerType::getUnqual(*TSCtx); + + auto AddShim = [&](llvm::StringRef Name, llvm::StringRef Helper, + unsigned NumArgs) { + llvm::SmallVector ShimArgs(NumArgs, PtrTy); + llvm::Function* F = llvm::Function::Create( + llvm::FunctionType::get(IntTy, ShimArgs, /*isVarArg=*/false), + llvm::GlobalValue::ExternalLinkage, Name, M.get()); + llvm::SmallVector HelperArgs(NumArgs + 1, PtrTy); + llvm::FunctionCallee Callee = M->getOrInsertFunction( + Helper, llvm::FunctionType::get(IntTy, HelperArgs, false)); + llvm::IRBuilder<> B(llvm::BasicBlock::Create(*TSCtx, "entry", F)); + llvm::Value* OwnerV = B.CreateIntToPtr( + llvm::ConstantInt::get( + M->getDataLayout().getIntPtrType(*TSCtx), + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(Owner)), + PtrTy); + llvm::SmallVector Args{OwnerV}; + for (llvm::Argument& A : F->args()) + Args.push_back(&A); + B.CreateRet(B.CreateCall(Callee, Args)); + }; + AddShim("at_quick_exit", "__cppinterop_at_quick_exit", 1); + AddShim("pthread_atfork", "__cppinterop_pthread_atfork", 3); + + return J.addIRModule( + llvm::orc::ThreadSafeModule(std::move(M), std::move(TSCtx))); +} + +} // namespace compat +#endif // __GLIBC__ + +#endif // CPPINTEROP_COMPATIBILITY_GLIBC_H diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp index 7982a3f4c39e5..20f5988bd50b1 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -79,6 +79,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Demangle/Demangle.h" #include "llvm/ExecutionEngine/JITSymbol.h" @@ -656,13 +657,8 @@ static void InstantiateFunctionDefinition(Decl* D) { getSema().InstantiateFunctionDefinition(SourceLocation(), FD, /*Recursive=*/true, /*DefinitionRequired=*/true); - // FIXME: this can go into a RAII object - clang::DiagnosticsEngine& Diags = getSema().getDiagnostics(); - if (!FD->isDefined() && Diags.hasErrorOccurred()) { - // instantiation failed, need to reset DiagnosticsEngine - Diags.Reset(/*soft=*/true); - Diags.getClient()->clear(); - } + compat::DiagnosticsEngineRAII diagsRAII(getSema().getDiagnostics(), + !FD->isDefined()); } } @@ -1405,6 +1401,15 @@ DeclRef GetParentScope(ConstDeclRef DRef) { D = UnwrapUsingShadowToFunction(D); auto* ParentDC = D->getDeclContext(); + // A linkage spec (`extern "C++" { ... }`) or C++20 `export` block is not a + // scope, skip to the enclosing scope. E.g. on Windows the canonical + // declaration of namespace std comes from an `extern "C++"` block in the + // MSVC CRT headers, which would otherwise become an "" parent of + // std. + while (ParentDC && (llvm::isa(ParentDC) || + llvm::isa(ParentDC))) + ParentDC = ParentDC->getParent(); + if (!ParentDC) return INTEROP_RETURN(nullptr); @@ -1767,31 +1772,125 @@ TypeRef GetFunctionReturnType(ConstFuncRef func) { return INTEROP_RETURN(nullptr); } -bool IsAllocator(ConstFuncRef Fn) { +OwnershipBehaviour GetOwnershipBehaviour(ConstFuncRef Fn) { INTEROP_TRACE(Fn); if (!Fn) - return INTEROP_RETURN(false); - const auto* D = unwrap(Fn); + return INTEROP_RETURN(OwnershipBehaviour::Unknown); + const auto* D = UnwrapUsingShadowToFunction(unwrap(Fn)); + if (const auto* FTD = dyn_cast(D)) + D = FTD->getTemplatedDecl(); + const auto* FD = dyn_cast(D); + if (!FD) + return INTEROP_RETURN(OwnershipBehaviour::Unknown); + + OwnershipBehaviour result = OwnershipBehaviour::Unknown; + for (const auto* FDA : FD->specific_attrs()) { + switch (FDA->getOwnKind()) { + case OwnershipAttr::Returns: + result = result | OwnershipBehaviour::OwnershipReturns; + break; + case OwnershipAttr::Takes: + result = result | OwnershipBehaviour::OwnershipTakes; + break; + case OwnershipAttr::Holds: + result = result | OwnershipBehaviour::OwnershipHolds; + break; + } + } + return INTEROP_RETURN(result); +} + +uint64_t GetDeallocationIndexes(ConstFuncRef Fn) { + INTEROP_TRACE(Fn); + if (!Fn) + return INTEROP_RETURN(uint64_t{0}); + const auto* D = UnwrapUsingShadowToFunction(unwrap(Fn)); + if (const auto* FTD = dyn_cast(D)) + D = FTD->getTemplatedDecl(); + const auto* FD = dyn_cast(D); + if (!FD) + return INTEROP_RETURN(uint64_t{0}); + uint64_t result = 0; + for (const auto* attr : FD->specific_attrs()) { + if (attr->getOwnKind() == OwnershipAttr::Returns) + continue; + for (const auto& Idx : attr->args()) { + unsigned index = Idx.getASTIndex(); + if (index < 64) + result |= (uint64_t{1} << index); + } + } + return INTEROP_RETURN(result); +} + +int GetAllocationSizeParamIndex(ConstFuncRef Fn) { + INTEROP_TRACE(Fn); + if (!Fn) + return INTEROP_RETURN(-1); + const auto* D = UnwrapUsingShadowToFunction(unwrap(Fn)); + if (const auto* FTD = dyn_cast(D)) + D = FTD->getTemplatedDecl(); + const auto* FD = dyn_cast(D); + if (!FD) + return INTEROP_RETURN(-1); + for (const auto* attr : FD->specific_attrs()) { + if (attr->getOwnKind() != OwnershipAttr::Returns || attr->args().empty()) + continue; + return INTEROP_RETURN(static_cast(attr->args_begin()->getASTIndex())); + } + return INTEROP_RETURN(-1); +} + +AllocType IsAllocator(ConstFuncRef Fn) { + INTEROP_TRACE(Fn); + if (!Fn) + return INTEROP_RETURN(AllocType::Unknown); + const auto* D = UnwrapUsingShadowToFunction(unwrap(Fn)); + if (const auto* FTD = dyn_cast(D)) + D = FTD->getTemplatedDecl(); if (const auto* FD = dyn_cast(D)) { if (FD->getBuiltinID() == Builtin::ID::BImalloc) - return INTEROP_RETURN(true); + return INTEROP_RETURN(AllocType::Malloc); if (const auto* FDA = FD->getAttr()) { if (FDA->getSemanticSpelling() != RestrictAttr::Declspec_restrict) - return INTEROP_RETURN(true); + return INTEROP_RETURN(AllocType::Malloc); } if (const auto* FDA = FD->getAttr()) { if (FDA->getOwnKind() == OwnershipAttr::Returns) - return INTEROP_RETURN(true); + return INTEROP_RETURN(AllocType::Malloc); } if (FD->hasAttr() || FD->hasAttr() || FD->hasAttr()) - return INTEROP_RETURN(true); + return INTEROP_RETURN(AllocType::Malloc); + + for (const auto* attr : FD->attrs()) { + llvm::StringRef attrName; + if (const auto* swiftAttr = dyn_cast(attr)) + attrName = swiftAttr->getAttribute(); + else if (const auto* annotateAttr = dyn_cast(attr)) + attrName = annotateAttr->getAnnotation(); + else + continue; + attrName.consume_front("returns_"); + if (attrName == "cppAllocNone") + return INTEROP_RETURN(AllocType::None); + if (attrName == "cppAllocNew") + return INTEROP_RETURN(AllocType::New); + if (attrName == "cppAllocNewArr") + return INTEROP_RETURN(AllocType::NewArr); + if (attrName == "cppAllocMalloc") + return INTEROP_RETURN(AllocType::Malloc); + if (attrName == "cppAllocOperatorNew") + return INTEROP_RETURN(AllocType::OperatorNew); + if (attrName == "cppAllocOperatorNewArr") + return INTEROP_RETURN(AllocType::OperatorNewArr); + } } - return INTEROP_RETURN(false); + return INTEROP_RETURN(AllocType::Unknown); } bool IsDeallocator(ConstFuncRef Fn) { @@ -2007,7 +2106,10 @@ struct AllocationTraverser : RecursiveASTVisitor { } auto it = visitedFuncs.find(FD); if (it == visitedFuncs.end()) { - visitedFuncs[FD] = std::nullopt; + auto storedResult = IsAllocator(wrap(FD)); + visitedFuncs[FD] = storedResult; + if (storedResult != AllocType::Unknown) + return storedResult; return AnalyzeAllocType(FD, visitedFuncs); } return it->second; @@ -5746,10 +5848,26 @@ std::string LookupLibrary(const char* lib_name) { getInterp().getDynamicLibraryManager()->lookupLibrary(lib_name)); } -bool LoadLibrary(const char* lib_stem, bool lookup) { - INTEROP_TRACE(lib_stem, lookup); +bool LoadLibrary(const char* lib_stem, bool lookup, std::string* error) { + INTEROP_TRACE(lib_stem, lookup, error); + if (error) + error->clear(); +#ifdef CPPINTEROP_USE_CLING + // cling::Interpreter::loadLibrary has no reason channel; report what the + // lookup alone can tell. compat::Interpreter::CompilationResult res = getInterp().loadLibrary(lib_stem, lookup); + if (res != compat::Interpreter::kSuccess && error) { + bool NotFound = + lookup && + getInterp().getDynamicLibraryManager()->lookupLibrary(lib_stem).empty(); + *error = std::string(lib_stem) + + (NotFound ? ": library not found" : ": failed to load"); + } +#else + compat::Interpreter::CompilationResult res = + getInterp().loadLibrary(lib_stem, lookup, error); +#endif return INTEROP_RETURN(res == compat::Interpreter::kSuccess); } @@ -6141,6 +6259,11 @@ std::string GetFunctionArgDefault(ConstFuncRef func, size_t param_index) { PI = (FD->getTemplatedDecl())->getNonObjectParameter(param_index); if (PI->hasDefaultArg()) { + // Print the AST with ConstantsAsWritten and the interpreter's ASTContext: + // literal leaves with valid source ranges then render from source text + // ("3.14", not the representation-precision "3.1400000000000001"). The + // previous std::stod normalization terminated the exception-free build on + // symbolic defaults such as `double ratio = kDefaultRatio`. std::string Result; llvm::raw_string_ostream OS(Result); const Expr* DefaultArgExpr = nullptr; @@ -6149,20 +6272,30 @@ std::string GetFunctionArgDefault(ConstFuncRef func, size_t param_index) { DefaultArgExpr = PI->getUninstantiatedDefaultArg(); else DefaultArgExpr = PI->getDefaultArg(); - DefaultArgExpr->printPretty(OS, nullptr, PrintingPolicy(LangOptions())); - - // FIXME: Floats are printed in clang with the precision of their underlying - // representation and not as written. This is a deficiency in the printing - // mechanism of clang which we require extra work to mitigate. For example - // float PI = 3.14 is printed as 3.1400000000000001 - if (PI->getType()->isFloatingType()) { - if (!Result.empty() && Result.back() == '.') - return INTEROP_RETURN(Result); - auto DefaultArgValue = std::stod(Result); - std::ostringstream oss; - oss << DefaultArgValue; - Result = oss.str(); - } + ASTContext& Ctx = getASTContext(); + PrintingPolicy Policy(Ctx.getLangOpts()); + Policy.ConstantsAsWritten = true; + DefaultArgExpr->printPretty(OS, nullptr, Policy, /*Indentation=*/0, + /*NewlineSymbol=*/"\n", &Ctx); + + // CPyCppyy evaluates numeric defaults in Python after stripping + // uppercase literal suffixes, so print the suffix in its canonical + // uppercase form ("5.f" -> "5.F"). The per-kind alphabets keep hex + // digits safe: 'f' in 0x1f is a digit of an IntegerLiteral, whose + // suffix alphabet has no 'f'. + const Expr* Leaf = DefaultArgExpr->IgnoreImpCasts(); + if (const auto* UO = llvm::dyn_cast(Leaf)) + Leaf = UO->getSubExpr()->IgnoreImpCasts(); + const char* SuffixAlphabet = nullptr; + if (llvm::isa(Leaf)) + SuffixAlphabet = "uUlLzZ"; + else if (llvm::isa(Leaf)) + SuffixAlphabet = "fFlL"; + if (SuffixAlphabet) + for (size_t I = Result.find_last_not_of(SuffixAlphabet) + 1; + I < Result.size(); ++I) + Result[I] = llvm::toUpper(Result[I]); + return INTEROP_RETURN(Result); } return INTEROP_RETURN(""); diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td index 4f4c45e86ea4c..9681c61ce9358 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td @@ -245,12 +245,20 @@ GetResourceDir() function.}]; def LoadLibrary : CppInterOpAPI { let Doc = [{Finds \c lib_stem considering the list of search paths and loads it by calling dlopen. +\param[out] error - if non-null, receives the reason a load failed (the + dlerror() text, or a not-found note when lookup fails). When it + is set the reason is not also written to stderr. The Cling + backend only reports a generic note. \returns true on success.}]; + // std::string* cannot cross the C ABI; CXCppInterOp.cpp keeps the + // two-argument cppinterop_LoadLibrary wrapper by hand. + let NoCWrapper = true; let ReturnType = "bool"; let Args = [ Arg<"const char*", "lib_stem">, - Arg<"bool", "lookup", "true"> + Arg<"bool", "lookup", "true">, + Arg<"std::string*", "error", "nullptr"> ]; } @@ -783,6 +791,39 @@ def IsFunctionProtoType : CppInterOpAPI { ]; } +def GetOwnershipBehaviour : CppInterOpAPI { + let Doc = [{Returns the ownership behaviour the function declares, as a +bitmask of OwnershipBehaviour flags read off its ownership_returns, +ownership_takes and ownership_holds attributes.}]; + + let ReturnType = "OwnershipBehaviour"; + let Args = [ + Arg<"ConstFuncRef", "func"> + ]; +} + +def GetDeallocationIndexes : CppInterOpAPI { + let Doc = [{Returns a bitmask of the parameters whose ownership the function +takes or holds, as annotated by ownership_takes/ownership_holds: The mask is meaningful only when +GetOwnershipBehaviour reports OwnershipTakes or OwnershipHolds.}]; + + let ReturnType = "uint64_t"; + let Args = [ + Arg<"ConstFuncRef", "func"> + ]; +} + +def GetAllocationSizeParamIndex : CppInterOpAPI { + let Doc = [{Returns the 0-based index of the parameter carrying the allocation +size for a function annotated with ownership_returns, or -1 if the function is +not so annotated or the size argument was omitted.}]; + + let ReturnType = "int"; + let Args = [ + Arg<"ConstFuncRef", "func"> + ]; +} + def GetAllocType: CppInterOpAPI { let Doc = "Analyzes function's body to detect memory allocation behaviour"; @@ -1304,7 +1345,7 @@ def GetFunctionReturnType : CppInterOpAPI { def IsAllocator : CppInterOpAPI { let Doc = "Checks if the provided function is an allocator function"; - let ReturnType = "bool"; + let ReturnType = "AllocType"; let Args = [ Arg<"ConstFuncRef", "func"> ]; diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td index b5e6f56d73262..95a4e78a367f1 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpAPI.td @@ -142,6 +142,9 @@ def CMap_InterpLangStd : CTypeMap<"InterpreterLanguageStandard", "unsigned char def CMap_ValueKind : CTypeMap<"ValueKind", "unsigned char", /*ArgConv=*/"", /*RetConv=*/"static_cast($result)">; +def CMap_OwnershipBehaviour : CTypeMap<"OwnershipBehaviour", "unsigned char", + /*ArgConv=*/"", + /*RetConv=*/"static_cast($result)">; def CMap_Signedness : CTypeMap<"Signedness*", "unsigned*", /*ArgConv=*/"reinterpret_cast($name)">; diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpInterpreter.h b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpInterpreter.h index cd3a9ba73ea64..87ff82343db5b 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOpInterpreter.h +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOpInterpreter.h @@ -7,6 +7,7 @@ #define CPPINTEROP_INTERPRETER_H #include "Compatibility.h" +#include "CompatibilityGLIBC.h" #include "DynamicLibraryManager.h" #include "Paths.h" @@ -18,6 +19,7 @@ #include "clang/AST/GlobalDecl.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/TargetOptions.h" +#include "clang/Basic/Version.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendOptions.h" #include "clang/Lex/Preprocessor.h" @@ -29,8 +31,16 @@ #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/ExecutionEngine/JITSymbol.h" +#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h" +#include "llvm/ExecutionEngine/Orc/Core.h" +#include "llvm/ExecutionEngine/Orc/CoreContainers.h" #include "llvm/ExecutionEngine/Orc/LLJIT.h" +#include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h" +#include "llvm/ExecutionEngine/Orc/Shared/ExecutorSymbolDef.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/Error.h" #include "llvm/Support/TargetSelect.h" @@ -42,7 +52,6 @@ #include #endif #if defined(_WIN32) && (defined(_M_IX86) || defined(__i386__)) -#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h" #include "llvm/Support/DynamicLibrary.h" #include #endif @@ -205,6 +214,62 @@ class COFFi386SymbolGenerator : public llvm::orc::DefinitionGenerator { }; #endif // _WIN32 && i386 +#ifdef __GLIBC__ +/// Resolves the pure-code libc_nonshared.a symbols (per-module copies +/// invisible to dlsym) to this library's own. The registration functions +/// (at_quick_exit, pthread_atfork) resolve to JIT-side shims instead; see +/// compat::glibcNonsharedSymbols and compat::addGlibcNonsharedShims. +class GlibcNonsharedSymbolGenerator : public llvm::orc::DefinitionGenerator { +public: + llvm::Error + tryToGenerate(llvm::orc::LookupState& LS, llvm::orc::LookupKind K, + llvm::orc::JITDylib& JD, + llvm::orc::JITDylibLookupFlags JDLookupFlags, + const llvm::orc::SymbolLookupSet& LookupSet) override { + const llvm::StringMap& Known = compat::glibcNonsharedSymbols(); + llvm::orc::SymbolMap NewSymbols; + for (const auto& KV : LookupSet) { + auto It = Known.find(*KV.first); + if (It == Known.end()) + continue; + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) + NewSymbols[KV.first] = {llvm::orc::ExecutorAddr::fromPtr(It->second), + llvm::JITSymbolFlags::Exported | + llvm::JITSymbolFlags::Callable}; + } + if (NewSymbols.empty()) + return llvm::Error::success(); + return JD.define(llvm::orc::absoluteSymbols(std::move(NewSymbols))); + } +}; + +/// Installs the libc_nonshared.a support on the main JITDylib: the fallback +/// generator for pure-code symbols, the host registration entry points, and +/// the at_quick_exit/pthread_atfork shims scoped to \p Owner (see +/// compat::JitGlibcHandlerRegistry). +inline void installGlibcNonsharedSupport(llvm::orc::LLJIT& J, void* Owner) { + J.getMainJITDylib().addGenerator( + std::make_unique()); + + llvm::orc::SymbolMap Syms; + Syms[J.mangleAndIntern("__cppinterop_at_quick_exit")] = + llvm::orc::ExecutorSymbolDef( + llvm::orc::ExecutorAddr::fromPtr(&compat::jitAtQuickExit), + llvm::JITSymbolFlags::Exported | llvm::JITSymbolFlags::Callable); + Syms[J.mangleAndIntern("__cppinterop_pthread_atfork")] = + llvm::orc::ExecutorSymbolDef( + llvm::orc::ExecutorAddr::fromPtr(&compat::jitPthreadAtfork), + llvm::JITSymbolFlags::Exported | llvm::JITSymbolFlags::Callable); + llvm::Error Err = + J.getMainJITDylib().define(llvm::orc::absoluteSymbols(std::move(Syms))); + if (!Err) + Err = compat::addGlibcNonsharedShims(J, Owner); + if (Err) + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "Failed to install the glibc shims:"); +} +#endif // __GLIBC__ + /// CppInterOp Interpreter /// class Interpreter { @@ -261,6 +326,12 @@ class Interpreter { mutable std::once_flag sDLMInit; bool outOfProcess; +#if CLANG_VERSION_MAJOR < 24 + // Weak thread_local definitions already handed to the JIT, so later modules + // demote their duplicates. See compat::dedupeWeakEmulatedTLS. + llvm::StringSet<> DedupedWeakTLS; +#endif + public: Interpreter(std::unique_ptr CI, std::unique_ptr ctx = nullptr, bool oop = false) @@ -313,11 +384,26 @@ class Interpreter { std::make_unique()); #endif +#ifdef __GLIBC__ + // In-process only (the shims and the generator hand out this process's + // addresses); the generator is appended last so it is consulted only + // when the process-symbol generator fails. The inner interpreter keys + // the handler registry; ~Interpreter flushes it. + if (!outOfProcess) + installGlibcNonsharedSupport(*compat::getExecutionEngine(*CI), CI.get()); +#endif + return std::make_unique(std::move(CI), std::move(io_ctx), outOfProcess); } - ~Interpreter() {} + ~Interpreter() { +#ifdef __GLIBC__ + // Run jitted quick-exit handlers while their code is still mapped and + // drop this interpreter's atfork entries. + compat::JitGlibcHandlerRegistry::instance().flushOwner(inner.get()); +#endif + } operator const clang::Interpreter&() const { return *inner; } operator clang::Interpreter&() { return *inner; } @@ -368,7 +454,28 @@ class Interpreter { } llvm::Error ParseAndExecute(llvm::StringRef Code, clang::Value* V = nullptr) { - return inner->ParseAndExecute(Code, V); + // Value-returning execution keeps clang's LastValue handling (private to + // clang::Interpreter), so delegate. The no-value path -- used by wrapper + // compilation -- is split so the module can be sanitized before Execute. + if (V) + return inner->ParseAndExecute(Code, V); + auto PTU = inner->Parse(Code); + if (!PTU) + return PTU.takeError(); + if (PTU->TheModule) { +#if CLANG_VERSION_MAJOR < 24 + compat::dedupeWeakEmulatedTLS(*PTU->TheModule, DedupedWeakTLS); +#endif + // WORKAROUND: see bindProcessWeakGlobals in Compatibility.h -- remove + // with the pass once the clang JIT fix lands. +#if !defined(_WIN32) && CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS + if (!outOfProcess) + compat::bindProcessWeakGlobals(*PTU->TheModule); +#endif + if (llvm::Error Err = inner->Execute(*PTU)) + return Err; + } + return llvm::Error::success(); } llvm::Error Undo(unsigned N = 1) { return compat::Undo(*inner, N); } @@ -463,6 +570,17 @@ class Interpreter { if (PTU) *PTU = &*PTUOrErr; +#if CLANG_VERSION_MAJOR < 24 + if (PTUOrErr->TheModule) + compat::dedupeWeakEmulatedTLS(*PTUOrErr->TheModule, DedupedWeakTLS); +#endif + // WORKAROUND: see bindProcessWeakGlobals in Compatibility.h -- remove + // with the pass once the clang JIT fix lands. +#if !defined(_WIN32) && CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS + if (PTUOrErr->TheModule && !outOfProcess) + compat::bindProcessWeakGlobals(*PTUOrErr->TheModule); +#endif + if (auto Err = Execute(*PTUOrErr)) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Failed to execute via ::process:"); @@ -586,16 +704,22 @@ class Interpreter { incpaths, withSystem, withFlags); } - CompilationResult loadLibrary(const std::string& filename, bool lookup) { + CompilationResult loadLibrary(const std::string& filename, bool lookup, + std::string* error = nullptr) { llvm::Triple triple(getCompilerInstance()->getTargetOpts().Triple); if (triple.isWasm()) { + // LCOV_EXCL_START -- no coverage lane runs the wasm path. // On WASM, dlopen-style canonical lookup has no effect. if (auto Err = inner->LoadDynamicLibrary(filename.c_str())) { - llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), - "loadLibrary: "); + std::string Msg = llvm::toString(std::move(Err)); + if (error) + *error = std::move(Msg); + else + llvm::errs() << "loadLibrary: " << Msg << '\n'; return kFailure; } return kSuccess; + // LCOV_EXCL_STOP } DynamicLibraryManager* DLM = getDynamicLibraryManager(); @@ -604,9 +728,14 @@ class Interpreter { canonicalLib = DLM->lookupLibrary(filename); const std::string& library = lookup ? canonicalLib : filename; - if (!library.empty()) { - switch ( - DLM->loadLibrary(library, /*permanent*/ false, /*resolved*/ true)) { + if (library.empty()) { + if (error) + *error = filename + ": library not found"; + return kMoreInputExpected; + } + { + switch (DLM->loadLibrary(library, /*permanent*/ false, /*resolved*/ true, + error)) { case DynamicLibraryManager::kLoadLibSuccess: // Intentional fall through case DynamicLibraryManager::kLoadLibAlreadyLoaded: return kSuccess; @@ -618,7 +747,6 @@ class Interpreter { return kFailure; } } - return kMoreInputExpected; } std::string toString(const char* type, void* obj) { diff --git a/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.cpp b/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.cpp index a1ddd57aba156..3b93e10ebcccd 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.cpp @@ -24,6 +24,7 @@ #endif #include +#include #include #include @@ -343,7 +344,7 @@ std::string DynamicLibraryManager::lookupLibrary( DynamicLibraryManager::LoadLibResult DynamicLibraryManager::loadLibrary(StringRef libStem, bool permanent, - bool resolved) { + bool resolved, std::string* errMsg) { #define DEBUG_TYPE "Dyld::loadLibrary:" LLVM_DEBUG(dbgs() << "Dyld::loadLibrary: " << libStem.str() << ", " << (permanent ? "permanent" : "not-permanent") << ", " @@ -363,14 +364,19 @@ DynamicLibraryManager::loadLibrary(StringRef libStem, bool permanent, // TODO: !permanent case - std::string errMsg; - DyLibHandle dyLibHandle = platform::DLOpen(canonicalLoadedLib, &errMsg); + std::string loadErr; + DyLibHandle dyLibHandle = platform::DLOpen(canonicalLoadedLib, &loadErr); if (!dyLibHandle) { // We emit callback to LibraryLoadingFailed when we get error with error // message. // TODO: Implement callbacks - LLVM_DEBUG(dbgs() << "DynamicLibraryManager::loadLibrary(): " << errMsg); + if (errMsg) + *errMsg = loadErr; + else if (!loadErr.empty()) + std::cerr << loadErr << '\n'; + + LLVM_DEBUG(dbgs() << "DynamicLibraryManager::loadLibrary(): " << loadErr); return kLoadLibLoadError; } diff --git a/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.h b/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.h index 167bd0c7c13c7..54c5c87761f9f 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.h +++ b/interpreter/CppInterOp/lib/CppInterOp/DynamicLibraryManager.h @@ -163,13 +163,16 @@ class DynamicLibraryManager { ///\param [in] permanent - If false, the file can be unloaded later. ///\param [in] resolved - Whether libStem is an absolute path or resolved /// from a previous call to DynamicLibraryManager::lookupLibrary + ///\param [out] errMsg - If non-null, receives the loader's error text on + /// kLoadLibLoadError instead of it going to stderr. /// ///\returns kLoadLibSuccess on success, kLoadLibAlreadyLoaded if the library /// was already loaded, kLoadLibError if the library cannot be found or any /// other error was encountered. /// LoadLibResult loadLibrary(llvm::StringRef, bool permanent, - bool resolved = false); + bool resolved = false, + std::string* errMsg = nullptr); void unloadLibrary(llvm::StringRef libStem); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestAttributeMerge.h b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestAttributeMerge.h new file mode 100644 index 0000000000000..12aad410329fa --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestAttributeMerge.h @@ -0,0 +1,14 @@ +#ifndef UNITTESTS_CPPINTEROP_TESTATTRIBUTEMERGE_H +#define UNITTESTS_CPPINTEROP_TESTATTRIBUTEMERGE_H + +[[clang::annotate("cppAllocMalloc")]] void* mergeFunc(); + +#pragma clang attribute push([[clang::annotate("cppAllocNew")]], \ + apply_to = function) +int* overloadFunc(); +int* overloadFunc(int n); +#pragma clang attribute pop + +[[clang::annotate("cppAllocMalloc")]] void* func71_helper(); + +#endif diff --git a/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.apinotes b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.apinotes index 5f14e0a6f3650..16bc930ba0544 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.apinotes +++ b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.apinotes @@ -4,3 +4,17 @@ Functions: RetainCountConvention: CFReturnsRetained - Name: testNotAlloc RetainCountConvention: CFReturnsNotRetained + - Name: testMalloc + SwiftReturnOwnership: cppAllocMalloc + - Name: testNew + SwiftReturnOwnership: cppAllocNew + - Name: testNewArr + SwiftReturnOwnership: cppAllocNewArr + - Name: testOperatorNew + SwiftReturnOwnership: cppAllocOperatorNew + - Name: testOperatorNewArr + SwiftReturnOwnership: cppAllocOperatorNewArr + - Name: testNone + SwiftReturnOwnership: cppAllocNone + - Name: testWeirdAttr + SwiftReturnOwnership: someWeirdAttr \ No newline at end of file diff --git a/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.h b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.h index 6478c632fe7b6..7a10e22dfd802 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.h +++ b/interpreter/CppInterOp/unittests/CppInterOp/APINotes/TestHeader.h @@ -4,4 +4,11 @@ void* testAlloc(int value); void testNotAlloc(void* ptr); +void* testMalloc(); +void* testNew(); +void* testNewArr(); +void* testOperatorNew(); +void* testOperatorNewArr(); +void* testNone(); +void* testWeirdAttr(); #endif diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp index 71f709780238e..aba59c7099828 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/CAPITest.cpp @@ -18,6 +18,12 @@ TYPED_TEST(CppInterOpTest, CAPI_DeclareAndProcess) { EXPECT_EQ(0, cppinterop_Process("capi_var++;")); } +TYPED_TEST(CppInterOpTest, CAPI_LoadLibrary) { + TestFixture::CreateInterpreter(); + // The hand-written two-argument wrapper still exists and forwards. + EXPECT_FALSE(cppinterop_LoadLibrary("no-such-cppinterop-lib", true)); +} + TYPED_TEST(CppInterOpTest, CAPI_ScopeQueries) { TestFixture::CreateInterpreter(); Cpp::Declare("namespace CAPINs { class Foo {}; }"); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt index 87492280a974e..d6ebe6e43498c 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/unittests/CppInterOp/CMakeLists.txt @@ -86,7 +86,7 @@ if(TARGET CppInterOpTableGen) endif() target_link_libraries(CppInterOpTests PRIVATE CAPITestC) target_compile_definitions(CppInterOpTests PRIVATE - "CPPINTEROP_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/../../\"" + "CPPINTEROP_SRC_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/../..\"" ) set_source_files_properties(InterpreterTest.cpp PROPERTIES COMPILE_DEFINITIONS @@ -109,9 +109,12 @@ add_cppinterop_unittest(TracingTests TracingTests.cpp Utils.cpp ) +# Derive both prefixes from this directory: CMAKE_BINARY_DIR is the top +# project's build dir when CppInterOp is a subproject. The CPPINTEROP_BIN_DIR +# environment variable overrides the artifacts prefix at run time (TestPaths.h). target_compile_definitions(TracingTests PRIVATE - "CPPINTEROP_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/../../\"" - "CPPINTEROP_BINARY_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/../../\"" + "CPPINTEROP_SRC_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/../..\"" + "CPPINTEROP_BIN_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/../..\"" ) # GCC 12 and (less often) 13/14 emit a -Wmaybe-uninitialized false positive # inside libstdc++'s headers when the std::function subobject of @@ -185,8 +188,10 @@ if(NOT EMSCRIPTEN AND BUILD_SHARED_LIBS) ${CMAKE_CURRENT_BINARY_DIR} ${GTEST_INCLUDE_DIR}) target_link_libraries(DispatchTests PRIVATE ${gtest_libs} ${link_pthreads_lib}) # Deliberately no target_link_libraries(... clangCppInterOp). + # CMAKE_BINARY_DIR, not this directory: the shared library goes to the + # top-level lib/. target_compile_definitions(DispatchTests PRIVATE - CPPINTEROP_LIB_PATH="${CMAKE_BINARY_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}" + CPPINTEROP_BIN_DIR="${CMAKE_BINARY_DIR}" ) set_output_directory(DispatchTests BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin/$/ diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp index 7a5c5723b9734..bca63aed96350 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchInit.cpp @@ -1,8 +1,13 @@ // Storage definitions and initialization for dispatch function pointers. // Linked only by DispatchTests — not by the normal test suite. +#include "TestPaths.h" #include "CppInterOp/Dispatch.h" +#include +#include +#include + // Define storage for all raw dispatch function pointers. using namespace Cpp; #define CPPINTEROP_API_FUNC(DN, CN, Ret, DeclArgs, CallArgs, RawTypes) \ @@ -12,8 +17,18 @@ using namespace Cpp; namespace { struct DispatchInitializer { DispatchInitializer() { - if (!Cpp::LoadDispatchAPI(CPPINTEROP_LIB_PATH)) { - std::abort(); + std::string libPath = TestUtils::GetCppInterOpLibPath(); + if (libPath.empty()) { + std::cerr << "DispatchTests: cannot locate libclangCppInterOp: set the " + "CPPINTEROP_BIN_DIR environment variable to the CppInterOp " + "artifacts prefix (the directory containing lib/ and " + "include/).\n"; + std::exit(EXIT_FAILURE); + } + if (!Cpp::LoadDispatchAPI(libPath.c_str())) { + std::cerr << "DispatchTests: failed to load dispatch API from '" + << libPath << "'.\n"; + std::exit(EXIT_FAILURE); } } }; diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp index 11d4ae0b0b8ee..c5fc8e90b0965 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchSmokeTest.cpp @@ -1,3 +1,4 @@ +#include "TestPaths.h" #include "CppInterOp/Dispatch.h" #include "gtest/gtest.h" @@ -186,8 +187,11 @@ TEST(DispatchSmokeTest, CAPI_ScalarFunctions) { Cpp::CreateInterpreter({}); Cpp::Declare("namespace DispCAPI { class Cls {}; int var = 1; }"); - DlHandle lib(CPPINTEROP_LIB_PATH); - ASSERT_TRUE(lib) << "Failed to dlopen " << CPPINTEROP_LIB_PATH; + std::string libPath = TestUtils::GetCppInterOpLibPath(); + ASSERT_FALSE(libPath.empty()) + << "Set CPPINTEROP_BIN_DIR to the CppInterOp artifacts prefix"; + DlHandle lib(libPath.c_str()); + ASSERT_TRUE(lib) << "Failed to dlopen " << libPath; using IsClassFn = bool (*)(void*); using GetNameFn = char* (*)(void*); @@ -215,8 +219,11 @@ TEST(DispatchSmokeTest, CAPI_CollectionFunctions) { Cpp::CreateInterpreter({}); Cpp::Declare("enum DispCAPIEnum { DA, DB, DC };"); - DlHandle lib(CPPINTEROP_LIB_PATH); - ASSERT_TRUE(lib) << "Failed to dlopen " << CPPINTEROP_LIB_PATH; + std::string libPath = TestUtils::GetCppInterOpLibPath(); + ASSERT_FALSE(libPath.empty()) + << "Set CPPINTEROP_BIN_DIR to the CppInterOp artifacts prefix"; + DlHandle lib(libPath.c_str()); + ASSERT_TRUE(lib) << "Failed to dlopen " << libPath; using GetNamedFn = void* (*)(const char*, void*); using GetEnumConstantsFn = Cpp::CppInterOpArray (*)(void*); diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp index 157bb395c1bd1..7c141963afdc3 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DispatchTest.cpp @@ -1,4 +1,5 @@ #include "CppInterOp/Dispatch.h" +#include "TestPaths.h" #include "gtest/gtest.h" @@ -39,18 +40,22 @@ TEST(DispatchTest, DlGetProcAddress_Unimplemented) { } TEST(DispatchTest, LoadUnloadCycle) { + std::string libPath = TestUtils::GetCppInterOpLibPath(); + ASSERT_FALSE(libPath.empty()) + << "Set CPPINTEROP_BIN_DIR to the CppInterOp artifacts prefix"; + Cpp::UnloadDispatchAPI(); // make sure we're not loaded already... EXPECT_FALSE(Cpp::LoadDispatchAPI("some/random/invalid/directory.so")); Cpp::UnloadDispatchAPI(); // should reset for next load to be successful - EXPECT_TRUE(Cpp::LoadDispatchAPI(CPPINTEROP_LIB_PATH)); + EXPECT_TRUE(Cpp::LoadDispatchAPI(libPath.c_str())); Cpp::UnloadDispatchAPI(); // should reset for next load to fail EXPECT_FALSE(Cpp::LoadDispatchAPI("some/other/random/invalid/directory.so")); // NOTE: minimize side-effects: reload assuming the static set is still // and other test may depend on this being loaded - EXPECT_TRUE(Cpp::LoadDispatchAPI(CPPINTEROP_LIB_PATH)); + EXPECT_TRUE(Cpp::LoadDispatchAPI(libPath.c_str())); } // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) diff --git a/interpreter/CppInterOp/unittests/CppInterOp/DynamicLibraryManagerTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/DynamicLibraryManagerTest.cpp index 136d5acc60828..51b55c3860f4b 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/DynamicLibraryManagerTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/DynamicLibraryManagerTest.cpp @@ -1,13 +1,37 @@ #include "Utils.h" #include "CppInterOp/CppInterOp.h" +#include "../../lib/CppInterOp/Compatibility.h" + #include "clang/Basic/Version.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/Constants.h" +#include "llvm/IR/DerivedTypes.h" +#include "llvm/IR/GlobalValue.h" +#include "llvm/IR/GlobalVariable.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Type.h" +#include "llvm/IR/Verifier.h" +#include "llvm/Support/Casting.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/TargetParser/Host.h" +#include "llvm/TargetParser/Triple.h" #include "gtest/gtest.h" +#include + +#ifndef _WIN32 +#include +#endif + // This function isn't referenced outside its translation unit, but it // can't use the "static" keyword because its address is used for // GetMainExecutable (since some platforms don't support taking the @@ -20,6 +44,24 @@ std::string GetExecutablePath(const char* Argv0) { return llvm::sys::fs::getMainExecutable(Argv0, MainAddr); } +// Resolve TestSharedLib by path the way DynamicLibraryManager_SharedWeakGlobals +// does: SearchLibrariesForSymbol may not run twice in one process, so probe +// beside the test binary (cmake) then the working directory (bazel). Empty if +// not found. +static std::string ResolveTestSharedLibPath() { + std::string BinaryPath = GetExecutablePath(/*Argv0=*/nullptr); + llvm::StringRef Dir = llvm::sys::path::parent_path(BinaryPath); + for (const char* Candidate : {"libTestSharedLib.so", "TestSharedLib.so"}) { + llvm::SmallString<256> P(Dir); + llvm::sys::path::append(P, Candidate); + if (llvm::sys::fs::exists(P)) + return P.str().str(); + if (llvm::sys::fs::exists(Candidate)) + return Candidate; + } + return {}; +} + TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_Sanity) { #ifdef EMSCRIPTEN GTEST_SKIP() << "Test fails for Emscipten builds"; @@ -69,6 +111,63 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_Sanity) { // EXPECT_FALSE(Cpp::GetFunctionAddress("ret_zero")); } +// A failed dlopen must report the dlerror() text on stderr. +TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_LoadFailureReason) { +#if defined(EMSCRIPTEN) || defined(_WIN32) + GTEST_SKIP() << "Test checks dlerror-style messages"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + EXPECT_TRUE(TestFixture::CreateInterpreter()); + + // A file that exists but is not a shared library makes dlopen fail. + int FD = -1; + llvm::SmallString<256> BadLib; + ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("bad-lib", "so", FD, BadLib)); + { + llvm::raw_fd_ostream OS(FD, /*shouldClose=*/true); + OS << "not a shared library"; + } + + testing::internal::CaptureStderr(); + EXPECT_FALSE(Cpp::LoadLibrary(BadLib.c_str(), /*lookup=*/false)); + std::string Err = testing::internal::GetCapturedStderr(); + // dlerror() names the failing file. + EXPECT_NE(Err.find(llvm::sys::path::filename(BadLib).str()), + std::string::npos) + << "stderr was: '" << Err << "'"; + + // With an out-parameter the reason goes there and not to stderr. + std::string Reason = "stale"; + testing::internal::CaptureStderr(); + EXPECT_FALSE(Cpp::LoadLibrary(BadLib.c_str(), /*lookup=*/false, &Reason)); +#ifdef CPPINTEROP_USE_CLING + testing::internal::GetCapturedStderr(); // cling still prints its own text +#else + EXPECT_EQ(testing::internal::GetCapturedStderr(), ""); +#endif + EXPECT_NE(Reason.find(llvm::sys::path::filename(BadLib).str()), + std::string::npos) + << "reason was: '" << Reason << "'"; + + // A failed lookup reports that too. + EXPECT_FALSE( + Cpp::LoadLibrary("no-such-cppinterop-lib", /*lookup=*/true, &Reason)); + EXPECT_NE(Reason.find("no-such-cppinterop-lib"), std::string::npos) + << "reason was: '" << Reason << "'"; + + // Success clears a stale reason. + std::string BinaryPath = GetExecutablePath(/*Argv0=*/nullptr); + Cpp::AddSearchPath(llvm::sys::path::parent_path(BinaryPath).str().c_str()); + Reason = "stale"; + EXPECT_TRUE(Cpp::LoadLibrary("TestSharedLib", /*lookup=*/true, &Reason)) + << "reason was: '" << Reason << "'"; + EXPECT_EQ(Reason, ""); + + EXPECT_FALSE(llvm::sys::fs::remove(BadLib)); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_BasicSymbolLookup) { #ifndef EMSCRIPTEN GTEST_SKIP() << "This test is only intended for Emscripten builds."; @@ -92,3 +191,438 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_BasicSymbolLookup) { auto Fn = reinterpret_cast(Addr); EXPECT_EQ(Fn(), 0); } + +// Weak globals a loaded library already defines -- header-defined singletons: +// a function-local static in an inline function and a C++17 inline static +// data member -- must be bound by jitted code rather than materialized as +// duplicates (compat::bindProcessWeakGlobals). Otherwise the library and the +// JIT each hold their own instance of what the program means to be one +// singleton, and teardown destroys it twice. +TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_SharedWeakGlobals) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test not intended for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "bindProcessWeakGlobals is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the dlsym-based weak-global binding is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + + // Resolve the library by path (SearchLibrariesForSymbol cannot run twice in + // one process, and DynamicLibraryManager_Sanity already used it): beside + // the test binary (cmake), or in the working directory (bazel). + std::string BinaryPath = GetExecutablePath(/*Argv0=*/nullptr); + llvm::StringRef Dir = llvm::sys::path::parent_path(BinaryPath); + std::string PathToTestSharedLib; + for (const char* Candidate : {"libTestSharedLib.so", "TestSharedLib.so"}) { + llvm::SmallString<256> P(Dir); + llvm::sys::path::append(P, Candidate); + if (llvm::sys::fs::exists(P)) { + PathToTestSharedLib = P.str().str(); + break; + } + if (llvm::sys::fs::exists(Candidate)) { + PathToTestSharedLib = Candidate; + break; + } + } + ASSERT_STRNE("", PathToTestSharedLib.c_str()); + ASSERT_TRUE(Cpp::LoadLibrary(PathToTestSharedLib.c_str())); + Cpp::Process(""); // force the execution engine into existence + + // Repeat TestSharedLib.h's singleton definitions in the JIT (the mangled + // names must match the library's) and take each side's addresses. + ASSERT_FALSE(Cpp::Process(R"( + struct SingletonFixture { + static SingletonFixture& get() { + static SingletonFixture instance; + return instance; + } + static inline int s_inline_member = 0; + int value = 0; + }; + extern "C" void* singleton_probe_jit_meyers_addr() { + return &SingletonFixture::get(); + } + extern "C" void* singleton_probe_jit_member_addr() { + return &SingletonFixture::s_inline_member; + } + extern "C" void singleton_probe_jit_set(int v) { + SingletonFixture::get().value = v; + SingletonFixture::s_inline_member = v + 1; + } + )")); + + auto GetFn = [](const char* Name) { + void* A = Cpp::GetFunctionAddress(Name); + EXPECT_NE(A, nullptr) << Name; + return A; + }; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* JitMeyersAddr = + reinterpret_cast(GetFn("singleton_probe_jit_meyers_addr")); + auto* JitMemberAddr = + reinterpret_cast(GetFn("singleton_probe_jit_member_addr")); + auto* JitSet = + reinterpret_cast(GetFn("singleton_probe_jit_set")); + auto* AotMeyersAddr = + reinterpret_cast(GetFn("singleton_fixture_meyers_addr")); + auto* AotMeyersValue = + reinterpret_cast(GetFn("singleton_fixture_meyers_value")); + auto* AotMemberAddr = + reinterpret_cast(GetFn("singleton_fixture_member_addr")); + auto* AotMemberValue = + reinterpret_cast(GetFn("singleton_fixture_member_value")); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + + // One instance, not two: both flavors' addresses agree across the boundary. + EXPECT_EQ(JitMeyersAddr(), AotMeyersAddr()); + EXPECT_EQ(JitMemberAddr(), AotMemberAddr()); + + // And mutations through the jitted side are visible to the library. + JitSet(41); + EXPECT_EQ(AotMeyersValue(), 41); + EXPECT_EQ(AotMemberValue(), 42); +} + +// A dynamically initialized header singleton: its function-local static carries +// a _ZGV guard beside the data. This pins bindProcessWeakGlobals's +// guard-pairing branch -- the guard is demoted together with the data (and only +// because the process exports both), so the jitted copy shares the library's +// storage AND its already-run initialization instead of re-initializing a +// private duplicate. +TYPED_TEST(CPPINTEROP_TEST_MODE, + DynamicLibraryManager_SharedDynamicInitSingleton) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test not intended for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "bindProcessWeakGlobals is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the dlsym-based weak-global binding is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + std::string PathToTestSharedLib = ResolveTestSharedLibPath(); + ASSERT_STRNE("", PathToTestSharedLib.c_str()); + ASSERT_TRUE(Cpp::LoadLibrary(PathToTestSharedLib.c_str())); + Cpp::Process(""); // force the execution engine into existence + + auto GetFn = [](const char* Name) { + void* A = Cpp::GetFunctionAddress(Name); + EXPECT_NE(A, nullptr) << Name; + return A; + }; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* AotAddr = + reinterpret_cast(GetFn("dynamic_singleton_addr")); + auto* AotValue = + reinterpret_cast(GetFn("dynamic_singleton_value")); + + // Initialize the library's copy first; its guard variable is now set. + EXPECT_EQ(AotValue(), 7); + + // Repeat the dynamically initialized singleton in the JIT (matching mangled + // names). The out-of-line constructor makes clang emit a guarded init. + ASSERT_FALSE(Cpp::Process(R"( + struct DynamicInitSingleton { + int value; + DynamicInitSingleton(); + static DynamicInitSingleton& get() { + static DynamicInitSingleton instance; + return instance; + } + }; + extern "C" void* dyn_jit_addr() { return &DynamicInitSingleton::get(); } + extern "C" int dyn_jit_value() { return DynamicInitSingleton::get().value; } + extern "C" void dyn_jit_set(int v) { DynamicInitSingleton::get().value = v; } + )")); + + auto* JitAddr = reinterpret_cast(GetFn("dyn_jit_addr")); + auto* JitValue = reinterpret_cast(GetFn("dyn_jit_value")); + auto* JitSet = reinterpret_cast(GetFn("dyn_jit_set")); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + + // One instance: the addresses agree across the boundary. + EXPECT_EQ(JitAddr(), AotAddr()); + // Shared guard: the jitted side observes the library's initialization rather + // than re-running the constructor on a private copy. + EXPECT_EQ(JitValue(), 7); + // Shared data: a jitted mutation is visible through the library accessor. + JitSet(31); + EXPECT_EQ(AotValue(), 31); +} + +// A weak global the jitted module marks __attribute__((used)) lands in +// @llvm.compiler.used. Demoting it forces bindProcessWeakGlobals to rewrite +// that list (a declaration there fails the IR verifier); a second, non-demoted +// used global keeps the list non-empty, so it is rebuilt rather than erased. A +// clean Execute proves the rewrite ran; the shared address proves the demotion. +TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_SharedUsedWeakGlobal) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test not intended for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "bindProcessWeakGlobals is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the dlsym-based weak-global binding is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + std::string PathToTestSharedLib = ResolveTestSharedLibPath(); + ASSERT_STRNE("", PathToTestSharedLib.c_str()); + ASSERT_TRUE(Cpp::LoadLibrary(PathToTestSharedLib.c_str())); + Cpp::Process(""); // force the execution engine into existence + + // g_used_singleton is mutable and process-defined (demoted); g_used_kept is + // const (excluded) and survives, forcing the used-list to be rebuilt. + ASSERT_FALSE(Cpp::Process(R"( + inline int g_used_singleton __attribute__((used)) = 0; + inline const int g_used_kept __attribute__((used)) = 5; + extern "C" void* used_jit_addr() { return &g_used_singleton; } + extern "C" void used_jit_set(int v) { g_used_singleton = v; } + extern "C" int used_kept_value() { return g_used_kept; } + )")); + + auto GetFn = [](const char* Name) { + void* A = Cpp::GetFunctionAddress(Name); + EXPECT_NE(A, nullptr) << Name; + return A; + }; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* JitAddr = reinterpret_cast(GetFn("used_jit_addr")); + auto* JitSet = reinterpret_cast(GetFn("used_jit_set")); + auto* KeptValue = reinterpret_cast(GetFn("used_kept_value")); + auto* AotAddr = reinterpret_cast(GetFn("g_used_singleton_addr")); + auto* AotValue = reinterpret_cast(GetFn("g_used_singleton_value")); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + + // The demoted used global binds the library's copy. + EXPECT_EQ(JitAddr(), AotAddr()); + JitSet(17); + EXPECT_EQ(AotValue(), 17); + // The non-demoted (const) used global was kept and still reads correctly. + EXPECT_EQ(KeptValue(), 5); +} + +// A header singleton the process does NOT define: dlsym misses, so the pass +// leaves the definition alone and the JIT keeps its own working private copy. +// Pins the dlsym-miss branch (candidate left untouched). +TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_JitOnlyWeakGlobalKept) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test not intended for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "bindProcessWeakGlobals is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the dlsym-based weak-global binding is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + Cpp::Process(""); // force the execution engine into existence + + ASSERT_FALSE(Cpp::Process(R"( + struct JitOnlySingleton { + static JitOnlySingleton& get() { static JitOnlySingleton i; return i; } + int value = 0; + }; + extern "C" void* jitonly_addr() { return &JitOnlySingleton::get(); } + extern "C" void jitonly_set(int v) { JitOnlySingleton::get().value = v; } + extern "C" int jitonly_get() { return JitOnlySingleton::get().value; } + )")); + + auto GetFn = [](const char* Name) { + void* A = Cpp::GetFunctionAddress(Name); + EXPECT_NE(A, nullptr) << Name; + return A; + }; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* Addr = reinterpret_cast(GetFn("jitonly_addr")); + auto* Set = reinterpret_cast(GetFn("jitonly_set")); + auto* Get = reinterpret_cast(GetFn("jitonly_get")); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + + // The kept copy is real, mutable storage. + EXPECT_NE(Addr(), nullptr); + Set(23); + EXPECT_EQ(Get(), 23); +} + +// A weak *constant* is excluded from demotion (duplicated constants are +// ODR-harmless). Pins the isConstant() guard: nothing breaks and the jitted +// copy reads the right value even though the process may hold its own. +TYPED_TEST(CPPINTEROP_TEST_MODE, DynamicLibraryManager_WeakConstantNotDemoted) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test not intended for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "bindProcessWeakGlobals is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the dlsym-based weak-global binding is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + Cpp::Process(""); // force the execution engine into existence + + // Address-taken so clang emits the constant global (rather than folding it). + ASSERT_FALSE(Cpp::Process(R"( + inline constexpr int k_shared_const = 123; + extern "C" int const_jit_value() { return k_shared_const; } + extern "C" const void* const_jit_addr() { return &k_shared_const; } + )")); + + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto* Value = + reinterpret_cast(Cpp::GetFunctionAddress("const_jit_value")); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + ASSERT_NE(Value, nullptr); + EXPECT_EQ(Value(), 123); +} + +#if !defined(_WIN32) && !defined(CPPINTEROP_USE_CLING) && \ + CPPINTEROP_WORKAROUND_BIND_PROCESS_WEAK_GLOBALS + +// Three of bindProcessWeakGlobals's decisions about a module are invisible to +// the interpreter tests above, which only see whether a singleton was shared. +// They are driven here on a hand-built module, where the resulting IR shows +// which decision was taken. +// TestSharedLib exports this inline static data member. It is constant +// initialized, so the process has no guard symbol for it. +constexpr const char* kProcessData = "_ZN16SingletonFixture15s_inline_memberE"; +constexpr const char* kProcessGuard = + "_ZGVN16SingletonFixture15s_inline_memberE"; + +static std::unique_ptr MakeElfModule(llvm::LLVMContext& Ctx) { + auto M = std::make_unique("bindProcessWeakGlobals", Ctx); + llvm::Triple HostTriple(llvm::sys::getProcessTriple()); +#if CLANG_VERSION_MAJOR < 21 + M->setTargetTriple(HostTriple.str()); +#else + M->setTargetTriple(HostTriple); +#endif + return M; +} + +// The shape clang gives an inline variable: a mutable weak definition. The +// module takes ownership of every global built here. +static llvm::GlobalVariable* AddWeakGlobal(llvm::Module& M, const char* Name) { + llvm::Type* Ty = llvm::Type::getInt32Ty(M.getContext()); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + return new llvm::GlobalVariable(M, Ty, /*isConstant=*/false, + llvm::GlobalValue::LinkOnceODRLinkage, + llvm::ConstantInt::get(Ty, 0), Name); +} + +static llvm::GlobalVariable* AddStrongGlobal(llvm::Module& M, + const char* Name) { + llvm::Type* Ty = llvm::Type::getInt32Ty(M.getContext()); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + return new llvm::GlobalVariable(M, Ty, /*isConstant=*/false, + llvm::GlobalValue::ExternalLinkage, + llvm::ConstantInt::get(Ty, 0), Name); +} + +static llvm::GlobalVariable* +AddUsedList(llvm::Module& M, llvm::ArrayRef Els) { + llvm::SmallVector Ops(Els.begin(), Els.end()); + auto* ATy = llvm::ArrayType::get(llvm::PointerType::getUnqual(M.getContext()), + Ops.size()); + auto* Used = new llvm::GlobalVariable( + M, ATy, /*isConstant=*/false, llvm::GlobalValue::AppendingLinkage, + llvm::ConstantArray::get(ATy, Ops), "llvm.used"); + Used->setSection("llvm.metadata"); + return Used; +} + +class BindProcessWeakGlobals : public ::testing::Test { +protected: + void SetUp() override { + if (!llvm::Triple(llvm::sys::getProcessTriple()).isOSBinFormatELF()) + GTEST_SKIP() << "the dlsym-based weak-global binding targets ELF"; + + std::string Path = ResolveTestSharedLibPath(); + ASSERT_STRNE("", Path.c_str()); + ASSERT_NE(::dlopen(Path.c_str(), RTLD_NOW | RTLD_GLOBAL), nullptr); + + // The premise of every case below. + ASSERT_NE(::dlsym(RTLD_DEFAULT, kProcessData), nullptr); + ASSERT_EQ(::dlsym(RTLD_DEFAULT, kProcessGuard), nullptr); + } +}; + +// Sharing the data but not the guard would re-run initialization on the +// process copy, so a module-local guard the process lacks blocks the demotion. +TEST_F(BindProcessWeakGlobals, GuardMissingFromProcessKeepsPair) { + llvm::LLVMContext Ctx; + auto M = MakeElfModule(Ctx); + llvm::GlobalVariable* Data = AddWeakGlobal(*M, kProcessData); + llvm::GlobalVariable* Guard = AddWeakGlobal(*M, kProcessGuard); + + EXPECT_FALSE(compat::bindProcessWeakGlobals(*M)); + + EXPECT_TRUE(Data->hasInitializer()); + EXPECT_TRUE(Guard->hasInitializer()); +} + +// A used list that names nothing demoted stays in place. +TEST_F(BindProcessWeakGlobals, UsedListWithoutDemotedEntriesIsKept) { + llvm::LLVMContext Ctx; + auto M = MakeElfModule(Ctx); + llvm::GlobalVariable* Data = AddWeakGlobal(*M, kProcessData); + llvm::GlobalVariable* Keep = AddStrongGlobal(*M, "keep_me"); + llvm::GlobalVariable* Used = AddUsedList(*M, {Keep}); + + EXPECT_TRUE(compat::bindProcessWeakGlobals(*M)); + + EXPECT_TRUE(Data->isDeclaration()); + // The same object, so the list was never erased and rebuilt. + EXPECT_EQ(M->getNamedGlobal("llvm.used"), Used); +} + +// A used list that names a demoted global is rebuilt around the survivors; a +// declaration left in it fails the IR verifier. +TEST_F(BindProcessWeakGlobals, UsedListIsRebuiltAroundSurvivors) { + llvm::LLVMContext Ctx; + auto M = MakeElfModule(Ctx); + llvm::GlobalVariable* Data = AddWeakGlobal(*M, kProcessData); + llvm::GlobalVariable* Keep = AddStrongGlobal(*M, "keep_me"); + AddUsedList(*M, {Data, Keep}); + + EXPECT_TRUE(compat::bindProcessWeakGlobals(*M)); + + EXPECT_TRUE(Data->isDeclaration()); + llvm::GlobalVariable* Rebuilt = M->getNamedGlobal("llvm.used"); + ASSERT_NE(Rebuilt, nullptr); + EXPECT_EQ(Rebuilt->getSection(), "llvm.metadata"); + auto* Init = llvm::cast(Rebuilt->getInitializer()); + llvm::SmallVector Survivors; + for (llvm::Value* Op : Init->operand_values()) + Survivors.push_back(Op->getName()); + ASSERT_EQ(Survivors.size(), 1U); + EXPECT_EQ(Survivors.front(), "keep_me"); + EXPECT_FALSE(llvm::verifyModule(*M, &llvm::errs())); +} + +#endif // bindProcessWeakGlobals is declared diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 2de358977073f..5d41fc3cfae38 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -3,6 +3,7 @@ #include "CppInterOp/CppInterOp.h" #include "clang/AST/ASTContext.h" +#include "clang/AST/PrettyPrinter.h" #include "clang/Basic/Version.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Sema/Sema.h" @@ -10,6 +11,7 @@ #include #include #include +#include #include "gtest/gtest.h" @@ -844,7 +846,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsAllocator) { std::vector Decls; std::string code = R"( class Klass{ - int val; + int val = 0; + int* __attribute__((annotate("cppAllocNone"))) getValAdress(){ + return &val; + } }; __attribute__((ownership_returns(malloc))) Klass* Allocator(){ @@ -856,25 +861,75 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsAllocator) { return obj; } void foo(); - )"; - GetAllTopLevelDecls(code, Decls, true); - EXPECT_TRUE(Cpp::IsAllocator(Decls[1])); - EXPECT_TRUE(Cpp::IsAllocator(Decls[2])); - EXPECT_FALSE(Cpp::IsAllocator(Decls[3])); - // Builtin check - code = R"( - //There is nothing lstdlib.h is included at args + + void __attribute__((ownership_takes(malloc, 1))) Deallocator(void* p); + void* __attribute__((cf_returns_retained)) CFAllocFunc(); + + int* __attribute__((annotate("cppAllocNone"))) NoneFunc(); + int* __attribute__((annotate("cppAllocNew"))) NewFunc(); + int* __attribute__((annotate("cppAllocNewArr"))) NewArrFunc(); + int* __attribute__((annotate("cppAllocMalloc"))) MallocFunc(); + int* __attribute__((annotate("cppAllocOperatorNew"))) OpNewFunc(); + int* __attribute__((annotate("cppAllocOperatorNewArr"))) OpNewArrFunc(); + int* __attribute__((annotate("unrelatedAttr"))) UnrelatedFunc(); + __declspec(restrict) int* DeclspecRestrictFunc(); + template + __attribute__((annotate("cppAllocNew"))) T* TemplatedFunc(){ + return new T; + } + template <> int* TemplatedFunc(); + template char* TemplatedFunc(); )"; - TestFixture::CreateInterpreter({"-include", "stdlib.h"}); - Interp->process(code); - auto mallocDecl = Cpp::GetNamed("malloc"); - EXPECT_TRUE(Cpp::IsAllocator(Cpp::ConstFuncRef{mallocDecl.data})); - Cpp::DeleteInterpreter(); + GetAllTopLevelDecls(code, Decls, true, + {"-std=c++17", "-include", "stdlib.h", "-fdeclspec"}); +#define TESTIA(N, EXP) \ + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef { Cpp::GetNamed(#N).data }), EXP) + + TESTIA(malloc, Cpp::AllocType::Malloc); + TESTIA(Allocator, Cpp::AllocType::Malloc); + TESTIA(Allocator2, Cpp::AllocType::Malloc); + TESTIA(foo, Cpp::AllocType::Unknown); + TESTIA(Deallocator, Cpp::AllocType::Unknown); + TESTIA(CFAllocFunc, Cpp::AllocType::Malloc); + TESTIA(NoneFunc, Cpp::AllocType::None); + TESTIA(NewFunc, Cpp::AllocType::New); + TESTIA(NewArrFunc, Cpp::AllocType::NewArr); + TESTIA(MallocFunc, Cpp::AllocType::Malloc); + TESTIA(OpNewFunc, Cpp::AllocType::OperatorNew); + TESTIA(OpNewArrFunc, Cpp::AllocType::OperatorNewArr); + TESTIA(UnrelatedFunc, Cpp::AllocType::Unknown); + TESTIA(DeclspecRestrictFunc, Cpp::AllocType::Unknown); + + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{Decls[14]}), + Cpp::AllocType::New); + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{Decls[15]}), + Cpp::AllocType::New); + ASTContext& C = Interp->getCI()->getASTContext(); + std::vector charArg = {C.CharTy.getAsOpaquePtr()}; + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{ + Cpp::InstantiateTemplate(Decls[14], charArg).data}), + Cpp::AllocType::New); + + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{ + Cpp::GetNamed("getValAdress", Cpp::GetNamed("Klass")).data}), + Cpp::AllocType::None); + + //! Fn coverage + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{nullptr}), + Cpp::AllocType::Unknown); + // casting coverage + EXPECT_EQ(Cpp::IsAllocator(Cpp::ConstFuncRef{Cpp::GetNamed("Klass").data}), + Cpp::AllocType::Unknown); + Cpp::DeleteInterpreter(); +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscipten builds"; +#endif + std::string include_flag; // APINotes check -#if !defined(CPPINTEROP_USE_CLING) && !defined(__EMSCRIPTEN__) - std::string include_flag = - "-I" + std::string(CPPINTEROP_DIR) + "unittests/CppInterOp/APINotes"; +#ifndef CPPINTEROP_USE_CLING + include_flag = + "-I" + std::string(CPPINTEROP_SRC_DIR) + "/unittests/CppInterOp/APINotes"; std::vector interpreter_args = { "-fmodules", "-fimplicit-module-maps", "-fapinotes-modules", include_flag.c_str()}; @@ -883,13 +938,48 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsAllocator) { #include "TestHeader.h" )"; Interp->process(code); - auto testAllocDecl = Cpp::GetNamed("testAlloc"); - EXPECT_TRUE(Cpp::IsAllocator(Cpp::ConstFuncRef{testAllocDecl.data})); - auto testNotAllocDecl = Cpp::GetNamed("testNotAlloc"); - EXPECT_FALSE(Cpp::IsAllocator(Cpp::ConstFuncRef{testNotAllocDecl.data})); + TESTIA(testAlloc, Cpp::AllocType::Malloc); + TESTIA(testNotAlloc, Cpp::AllocType::Unknown); + TESTIA(testMalloc, Cpp::AllocType::Malloc); + TESTIA(testNew, Cpp::AllocType::New); + TESTIA(testNewArr, Cpp::AllocType::NewArr); + TESTIA(testOperatorNew, Cpp::AllocType::OperatorNew); + TESTIA(testOperatorNewArr, Cpp::AllocType::OperatorNewArr); + TESTIA(testNone, Cpp::AllocType::None); + TESTIA(testWeirdAttr, Cpp::AllocType::Unknown); + Cpp::DeleteInterpreter(); #endif +#undef TESTIA + include_flag = + "-I" + std::string(CPPINTEROP_SRC_DIR) + "/unittests/CppInterOp/APINotes"; + Decls.clear(); + code = R"( + void* mergeFunc() { + return malloc(sizeof(int)); + } + )"; + GetAllTopLevelDecls(code, Decls, true, + {"-std=c++17", include_flag.c_str(), "-include", + "stdlib.h", "-include", "TestAttributeMerge.h"}); + EXPECT_EQ(Cpp::IsAllocator(Decls[0]), Cpp::AllocType::Malloc); + + Decls.clear(); + code = R"( + int* overloadFunc(){ + return new int; + } + + int* overloadFunc(int n){ + return new int(n); + } + )"; + GetAllTopLevelDecls( + code, Decls, true, + {"-std=c++17", include_flag.c_str(), "-include", "TestAttributeMerge.h"}); + EXPECT_EQ(Cpp::IsAllocator(Decls[0]), Cpp::AllocType::New); + EXPECT_EQ(Cpp::IsAllocator(Decls[1]), Cpp::AllocType::New); } TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_IsDeallocator) { @@ -1093,6 +1183,82 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FunctionTypes) { EXPECT_TRUE(Cpp::IsSameType(typ1, typ2)); } +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_OwnershipAttributes) { + std::vector Decls; + std::string code = R"( + class Klass { + int val; + }; + + __attribute__((ownership_returns(malloc))) + void* alloc_no_size(unsigned long sz); + + __attribute__((ownership_returns(malloc, 1))) + void* alloc_sized(unsigned long sz); + + __attribute__((ownership_returns(malloc, 2))) + void* alloc_sized2(void* hint, unsigned long sz); + + __attribute__((ownership_takes(malloc, 1))) + void dealloc(void* p); + + __attribute__((ownership_holds(malloc, 1, 2))) + void hold_two(void* p, void* q); + + __attribute__((ownership_returns(malloc))) + __attribute__((ownership_takes(malloc, 1))) + void* realloc_like(void* p, unsigned long sz); + + void plain(void* p); + + template + __attribute__((ownership_returns(malloc, 2))) + __attribute__((ownership_takes(malloc, 1))) + T* templated_alloc(void* p, unsigned long sz); + )"; + GetAllTopLevelDecls(code, Decls, true); + + Cpp::ConstFuncRef Klass{Decls[0]}; + Cpp::ConstFuncRef AllocNoSize{Decls[1]}; + Cpp::ConstFuncRef AllocSized{Decls[2]}; + Cpp::ConstFuncRef AllocSized2{Decls[3]}; + Cpp::ConstFuncRef Dealloc{Decls[4]}; + Cpp::ConstFuncRef HoldTwo{Decls[5]}; + Cpp::ConstFuncRef ReallocLike{Decls[6]}; + Cpp::ConstFuncRef Plain{Decls[7]}; + Cpp::ConstFuncRef TemplatedAlloc{Decls[8]}; + + using OB = Cpp::OwnershipBehaviour; + EXPECT_EQ(Cpp::GetOwnershipBehaviour(AllocNoSize), OB::OwnershipReturns); + EXPECT_EQ(Cpp::GetOwnershipBehaviour(Dealloc), OB::OwnershipTakes); + EXPECT_EQ(Cpp::GetOwnershipBehaviour(HoldTwo), OB::OwnershipHolds); + EXPECT_EQ(Cpp::GetOwnershipBehaviour(ReallocLike), + OB::OwnershipReturns | OB::OwnershipTakes); + EXPECT_EQ(Cpp::GetOwnershipBehaviour(Plain), OB::Unknown); + EXPECT_EQ(Cpp::GetOwnershipBehaviour(Klass), OB::Unknown); + EXPECT_EQ(Cpp::GetOwnershipBehaviour({nullptr}), OB::Unknown); + + EXPECT_EQ(Cpp::GetOwnershipBehaviour(TemplatedAlloc), + OB::OwnershipReturns | OB::OwnershipTakes); + EXPECT_EQ(Cpp::GetDeallocationIndexes(TemplatedAlloc), uint64_t{0b1}); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(TemplatedAlloc), 1); + + EXPECT_EQ(Cpp::GetDeallocationIndexes(Dealloc), uint64_t{0b1}); + EXPECT_EQ(Cpp::GetDeallocationIndexes(HoldTwo), uint64_t{0b11}); + EXPECT_EQ(Cpp::GetDeallocationIndexes(ReallocLike), uint64_t{0b1}); + EXPECT_EQ(Cpp::GetDeallocationIndexes(AllocNoSize), uint64_t{0}); + EXPECT_EQ(Cpp::GetDeallocationIndexes(Plain), uint64_t{0}); + EXPECT_EQ(Cpp::GetDeallocationIndexes(Klass), uint64_t{0}); + EXPECT_EQ(Cpp::GetDeallocationIndexes({nullptr}), uint64_t{0}); + + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(AllocSized), 0); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(AllocSized2), 1); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(AllocNoSize), -1); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(Dealloc), -1); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(Plain), -1); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex(Klass), -1); + EXPECT_EQ(Cpp::GetAllocationSizeParamIndex({nullptr}), -1); +} TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetDeallocType) { std::string code = R"( @@ -1749,10 +1915,24 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { a = new int; return a; } + + //Not analyzed, attribute is merged in header + void* func71_helper(); + + void* func71(){ + return func71_helper(); + } )"; + std::string include_flag = + "-I" + std::string(CPPINTEROP_SRC_DIR) + "/unittests/CppInterOp/APINotes"; +#ifndef EMSCRIPTEN + TestFixture::CreateInterpreter( + {"-std=c++17", include_flag.c_str(), "-include", "TestAttributeMerge.h"}); +#else TestFixture::CreateInterpreter({"-std=c++17"}); - Interp->declare(code); +#endif + Interp->declare(code); #define TESTAC(N, EXP) \ EXPECT_EQ( \ Cpp::GetAllocType(Cpp::ConstFuncRef { Cpp::GetNamed("func" #N).data }), \ @@ -1819,10 +1999,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(58, OperatorNew); TESTAC(59, Malloc); TESTAC(70, Unknown); +#ifndef EMSCRIPTEN + TESTAC(71, Malloc); +#endif #undef TESTAC Cpp::DeleteInterpreter(); } + TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionSignature) { std::vector Decls; std::string code = R"( @@ -3588,10 +3772,11 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_WrapAliasTemplateReturnType) { // Regression test for cppyy issue // https://github.com/compiler-research/cppyy/issues/218 (original reproducer: - // `std::make_any<...>`, return type `std::enable_if_t, - // std::any>`). Building a wrapper for a function template whose return type is - // a type-alias-template specialisation used to fail to compile; the snippet - // below is a stdlib-free distillation. It needs three ingredients: an alias + // `std::make_any<...>`, return type + // `std::enable_if_t, std::any>`). Building a wrapper + // for a function template whose return type is a type-alias-template + // specialisation used to fail to compile; the snippet below is a stdlib-free + // distillation. It needs three ingredients: an alias // (`enable_if_t`) whose sugar carries a non-type argument that prints as an // *expression* (`trait_v`, which FullyQualifiedName does not qualify); a // predicate in a namespace, so unqualified `trait_v` does not resolve in the @@ -3721,10 +3906,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgDefault) { GetAllTopLevelDecls(code, Decls); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[0], 0), ""); - EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[0], 1), "4."); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[0], 1), "4.0"); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[0], 2), "\"default\""); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[0], 3), "\'c\'"); - EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[1], 0), "0."); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[1], 0), "0.0"); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[1], 1), "3.123"); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[1], 2), "34126"); @@ -3739,7 +3924,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgDefault) { EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 0), ""); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 1), ""); EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 2), "\'a\'"); - EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 3), "0."); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 3), "0.0"); ASTContext& C = Interp->getCI()->getASTContext(); std::vector template_args = {C.IntTy.getAsOpaquePtr()}; @@ -3754,6 +3939,71 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetFunctionArgDefault) { EXPECT_EQ(Cpp::GetFunctionArgDefault(fn, 1), "S()"); } +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_GetFunctionArgDefaultSymbolic) { + std::vector Decls; + std::string code = R"( + constexpr double kDefaultRatio = 0.5; + double default_ratio(); + double scaled(double ratio = kDefaultRatio); + double rescaled(double ratio = default_ratio()); + double inverted(double ratio = 2.0 / kDefaultRatio); + double pi_ish(double p = 3.14); + float take_float(float a = 5.f); + long take_long(long a = -5l); + unsigned long take_ulong(unsigned long a = 5ul); + int take_hex(int a = 0x1f); + )"; + + GetAllTopLevelDecls(code, Decls); + + // A floating-typed default need not be a numeric literal. Formatting a + // symbolic default must not crash (the removed std::stod normalization + // terminated the exception-free build) and must render it as written. + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[2], 0), "kDefaultRatio"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[3], 0), "default_ratio()"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[4], 0), "2.0 / kDefaultRatio"); + // Literals render exactly as written, not at representation precision. + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[5], 0), "3.14"); + // Numeric-literal suffixes render uppercase: cppyy strips only that form + // before it evaluates the default in Python ("5.f" is a Python syntax + // error, "5.F" strips to "5."). Hex digits are not suffixes. + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[6], 0), "5.F"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[7], 0), "-5L"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[8], 0), "5UL"); + EXPECT_EQ(Cpp::GetFunctionArgDefault(Decls[9], 0), "0x1f"); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_FloatingDefaultPrinterCanary) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "The wasm test binary does not link the raw clang printer " + "symbols (printPretty, getDefaultArg) this test needs."; +#else + std::vector Decls; + GetAllTopLevelDecls("void canary(double x = 3.14);", Decls); + + // Before clang 24 the raw printer expands floats to maximum precision; + // UDL and invalid-range defaults hit it. llvm/llvm-project#218471 fixes + // the printer for clang 24. Each branch failing is a signal: see its + // message. + const auto* PD = cast(Decls[0])->getParamDecl(0); + std::string Raw; + llvm::raw_string_ostream OS(Raw); + PD->getDefaultArg()->printPretty(OS, nullptr, PrintingPolicy(LangOptions())); +#if CLANG_VERSION_MAJOR < 24 + EXPECT_EQ(Raw, "3.1400000000000001") + << "clang's pretty-printer round-trips floating literals earlier than " + "expected (llvm/llvm-project#218471 cherry-picked?). Re-check UDL " + "and invalid-range defaults, then move this guard."; +#else + EXPECT_EQ(Raw, "3.14") + << "llvm/llvm-project#218471 did not land in clang 24. Raise the " + "version in this guard."; +#endif +#endif // EMSCRIPTEN +} + TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { #ifdef _WIN32 GTEST_SKIP() << "Disabled on Windows. Needs fixing."; @@ -5053,3 +5303,132 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_MoveOnlyByValueArgs) { Cpp::JitCall::kUnknown); EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); } + +// A weak (linkonce_odr) thread_local with a non-zero initializer, materialized +// in two MaterializationUnits, drives llvm::orc::IRMaterializationUnit::discard +// over the duplicate. On the buggy LLVM path that dereferences end(): the +// emulated-TLS branch of the IRMaterializationUnit constructor registers +// __emutls_t. in SymbolFlags but not SymbolToDefinition, so discarding the +// duplicate crashes (assertion in +Asserts builds, heap corruption otherwise). +// +// Shape: an `inline` worker() odr-uses HeavyThing<1>::tls (a non-zero-init +// thread_local template static). Two functions are process()'d into separate +// TUs/modules and each call worker(), so each module re-emits worker() and the +// tls as linkonce_odr; defining the second module runs discard over the +// duplicate emulated-TLS symbol. +// +// This always passes: on LLVM < 24 the CppInterOp-side workaround +// (compat::dedupeWeakEmulatedTLS) defuses the crash, and on LLVM >= 24 the +// upstream fix (llvm/llvm-project#208413) does. It guards against regressions +// in either. Kept deliberately minimal and heap-free so it is portable and +// clean under ASan/LSan. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DiscardDuplicateWeakEmulatedTLS) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "dedupeWeakEmulatedTLS is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "weak thread_local in JITted code fails to resolve " + "__emutls_get_address on COFF/Mach-O; the discard " + "workaround targets ELF emulated TLS"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test targets the in-process JIT discard path"; + + std::vector Decls; + std::string header = R"( + template struct HeavyThing { static thread_local int tls; }; + template thread_local int HeavyThing::tls = Tag + 1; + inline int worker() { return HeavyThing<1>::tls; } + )"; + // -include new: MakeFunctionCallable's wrapper uses placement new. + GetAllTopLevelDecls(header, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + // Two distinct modules, each odr-using worker() -> each re-emits worker() and + // the non-zero-init thread_local as linkonce_odr. + Interp->process("int callA() { return worker(); }"); + Interp->process("int callB() { return worker(); }"); + + Cpp::DeclRef A = Cpp::GetNamed("callA"); + Cpp::DeclRef B = Cpp::GetNamed("callB"); + ASSERT_TRUE(A); + ASSERT_TRUE(B); + + Cpp::JitCall JA = Cpp::MakeFunctionCallable(Cpp::FuncRef{A.data}); + Cpp::JitCall JB = Cpp::MakeFunctionCallable(Cpp::FuncRef{B.data}); + ASSERT_EQ(JA.getKind(), Cpp::JitCall::kGenericCall); + ASSERT_EQ(JB.getKind(), Cpp::JitCall::kGenericCall); + + int ra = 0; + int rb = 0; + JA.Invoke(&ra, {}); // materialize module A's copy of the weak set + JB.Invoke(&rb, {}); // ... and module B's; discard ran over the duplicate + + EXPECT_EQ(ra, 2); // HeavyThing<1>::tls == Tag + 1 + EXPECT_EQ(rb, 2); +} + +// Companion to the above for a *zero-init* weak thread_local: one whose C++ +// initializer is non-trivial (a ctor that runs in a __tls_init function), so +// its IR initializer is zeroinitializer. The earlier "non-zero initializer +// only" filter skipped these, yet they still emit the emulated-TLS companion +// that trips discard -- so dedupeWeakEmulatedTLS must cover every weak +// thread_local regardless of initializer. Same two-module materialization as +// above. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_DiscardDuplicateWeakEmulatedTLSZeroInit) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscripten builds"; +#endif +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "dedupeWeakEmulatedTLS is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif +#if defined(_WIN32) || defined(__APPLE__) + GTEST_SKIP() << "weak thread_local in JITted code fails to resolve " + "__emutls_get_address on COFF/Mach-O; the discard " + "workaround targets ELF emulated TLS"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test targets the in-process JIT discard path"; + + std::vector Decls; + std::string header = R"( + struct NonTrivial { int id; NonTrivial() : id(7) {} }; + template struct HeavyZero { static thread_local NonTrivial tls; }; + template thread_local NonTrivial HeavyZero::tls{}; + inline int workerZero() { return HeavyZero<1>::tls.id; } + )"; + // -include new: MakeFunctionCallable's wrapper uses placement new. + GetAllTopLevelDecls(header, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-std=c++23", "-include", "new"}); + + // Two distinct modules, each odr-using workerZero() -> each re-emits it and + // the zero-init thread_local as linkonce_odr; defining the second runs + // discard over the duplicate emulated-TLS symbol. + Interp->process("int callZeroA() { return workerZero(); }"); + Interp->process("int callZeroB() { return workerZero(); }"); + + Cpp::DeclRef A = Cpp::GetNamed("callZeroA"); + Cpp::DeclRef B = Cpp::GetNamed("callZeroB"); + ASSERT_TRUE(A); + ASSERT_TRUE(B); + + Cpp::JitCall JA = Cpp::MakeFunctionCallable(Cpp::FuncRef{A.data}); + Cpp::JitCall JB = Cpp::MakeFunctionCallable(Cpp::FuncRef{B.data}); + ASSERT_EQ(JA.getKind(), Cpp::JitCall::kGenericCall); + ASSERT_EQ(JB.getKind(), Cpp::JitCall::kGenericCall); + + int ra = 0; + int rb = 0; + JA.Invoke(&ra, {}); + JB.Invoke(&rb, {}); + + EXPECT_EQ(ra, 7); // HeavyZero<1>::tls.id set by the NonTrivial ctor + EXPECT_EQ(rb, 7); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp index c5b35430696f8..3c0ab07786377 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp @@ -1,6 +1,8 @@ #include "Utils.h" +#include "../../lib/CppInterOp/CompatibilityGLIBC.h" + #include "CppInterOp/CppInterOp.h" #ifdef CPPINTEROP_USE_CLING @@ -26,6 +28,11 @@ #include #include +#ifndef _WIN32 +#include +#include +#endif + using ::testing::StartsWith; TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Version) { @@ -422,6 +429,283 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_Process) { EXPECT_FALSE(Cpp::Process("int f(); int res = f();") == 0); } +// libc_nonshared.a symbols are per-module and invisible to dlsym; jitted +// calls resolve to this library's copy (pure code) or to the JIT-side +// registration shims (at_quick_exit, pthread_atfork). +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_GlibcNonsharedSymbols) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + ASSERT_TRUE(TestFixture::CreateInterpreter()); + + // The registration must execute, not just materialize. The handler fires + // only at quick_exit or this interpreter's teardown. + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + ASSERT_EQ(0, Cpp::Process(R"( + extern "C" int at_quick_exit(void (*)(void)); + extern "C" void __stack_chk_fail_local(); + extern "C" void glibc_nonshared_probe_handler() {} + extern "C" int glibc_nonshared_probe_register() { + return at_quick_exit(&glibc_nonshared_probe_handler); + } + extern "C" void* glibc_nonshared_probe_sibling() { + return reinterpret_cast(&__stack_chk_fail_local); + } + )")); + + auto* Register = reinterpret_cast( + Cpp::GetFunctionAddress("glibc_nonshared_probe_register")); + ASSERT_NE(Register, nullptr); + EXPECT_EQ(Register(), 0); + + auto* Sibling = reinterpret_cast( + Cpp::GetFunctionAddress("glibc_nonshared_probe_sibling")); + ASSERT_NE(Sibling, nullptr); + EXPECT_NE(Sibling(), nullptr); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) +#endif +} + +// Jitted at_quick_exit handlers live in a host-side registry scoped to the +// interpreter; DeleteInterpreter runs them exactly once while the JIT can +// still execute them. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_JitAtQuickExitRunsBeforeTeardown) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + auto I = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I); + + // Host-side counter the JIT writes to; survives the interpreter deletion. + // Reset explicitly for --gtest_repeat. + static int HostCount; + HostCount = 0; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + std::string Inject = R"( + extern "C" void* qe_sink = (void*)@SINK@; + )"; + const std::string SinkTok = "@SINK@"; + Inject.replace(Inject.find(SinkTok), SinkTok.size(), + std::to_string(reinterpret_cast(&HostCount))); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + ASSERT_EQ(0, Cpp::Declare(Inject.c_str())); + ASSERT_EQ(0, Cpp::Process(R"( + extern "C" int at_quick_exit(void (*)(void)); + extern "C" void qe_handler() { ++*static_cast(qe_sink); } + static int qe_reg = at_quick_exit(&qe_handler); + )")); + + EXPECT_EQ(HostCount, 0); + Cpp::DeleteInterpreter(I); + EXPECT_EQ(HostCount, 1) + << "jitted at_quick_exit handler must run exactly once, pre-teardown"; +#endif +} + +// A real quick_exit after DeleteInterpreter runs no jitted handlers: the +// teardown flush removed them from every host-visible list, so the process +// exits cleanly instead of calling freed JIT memory. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_JitAtQuickExitSafeAfterTeardown) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + EXPECT_EXIT( + { + auto I = TestFixture::CreateInterpreter(); + Cpp::Process(R"( + extern "C" int at_quick_exit(void (*)(void)); + extern "C" void qe_noop_handler() {} + static int qe_reg = at_quick_exit(&qe_noop_handler); + )"); + Cpp::DeleteInterpreter(I); + std::quick_exit(0); + }, + ::testing::ExitedWithCode(0), ""); +#endif +} + +// The hook glibc calls on quick_exit drains every live handler exactly +// once; interpreter teardown then has nothing left to run. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_JitAtQuickExitHookDrainsOnce) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +// The body is excluded, not skipped at run time: it names a compat helper +// that exists only on the clang-repl path. +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#else + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + auto I = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I); + + // Reset explicitly for --gtest_repeat. + static int HostCount; + HostCount = 0; + std::string Inject = R"( + extern "C" void* hook_sink = (void*)@SINK@; + )"; + const std::string SinkTok = "@SINK@"; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + Inject.replace(Inject.find(SinkTok), SinkTok.size(), + std::to_string(reinterpret_cast(&HostCount))); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + ASSERT_EQ(0, Cpp::Declare(Inject.c_str())); + ASSERT_EQ(0, Cpp::Process(R"( + extern "C" int at_quick_exit(void (*)(void)); + extern "C" void hook_handler() { ++*static_cast(hook_sink); } + static int hook_reg = at_quick_exit(&hook_handler); + )")); + + EXPECT_EQ(HostCount, 0); + // Drive the registered quick_exit hook directly; a real quick_exit would + // end the test process. + compat::JitGlibcHandlerRegistry::runAllQuickExitHandlers(); + EXPECT_EQ(HostCount, 1); + Cpp::DeleteInterpreter(I); + EXPECT_EQ(HostCount, 1) << "drained handlers must not run again at teardown"; +#endif +#endif +} + +// Jitted pthread_atfork handlers dispatch through host-resident hooks: +// prepare/parent run in this process on fork, child runs in the child, and +// DeleteInterpreter drops the entries. +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_JitPthreadAtforkHandlers) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + auto I = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I); + + // Reset explicitly for --gtest_repeat. + static int PrepareCount; + static int ParentCount; + static int ChildCount; + PrepareCount = ParentCount = ChildCount = 0; + std::string Inject = R"( + extern "C" void* prep_sink = (void*)@PREP@; + extern "C" void* parent_sink = (void*)@PARENT@; + extern "C" void* child_sink = (void*)@CHILD@; + )"; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + auto ReplaceTok = [&Inject](const std::string& Tok, int* Addr) { + Inject.replace(Inject.find(Tok), Tok.size(), + std::to_string(reinterpret_cast(Addr))); + }; + ReplaceTok("@PREP@", &PrepareCount); + ReplaceTok("@PARENT@", &ParentCount); + ReplaceTok("@CHILD@", &ChildCount); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + ASSERT_EQ(0, Cpp::Declare(Inject.c_str())); + ASSERT_EQ(0, Cpp::Process(R"( + extern "C" int pthread_atfork(void (*)(void), void (*)(void), + void (*)(void)); + extern "C" void fork_prep() { ++*static_cast(prep_sink); } + extern "C" void fork_parent() { ++*static_cast(parent_sink); } + extern "C" void fork_child() { ++*static_cast(child_sink); } + static int fork_reg = pthread_atfork(&fork_prep, &fork_parent, + &fork_child); + )")); + + const int Child = fork(); // pid_t is int on every glibc target + ASSERT_GE(Child, 0); + if (Child == 0) { + // The child hook ran here; report through the exit code. exit() (not + // _exit) also flushes coverage counters. + std::exit(ChildCount == 1 ? 0 : 7); + } + int Status = -1; + ASSERT_EQ(waitpid(Child, &Status, 0), Child); + // 0 == clean exit with code 0 == the jitted child hook ran in the child. + EXPECT_EQ(Status, 0); + EXPECT_EQ(PrepareCount, 1); + EXPECT_EQ(ParentCount, 1); + + Cpp::DeleteInterpreter(I); + EXPECT_EQ(PrepareCount, 1); + EXPECT_EQ(ParentCount, 1); +#endif +} + +// A fork after DeleteInterpreter runs no jitted atfork handlers: the entries +// are dropped at teardown, so the fork completes instead of calling freed +// JIT memory. +TYPED_TEST(CPPINTEROP_TEST_MODE, + Interpreter_JitPthreadAtforkSafeAfterTeardown) { +#ifndef __GLIBC__ + GTEST_SKIP() << "the libc_nonshared.a fallback targets glibc"; +#else +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "the fallback generator is wired into the clang-repl " + "CppInternal::Interpreter path, not cling's interpreter"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the libc_nonshared.a fallback is in-process only"; + + static int HostCount; + HostCount = 0; + EXPECT_EXIT( + { + auto I = TestFixture::CreateInterpreter(); + std::string Inject = R"( + extern "C" void* fork_sink = (void*)@SINK@; + )"; + const std::string SinkTok = "@SINK@"; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + Inject.replace(Inject.find(SinkTok), SinkTok.size(), + std::to_string(reinterpret_cast(&HostCount))); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + Cpp::Declare(Inject.c_str()); + Cpp::Process(R"( + extern "C" int pthread_atfork(void (*)(void), void (*)(void), + void (*)(void)); + extern "C" void fork_prep() { ++*static_cast(fork_sink); } + static int fork_reg = pthread_atfork(&fork_prep, nullptr, nullptr); + )"); + + Cpp::DeleteInterpreter(I); + const int Child = fork(); + if (Child == 0) + _exit(0); + waitpid(Child, nullptr, 0); + std::_Exit(HostCount == 0 ? 0 : 5); + }, + ::testing::ExitedWithCode(0), ""); +#endif +} + TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_DeclareSilent) { #if CLANG_VERSION_MAJOR > 21 GTEST_SKIP() << "Test crashes gtest for llvm 22 based build"; diff --git a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp index 9afa1f66a8825..304912b3c50c3 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp @@ -947,6 +947,24 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetParentScope) { EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(en_E)), "N1::N2::C"); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(en_A)), "N1::N2::C::E"); EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(en_B)), "N1::N2::C::E"); + + // A linkage spec is a transparent context, not a scope: the parent must be + // the enclosing scope. This is the shape of namespace std in the MSVC CRT + // headers, whose canonical declaration sits inside `extern "C++" { ... }`. + Interp->declare(R"( + extern "C++" { namespace NLnk { } } + namespace NLnk { class InLnk {}; } + extern "C" { namespace NC { class InC {}; } } + )"); + Cpp::DeclRef ns_NLnk = Cpp::GetNamed("NLnk"); + Cpp::DeclRef cl_InLnk = Cpp::GetNamed("InLnk", ns_NLnk); + Cpp::DeclRef ns_NC = Cpp::GetNamed("NC"); + EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetParentScope(cl_InLnk)), "NLnk"); + EXPECT_EQ(Cpp::GetParentScope(ns_NLnk).data, + Cpp::GetParentScope(ns_N1).data) + << "parent of a namespace declared in a linkage spec should be the TU"; + EXPECT_EQ(Cpp::GetParentScope(ns_NC).data, Cpp::GetParentScope(ns_N1).data) + << "parent of a namespace declared in extern \"C\" should be the TU"; } TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetScopeFromType) { diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestPaths.h b/interpreter/CppInterOp/unittests/CppInterOp/TestPaths.h new file mode 100644 index 0000000000000..dd527819e70f6 --- /dev/null +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestPaths.h @@ -0,0 +1,40 @@ +#ifndef CPPINTEROP_UNITTESTS_LIBCPPINTEROP_TESTPATHS_H // NOLINT(llvm-header-guard) +#define CPPINTEROP_UNITTESTS_LIBCPPINTEROP_TESTPATHS_H + +#include +#include + +namespace TestUtils { + +/// CppInterOp artifacts prefix: the directory that holds lib/ and include/. +/// The CPPINTEROP_BIN_DIR environment variable overrides the compile-time +/// default, which lets a relocated test binary find the library. Empty if +/// neither is set. +inline std::string GetCppInterOpDirPath() { + if (const char* env = std::getenv("CPPINTEROP_BIN_DIR")) + return env; +#ifdef CPPINTEROP_BIN_DIR + return CPPINTEROP_BIN_DIR; +#else + return {}; +#endif +} + +/// Full path to libclangCppInterOp under the artifacts prefix; empty if the +/// prefix is unknown. +inline std::string GetCppInterOpLibPath() { + std::string dir = GetCppInterOpDirPath(); + if (dir.empty()) + return {}; +#if defined(_WIN32) + return dir + "/lib/libclangCppInterOp.dll"; +#elif defined(__APPLE__) + return dir + "/lib/libclangCppInterOp.dylib"; +#else + return dir + "/lib/libclangCppInterOp.so"; +#endif +} + +} // end namespace TestUtils + +#endif // CPPINTEROP_UNITTESTS_LIBCPPINTEROP_TESTPATHS_H diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp index 45dbe9dd8b532..f41b9e5f53bdc 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.cpp @@ -7,3 +7,22 @@ OverlayBase::~OverlayBase() {} int OverlayBase::frob(int x) { return x; } int OverlayDispatchOnce(OverlayBase* b, int x) { return b->frob(x); } + +void* singleton_fixture_meyers_addr() { return &SingletonFixture::get(); } +int singleton_fixture_meyers_value() { return SingletonFixture::get().value; } +void* singleton_fixture_member_addr() { + return &SingletonFixture::s_inline_member; +} +int singleton_fixture_member_value() { + return SingletonFixture::s_inline_member; +} + +DynamicInitSingleton::DynamicInitSingleton() : value(7) {} +void* dynamic_singleton_addr() { return &DynamicInitSingleton::get(); } +int dynamic_singleton_value() { return DynamicInitSingleton::get().value; } + +// Mutable on purpose: the JIT-side copy is the demotion target under test. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +int g_used_singleton = 0; +void* g_used_singleton_addr() { return &g_used_singleton; } +int g_used_singleton_value() { return g_used_singleton; } diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h index 6b32130a964c6..940c93df15eaf 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h +++ b/interpreter/CppInterOp/unittests/CppInterOp/TestSharedLib/TestSharedLib.h @@ -23,4 +23,51 @@ struct TESTSHAREDLIB_API OverlayBase { extern "C" TESTSHAREDLIB_API int OverlayDispatchOnce(OverlayBase* b, int x); +// Header-*defined* singleton state, in the two shapes that compile to weak +// globals: a function-local static in an inline function and a C++17 inline +// static data member. TestSharedLib.cpp compiles an AOT copy of both (the +// accessors below); jitted code that repeats these definitions must bind the +// library's copies rather than materialize duplicates. +struct TESTSHAREDLIB_API SingletonFixture { + static SingletonFixture& get() { + static SingletonFixture instance; + return instance; + } + + static inline int s_inline_member = 0; + + int value = 0; +}; + +extern "C" TESTSHAREDLIB_API void* singleton_fixture_meyers_addr(); +extern "C" TESTSHAREDLIB_API int singleton_fixture_meyers_value(); +extern "C" TESTSHAREDLIB_API void* singleton_fixture_member_addr(); +extern "C" TESTSHAREDLIB_API int singleton_fixture_member_value(); + +// A *dynamically* initialized header singleton: the local static's initializer +// calls an out-of-line constructor the compiler cannot fold, so clang emits a +// _ZGV<...> guard variable beside the data. bindProcessWeakGlobals must demote +// the guard together with the data -- and only when the process exports both -- +// so jitted code neither re-runs initialization nor reads an uninitialized +// instance. The library's accessors init this copy AOT (value 7). +struct TESTSHAREDLIB_API DynamicInitSingleton { + int value; + DynamicInitSingleton(); // out-of-line: forces a guarded dynamic init + static DynamicInitSingleton& get() { + static DynamicInitSingleton instance; + return instance; + } +}; + +extern "C" TESTSHAREDLIB_API void* dynamic_singleton_addr(); +extern "C" TESTSHAREDLIB_API int dynamic_singleton_value(); + +// A weak global the jitted module forces into @llvm.compiler.used (via its +// __attribute__((used)) definition). Demoting it means it must also be stripped +// from that list -- a declaration there fails the IR verifier -- exercising the +// used-list rewrite. The library exports a strong copy for the JIT to bind. +extern "C" TESTSHAREDLIB_API int g_used_singleton; +extern "C" TESTSHAREDLIB_API void* g_used_singleton_addr(); +extern "C" TESTSHAREDLIB_API int g_used_singleton_value(); + #endif // UNITTESTS_CPPINTEROP_TESTSHAREDLIB_TESTSHAREDLIB_H diff --git a/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp b/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp index 116190154de24..48f4e754ab556 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/TracingTests.cpp @@ -1,5 +1,7 @@ #include "../../lib/CppInterOp/Tracing.h" +#include "TestPaths.h" + #include "clang/Basic/Version.h" #include "CppInterOp/CppInterOp.h" @@ -1193,9 +1195,9 @@ TEST_F(TracingTest, ReproducerCompilesViaInterpreter) { // Create a fresh interpreter and #include the reproducer file as-is. Cpp::CreateInterpreter({}); - Cpp::AddIncludePath(CPPINTEROP_DIR "/include"); - // Generated .inc files live under the build tree. - Cpp::AddIncludePath(CPPINTEROP_BINARY_DIR "/include"); + Cpp::AddIncludePath(CPPINTEROP_SRC_DIR "/include"); + // Generated .inc files live under the artifacts prefix. + Cpp::AddIncludePath((TestUtils::GetCppInterOpDirPath() + "/include").c_str()); std::string includeDirective = "#include \"" + Path + "\""; EXPECT_EQ(Cpp::Process(includeDirective.c_str()), 0) @@ -1391,7 +1393,7 @@ static std::string ReadFileToString(const std::string& path) { return {std::istreambuf_iterator(ifs), std::istreambuf_iterator()}; } -// This test reads source files from the build tree via CPPINTEROP_DIR. +// The tests below read source files via CPPINTEROP_SRC_DIR. TEST_F(TracingTest, StartStopTracingWritesToFile) { // StartTracing begins recording; StopTracing writes the file. std::string Path = CppInterOp::Tracing::StartTracing(/*WriteOnStdErr=*/false); @@ -1545,13 +1547,14 @@ TEST(TracingCoverageTest, AllPublicAPIsAreTraced) { // 1. Parse the header and generated declarations to extract all // CPPINTEROP_API function names. std::string Header = - ReadFileToString(CPPINTEROP_DIR "/include/CppInterOp/CppInterOp.h"); + ReadFileToString(CPPINTEROP_SRC_DIR "/include/CppInterOp/CppInterOp.h"); ASSERT_FALSE(Header.empty()) << "Could not read CppInterOp.h"; - // Function declarations are generated into CppInterOpDecl.inc. - std::string DeclInc = ReadFileToString( - CPPINTEROP_BINARY_DIR "/include/CppInterOp/CppInterOpDecl.inc"); + // CppInterOpDecl.inc is generated, so it lives under the artifacts prefix. + std::string DeclInc = + ReadFileToString(TestUtils::GetCppInterOpDirPath() + + "/include/CppInterOp/CppInterOpDecl.inc"); if (DeclInc.empty()) - DeclInc = ReadFileToString(CPPINTEROP_DIR + DeclInc = ReadFileToString(CPPINTEROP_SRC_DIR "/include/CppInterOp/CppInterOpDecl.inc"); Header += DeclInc; @@ -1580,7 +1583,7 @@ TEST(TracingCoverageTest, AllPublicAPIsAreTraced) { // 2. For each API name, find its definition in the implementation and // verify INTEROP_TRACE appears shortly after the opening brace. std::string Impl = - ReadFileToString(CPPINTEROP_DIR "/lib/CppInterOp/CppInterOp.cpp"); + ReadFileToString(CPPINTEROP_SRC_DIR "/lib/CppInterOp/CppInterOp.cpp"); ASSERT_FALSE(Impl.empty()) << "Could not read CppInterOp.cpp"; std::vector Missing;