From 1b9a82c3c9066639b830f54b215f7e2c2d7c7a2e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 11:55:42 +0200 Subject: [PATCH 01/18] feat: cross-language LTO to inline C TLS shim into Rust FFI Add an opt-in inline mode via the LIBDD_OTEL_THREAD_CTX_INLINE env var that uses cross-language LTO (clang + lld) to inline the C TLS shim directly into the Rust FFI functions, eliminating a function-call indirection on every TLS access. When the env var is set, build.rs validates that clang and a suitable LLD are available, compiles the C shim as LLVM bitcode (-flto=thin), and emits the linker flags needed for cross-language LTO. A companion build-optimized.sh wrapper sets the target-scoped RUSTFLAGS and env var. When the env var is absent, the previous behavior is preserved: the default cc compiles the shim with -mtls-dialect=gnu2 on x86-64, no LTO, no inlining. Also fixes #[cfg(target_os/arch)] in build scripts to use the correct CARGO_CFG_* env vars for cross-compilation correctness. Co-Authored-By: Claude Opus 4.6 --- build-optimized.sh | 45 ++++++++++++++ libdd-otel-thread-ctx-ffi/build.rs | 95 ++++++++++++++++++++++++------ libdd-otel-thread-ctx/build.rs | 84 +++++++++++++++++++++----- 3 files changed, 191 insertions(+), 33 deletions(-) create mode 100755 build-optimized.sh diff --git a/build-optimized.sh b/build-optimized.sh new file mode 100755 index 0000000000..28bb1275cf --- /dev/null +++ b/build-optimized.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 +# +# Build libdd-otel-thread-ctx-ffi with cross-language LTO so the C TLS shim is +# inlined into the Rust FFI functions, eliminating a function-call indirection +# on every TLS access. +# +# Requirements: clang, lld (rust-lld from the toolchain is used automatically). +# +# Usage: +# ./build-optimized.sh # auto-detect host triple +# ./build-optimized.sh --target aarch64-unknown-linux-gnu # explicit target +# +# Any extra arguments are forwarded to `cargo build`. +set -euo pipefail + +# Parse --target from args, or auto-detect the host triple. +TARGET="" +EXTRA_ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --target) + TARGET="$2"; shift 2 ;; + --target=*) + TARGET="${1#--target=}"; shift ;; + *) + EXTRA_ARGS+=("$1"); shift ;; + esac +done + +if [[ -z "$TARGET" ]]; then + TARGET=$(rustc -vV | sed -n 's/host: //p') +fi + +# CARGO_TARGET__RUSTFLAGS scopes the flags to the target only, keeping +# build scripts and proc-macros unaffected. +TARGET_ENV=$(echo "$TARGET" | tr 'a-z-' 'A-Z_') +export "CARGO_TARGET_${TARGET_ENV}_RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang" +export LIBDD_OTEL_THREAD_CTX_INLINE=1 + +exec cargo build --release \ + --target "$TARGET" \ + -p libdd-otel-thread-ctx-ffi \ + "${EXTRA_ARGS[@]}" diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 312820969b..5cc2482ec3 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -13,9 +13,9 @@ use std::process::Command; /// 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. +/// 1. Avoids the need for a system-wide LLD install. +/// 2. Picks a recent LLD, as opposed to e.g. CentOS 7's LLVM 7 which is too +/// old to handle TLSDESC relocations properly. fn find_rust_lld_dir() -> Option { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let target = env::var("TARGET").ok()?; @@ -35,30 +35,87 @@ fn find_rust_lld_dir() -> Option { dir.join("ld.lld").exists().then_some(dir) } +/// Parse the major version from `ld.lld --version` output. +/// +/// Typical formats: +/// "LLD 18.1.3 (compatible with GNU linkers)" +/// "LLD 19.1.0" +fn system_lld_major_version() -> Option { + let output = Command::new("ld.lld").arg("--version").output().ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); + text.split_whitespace() + .find_map(|tok| tok.split('.').next()?.parse::().ok()) +} + +const MIN_LLD_VERSION_FOR_TLSDESC: u32 = 18; + +/// Validate that a suitable LLD is available for cross-language LTO. +/// +/// Returns the rust-lld `gcc-ld/` directory if found; `None` means the system +/// `ld.lld` will be used instead. Panics with a clear message when the +/// requirements are not met. +fn require_lld_for_inline(target_arch: &str) -> Option { + if let Some(dir) = find_rust_lld_dir() { + return Some(dir); + } + + match system_lld_major_version() { + Some(v) if target_arch != "x86_64" || v >= MIN_LLD_VERSION_FOR_TLSDESC => None, + Some(v) => panic!( + "LIBDD_OTEL_THREAD_CTX_INLINE requires LLD >= {MIN_LLD_VERSION_FOR_TLSDESC} on \ + x86-64 (for -mllvm -enable-tlsdesc), but system ld.lld is version {v}. \ + Install a newer LLD or use a Rust toolchain that bundles rust-lld." + ), + None => panic!( + "LIBDD_OTEL_THREAD_CTX_INLINE requires LLD for cross-language LTO, but neither \ + rust-lld nor a system ld.lld was found." + ), + } +} + fn main() { generate_and_configure_header("otel-thread-ctx.h"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os != "linux" { + return; + } + + println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); + + let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some(); + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").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 inline_mode { + let rust_lld_dir = require_lld_for_inline(&target_arch); + // Emit link args for ALL link types (not just cdylib) so that test + // binaries also link correctly when RUSTFLAGS sets clang as the linker. + if let Some(dir) = rust_lld_dir { + println!("cargo:rustc-link-arg=-B{}", dir.display()); + } + println!("cargo:rustc-link-arg=-fuse-ld=lld"); + + // On x86-64, tell the LLVM backend to use TLSDESC during LTO codegen. + // On aarch64 TLSDESC is the only model, so no flag is needed. + if target_arch == "x86_64" { + println!("cargo:rustc-link-arg=-Wl,-mllvm,-enable-tlsdesc"); + } + } else { + // Default mode: only the cdylib needs lld (for the version script). 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" - ); } + + // Version script exports the TLS symbol to the dynamic symbol table so + // external readers (eBPF profiler) can discover it. + println!( + "cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt" + ); } diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs index 0eb1e232c4..44f0cfb677 100644 --- a/libdd-otel-thread-ctx/build.rs +++ b/libdd-otel-thread-ctx/build.rs @@ -1,20 +1,76 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use std::env; +use std::path::PathBuf; +use std::process::Command; + +fn clang_is_available() -> bool { + Command::new("clang") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Locate the `gcc-ld/` shim directory shipped with the Rust toolchain. +fn find_rust_lld_dir() -> Option { + 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() { - // Only compile the TLS shim on Linux. - #[cfg(target_os = "linux")] - { - let mut build = cc::Build::new(); - - // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. - // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force - // the use of TLSDESC as mandated by the spec. If it's not supported, this build will - // fail. - #[cfg(target_arch = "x86_64")] - build.flag("-mtls-dialect=gnu2"); - - build.file("src/tls_shim.c").compile("tls_shim"); - println!("cargo:rerun-if-changed=src/tls_shim.c"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + + if target_os != "linux" { + return; + } + + println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); + println!("cargo:rerun-if-changed=src/tls_shim.c"); + + let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some(); + + let mut build = cc::Build::new(); + + if inline_mode { + assert!( + clang_is_available(), + "LIBDD_OTEL_THREAD_CTX_INLINE is set but `clang` was not found. \ + Cross-language LTO requires clang as the C compiler." + ); + build.compiler("clang"); + build.flag("-flto=thin"); + + // Any binary linking this crate in inline mode (including test + // binaries) needs lld, because -Clinker-plugin-lto passes LTO plugin + // options that only lld understands. + if let Some(dir) = find_rust_lld_dir() { + println!("cargo:rustc-link-arg=-B{}", dir.display()); + } + println!("cargo:rustc-link-arg=-fuse-ld=lld"); + } else { + // On x86-64, force TLSDESC via -mtls-dialect=gnu2 (GCC 4.4+, Clang 19+). + // On aarch64, TLSDESC is the only dynamic TLS model so no flag is needed. + if target_arch == "x86_64" { + build.flag("-mtls-dialect=gnu2"); + } } + + build.file("src/tls_shim.c").compile("tls_shim"); } From 15ad936da897538ef1002499b5bc1bc6b5d0e89c Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 12:04:11 +0200 Subject: [PATCH 02/18] doc: add README for the FFI crate Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/README.md | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 libdd-otel-thread-ctx-ffi/README.md diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md new file mode 100644 index 0000000000..47d15bd71c --- /dev/null +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -0,0 +1,38 @@ +# libdd-otel-thread-ctx-ffi + +FFI bindings for the OTel thread-level context publisher. Exposes a C API +for attaching, detaching, and updating per-thread OpenTelemetry context records +that external readers (e.g. the eBPF profiler) can discover via the dynamic +symbol table. + +Currently Linux-only (x86-64 and aarch64). + +## Building + +### Default build + +```bash +cargo build --release -p libdd-otel-thread-ctx-ffi +``` + +The C TLS shim is compiled with the system `cc` (gcc or clang). On x86-64, +`-mtls-dialect=gnu2` forces TLSDESC. No cross-language inlining occurs. + +### Optimized build (cross-language LTO) + +```bash +./build-optimized.sh +``` + +This sets `LIBDD_OTEL_THREAD_CTX_INLINE=1` and the appropriate target-scoped +`RUSTFLAGS`, enabling cross-language LTO so the C TLS shim is inlined directly +into the Rust FFI functions. Requires `clang` and `lld` (the toolchain's +bundled `rust-lld` is used automatically when available). + +The script auto-detects the host triple. To cross-compile: + +```bash +./build-optimized.sh --target aarch64-unknown-linux-gnu +``` + +Extra arguments are forwarded to `cargo build`. From 08ca7aed99beb98f0d9c591670964b43a60f2d13 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 12:19:26 +0200 Subject: [PATCH 03/18] chore: move script to appropriate crate --- .../build-optimized.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename build-optimized.sh => libdd-otel-thread-ctx-ffi/build-optimized.sh (100%) diff --git a/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh similarity index 100% rename from build-optimized.sh rename to libdd-otel-thread-ctx-ffi/build-optimized.sh From e6d27c2614ec516d0ea80ae53abd4369178b2558 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 12:27:25 +0200 Subject: [PATCH 04/18] doc: improve otel thread ctx ffi README Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/README.md | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index 47d15bd71c..64d6477664 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -1,34 +1,38 @@ # libdd-otel-thread-ctx-ffi -FFI bindings for the OTel thread-level context publisher. Exposes a C API -for attaching, detaching, and updating per-thread OpenTelemetry context records -that external readers (e.g. the eBPF profiler) can discover via the dynamic -symbol table. +FFI bindings for the OTel thread-level context publisher. Exposes a C API for +attaching, detaching, and updating per-thread OpenTelemetry context records +that external readers (e.g. the eBPF profiler) can discover. Currently Linux-only (x86-64 and aarch64). -## Building +## Optimized build (cross-language inlining) -### Default build +The OTel thread-level conext sharing specification requires the use of the +TLSDESC dialect for the thread-local variable that holds the current context. +Because (stable) `rustc` doesn't currently provide a way to control the TLS +dialect, we need to use a small C shim that defines the variable and expose a +one-line getter. This unfortunately adds one level of indirection (a function +call) when attaching or detaching a context. -```bash -cargo build --release -p libdd-otel-thread-ctx-ffi -``` +With the right toolchain, it's possible to use Link-Time Optimization (LTO) to +inline the C wrapper at link time. The requirements are: -The C TLS shim is compiled with the system `cc` (gcc or clang). On x86-64, -`-mtls-dialect=gnu2` forces TLSDESC. No cross-language inlining occurs. +- `clang` is available to compile the C shim to LLVM IR (version requirements + aren't clear -- tested with clang18 and clang20, but ideally the version + should be the same or close to the LLVM version shipped with `rustc`) +- Either the Rust toolchain ships lld or there's a system-wide lld install + (Rust ships `rust-lld` for a long time, something like since 1.53+, however + some musl-based distro like Alpine might have Rust packages without LLD) +- lld version is at least 19 (TLSDESC support) -### Optimized build (cross-language LTO) +If those requirements are met, you can use the small wrapper provided in this +directory to build an optimized release version where the C shim is inlined. ```bash ./build-optimized.sh ``` -This sets `LIBDD_OTEL_THREAD_CTX_INLINE=1` and the appropriate target-scoped -`RUSTFLAGS`, enabling cross-language LTO so the C TLS shim is inlined directly -into the Rust FFI functions. Requires `clang` and `lld` (the toolchain's -bundled `rust-lld` is used automatically when available). - The script auto-detects the host triple. To cross-compile: ```bash From 7d565111ff59343ceecf30613a9c9d923ce47cd9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 12:27:47 +0200 Subject: [PATCH 05/18] feat: add post-build sanity check to build-optimized.sh After a successful build, verify with nm that the C TLS shim symbol was actually inlined away. Warns and exits 1 if inlining failed, or warns (but succeeds) if nm is not available. Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/build-optimized.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index 28bb1275cf..4be4a668a7 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -39,7 +39,22 @@ TARGET_ENV=$(echo "$TARGET" | tr 'a-z-' 'A-Z_') export "CARGO_TARGET_${TARGET_ENV}_RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang" export LIBDD_OTEL_THREAD_CTX_INLINE=1 -exec cargo build --release \ +cargo build --release \ --target "$TARGET" \ -p libdd-otel-thread-ctx-ffi \ "${EXTRA_ARGS[@]}" + +# Sanity-check that the C shim was actually inlined. +if ! command -v nm &>/dev/null; then + echo >&2 "WARNING: nm not found — skipping sanity check that the C TLS shim was inlined." +else + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + SO="$REPO_ROOT/target/$TARGET/release/liblibdd_otel_thread_ctx_ffi.so" + + if [[ -f "$SO" ]] && nm "$SO" 2>/dev/null | grep -q 'libdd_get_otel_thread_ctx'; then + echo >&2 "WARNING: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." + echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are compatible with the Rust toolchain's LLVM." + exit 1 + fi +fi From 87cff7be3dca3bc008766ad6b0659faf6acfb343 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 14:50:57 +0200 Subject: [PATCH 06/18] doc: explain why the wrapper script is needed Co-Authored-By: Claude Opus 4.6 --- libdd-otel-thread-ctx-ffi/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index 64d6477664..1e6ca4c541 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -26,8 +26,14 @@ inline the C wrapper at link time. The requirements are: some musl-based distro like Alpine might have Rust packages without LLD) - lld version is at least 19 (TLSDESC support) -If those requirements are met, you can use the small wrapper provided in this -directory to build an optimized release version where the C shim is inlined. +If those requirements are met, you can use the small wrapper script provided in +this directory to build an optimized release version where the C shim is +inlined. A wrapper script is needed because cross-language LTO requires two +`rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that cannot +be set from a Cargo build script — they must come from `RUSTFLAGS` or +`.cargo/config.toml`. The script sets them via the target-scoped +`CARGO_TARGET__RUSTFLAGS` env var so they don't leak to build scripts +or proc-macros. ```bash ./build-optimized.sh From 0692dbf5aa0507a9ce7250de628a012ee29b1761 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 15:17:58 +0200 Subject: [PATCH 07/18] chore: self-review misc fixes --- libdd-otel-thread-ctx-ffi/README.md | 8 ++++---- libdd-otel-thread-ctx-ffi/build-optimized.sh | 5 +++-- libdd-otel-thread-ctx-ffi/build.rs | 13 ++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index 1e6ca4c541..d19299c7dc 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -21,16 +21,16 @@ inline the C wrapper at link time. The requirements are: - `clang` is available to compile the C shim to LLVM IR (version requirements aren't clear -- tested with clang18 and clang20, but ideally the version should be the same or close to the LLVM version shipped with `rustc`) -- Either the Rust toolchain ships lld or there's a system-wide lld install +- Either the Rust toolchain ships `lld` or there's a system-wide `lld` install (Rust ships `rust-lld` for a long time, something like since 1.53+, however - some musl-based distro like Alpine might have Rust packages without LLD) -- lld version is at least 19 (TLSDESC support) + some musl-based distro like Alpine might have the Rust toolchain without LLD) +- `lld` version is at least 19 (TLSDESC support) If those requirements are met, you can use the small wrapper script provided in this directory to build an optimized release version where the C shim is inlined. A wrapper script is needed because cross-language LTO requires two `rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that cannot -be set from a Cargo build script — they must come from `RUSTFLAGS` or +be set from a Cargo build script: they must come from `RUSTFLAGS` or `.cargo/config.toml`. The script sets them via the target-scoped `CARGO_TARGET__RUSTFLAGS` env var so they don't leak to build scripts or proc-macros. diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index 4be4a668a7..3696bcf696 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -7,6 +7,7 @@ # on every TLS access. # # Requirements: clang, lld (rust-lld from the toolchain is used automatically). +# The requirements are checked by the build.rs script. # # Usage: # ./build-optimized.sh # auto-detect host triple @@ -44,9 +45,9 @@ cargo build --release \ -p libdd-otel-thread-ctx-ffi \ "${EXTRA_ARGS[@]}" -# Sanity-check that the C shim was actually inlined. +# Sanity-check that the C shim was actually inlined, if `nm` is available. if ! command -v nm &>/dev/null; then - echo >&2 "WARNING: nm not found — skipping sanity check that the C TLS shim was inlined." + echo >&2 "WARNING: skipping sanity check that the C TLS shim was inlined (\`nm\` not found)" else SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 5cc2482ec3..49a80da0f9 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -3,9 +3,7 @@ extern crate build_common; use build_common::generate_and_configure_header; -use std::env; -use std::path::PathBuf; -use std::process::Command; +use std::{env, path::PathBuf, process::Command}; /// Locate the `gcc-ld/` shim directory shipped with the Rust toolchain. /// @@ -86,22 +84,23 @@ fn main() { println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); - let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some(); + let inline_mode = env::var("LIBDD_OTEL_THREAD_CTX_INLINE").unwrap(); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - if inline_mode { + if &inline_mode == "1" { let rust_lld_dir = require_lld_for_inline(&target_arch); // Emit link args for ALL link types (not just cdylib) so that test - // binaries also link correctly when RUSTFLAGS sets clang as the linker. + // binaries also link correctly when RUSTFLAGS sets clang as the linker (although we should + // only build the shared object file in inline mode). if let Some(dir) = rust_lld_dir { println!("cargo:rustc-link-arg=-B{}", dir.display()); } println!("cargo:rustc-link-arg=-fuse-ld=lld"); // On x86-64, tell the LLVM backend to use TLSDESC during LTO codegen. - // On aarch64 TLSDESC is the only model, so no flag is needed. + // On aarch64 TLSDESC is the default and the only model. if target_arch == "x86_64" { println!("cargo:rustc-link-arg=-Wl,-mllvm,-enable-tlsdesc"); } From a4188ddfb3d8dba9d5410f9ceea337dbe419f2f9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 15:25:28 +0200 Subject: [PATCH 08/18] fix: wrong env variable handling --- libdd-otel-thread-ctx-ffi/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 49a80da0f9..18d8b57b64 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -84,11 +84,11 @@ fn main() { println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); - let inline_mode = env::var("LIBDD_OTEL_THREAD_CTX_INLINE").unwrap(); + let inline_mode = env::var("LIBDD_OTEL_THREAD_CTX_INLINE").is_ok_and(|v| v == "1"); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - if &inline_mode == "1" { + if inline_mode { let rust_lld_dir = require_lld_for_inline(&target_arch); // Emit link args for ALL link types (not just cdylib) so that test From 806545a88879d63d96cc176aaa6c5045e57d9ba9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 13 May 2026 15:55:30 +0200 Subject: [PATCH 09/18] refactor: move find_rust_lld_dir to build_common Deduplicate the function that locates the toolchain's bundled rust-lld by moving it into build_common, where both build scripts can reuse it. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 1 + build-common/src/lib.rs | 32 ++++++++++++++++++++++++++++++ libdd-otel-thread-ctx-ffi/build.rs | 30 +--------------------------- libdd-otel-thread-ctx/Cargo.toml | 1 + libdd-otel-thread-ctx/build.rs | 30 ++++++---------------------- 5 files changed, 41 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8ac24a225..8f10842d5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3154,6 +3154,7 @@ dependencies = [ name = "libdd-otel-thread-ctx" version = "1.0.0" dependencies = [ + "build_common", "cc", ] diff --git a/build-common/src/lib.rs b/build-common/src/lib.rs index 4d27ea5dd5..1037be0231 100644 --- a/build-common/src/lib.rs +++ b/build-common/src/lib.rs @@ -1,6 +1,10 @@ // Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use std::env; +use std::path::PathBuf; +use std::process::Command; + #[cfg(not(feature = "cbindgen"))] pub fn generate_and_configure_header(_header_name: &str) {} #[cfg(not(feature = "cbindgen"))] @@ -10,3 +14,31 @@ pub fn copy_and_configure_headers() {} mod cbindgen; #[cfg(feature = "cbindgen")] pub use crate::cbindgen::*; + +/// 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. Avoids the need for a system-wide LLD install. +/// 2. Picks a recent LLD, as opposed to e.g. CentOS 7's LLVM 7 which is too old to handle TLSDESC +/// relocations properly. +pub fn find_rust_lld_dir() -> Option { + 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) +} diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 18d8b57b64..81fec66207 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -2,37 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 extern crate build_common; -use build_common::generate_and_configure_header; +use build_common::{find_rust_lld_dir, generate_and_configure_header}; use std::{env, path::PathBuf, 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. Avoids the need for a system-wide LLD install. -/// 2. Picks a recent LLD, as opposed to e.g. CentOS 7's LLVM 7 which is too -/// old to handle TLSDESC relocations properly. -fn find_rust_lld_dir() -> Option { - 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) -} - /// Parse the major version from `ld.lld --version` output. /// /// Typical formats: diff --git a/libdd-otel-thread-ctx/Cargo.toml b/libdd-otel-thread-ctx/Cargo.toml index 7c4d1af083..bd8ce4c372 100644 --- a/libdd-otel-thread-ctx/Cargo.toml +++ b/libdd-otel-thread-ctx/Cargo.toml @@ -17,4 +17,5 @@ crate-type = ["lib"] bench = false [build-dependencies] +build_common = { path = "../build-common" } cc = "1.1.31" diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs index 44f0cfb677..b1c29d62d9 100644 --- a/libdd-otel-thread-ctx/build.rs +++ b/libdd-otel-thread-ctx/build.rs @@ -1,8 +1,8 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +extern crate build_common; use std::env; -use std::path::PathBuf; use std::process::Command; fn clang_is_available() -> bool { @@ -13,26 +13,6 @@ fn clang_is_available() -> bool { .unwrap_or(false) } -/// Locate the `gcc-ld/` shim directory shipped with the Rust toolchain. -fn find_rust_lld_dir() -> Option { - 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() { let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); @@ -60,13 +40,15 @@ fn main() { // Any binary linking this crate in inline mode (including test // binaries) needs lld, because -Clinker-plugin-lto passes LTO plugin // options that only lld understands. - if let Some(dir) = find_rust_lld_dir() { + if let Some(dir) = build_common::find_rust_lld_dir() { println!("cargo:rustc-link-arg=-B{}", dir.display()); } println!("cargo:rustc-link-arg=-fuse-ld=lld"); } else { - // On x86-64, force TLSDESC via -mtls-dialect=gnu2 (GCC 4.4+, Clang 19+). - // On aarch64, TLSDESC is the only dynamic TLS model so no flag is needed. + // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. + // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force + // the use of TLSDESC as mandated by the spec. If it's not supported, this build will + // fail. if target_arch == "x86_64" { build.flag("-mtls-dialect=gnu2"); } From 74353e163afb5b8776a9264b73c61c93d9c2f089 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 18 May 2026 11:43:14 +0200 Subject: [PATCH 10/18] chore: self-review --- build-common/src/lib.rs | 3 +- libdd-otel-thread-ctx-ffi/README.md | 17 +++--- libdd-otel-thread-ctx-ffi/build-optimized.sh | 8 ++- libdd-otel-thread-ctx-ffi/build.rs | 60 +++++++++++++++----- libdd-otel-thread-ctx/build.rs | 8 ++- 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/build-common/src/lib.rs b/build-common/src/lib.rs index 1037be0231..c0fa19eea7 100644 --- a/build-common/src/lib.rs +++ b/build-common/src/lib.rs @@ -22,8 +22,7 @@ pub use crate::cbindgen::*; /// before any system-wide lld, which /// /// 1. Avoids the need for a system-wide LLD install. -/// 2. Picks a recent LLD, as opposed to e.g. CentOS 7's LLVM 7 which is too old to handle TLSDESC -/// relocations properly. +/// 2. Picks a recent LLD that match the Rust toolchain's LLVM version pub fn find_rust_lld_dir() -> Option { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let target = env::var("TARGET").ok()?; diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index d19299c7dc..30286d4741 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -8,7 +8,7 @@ Currently Linux-only (x86-64 and aarch64). ## Optimized build (cross-language inlining) -The OTel thread-level conext sharing specification requires the use of the +The OTel thread-level context sharing specification requires the use of the TLSDESC dialect for the thread-local variable that holds the current context. Because (stable) `rustc` doesn't currently provide a way to control the TLS dialect, we need to use a small C shim that defines the variable and expose a @@ -22,18 +22,21 @@ inline the C wrapper at link time. The requirements are: aren't clear -- tested with clang18 and clang20, but ideally the version should be the same or close to the LLVM version shipped with `rustc`) - Either the Rust toolchain ships `lld` or there's a system-wide `lld` install - (Rust ships `rust-lld` for a long time, something like since 1.53+, however - some musl-based distro like Alpine might have the Rust toolchain without LLD) -- `lld` version is at least 19 (TLSDESC support) + (Rust has been shipping `rust-lld` for a long time now, something like since + 1.53+, however some musl-based distro like Alpine might have the Rust + toolchain without `rust-lld`) +- `lld` version is at least 18.1 (TLSDESC support) If those requirements are met, you can use the small wrapper script provided in this directory to build an optimized release version where the C shim is inlined. A wrapper script is needed because cross-language LTO requires two `rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that cannot be set from a Cargo build script: they must come from `RUSTFLAGS` or -`.cargo/config.toml`. The script sets them via the target-scoped -`CARGO_TARGET__RUSTFLAGS` env var so they don't leak to build scripts -or proc-macros. +`.cargo/config.toml`, which can't be entirely automated from Rust only. The +script sets them via the target-scoped `CARGO_TARGET__RUSTFLAGS` env +var so they don't leak to build scripts or proc-macros if cross-compiling. + +### Example usage ```bash ./build-optimized.sh diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index 3696bcf696..3dfcba4419 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -10,8 +10,10 @@ # The requirements are checked by the build.rs script. # # Usage: -# ./build-optimized.sh # auto-detect host triple -# ./build-optimized.sh --target aarch64-unknown-linux-gnu # explicit target +# # auto-detect host triple +# ./build-optimized.sh +# # explicit target +# ./build-optimized.sh --target aarch64-unknown-linux-gnu # # Any extra arguments are forwarded to `cargo build`. set -euo pipefail @@ -55,7 +57,7 @@ else if [[ -f "$SO" ]] && nm "$SO" 2>/dev/null | grep -q 'libdd_get_otel_thread_ctx'; then echo >&2 "WARNING: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." - echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are compatible with the Rust toolchain's LLVM." + echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are recent enough and compatible with the Rust toolchain's LLVM." exit 1 fi fi diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 81fec66207..5719554176 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -3,24 +3,46 @@ extern crate build_common; use build_common::{find_rust_lld_dir, generate_and_configure_header}; -use std::{env, path::PathBuf, process::Command}; +use std::{env, fmt::Display, path::PathBuf, process::Command}; -/// Parse the major version from `ld.lld --version` output. +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +struct LldVersion { + major: u32, + minor: u32, +} + +impl Display for LldVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +/// Parse the major and minor version from `ld.lld --version` output. /// /// Typical formats: /// "LLD 18.1.3 (compatible with GNU linkers)" /// "LLD 19.1.0" -fn system_lld_major_version() -> Option { +fn system_lld_version() -> Option { let output = Command::new("ld.lld").arg("--version").output().ok()?; if !output.status.success() { return None; } - let text = String::from_utf8_lossy(&output.stdout); - text.split_whitespace() - .find_map(|tok| tok.split('.').next()?.parse::().ok()) + String::from_utf8_lossy(&output.stdout) + .split_whitespace() + .find_map(|tok| { + let mut splitted = tok.split('.'); + let major = splitted.next()?.parse::().ok()?; + let minor = splitted.next()?.parse::().ok()?; + + Some(LldVersion { major, minor }) + }) } -const MIN_LLD_VERSION_FOR_TLSDESC: u32 = 18; +/// TLSDESC is supported in LLD from version 18.1. +const MIN_LLD_VERSION_FOR_TLSDESC: LldVersion = LldVersion { + major: 18, + minor: 1, +}; /// Validate that a suitable LLD is available for cross-language LTO. /// @@ -32,7 +54,7 @@ fn require_lld_for_inline(target_arch: &str) -> Option { return Some(dir); } - match system_lld_major_version() { + match system_lld_version() { Some(v) if target_arch != "x86_64" || v >= MIN_LLD_VERSION_FOR_TLSDESC => None, Some(v) => panic!( "LIBDD_OTEL_THREAD_CTX_INLINE requires LLD >= {MIN_LLD_VERSION_FOR_TLSDESC} on \ @@ -60,12 +82,26 @@ fn main() { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let target_arch = env::var("CARGO_CFG_TARGET_ARCH").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 `LIBDD_OTEL_THREAD_CTX_INLINE` is set to `1`, we try to inline the C shim. See the README + // for more details. if inline_mode { let rust_lld_dir = require_lld_for_inline(&target_arch); - // Emit link args for ALL link types (not just cdylib) so that test - // binaries also link correctly when RUSTFLAGS sets clang as the linker (although we should - // only build the shared object file in inline mode). + // Emit link args for ALL link types (not just cdylib) so that test binaries also link + // correctly when RUSTFLAGS sets clang as the linker (in practice we should only build/care + // about the shared object file in inline mode). if let Some(dir) = rust_lld_dir { println!("cargo:rustc-link-arg=-B{}", dir.display()); } @@ -84,8 +120,6 @@ fn main() { println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld"); } - // Version script exports the TLS symbol to the dynamic symbol table so - // external readers (eBPF profiler) can discover it. println!( "cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt" ); diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs index b1c29d62d9..bbffc0ad54 100644 --- a/libdd-otel-thread-ctx/build.rs +++ b/libdd-otel-thread-ctx/build.rs @@ -24,7 +24,10 @@ fn main() { println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); println!("cargo:rerun-if-changed=src/tls_shim.c"); - let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some(); + // The otel-thread-ctx FFI crate has a special flag to inline the C shim inside the final + // library. This setup has additional requirements for the build of this crate, which are + // enforced below when the flag is set. + let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some_and(|v| v == "1"); let mut build = cc::Build::new(); @@ -44,6 +47,9 @@ fn main() { println!("cargo:rustc-link-arg=-B{}", dir.display()); } println!("cargo:rustc-link-arg=-fuse-ld=lld"); + + // Note: in the inline setup, TLS dialect selection is handled by the linker and is taken + // care of by the build script of otel-thread-ctx-ffi } else { // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force From 537b9a135e64c87d35c9df22d9c1b51253edeee5 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 21 May 2026 12:35:20 +0200 Subject: [PATCH 11/18] doc: put back CentOS LLVM example Co-authored-by: Scott Gerring --- build-common/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-common/src/lib.rs b/build-common/src/lib.rs index c0fa19eea7..65d16dfc35 100644 --- a/build-common/src/lib.rs +++ b/build-common/src/lib.rs @@ -22,7 +22,7 @@ pub use crate::cbindgen::*; /// before any system-wide lld, which /// /// 1. Avoids the need for a system-wide LLD install. -/// 2. Picks a recent LLD that match the Rust toolchain's LLVM version +/// 2. Picks a recent LLD that matches the Rust toolchain's LLVM version, as opposed to e.g. CentOS 7' LLVM7 which is too old to handle TLSDESC pub fn find_rust_lld_dir() -> Option { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let target = env::var("TARGET").ok()?; From 9cfe9d0bd6fc88feeb5dfddf9209b8574903a085 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:09:47 +0200 Subject: [PATCH 12/18] doc: document optimized build without using the script --- libdd-otel-thread-ctx-ffi/README.md | 30 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/README.md b/libdd-otel-thread-ctx-ffi/README.md index 30286d4741..ae268d5770 100644 --- a/libdd-otel-thread-ctx-ffi/README.md +++ b/libdd-otel-thread-ctx-ffi/README.md @@ -27,16 +27,26 @@ inline the C wrapper at link time. The requirements are: toolchain without `rust-lld`) - `lld` version is at least 18.1 (TLSDESC support) -If those requirements are met, you can use the small wrapper script provided in -this directory to build an optimized release version where the C shim is -inlined. A wrapper script is needed because cross-language LTO requires two -`rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that cannot -be set from a Cargo build script: they must come from `RUSTFLAGS` or -`.cargo/config.toml`, which can't be entirely automated from Rust only. The -script sets them via the target-scoped `CARGO_TARGET__RUSTFLAGS` env -var so they don't leak to build scripts or proc-macros if cross-compiling. - -### Example usage +**If those requirements are met, setting the environment variables +`CARGO_TARGET__RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang` and +`LIBDD_OTEL_THREAD_CTX_INLINE=1` when calling to `cargo` will trigger the +optimized build where the C shim is inlined.** Here, `` is the target +triple in screaming snake case. + +External environment variables are needed because cross-language LTO requires +two `rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that +cannot be set from a Cargo build script: they must come from `RUSTFLAGS` or +`.cargo/config.toml`, which can't be entirely automated from Rust only. We +advise to set those flags via the target-scoped +`CARGO_TARGET__RUSTFLAGS` env var so they don't leak to build scripts +or proc-macros if cross-compiling. + +### Build script + +The `build-optimized.sh` wrapper script is provided as a convenience and as an +example. + +#### Usage ```bash ./build-optimized.sh From b9f5fcd75c76922e8bb9e671297582dd062e6526 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:20:40 +0200 Subject: [PATCH 13/18] feat: warn if sanity check can't be run --- libdd-otel-thread-ctx-ffi/build-optimized.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index 3dfcba4419..dd3f6bf63f 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -55,9 +55,13 @@ else REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SO="$REPO_ROOT/target/$TARGET/release/liblibdd_otel_thread_ctx_ffi.so" - if [[ -f "$SO" ]] && nm "$SO" 2>/dev/null | grep -q 'libdd_get_otel_thread_ctx'; then - echo >&2 "WARNING: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." - echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are recent enough and compatible with the Rust toolchain's LLVM." - exit 1 + if [[ -f "$SO" ]]; then + if ! NM_OUTPUT=$(nm "$SO" 2>&1); then + echo >&2 "WARNING: command \`nm\` failed on $SO. Skipping sanity check that the C TLS shim was inlined." + elif echo "$NM_OUTPUT" | grep -q 'libdd_get_otel_thread_ctx'; then + echo >&2 "WARNING: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." + echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are recent enough and compatible with the Rust toolchain's LLVM." + exit 1 + fi fi fi From 15d95329a74537972492f5a69faedf4d8dc604fd Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:26:31 +0200 Subject: [PATCH 14/18] fix: preserve RUST_FLAGS in the build script --- libdd-otel-thread-ctx-ffi/build-optimized.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index dd3f6bf63f..05c4e7b119 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -39,7 +39,9 @@ fi # CARGO_TARGET__RUSTFLAGS scopes the flags to the target only, keeping # build scripts and proc-macros unaffected. TARGET_ENV=$(echo "$TARGET" | tr 'a-z-' 'A-Z_') -export "CARGO_TARGET_${TARGET_ENV}_RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang" +FLAGS_VAR="CARGO_TARGET_${TARGET_ENV}_RUSTFLAGS" +EXISTING_FLAGS="${!FLAGS_VAR:-}" +export "$FLAGS_VAR=${EXISTING_FLAGS:+$EXISTING_FLAGS }-Clinker-plugin-lto -Clinker=clang" export LIBDD_OTEL_THREAD_CTX_INLINE=1 cargo build --release \ From a15526f7bc61dbf7675a6d46bed937c4e48bdfbe Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:36:50 +0200 Subject: [PATCH 15/18] build: panic on unsupported archs for otel-thread-ctx --- libdd-otel-thread-ctx/build.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs index bbffc0ad54..bdedc719e1 100644 --- a/libdd-otel-thread-ctx/build.rs +++ b/libdd-otel-thread-ctx/build.rs @@ -21,6 +21,13 @@ fn main() { return; } + if !matches!(target_arch.as_str(), "x86_64" | "aarch64") { + panic!( + "Unsupported architecture `{}` for otel-thread-ctx on Linux. Only x86_64 and aarch64 are currently supported.", + target_arch + ) + } + println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE"); println!("cargo:rerun-if-changed=src/tls_shim.c"); @@ -50,14 +57,12 @@ fn main() { // Note: in the inline setup, TLS dialect selection is handled by the linker and is taken // care of by the build script of otel-thread-ctx-ffi - } else { + } else if target == "x86_64" { // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force // the use of TLSDESC as mandated by the spec. If it's not supported, this build will // fail. - if target_arch == "x86_64" { - build.flag("-mtls-dialect=gnu2"); - } + build.flag("-mtls-dialect=gnu2"); } build.file("src/tls_shim.c").compile("tls_shim"); From 924a87889651f60a776611c0f6a8ef18316ac600 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:37:47 +0200 Subject: [PATCH 16/18] chore: fatal warning -> error --- libdd-otel-thread-ctx-ffi/build-optimized.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-otel-thread-ctx-ffi/build-optimized.sh b/libdd-otel-thread-ctx-ffi/build-optimized.sh index 05c4e7b119..b52fd657a1 100755 --- a/libdd-otel-thread-ctx-ffi/build-optimized.sh +++ b/libdd-otel-thread-ctx-ffi/build-optimized.sh @@ -61,7 +61,7 @@ else if ! NM_OUTPUT=$(nm "$SO" 2>&1); then echo >&2 "WARNING: command \`nm\` failed on $SO. Skipping sanity check that the C TLS shim was inlined." elif echo "$NM_OUTPUT" | grep -q 'libdd_get_otel_thread_ctx'; then - echo >&2 "WARNING: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." + echo >&2 "ERROR: build succeeded but the C TLS shim (libdd_get_otel_thread_ctx_v1) was NOT inlined." echo >&2 "Cross-language LTO may not be working. Check that clang and lld versions are recent enough and compatible with the Rust toolchain's LLVM." exit 1 fi From 31a7e9c4a687d154829e6ab1137d5b612891bfc8 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:39:26 +0200 Subject: [PATCH 17/18] refactor: require -> resolve --- libdd-otel-thread-ctx-ffi/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-otel-thread-ctx-ffi/build.rs b/libdd-otel-thread-ctx-ffi/build.rs index 5719554176..61fa63764b 100644 --- a/libdd-otel-thread-ctx-ffi/build.rs +++ b/libdd-otel-thread-ctx-ffi/build.rs @@ -49,7 +49,7 @@ const MIN_LLD_VERSION_FOR_TLSDESC: LldVersion = LldVersion { /// Returns the rust-lld `gcc-ld/` directory if found; `None` means the system /// `ld.lld` will be used instead. Panics with a clear message when the /// requirements are not met. -fn require_lld_for_inline(target_arch: &str) -> Option { +fn resolve_lld_for_inline(target_arch: &str) -> Option { if let Some(dir) = find_rust_lld_dir() { return Some(dir); } @@ -97,7 +97,7 @@ fn main() { // If `LIBDD_OTEL_THREAD_CTX_INLINE` is set to `1`, we try to inline the C shim. See the README // for more details. if inline_mode { - let rust_lld_dir = require_lld_for_inline(&target_arch); + let rust_lld_dir = resolve_lld_for_inline(&target_arch); // Emit link args for ALL link types (not just cdylib) so that test binaries also link // correctly when RUSTFLAGS sets clang as the linker (in practice we should only build/care From f83339bfaccd1efc8e23f4f69b37c60da4d85a73 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 26 May 2026 11:41:25 +0200 Subject: [PATCH 18/18] fix: auto-fix clippy issues Co-Authored-By: Claude --- build-common/src/lib.rs | 3 ++- libdd-otel-thread-ctx/build.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build-common/src/lib.rs b/build-common/src/lib.rs index 65d16dfc35..377bbe9b99 100644 --- a/build-common/src/lib.rs +++ b/build-common/src/lib.rs @@ -22,7 +22,8 @@ pub use crate::cbindgen::*; /// before any system-wide lld, which /// /// 1. Avoids the need for a system-wide LLD install. -/// 2. Picks a recent LLD that matches the Rust toolchain's LLVM version, as opposed to e.g. CentOS 7' LLVM7 which is too old to handle TLSDESC +/// 2. Picks a recent LLD that matches the Rust toolchain's LLVM version, as opposed to e.g. CentOS +/// 7' LLVM7 which is too old to handle TLSDESC pub fn find_rust_lld_dir() -> Option { let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); let target = env::var("TARGET").ok()?; diff --git a/libdd-otel-thread-ctx/build.rs b/libdd-otel-thread-ctx/build.rs index bdedc719e1..adfda34153 100644 --- a/libdd-otel-thread-ctx/build.rs +++ b/libdd-otel-thread-ctx/build.rs @@ -57,7 +57,7 @@ fn main() { // Note: in the inline setup, TLS dialect selection is handled by the linker and is taken // care of by the build script of otel-thread-ctx-ffi - } else if target == "x86_64" { + } else if target_arch == "x86_64" { // - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed. // - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force // the use of TLSDESC as mandated by the spec. If it's not supported, this build will