Skip to content

Commit d317206

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 321657d commit d317206

2 files changed

Lines changed: 242 additions & 3 deletions

File tree

lib/CppInterOp/CppInterOp.cpp

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
#include "clang/Basic/CharInfo.h"
6060
#include "clang/Basic/Diagnostic.h"
6161
#include "clang/Basic/DiagnosticSema.h"
62+
#include "clang/Basic/LLVM.h"
6263
#include "clang/Basic/LangStandard.h"
6364
#include "clang/Basic/Linkage.h"
6465
#include "clang/Basic/OperatorKinds.h"
@@ -402,6 +403,90 @@ static void ForceCodeGen(Decl* D, compat::Interpreter& I) {
402403
#endif
403404
}
404405

406+
// Force emission of a variable's definition by synthesizing an odr-use of
407+
// it, instead of ForceCodeGen's UsedAttr route. The UsedAttr route records
408+
// the emitted global in codegen's llvm.used list as a weak handle; the
409+
// handle can go null (global deleted) before a later incremental PTU's
410+
// emitUsed runs, and release-built clang dereferences it without checking —
411+
// a process crash that accumulates with interpreter state rather than
412+
// tracing to any one declaration. The odr-use anchor is a dummy global
413+
// initialized with the variable's address, built directly through Sema (the
414+
// AST that parsing "&var" would produce — no source-text round-trip), so
415+
// the definition flows through the regular deferred-decl path and leaves no
416+
// used-list residue. Returns false when the odr-use cannot be built — the
417+
// caller falls back to ForceCodeGen.
418+
static bool EmitVariableViaOdrUse(compat::Interpreter& I, VarDecl* VD) {
419+
// A reference cannot conjure a definition that does not exist: the dummy
420+
// would carry an unresolvable symbol into the JIT (Emscripten's dynamic
421+
// loader rejects the whole module over it, and the stale entry then
422+
// breaks every later load in the process). The UsedAttr fallback defers
423+
// harmlessly for definition-less declarations.
424+
VarDecl* Def = VD->getDefinition();
425+
if (!Def)
426+
return false;
427+
428+
Sema& S = I.getCI()->getSema();
429+
ASTContext& C = S.getASTContext();
430+
431+
// Build '&VD' the way the parser would; the address-of marks Def
432+
// odr-used, which is what schedules the deferred definition.
433+
ExprResult Ref = S.BuildDeclRefExpr(Def, Def->getType().getNonReferenceType(),
434+
VK_LValue, SourceLocation());
435+
if (Ref.isInvalid())
436+
return false;
437+
ExprResult AddrOf =
438+
S.CreateBuiltinUnaryOp(SourceLocation(), UO_AddrOf, Ref.get());
439+
if (AddrOf.isInvalid())
440+
return false;
441+
442+
// Anchor the odr-use in an external-linkage dummy the JIT must emit; the
443+
// initializer's implicit conversion to 'const void*' is Sema-checked, so
444+
// exotic types fail over to the fallback instead of asserting.
445+
// FIXME: The synthesized nodes are parked in the TU for the rest of the
446+
// session; move them under a scratch area whose AST nodes get deallocated
447+
// after emission.
448+
// FIXME: Remove this whole anchor mechanism once clang grows a direct
449+
// interpreter emission API for a GlobalDecl (the emitUsed hardening it
450+
// depends on landed via llvm/llvm-project#210959):
451+
#if CLANG_VERSION_MAJOR >= 24
452+
#warning "Check whether clang's direct-emission API landed; if so, replace \
453+
EmitVariableViaOdrUse's synthesized dummy with it."
454+
#endif
455+
static unsigned Counter = 0;
456+
std::string DummyName = "__cppinterop_odr_use_v" + std::to_string(Counter++);
457+
QualType Ty = C.getPointerType(C.VoidTy.withConst());
458+
TranslationUnitDecl* TU = C.getTranslationUnitDecl();
459+
VarDecl* Dummy = VarDecl::Create(C, TU, SourceLocation(), SourceLocation(),
460+
&C.Idents.get(DummyName), Ty,
461+
C.getTrivialTypeSourceInfo(Ty), SC_None);
462+
S.AddInitializerToDecl(Dummy, AddrOf.get(), /*DirectInit=*/false);
463+
if (Dummy->isInvalidDecl())
464+
return false;
465+
TU->addDecl(Dummy);
466+
467+
// Under cling the RAII's transaction commit triggers emission; under
468+
// clang-repl it is still a stub (TODO destructor in Compatibility.h) and
469+
// only annotates the synthesizing region.
470+
compat::SynthesizingCodeRAII RAII(&I);
471+
I.getCI()->getASTConsumer().HandleTopLevelDecl(DeclGroupRef(Dummy));
472+
#ifndef CPPINTEROP_USE_CLING
473+
// FIXME: Parsing an empty string is the only way to flush incremental
474+
// CodeGen for a decl handed straight to the consumer — the state-reset
475+
// bug SynthesizingCodeRAII's clang-repl destructor should eventually
476+
// own. Drop this when it does.
477+
auto GeneratedPTU = I.Parse("");
478+
if (!GeneratedPTU) {
479+
llvm::consumeError(GeneratedPTU.takeError());
480+
return false;
481+
}
482+
if (auto Err = I.Execute(*GeneratedPTU)) {
483+
llvm::consumeError(std::move(Err));
484+
return false;
485+
}
486+
#endif
487+
return true;
488+
}
489+
405490
#define DEBUG_TYPE "jitcall"
406491
bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self,
407492
size_t nary) const {
@@ -2806,7 +2891,14 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D,
28062891
}
28072892
if (!address) {
28082893
auto Linkage = C.GetGVALinkageForVariable(VD);
2809-
if (isDiscardableGVALinkage(Linkage))
2894+
// Odr-use emission only for discardable-ODR entities (inline/constexpr
2895+
// statics) — the class the used-list crash traced to. Internal-linkage
2896+
// variables cannot be odr-used from a later PTU (module-local symbol:
2897+
// the reference duplicates or misses the entity), and an
2898+
// available-externally definition would not be emitted by a mere
2899+
// reference; both stay on the stock UsedAttr path.
2900+
if (isDiscardableGVALinkage(Linkage) &&
2901+
(Linkage != GVA_DiscardableODR || !EmitVariableViaOdrUse(I, VD)))
28102902
ForceCodeGen(VD, I);
28112903
}
28122904
auto VDAorErr = compat::getSymbolAddress(I, StringRef(mangledName));
@@ -4989,7 +5081,7 @@ class clangSilent {
49895081
};
49905082
} // namespace
49915083

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

