-
Notifications
You must be signed in to change notification settings - Fork 19
feat: otel thread ctx FFI #1915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yannham
wants to merge
20
commits into
main
Choose a base branch
from
yannham/otel-thread-ctx-ffi
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
67463a5
feat(ffi): export TLSDESC TLS symbol to dynsym via version script
yannham 24a0b3b
refactor: refine the FFI
yannham 7247d91
fix(ffi): map atomic to u8 in the FFI
yannham ed8bf0a
test: ensure the TLS setup follows the spec
yannham 16b3240
style: improve otel thread ctx FFI and its doc
yannham a893b7c
style: formatting
yannham bf19209
chore: add CODEOWNERS entry for libdd-otel-thread-ctx-ffi
yannham 918ab3b
style: drop -> free
yannham 05868a5
fix: add missing line for new crates in dockerfile
yannham 41b2e37
test(ffi): verify TLSDESC relocation and dynsym export of otel TLS sy…
yannham e6a7b10
style: cosmetic changes
yannham 75add55
test(ffi): assert cdylib exists and is readable before ELF inspection
yannham 2b39efc
fix: missing constant in FFI
yannham 8ff86fa
test: fix lib finding code
yannham e131364
ci: install LLD for otel-thread-ctx test to run
yannham 2acc549
ci: remove lld invocation
yannham d406bf8
ci: tentative fix for missing ld command
yannham 3da7d3e
ci: tentative fix for missing ld command, part II
yannham abf827e
ci: fix build error by relaxing need for system wide lld
yannham 2375640
Revert "ci: fix build error by relaxing need for system wide lld"
yannham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| [package] | ||
| name = "libdd-otel-thread-ctx-ffi" | ||
| version = "1.0.0" | ||
| description = "FFI bindings for the OTel thread-level context publisher" | ||
| edition.workspace = true | ||
| rust-version.workspace = true | ||
| license.workspace = true | ||
| publish = false | ||
|
|
||
| [lib] | ||
| crate-type = ["staticlib", "cdylib", "lib"] | ||
| bench = false | ||
|
|
||
| [dependencies] | ||
| libdd-common-ffi = { path = "../libdd-common-ffi", default-features = false } | ||
| libdd-otel-thread-ctx = { path = "../libdd-otel-thread-ctx" } | ||
|
|
||
| [features] | ||
| default = ["cbindgen"] | ||
| cbindgen = ["build_common/cbindgen", "libdd-common-ffi/cbindgen"] | ||
|
|
||
| [build-dependencies] | ||
| build_common = { path = "../build-common" } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| extern crate build_common; | ||
|
|
||
| use build_common::generate_and_configure_header; | ||
|
|
||
| fn main() { | ||
| let header_name = "otel-thread-ctx.h"; | ||
| generate_and_configure_header(header_name); | ||
|
|
||
| let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); | ||
|
|
||
| // Export the TLSDESC thread-local variable to the dynamic symbol table so | ||
| // external readers (e.g. the eBPF profiler) can locate it. Rust's cdylib | ||
| // linker applies a version script with `local: *` that hides all symbols | ||
| // not explicitly whitelisted, and also causes lld to relax the TLSDESC | ||
| // access to local-exec (LE), eliminating the dynsym entry entirely. | ||
| // Passing our own version script with an explicit `global:` entry for the | ||
| // symbol beats the `local: *` wildcard and prevents that relaxation. | ||
| // | ||
| // Merging multiple version scripts is not supported by GNU ld, so we also | ||
| // force lld explicitly. | ||
| if target_os == "linux" { | ||
| let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); | ||
| println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld"); | ||
| println!( | ||
| "cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt" | ||
| ); | ||
|
|
||
| // Expose the profile output directory to integration tests so they can | ||
| // locate the cdylib without fragile path-walking. | ||
| // OUT_DIR = <target>/[<triple>/]<profile>/build/<pkg>-<hash>/out | ||
| // Three levels up lands on <target>/[<triple>/]<profile>. | ||
| let out_dir = std::env::var("OUT_DIR").unwrap(); | ||
| let profile_dir = std::path::PathBuf::from(&out_dir) | ||
| .ancestors() | ||
| .nth(3) | ||
| .unwrap() | ||
| .to_str() | ||
| .unwrap() | ||
| .to_owned(); | ||
| println!("cargo:rustc-env=CDYLIB_PROFILE_DIR={profile_dir}"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| language = "C" | ||
| cpp_compat = true | ||
| tab_width = 2 | ||
| header = """// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| """ | ||
| include_guard = "DDOG_OTEL_THREAD_CTX_H" | ||
| style = "both" | ||
| pragma_once = true | ||
| no_includes = true | ||
| sys_includes = ["stdbool.h", "stddef.h", "stdint.h"] | ||
|
|
||
| [parse] | ||
| parse_deps = true | ||
| include = ["libdd-common-ffi", "libdd-otel-thread-ctx"] | ||
|
|
||
| [export] | ||
| prefix = "ddog_" | ||
| renaming_overrides_prefixing = true | ||
|
|
||
| [export.rename] | ||
| # AtomicU8 doesn't have a proper mapping, and is a Rust implementation detail. | ||
| # We map it to plain uint8_t in the C header, since it has the same | ||
| # representation. | ||
| "AtomicU8" = "uint8_t" | ||
|
|
||
| [export.mangle] | ||
| rename_types = "PascalCase" | ||
|
|
||
| [enum] | ||
| prefix_with_name = true | ||
| rename_variants = "ScreamingSnakeCase" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //! FFI bindings for the OTel thread-level context publisher. | ||
| //! | ||
| //! All symbols are only available on Linux, since the TLSDESC TLS mechanism | ||
| //! required by the spec is Linux-specific. | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| pub use linux::*; | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| mod linux { | ||
| use libdd_otel_thread_ctx::linux::{ThreadContext, ThreadContextRecord}; | ||
| use std::ptr::NonNull; | ||
|
|
||
| /// Allocate and initialise a new thread context. | ||
| /// | ||
| /// Returns a non-null owned handle that must eventually be released with | ||
| /// `ddog_otel_thread_ctx_free`. | ||
| #[no_mangle] | ||
| pub extern "C" fn ddog_otel_thread_ctx_new( | ||
| trace_id: &[u8; 16], | ||
| span_id: &[u8; 8], | ||
| local_root_span_id: &[u8; 8], | ||
| ) -> NonNull<ThreadContextRecord> { | ||
| ThreadContext::new(*trace_id, *span_id, *local_root_span_id, &[]).into_ptr() | ||
| } | ||
|
|
||
| /// Free an owned thread context. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// `ctx` must be a valid non-null pointer obtained from `ddog_otel_thread_ctx_new` or | ||
| /// `ddog_otel_thread_ctx_detach`, and must not be used after this call. In particular, `ctx` | ||
| /// must not be currently attached to a thread. | ||
| #[no_mangle] | ||
| pub unsafe extern "C" fn ddog_otel_thread_ctx_free(ctx: *mut ThreadContextRecord) { | ||
| if let Some(ctx) = NonNull::new(ctx) { | ||
| let _ = ThreadContext::from_ptr(ctx); | ||
| } | ||
| } | ||
|
|
||
| /// Attach `ctx` to the current thread. Returns the previously attached context if any, or null | ||
| /// otherwise. | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// `ctx` must be a valid non-null pointer obtained from this API. Ownership of `ctx` is | ||
| /// transferred to the TLS slot: the caller must not drop `ctx` while it is still actively | ||
| /// attached. | ||
| /// | ||
| /// ## In-place update | ||
| /// | ||
| /// The preferred method to update the thread context in place is [ddog_otel_thread_ctx_update]. | ||
| /// | ||
| /// If calling into native code is too costly, it is possible to update an attached context | ||
| /// directly in-memory without going through libdatadog (contexts are guaranteed to have a | ||
| /// stable address through their lifetime). **HOWEVER, IF DOING SO, PLEASE BE VERY CAUTIOUS OF | ||
| /// THE FOLLOWING POINTS**: | ||
| /// | ||
| /// 1. The update process requires a [seqlock](https://en.wikipedia.org/wiki/Seqlock)-like | ||
| /// pattern: [ThreadContextRecord::valid] must be first set to `0` before the update and set | ||
| /// to `1` again at the end. Additionally, depending on your language's memory model, you | ||
| /// might need specific synchronization primitives (compiler fences, atomics, etc.), since | ||
| /// the context can be read by an asynchronous signal handler at any point in time. See the | ||
| /// [Otel thread context | ||
| /// specification](https://github.com/open-telemetry/opentelemetry-specification/pull/4947) | ||
| /// for more details. | ||
| /// 2. Only update the context from the thread it's attached to. Contexts are designed to be | ||
| /// attached, written to and read from on the same thread (whether from signal code or | ||
| /// program code). Thus, they are NOT thread-safe. Given the current specification, I don't | ||
| /// think it's possible to safely update an attached context from a different thread, since | ||
| /// the signal handler doesn't assume the context can be written to concurrently from another | ||
| /// thread. | ||
| #[no_mangle] | ||
| pub unsafe extern "C" fn ddog_otel_thread_ctx_attach( | ||
| ctx: *mut ThreadContextRecord, | ||
| ) -> Option<NonNull<ThreadContextRecord>> { | ||
| ThreadContext::from_ptr(NonNull::new(ctx)?) | ||
| .attach() | ||
| .map(ThreadContext::into_ptr) | ||
| } | ||
|
|
||
| /// Remove the currently attached context from the TLS slot. | ||
| /// | ||
| /// Returns the detached context (caller now owns it and must release it with | ||
| /// `ddog_otel_thread_ctx_free`), or null if the slot was empty. | ||
| #[no_mangle] | ||
| pub extern "C" fn ddog_otel_thread_ctx_detach() -> Option<NonNull<ThreadContextRecord>> { | ||
| ThreadContext::detach().map(ThreadContext::into_ptr) | ||
| } | ||
|
|
||
| /// Update the currently attached context in-place. | ||
| /// | ||
| /// If no context is currently attached, one is created and attached, equivalent to calling | ||
| /// `ddog_otel_thread_ctx_new` followed by `ddog_otel_thread_ctx_attach`. | ||
| #[no_mangle] | ||
| pub extern "C" fn ddog_otel_thread_ctx_update( | ||
| trace_id: &[u8; 16], | ||
| span_id: &[u8; 8], | ||
| local_root_span_id: &[u8; 8], | ||
| ) { | ||
| ThreadContext::update(*trace_id, *span_id, *local_root_span_id, &[]); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| //! Verify ELF properties of the built cdylib on Linux. | ||
| //! | ||
| //! These tests check that: | ||
| //! - `otel_thread_ctx_v1` is exported in the dynamic symbol table as a TLS GLOBAL symbol. | ||
| //! - `otel_thread_ctx_v1` is accessed via TLSDESC relocations (R_X86_64_TLSDESC or | ||
| //! R_AARCH64_TLSDESC), as required by the OTel thread-level context sharing spec. | ||
| //! | ||
| //! The cdylib path is injected at compile time via `build.rs` from `OUT_DIR`. | ||
|
|
||
| #![cfg(target_os = "linux")] | ||
|
|
||
| use std::path::PathBuf; | ||
| use std::process::Command; | ||
|
|
||
| const SYMBOL: &str = "otel_thread_ctx_v1"; | ||
|
|
||
| fn cdylib_path() -> PathBuf { | ||
| PathBuf::from(env!("CDYLIB_PROFILE_DIR")).join("liblibdd_otel_thread_ctx_ffi.so") | ||
| } | ||
|
|
||
| fn readelf(args: &[&str], path: &PathBuf) -> String { | ||
| let out = Command::new("readelf") | ||
| .args(args) | ||
| .arg(path) | ||
| .output() | ||
| .expect("failed to run readelf — is binutils installed?"); | ||
| String::from_utf8_lossy(&out.stdout).into_owned() | ||
| } | ||
|
|
||
| #[test] | ||
| fn otel_thread_ctx_v1_in_dynsym() { | ||
| let path = cdylib_path(); | ||
| let output = readelf(&["-W", "--dyn-syms"], &path); | ||
| let line = output | ||
| .lines() | ||
| .find(|l| l.contains(SYMBOL)) | ||
| .unwrap_or_else(|| panic!("'{SYMBOL}' not found in dynsym of {}", path.display())); | ||
| assert!( | ||
| line.contains("TLS") && line.contains("GLOBAL"), | ||
| "'{SYMBOL}' is in dynsym but not as TLS GLOBAL — got:\n {line}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn otel_thread_ctx_v1_tlsdesc_reloc() { | ||
| let path = cdylib_path(); | ||
| let output = readelf(&["-W", "--relocs"], &path); | ||
| let found = output.lines().any(|l| { | ||
| l.contains(SYMBOL) && (l.contains("R_X86_64_TLSDESC") || l.contains("R_AARCH64_TLSDESC")) | ||
| }); | ||
| assert!( | ||
| found, | ||
| "No TLSDESC relocation found for '{SYMBOL}' in {}\n\ | ||
| All relocations mentioning the symbol:\n{}", | ||
| path.display(), | ||
| output | ||
| .lines() | ||
| .filter(|l| l.contains(SYMBOL)) | ||
| .map(|l| format!(" {l}")) | ||
| .collect::<Vec<_>>() | ||
| .join("\n") | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| global: otel_thread_ctx_v1; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
During
cargo test, Cargo emitscdylibartifacts undertarget/<profile>/deps, butcdylib_path()currently looks intarget/<profile>. That meansreadelfis invoked on a non-existent file in normal test runs, so these new ELF-property tests fail even when the library is built correctly. Resolve the path from thedepsdirectory (or otherwise discover the real artifact path) so the assertions run against the actual.so.Useful? React with 👍 / 👎.