Skip to content

Commit 79d2fdb

Browse files
author
Emery Conrad
committed
Emit discardable-ODR variables via odr-use, not UsedAttr, in GetVariableOffset
ForceCodeGen forces deferred emission by planting a permanent __attribute__((used)) on the decl; every global emitted that way gets a WeakTrackingVH in codegen's llvm.used list. When such a global is deleted before emitUsed runs (ORC freeing a materialized PTU's IR), the handle nulls and release-built clang dereferences it unchecked — a process crash that accumulates with interpreter state rather than tracing to any one declaration (large reflection sweeps died; per-type bisection never converged, so the crash itself has no compact unit repro — root-caused via gdb). GetVariableOffset now emits GVA_DiscardableODR variables (the inline/constexpr class the crash traced to) by declaring an external-linkage odr-use of the qualified name instead: the definition flows through the regular deferred-decl path and leaves no used-list residue, no permanent attribute. Everything else keeps the UsedAttr path, deliberately: - Internal-linkage variables cannot be odr-used from a later PTU (the module-local symbol duplicates or goes missing); getting this wrong broke VariableReflection_GetVariableOffset (static int S) and cppyy's Lifeline::count lookup. - Available-externally definitions would not be emitted by a mere reference. - Template-specialization and anonymous/lambda spellings do not reliably round-trip as source (a printed LLONG_MIN non-type argument re-parses as an overflowing literal), and a parse-failing declaration poisons the incremental interpreter. An interpreter-level alternative — Undo(1) on parse failure to drop the failed PTU — was evaluated and rejected: cppyy depends on failed parses leaving side-effect declarations (an explicit-instantiation probe of a declared-but-undefined template is expected to fail and leave the specialization decl behind; test_templates test32). The dummy-name counter is deliberately plain, matching gWrapperSerial: the interpreter API is caller-serialized. ForceCodeGen's function path (GetFunctionAddress) still uses UsedAttr — same theoretical hazard, never observed. The test pins the mechanism: after an offset query on a discardable-ODR static, the decl carries no UsedAttr and later PTU modules carry no llvm.used, with a sweep-shaped regression net over interleaved absorbed parse failures. Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
1 parent 3103be9 commit 79d2fdb

2 files changed

Lines changed: 159 additions & 3 deletions

File tree

lib/CppInterOp/CppInterOp.cpp

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,45 @@ static void ForceCodeGen(Decl* D, compat::Interpreter& I) {
402402
#endif
403403
}
404404

405+
static int Declare(compat::Interpreter& I, const char* code, bool silent);
406+
407+
// Force emission of a variable's definition by declaring an odr-use of it,
408+
// instead of ForceCodeGen's UsedAttr route. The UsedAttr route records the
409+
// emitted global in codegen's llvm.used list as a weak handle; the handle
410+
// can go null (global deleted) before a later incremental PTU's emitUsed
411+
// runs, and release-built clang dereferences it without checking — a
412+
// process crash that accumulates with interpreter state rather than
413+
// tracing to any one declaration. The odr-use emits the (discardable)
414+
// definition through the regular deferred-decl path and leaves no
415+
// used-list residue. Returns false when the variable cannot be named from
416+
// a fresh chunk of source (anonymous scopes, or printed names that fail to
417+
// parse) — the caller falls back to ForceCodeGen.
418+
static bool EmitVariableViaOdrUse(compat::Interpreter& I, VarDecl* VD) {
419+
std::string name;
420+
{
421+
llvm::raw_string_ostream OS(name);
422+
VD->printQualifiedName(OS);
423+
}
424+
if (name.find("(anonymous ") != std::string::npos ||
425+
name.find("(unnamed ") != std::string::npos ||
426+
name.find("(lambda ") != std::string::npos)
427+
return false;
428+
// Template-specialization spellings do not reliably round-trip as source
429+
// (e.g. a printed LLONG_MIN non-type argument re-parses as an overflowing
430+
// literal), and a parse-failing declaration poisons the incremental
431+
// interpreter; keep those on the caller's UsedAttr path.
432+
if (name.find('<') != std::string::npos)
433+
return false;
434+
435+
// External linkage on the dummy keeps it — and therefore the referenced
436+
// definition — from being discarded as unused internal state.
437+
static unsigned Counter = 0;
438+
std::string code = "namespace __cppinterop_odr_use { const void* __v" +
439+
std::to_string(Counter++) +
440+
" = (const void*)__builtin_addressof(::" + name + "); }";
441+
return Declare(I, code.c_str(), /*silent=*/true) == 0;
442+
}
443+
405444
#define DEBUG_TYPE "jitcall"
406445
bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self,
407446
size_t nary) const {
@@ -2794,7 +2833,14 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D,
27942833
}
27952834
if (!address) {
27962835
auto Linkage = C.GetGVALinkageForVariable(VD);
2797-
if (isDiscardableGVALinkage(Linkage))
2836+
// Odr-use emission only for discardable-ODR entities (inline/constexpr
2837+
// statics) — the class the used-list crash traced to. Internal-linkage
2838+
// variables cannot be odr-used from a later PTU (module-local symbol:
2839+
// the reference duplicates or misses the entity), and an
2840+
// available-externally definition would not be emitted by a mere
2841+
// reference; both stay on the stock UsedAttr path.
2842+
if (isDiscardableGVALinkage(Linkage) &&
2843+
(Linkage != GVA_DiscardableODR || !EmitVariableViaOdrUse(I, VD)))
27982844
ForceCodeGen(VD, I);
27992845
}
28002846
auto VDAorErr = compat::getSymbolAddress(I, StringRef(mangledName));
@@ -4977,7 +5023,7 @@ class clangSilent {
49775023
};
49785024
} // namespace
49795025

4980-
int Declare(compat::Interpreter& I, const char* code, bool silent) {
5026+
static int Declare(compat::Interpreter& I, const char* code, bool silent) {
49815027
// Trap diagnostics on both paths: I.declare's rc is 0 even when
49825028
// Parse recovered from emitted errors, so callers need the trap to
49835029
// distinguish "parsed cleanly" from "parsed with errors".

unittests/CppInterOp/VariableReflectionTest.cpp

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#include "Utils.h"
22

3+
#include "../../lib/CppInterOp/Unwrap.h"
4+
35
#include "CppInterOp/CppInterOp.h"
46

57
#include "clang/AST/ASTContext.h"
8+
#include "clang/AST/Attr.h"
69
#include "clang/Basic/Version.h"
710
#include "clang/Frontend/CompilerInstance.h"
811
#include "clang/Sema/Sema.h"
912

10-
#include "gtest/gtest.h"
13+
#include "llvm/Support/Error.h"
14+
1115
#include <string>
16+
#include <utility>
17+
#include "gtest/gtest.h"
1218

1319
#include <cstddef>
1420

@@ -340,6 +346,110 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) {
340346
EXPECT_TRUE(Cpp::GetVariableOffset(var));
341347
}
342348

349+
TYPED_TEST(CPPINTEROP_TEST_MODE,
350+
VariableReflection_GetVariableOffset_NoStaleUsedHandle) {
351+
// Emitting a discardable-ODR variable through the UsedAttr route records
352+
// the global as a weak handle in codegen's llvm.used list. If the global
353+
// is later replaced/erased (weak-def discard when a later PTU re-emits
354+
// the same entity), the next PTU's emitUsed dereferences the nulled
355+
// handle. The offset query must not leave used-list residue.
356+
TestFixture::CreateInterpreter();
357+
Cpp::Declare(R"(
358+
struct UsedHandle {
359+
inline static int probe = 3;
360+
};
361+
)");
362+
Cpp::DeclRef klass = Cpp::GetNamed("UsedHandle");
363+
EXPECT_TRUE(klass);
364+
Cpp::DeclRef var = Cpp::GetNamed("probe", klass);
365+
EXPECT_TRUE(var);
366+
EXPECT_TRUE(Cpp::GetVariableOffset(var));
367+
// ForceCodeGen's UsedAttr is planted permanently on the AST decl; the
368+
// odr-use route must not.
369+
EXPECT_FALSE(Cpp::unwrap<Decl>(var)->hasAttr<clang::UsedAttr>());
370+
#ifndef CPPINTEROP_USE_CLING
371+
// The UsedAttr lives on the AST decl, so every later PTU that re-emits
372+
// the entity re-adds it to that module's llvm.used — the residue the
373+
// stale-handle crash grows from. Neither a module that re-emits the
374+
// variable nor an unrelated one may carry llvm.used. (PTU/TheModule is
375+
// clang::Interpreter surface; cling covers this path via the UsedAttr
376+
// assert above.)
377+
{
378+
auto PTUOrErr = Interp->Parse("int consume_probe = UsedHandle::probe;");
379+
ASSERT_TRUE(bool(PTUOrErr));
380+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
381+
if (auto Err = Interp->Execute(*PTUOrErr))
382+
llvm::consumeError(std::move(Err));
383+
}
384+
{
385+
auto PTUOrErr = Interp->Parse("int flush_ptu = 0;");
386+
ASSERT_TRUE(bool(PTUOrErr));
387+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
388+
if (auto Err = Interp->Execute(*PTUOrErr))
389+
llvm::consumeError(std::move(Err));
390+
}
391+
EXPECT_TRUE(Cpp::GetNamed("flush_ptu"));
392+
#endif // !CPPINTEROP_USE_CLING
393+
// The crash this guards against was cumulative — many force-emitted
394+
// statics plus JIT materialization cycles, interleaved with absorbed
395+
// parse failures. Mimic that sweep shape as a regression net.
396+
for (int i = 0; i < 8; ++i) {
397+
std::string n = std::to_string(i);
398+
std::string decl = "struct Sweep";
399+
decl += n;
400+
decl += " { inline static int v";
401+
decl += n;
402+
decl += " = ";
403+
decl += n;
404+
decl += "; };";
405+
Cpp::Declare(decl.c_str());
406+
Cpp::DeclRef k = Cpp::GetNamed("Sweep" + n);
407+
ASSERT_TRUE(k);
408+
Cpp::DeclRef v = Cpp::GetNamed("v" + n, k);
409+
ASSERT_TRUE(v);
410+
EXPECT_TRUE(Cpp::GetVariableOffset(v));
411+
std::string probe = "template <> struct Sweep";
412+
probe += n;
413+
probe += "<int>;";
414+
Cpp::Declare(probe.c_str(), /*silent=*/true); // expected parse failure
415+
std::string use = "int use";
416+
use += n;
417+
use += " = Sweep";
418+
use += n;
419+
use += "::v";
420+
use += n;
421+
use += ";";
422+
Cpp::Declare(use.c_str());
423+
}
424+
Cpp::Declare("int sweep_done = 1;");
425+
EXPECT_TRUE(Cpp::GetNamed("sweep_done"));
426+
427+
// Template-specialization spellings bail out of the odr-use route
428+
// (printed non-type arguments do not reliably round-trip as source) and
429+
// keep the UsedAttr fallback.
430+
Cpp::Declare(R"(
431+
template <typename T> struct TmplStatic {
432+
inline static int member = 7;
433+
};
434+
)");
435+
Cpp::DeclRef tmpl = Cpp::GetNamed("TmplStatic");
436+
EXPECT_TRUE(tmpl);
437+
ASTContext& C = Interp->getCI()->getASTContext();
438+
std::vector<Cpp::TemplateArgInfo> template_args = {
439+
{C.IntTy.getAsOpaquePtr()}};
440+
Cpp::DeclRef inst = Cpp::InstantiateTemplate(tmpl, template_args);
441+
EXPECT_TRUE(inst);
442+
Cpp::DeclRef member = Cpp::GetNamed("member", inst);
443+
EXPECT_TRUE(member);
444+
EXPECT_TRUE(Cpp::GetVariableOffset(member));
445+
#ifndef CPPINTEROP_USE_CLING
446+
// cling's getAddressOfGlobal can emit on demand, resolving the address
447+
// before the forced-emission gate is reached; only clang-repl reliably
448+
// takes the UsedAttr fallback here.
449+
EXPECT_TRUE(Cpp::unwrap<Decl>(member)->hasAttr<clang::UsedAttr>());
450+
#endif
451+
}
452+
343453
#define CODE \
344454
class BaseA { \
345455
public: \

0 commit comments

Comments
 (0)