Ship libdd-shared-runtime-ffi as its own cdylib#52
Conversation
The trace exporter drives its fork-safety lifecycle through the generic shared-runtime FFI (`ddog_shared_runtime_new`, `_before_fork`, `_after_fork_parent`, `_after_fork_child`), but those symbols are not shipped. The aggregate `libdatadog_profiling.so` is built solely from `libdd-profiling-ffi`, which does not depend on `libdd-shared-runtime-ffi`, so only the opaque `ddog_SharedRuntime` type and `ddog_trace_exporter_config_set_shared_runtime` leak in transitively. Build that crate as its own cdylib (it already declares a `cdylib` crate type) and install it as `libdatadog_shared_runtime.so` alongside its cbindgen-generated `shared-runtime.h`, so consumers can link the lifecycle functions without changing the libdatadog source. Caveat: this leaves a second, statically linked copy of the shared-runtime and tokio code beside the copy already inside `libdatadog_profiling.so`. A `SharedRuntime` handle is created in one library and driven from the other. The two copies are ABI-compatible because they are built from the same source revision and the handle crosses the boundary as an opaque pointer, so this is sound at the link/ABI level. The behavior of a tokio runtime created in one library and quiesced/restarted from another across `fork()` is not yet verified; an integration test that forks with an exporter attached must confirm it before this is relied upon in production. Only source builds (via `LIBDATADOG_SOURCE`) are handled; tag/release builds skip this step and would need a checkout. The cleaner long-term fix is to have the aggregate library include the crate so a single library exports everything.
Outcome: this approach does not work — recording why before abandoning itI wired this branch into the dd-trace-rb native trace exporter to validate it end It compiles and links cleanly, but Root causeThe exporter FFI (
Control: skipping only the attach call lets ConclusionThe exporter FFI and the shared-runtime FFI must share a single copy of Leaving this PR as a draft for the record; not merging. |
The trace-exporter redesign on this branch drives fork safety through the generic shared-runtime FFI (ddog_shared_runtime_new / _free / _before_fork / _after_fork_parent / _after_fork_child), but those symbols aren't shipped today: the builder compiles one aggregate cdylib from libdd-profiling-ffi's dependency graph, which doesn't include libdd-shared-runtime-ffi. Shipping it as a SEPARATE library was tried and fails at runtime with a SIGSEGV, because each library statically embeds its own copy of libdd_shared_runtime and a SharedRuntime handle created in one library cannot be driven from the other. Evidence and analysis: DataDog/libdatadog-rb#52 (comment) Co-locate the FFI in the single profiling library so there is exactly one copy of the libdd_shared_runtime crate at runtime. cargo unifies it with the copy already pulled in via data-pipeline. The change is additive and opt-in behind a new `shared-runtime` feature, so other consumers are unaffected unless enabled. - libdd-profiling-ffi: add optional libdd-shared-runtime-ffi dependency behind a `shared-runtime` feature, re-export its symbols, and propagate cbindgen. - builder: add a `shared-runtime` feature (enabled by default), push the corresponding libdd-profiling-ffi feature in release.rs, and add shared-runtime.h to the header dedup/copy in profiling.rs.
Why?
The Ruby trace exporter needs libdatadog's shared-runtime fork-lifecycle
functions (
ddog_shared_runtime_new/_before_fork/_after_fork_parent/_after_fork_child) so it can quiesce and restart the exporter's backgroundruntime across
fork(). Those symbols live in thelibdd-shared-runtime-fficrate, but they are not currently shipped: the aggregate
libdatadog_profiling.sois built solely fromlibdd-profiling-ffi, which doesnot depend on
libdd-shared-runtime-ffi. Only the opaqueddog_SharedRuntimetype and
ddog_trace_exporter_config_set_shared_runtimeleak in transitively.What does this PR do?
Builds the existing
libdd-shared-runtime-fficrate as its own cdylib(
libdatadog_shared_runtime.so) and installs it, together with itscbindgen-generated
shared-runtime.h, into the vendored platform tree — withoutany change to the libdatadog source. The gem packager already globs the vendored
tree, so the extra library and header are picked up automatically.
This keeps the change entirely within libdatadog-rb (the repo whose purpose is
custom packaging of libdatadog artifacts).
How to test the change?
Build from a local libdatadog checkout and confirm the artifacts:
A standalone C program that links
-ldatadog_shared_runtimeand runsnew -> before_fork -> after_fork_parent -> freeexits cleanly.Additional Notes:
Currently only source builds (
LIBDATADOG_SOURCE) are handled; tag/releasebuilds skip the step and would need a checkout. See the commit message for the
known cross-library caveat (two statically linked copies of the shared-runtime
code).
Draft, kept for archival/decision-tracking purposes.
JIRA: