Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
67463a5
feat(ffi): export TLSDESC TLS symbol to dynsym via version script
yannham Mar 27, 2026
24a0b3b
refactor: refine the FFI
yannham Apr 22, 2026
7247d91
fix(ffi): map atomic to u8 in the FFI
yannham Apr 23, 2026
ed8bf0a
test: ensure the TLS setup follows the spec
yannham Apr 23, 2026
16b3240
style: improve otel thread ctx FFI and its doc
yannham Apr 23, 2026
a893b7c
style: formatting
yannham Apr 23, 2026
bf19209
chore: add CODEOWNERS entry for libdd-otel-thread-ctx-ffi
yannham Apr 23, 2026
918ab3b
style: drop -> free
yannham Apr 23, 2026
05868a5
fix: add missing line for new crates in dockerfile
yannham Apr 23, 2026
41b2e37
test(ffi): verify TLSDESC relocation and dynsym export of otel TLS sy…
yannham Apr 23, 2026
e6a7b10
style: cosmetic changes
yannham Apr 23, 2026
75add55
test(ffi): assert cdylib exists and is readable before ELF inspection
yannham Apr 23, 2026
2b39efc
fix: missing constant in FFI
yannham Apr 23, 2026
8ff86fa
test: fix lib finding code
yannham Apr 24, 2026
e131364
ci: install LLD for otel-thread-ctx test to run
yannham Apr 24, 2026
2acc549
ci: remove lld invocation
yannham Apr 24, 2026
d406bf8
ci: tentative fix for missing ld command
yannham Apr 24, 2026
3da7d3e
ci: tentative fix for missing ld command, part II
yannham Apr 24, 2026
abf827e
ci: fix build error by relaxing need for system wide lld
yannham Apr 24, 2026
2375640
Revert "ci: fix build error by relaxing need for system wide lld"
yannham Apr 24, 2026
9062b34
ci: we don't LLD v19+ but only 7+
yannham Apr 27, 2026
a0d46e0
doc: update misleading comment
yannham Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ libdd-http-client @DataDog/apm-common-components-core
libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust
libdd-log*/ @DataDog/apm-common-components-core
libdd-otel-thread-ctx/ @DataDog/apm-common-components-core
libdd-otel-thread-ctx-ffi/ @DataDog/apm-common-components-core
libdd-profiling*/ @DataDog/libdatadog-profiling
libdd-shared-runtime*/ @DataDog/apm-common-components-core
libdd-telemetry*/ @DataDog/apm-common-components-core
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ jobs:
large-packages: true
docker-images: false
swap-storage: true
# Building (and testing) libdd-otel-thread-ctx-ffi requires a custom
# linker step, which requires lld to be available.
- name: Install LLD
if: runner.os == 'Linux'
run: sudo apt-get install -y lld
- name: Checkout sources
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
with:
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"datadog-live-debugger",
"datadog-live-debugger-ffi",
"libdd-otel-thread-ctx",
"libdd-otel-thread-ctx-ffi",
"libdd-profiling",
"libdd-profiling-ffi",
"libdd-profiling-protobuf",
Expand Down
26 changes: 26 additions & 0 deletions libdd-otel-thread-ctx-ffi/Cargo.toml
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" }
29 changes: 29 additions & 0 deletions libdd-otel-thread-ctx-ffi/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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;
use std::env;

fn main() {
generate_and_configure_header("otel-thread-ctx.h");
let target_os = 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// not explicitly whitelisted, and also causes lld to relax the TLSDESC
// not explicitly allowlisted, 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 = 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"
);
}
}
35 changes: 35 additions & 0 deletions libdd-otel-thread-ctx-ffi/cbindgen.toml
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"
119 changes: 119 additions & 0 deletions libdd-otel-thread-ctx-ffi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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 spec is currently Linux-specific.

#[cfg(target_os = "linux")]
pub use linux::*;

#[cfg(target_os = "linux")]
mod linux {
use libdd_otel_thread_ctx::linux::{ThreadContext, ThreadContextRecord};

/// Maximum size in bytes of the `attrs_data` field of [`ddog_ThreadContextRecord`].
// This is ugly, but I couldn't get cbindgen to generate the corresponding #define in any other
// way. It doesn't like re-exports (pub use), and doing someting like `pub const
// MAX_ATTRS_DATA_SIZE = _MAX` (where `_MAX` has been imported properly), it generates something
// dumb such as `#define ddog_MAX_ATTRS_DATA_SIZE = _MAX` instead of propagating the actual
// value.
// This solution at leat marginally better than prepending a hardcoded define manually in
// build.rs, as it will at least keep the value in sync.
pub const MAX_ATTRS_DATA_SIZE: usize = 612;
const _: () = assert!(
MAX_ATTRS_DATA_SIZE == libdd_otel_thread_ctx::linux::MAX_ATTRS_DATA_SIZE,
"MAX_ATTRS_DATA_SIZE out of sync with libdd-otel-thread-ctx"
);
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, &[]);
}
}
84 changes: 84 additions & 0 deletions libdd-otel-thread-ctx-ffi/tests/elf_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 derived at runtime from the test executable location.
//! Both the test binary and the cdylib live in `target/<[triple/]profile>/deps/`.

#![cfg(target_os = "linux")]

use std::path::PathBuf;
use std::process::Command;

const SYMBOL: &str = "otel_thread_ctx_v1";

fn cdylib_path() -> PathBuf {
// test binary: target/<[triple/]profile>/deps/<name>
// cdylib: target/<[triple/]profile>/deps/liblibdd_otel_thread_ctx_ffi.so
let exe = std::env::current_exe().expect("failed to read current executable path");
exe.parent()
.expect("unexpected test executable path structure")
.join("liblibdd_otel_thread_ctx_ffi.so")
}

fn check_cdylib_readable(path: &PathBuf) {
assert!(
std::fs::File::open(path).is_ok(),
"cdylib at {} could not be opened for reading",
path.display()
);
}

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]
#[cfg_attr(miri, ignore)]
fn otel_thread_ctx_v1_in_dynsym() {
let path = cdylib_path();
check_cdylib_readable(&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]
#[cfg_attr(miri, ignore)]
fn otel_thread_ctx_v1_tlsdesc_reloc() {
let path = cdylib_path();
check_cdylib_readable(&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")
);
}
3 changes: 3 additions & 0 deletions libdd-otel-thread-ctx-ffi/tls-dynamic-list.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
global: otel_thread_ctx_v1;
};
Loading
Loading