unittests/CppInterOp/VariableReflectionTest.cpp

Lines changed: 148 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

@@ -376,6 +382,147 @@ TYPED_TEST(CPPINTEROP_TEST_MODE,
376382
EXPECT_FALSE(Cpp::GetVariableOffset(var2));
377383
}
378384

385+
TYPED_TEST(CPPINTEROP_TEST_MODE,
386+
VariableReflection_GetVariableOffset_NoStaleUsedHandle) {
387+
#ifdef __EMSCRIPTEN__
388+
// The stale-handle crash this test pins is native-JIT mechanics (ORC
389+
// freeing materialized IR), and a single failed module load poisons
390+
// Emscripten's process-global dynamic-linking state for every test that
391+
// follows (issue #1071) — not worth the fragility for a native-only
392+
// scenario.
393+
GTEST_SKIP() << "Stale-used-handle scenario is native-JIT specific";
394+
#endif
395+
// Emitting a discardable-ODR variable through the UsedAttr route records
396+
// the global as a weak handle in codegen's llvm.used list. If the global
397+
// is later replaced/erased (weak-def discard when a later PTU re-emits
398+
// the same entity), the next PTU's emitUsed dereferences the nulled
399+
// handle. The offset query must not leave used-list residue.
400+
TestFixture::CreateInterpreter();
401+
Cpp::Declare(R"(
402+
struct UsedHandle {
403+
inline static int probe = 3;
404+
};
405+
)");
406+
Cpp::DeclRef klass = Cpp::GetNamed("UsedHandle");
407+
EXPECT_TRUE(klass);
408+
Cpp::DeclRef var = Cpp::GetNamed("probe", klass);
409+
EXPECT_TRUE(var);
410+
EXPECT_TRUE(Cpp::GetVariableOffset(var));
411+
// ForceCodeGen's UsedAttr is planted permanently on the AST decl; the
412+
// odr-use route must not.
413+
EXPECT_FALSE(Cpp::unwrap<Decl>(var)->hasAttr<clang::UsedAttr>());
414+
#ifndef CPPINTEROP_USE_CLING
415+
// The UsedAttr lives on the AST decl, so every later PTU that re-emits
416+
// the entity re-adds it to that module's llvm.used — the residue the
417+
// stale-handle crash grows from. Neither a module that re-emits the
418+
// variable nor an unrelated one may carry llvm.used. (PTU/TheModule is
419+
// clang::Interpreter surface; cling covers this path via the UsedAttr
420+
// assert above.)
421+
{
422+
auto PTUOrErr = Interp->Parse("int consume_probe = UsedHandle::probe;");
423+
ASSERT_TRUE(bool(PTUOrErr));
424+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
425+
if (auto Err = Interp->Execute(*PTUOrErr))
426+
llvm::consumeError(std::move(Err));
427+
}
428+
{
429+
auto PTUOrErr = Interp->Parse("int flush_ptu = 0;");
430+
ASSERT_TRUE(bool(PTUOrErr));
431+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
432+
if (auto Err = Interp->Execute(*PTUOrErr))
433+
llvm::consumeError(std::move(Err));
434+
}
435+
EXPECT_TRUE(Cpp::GetNamed("flush_ptu"));
436+
#endif // !CPPINTEROP_USE_CLING
437+
// The crash this guards against was cumulative — many force-emitted
438+
// statics plus JIT materialization cycles, interleaved with absorbed
439+
// parse failures. Mimic that sweep shape as a regression net.
440+
for (int i = 0; i < 8; ++i) {
441+
std::string n = std::to_string(i);
442+
std::string decl = "struct Sweep";
443+
decl += n;
444+
decl += " { inline static int v";
445+
decl += n;
446+
decl += " = ";
447+
decl += n;
448+
decl += "; };";
449+
Cpp::Declare(decl.c_str());
450+
Cpp::DeclRef k = Cpp::GetNamed("Sweep" + n);
451+
ASSERT_TRUE(k);
452+
Cpp::DeclRef v = Cpp::GetNamed("v" + n, k);
453+
ASSERT_TRUE(v);
454+
EXPECT_TRUE(Cpp::GetVariableOffset(v));
455+
std::string probe = "template <> struct Sweep";
456+
probe += n;
457+
probe += "<int>;";
458+
Cpp::Declare(probe.c_str(), /*silent=*/true); // expected parse failure
459+
std::string use = "int use";
460+
use += n;
461+
use += " = Sweep";
462+
use += n;
463+
use += "::v";
464+
use += n;
465+
use += ";";
466+
Cpp::Declare(use.c_str());
467+
}
468+
Cpp::Declare("int sweep_done = 1;");
469+
EXPECT_TRUE(Cpp::GetNamed("sweep_done"));
470+
471+
// Template-specialization members take the odr-use route too — the
472+
// Sema-built address-of needs no source spelling of the specialization,
473+
// and the caller has already instantiated the definition.
474+
Cpp::Declare(R"(
475+
template <typename T> struct TmplStatic {
476+
inline static int member = 7;
477+
};
478+
)");
479+
Cpp::DeclRef tmpl = Cpp::GetNamed("TmplStatic");
480+
EXPECT_TRUE(tmpl);
481+
ASTContext& C = Interp->getCI()->getASTContext();
482+
std::vector<Cpp::TemplateArgInfo> template_args = {
483+
{C.IntTy.getAsOpaquePtr()}};
484+
Cpp::DeclRef inst = Cpp::InstantiateTemplate(tmpl, template_args);
485+
EXPECT_TRUE(inst);
486+
Cpp::DeclRef member = Cpp::GetNamed("member", inst);
487+
EXPECT_TRUE(member);
488+
EXPECT_TRUE(Cpp::GetVariableOffset(member));
489+
EXPECT_FALSE(Cpp::unwrap<Decl>(member)->hasAttr<clang::UsedAttr>());
490+
491+
// Anonymous-namespace members cannot be named from a fresh chunk of
492+
// source; they bail out of the odr-use route the same way.
493+
Cpp::Declare(R"(
494+
namespace {
495+
struct AnonNsStatic {
496+
inline static int member = 9;
497+
};
498+
}
499+
)");
500+
Cpp::DeclRef anon_klass = Cpp::GetNamed("AnonNsStatic");
501+
ASSERT_TRUE(anon_klass);
502+
Cpp::DeclRef anon_member = Cpp::GetNamed("member", anon_klass);
503+
ASSERT_TRUE(anon_member);
504+
EXPECT_TRUE(Cpp::GetVariableOffset(anon_member));
505+
#ifndef CPPINTEROP_USE_CLING
506+
EXPECT_TRUE(Cpp::unwrap<Decl>(anon_member)->hasAttr<clang::UsedAttr>());
507+
#endif
508+
509+
// A linkage-spec block is transparent for naming purposes and stays on
510+
// the odr-use route.
511+
Cpp::Declare(R"(
512+
extern "C++" {
513+
struct CxxLinkStatic {
514+
inline static int member = 11;
515+
};
516+
}
517+
)");
518+
Cpp::DeclRef link_klass = Cpp::GetNamed("CxxLinkStatic");
519+
ASSERT_TRUE(link_klass);
520+
Cpp::DeclRef link_member = Cpp::GetNamed("member", link_klass);
521+
ASSERT_TRUE(link_member);
522+
EXPECT_TRUE(Cpp::GetVariableOffset(link_member));
523+
EXPECT_FALSE(Cpp::unwrap<Decl>(link_member)->hasAttr<clang::UsedAttr>());
524+
}
525+
379526
#define CODE \
380527
class BaseA { \
381528
public: \

0 commit comments

Comments
 (0)