mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
feat(abi): define logos_module_set_call_caller, gated on protocol 0.6
The definition lands BEFORE the protocol declares the export. logos-protocol only DECLARES the module-impl C ABI and every backend owes the definition; that gap shipped twice, each time as an undefined symbol at dlopen, on Linux only, invisible on macOS. Declaring first would turn this repo, logos-rust-sdk and logos-module-builder red the night the bump merged. Defining first costs nothing: the guard is MAJOR-aware >= 0.6 and the current pin is 0.5, so nothing is emitted today and the ABI check sees declared == defined. This is the case #146 made possible. The next-MAJOR probe resolves the emitter at MAJOR+1, where a >= 6 guard IS true, so the emitted set there is legitimately a SUPERSET of the declared one. The probe used to demand equality and would have rejected this outright. cpp/logos_caller.h carries the LogosCaller type (std-typed, Qt-free) and logos::currentCaller(), reading a thread-local stack the generated export pushes to. Two things the audit corrected, both worth reading: * A present-but-unreadable `instance` is DROPPED and the module still identified. This backend already did that; Rust returned Unknown, and each had a passing test pinning its own answer, so neither suite could see the divergence. The protocol header now states the rule normatively and Rust is aligned to it. * The accessors are explicitly HIDDEN on ELF. The header argued this state must not be unified across images and then relied on being inline to achieve it — which is false: a function-local static in an inline function emits STB_GNU_UNIQUE at default visibility and the loader collapses every image's copy into one, even under RTLD_LOCAL. Measured across two dlopen'd images: default visibility let a push in A be read by B; hidden restored isolation. logos-module-builder sets no visibility anywhere, so real plugins were built the first way. An anonymous namespace would be worse — vague linkage is load-bearing WITHIN an image, since the generated TU pushes and the author's TU reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Dario Lipicar
co-authored by
Claude Opus 5
parent
43904ef1a3
commit
dbe1d63677
@@ -632,6 +632,10 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module,
|
||||
s << "#include \"logos_protocol.h\"\n";
|
||||
s << "#include \"logos_module_context.h\"\n";
|
||||
s << "#include \"logos_result.h\"\n";
|
||||
// The caller-of-a-dispatch reader. Unconditional: it is a logos-cpp-sdk
|
||||
// header with no protocol dependency of its own, so it costs nothing on an
|
||||
// older protocol where the export below is not emitted.
|
||||
s << "#include \"logos_caller.h\"\n";
|
||||
s << "#include <nlohmann/json.hpp>\n";
|
||||
s << "#include <cstdlib>\n";
|
||||
s << "#include <cstring>\n";
|
||||
@@ -966,6 +970,41 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module,
|
||||
s << "}\n";
|
||||
s << "#endif\n\n";
|
||||
|
||||
// THE CALLER OF A DISPATCH (protocol 0.6). The glue wraps one
|
||||
// logos_module_dispatch in one push/pop pair on the dispatching thread; a
|
||||
// non-NULL argument pushes, NULL pops the innermost.
|
||||
//
|
||||
// WHY THIS CROSSES THE C ABI AT ALL, since a thread_local the host set
|
||||
// would be so much simpler. It would not be the same object. Measured with
|
||||
// nm on built binaries rather than assumed, on both object formats: the
|
||||
// host image and the module plugin EACH define
|
||||
// ModuleProxy::callRemoteMethod and TokenManager::instance; the
|
||||
// function-local static behind the latter is a LOCAL bss symbol in each,
|
||||
// at a different address; and neither image holds an undefined reference
|
||||
// to the other's copy. The Mach-O plugin is MH_NOUNDEFS | MH_TWOLEVEL. So
|
||||
// the identity has to be handed over explicitly, exactly as the trust-root
|
||||
// grant above is. cpp/logos_caller.h carries the full measurement.
|
||||
//
|
||||
// Guarded MAJOR-aware, not on the MINOR alone. At 1.0 the MINOR resets to
|
||||
// 0 and a `MINOR >= 6` guard would go false, taking the definition and the
|
||||
// generated call away TOGETHER — everything would still build and load,
|
||||
// and modules would just silently stop being able to name their caller.
|
||||
// checks.module-impl-abi resolves this text at one MAJOR up for that
|
||||
// reason. Written expanded rather than behind a function-like macro
|
||||
// because unifdef has to be able to evaluate it.
|
||||
s << "#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && "
|
||||
"(LOGOS_PROTOCOL_VERSION_MAJOR > 0 || "
|
||||
"(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && "
|
||||
"LOGOS_PROTOCOL_VERSION_MINOR >= 6))\n";
|
||||
s << "void logos_module_set_call_caller(const char* caller_json)\n{\n";
|
||||
// One line, deliberately. Parsing the document, the per-thread stack and
|
||||
// the nesting rule all live in cpp/logos_caller.h where a unit test can
|
||||
// reach them BY VALUE; logic that lives in emitted text is logic no test
|
||||
// ever executes, only greps.
|
||||
s << " logos::detail::setCallCaller(caller_json);\n";
|
||||
s << "}\n";
|
||||
s << "#endif\n\n";
|
||||
|
||||
s << "const char* logos_module_get_protocol_version(void)\n{\n";
|
||||
s << " return LOGOS_PROTOCOL_VERSION_STRING;\n}\n\n";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user