Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c3de318
feat(ffi): export TLSDESC TLS symbol to dynsym via version script
yannham Mar 27, 2026
3ea8a40
refactor: refine the FFI
yannham Apr 22, 2026
ed8f30f
fix(ffi): map atomic to u8 in the FFI
yannham Apr 23, 2026
746bb06
test: ensure the TLS setup follows the spec
yannham Apr 23, 2026
a8a30df
style: improve otel thread ctx FFI and its doc
yannham Apr 23, 2026
297367c
style: formatting
yannham Apr 23, 2026
d4ba525
chore: add CODEOWNERS entry for libdd-otel-thread-ctx-ffi
yannham Apr 23, 2026
1f3ba70
style: drop -> free
yannham Apr 23, 2026
926df87
fix: add missing line for new crates in dockerfile
yannham Apr 23, 2026
6309be4
test(ffi): verify TLSDESC relocation and dynsym export of otel TLS sy…
yannham Apr 23, 2026
535efeb
style: cosmetic changes
yannham Apr 23, 2026
349bbfc
test(ffi): assert cdylib exists and is readable before ELF inspection
yannham Apr 23, 2026
df6a26e
fix: missing constant in FFI
yannham Apr 23, 2026
f40fa65
test: fix lib finding code
yannham Apr 24, 2026
f7652d1
ci: install LLD for otel-thread-ctx test to run
yannham Apr 24, 2026
2f0c63f
ci: remove lld invocation
yannham Apr 24, 2026
bc48986
ci: tentative fix for missing ld command
yannham Apr 24, 2026
291f3f5
ci: tentative fix for missing ld command, part II
yannham Apr 24, 2026
f731619
ci: fix build error by relaxing need for system wide lld
yannham Apr 24, 2026
710408c
Revert "ci: fix build error by relaxing need for system wide lld"
yannham Apr 24, 2026
3d1a404
ci: we don't LLD v19+ but only 7+
yannham Apr 27, 2026
4f63ea2
doc: update misleading comment
yannham Apr 27, 2026
9cbed7f
doc: fix typo
yannham Apr 27, 2026
6ab65d9
refactor(ffi): hide ThreadContextRecord behind opaque FFI type
yannham May 7, 2026
5fac71e
doc: more precise wording
yannham May 7, 2026
c44022d
fix(ffi): use rust-lld instead of system lld for cdylib linking
yannham May 11, 2026
0464997
feat: prefer rust-lld over system-wide lld when available
yannham May 11, 2026
0ea5438
fix: actually hide ThreadContextRecord so it's not public
yannham May 11, 2026
d2c0b5e
ci: revert lld change, now that we use rust-lld
yannham May 11, 2026
35f80d0
style: formatting
yannham May 12, 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-sampling/ @DataDog/apm-common-components-core
libdd-shared-runtime*/ @DataDog/apm-common-components-core
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" }
64 changes: 64 additions & 0 deletions libdd-otel-thread-ctx-ffi/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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;
use std::path::PathBuf;
use std::process::Command;

/// Locate the `gcc-ld/` shim directory shipped with the Rust toolchain.
///
/// This directory contains an `ld.lld` wrapper that delegates to `rust-lld`.
/// Passing it via `-B` to the C compiler driver makes it discover rust-lld
/// before any system-wide lld, which
///
/// 1. Avoid the need of a system-wide LLD install
/// 2. Pick a recent LLD, as opposed to e.g. CentOS 7' LLVM7 which is too old to handle TLSDESC
/// relocations properly.
fn find_rust_lld_dir() -> Option<PathBuf> {
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
let target = env::var("TARGET").ok()?;

let output = Command::new(&rustc)
.arg("--print")
.arg("sysroot")
.output()
.ok()?;

let sysroot = std::str::from_utf8(&output.stdout).ok()?.trim();
let dir = PathBuf::from(sysroot)
.join("lib/rustlib")
.join(&target)
.join("bin/gcc-ld");

dir.join("ld.lld").exists().then_some(dir)
}

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 discover it. Rust's cdylib linker applies a version script with
// `local: *` that hides all symbols not explicitly allowlisted, and also causes lld to relax
// the TLSDESC access, 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 need lld. We prefer the
// toolchain's bundled rust-lld (LLD 19+ since Rust 1.84) over the system lld (if it even
// exists). If rust-lld is not found we fall back to whatever `lld` the system provides.
if target_os == "linux" {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

if let Some(gcc_ld_dir) = find_rust_lld_dir() {
println!("cargo:rustc-cdylib-link-arg=-B{}", gcc_ld_dir.display());
}
println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld");
println!(
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt"
);
}
}
29 changes: 29 additions & 0 deletions libdd-otel-thread-ctx-ffi/cbindgen.toml
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

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.mangle]
rename_types = "PascalCase"

[enum]
prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
94 changes: 94 additions & 0 deletions libdd-otel-thread-ctx-ffi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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, ThreadContextHandle};
use std::ptr::NonNull;

/// Maximum size in bytes of the `attrs_data` field of a thread context record.
// 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 `pub const MAX_ATTRS_DATA_SIZE = _MAX`
// (where `_MAX` has been imported properly) generates something dumb such as `#define
// ddog_MAX_ATTRS_DATA_SIZE = _MAX` instead of propagating the actual value.
// This solution is at least 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"
);

/// 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<ThreadContextHandle> {
ThreadContext::new(*trace_id, *span_id, *local_root_span_id, &[]).into_opaque_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 ThreadContextHandle) {
if let Some(ctx) = NonNull::new(ctx) {
let _ = ThreadContext::from_opaque_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.
#[no_mangle]
pub unsafe extern "C" fn ddog_otel_thread_ctx_attach(
ctx: *mut ThreadContextHandle,
) -> Option<NonNull<ThreadContextHandle>> {
ThreadContext::from_opaque_ptr(NonNull::new(ctx)?)
.attach()
.map(ThreadContext::into_opaque_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<ThreadContextHandle>> {
ThreadContext::detach().map(ThreadContext::into_opaque_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