Skip to content

Commit ee8bb2e

Browse files
committed
More robust way to inline thread local var resolution
1 parent 2842d90 commit ee8bb2e

9 files changed

Lines changed: 81 additions & 312 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libdd-otel-thread-ctx-ffi/README.md

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,8 @@ that external readers (e.g. the eBPF profiler) can discover.
66

77
Currently Linux-only (x86-64 and aarch64).
88

9-
## Optimized build (cross-language inlining)
9+
## TLS
1010

11-
The OTel thread-level context sharing specification requires the use of the
12-
TLSDESC dialect for the thread-local variable that holds the current context.
13-
Because (stable) `rustc` doesn't currently provide a way to control the TLS
14-
dialect, we need to use a small C shim that defines the variable and expose a
15-
one-line getter. This unfortunately adds one level of indirection (a function
16-
call) when attaching or detaching a context.
17-
18-
With the right toolchain, it's possible to use Link-Time Optimization (LTO) to
19-
inline the C wrapper at link time. The requirements are:
20-
21-
- `clang` is available to compile the C shim to LLVM IR (version requirements
22-
aren't clear -- tested with clang18 and clang20, but ideally the version
23-
should be the same or close to the LLVM version shipped with `rustc`)
24-
- Either the Rust toolchain ships `lld` or there's a system-wide `lld` install
25-
(Rust has been shipping `rust-lld` for a long time now, something like since
26-
1.53+, however some musl-based distro like Alpine might have the Rust
27-
toolchain without `rust-lld`)
28-
- `lld` version is at least 18.1 (TLSDESC support)
29-
30-
**If those requirements are met, setting the environment variables
31-
`CARGO_TARGET_<TARGET>_RUSTFLAGS=-Clinker-plugin-lto -Clinker=clang` and
32-
`LIBDD_OTEL_THREAD_CTX_INLINE=1` when calling to `cargo` will trigger the
33-
optimized build where the C shim is inlined.** Here, `<TARGET>` is the target
34-
triple in screaming snake case.
35-
36-
External environment variables are needed because cross-language LTO requires
37-
two `rustc` codegen flags (`-Clinker-plugin-lto` and `-Clinker=clang`) that
38-
cannot be set from a Cargo build script: they must come from `RUSTFLAGS` or
39-
`.cargo/config.toml`, which can't be entirely automated from Rust only. We
40-
advise to set those flags via the target-scoped
41-
`CARGO_TARGET_<TARGET>_RUSTFLAGS` env var so they don't leak to build scripts
42-
or proc-macros if cross-compiling.
43-
44-
### Build script
45-
46-
The `build-optimized.sh` wrapper script is provided as a convenience and as an
47-
example.
48-
49-
#### Usage
50-
51-
```bash
52-
./build-optimized.sh
53-
```
54-
55-
The script auto-detects the host triple. To cross-compile:
56-
57-
```bash
58-
./build-optimized.sh --target aarch64-unknown-linux-gnu
59-
```
60-
61-
Extra arguments are forwarded to `cargo build`.
11+
The thread-local variable `otel_thread_ctx_v1` and its TLSDESC accessor are
12+
implemented in pure Rust using `global_asm!` and `asm!` in the
13+
`libdd-otel-thread-ctx` crate.

libdd-otel-thread-ctx-ffi/build-optimized.sh

Lines changed: 0 additions & 69 deletions
This file was deleted.

libdd-otel-thread-ctx-ffi/build.rs

Lines changed: 4 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,7 @@
33
extern crate build_common;
44

55
use build_common::{find_rust_lld_dir, generate_and_configure_header};
6-
use std::{env, fmt::Display, path::PathBuf, process::Command};
7-
8-
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
9-
struct LldVersion {
10-
major: u32,
11-
minor: u32,
12-
}
13-
14-
impl Display for LldVersion {
15-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16-
write!(f, "{}.{}", self.major, self.minor)
17-
}
18-
}
19-
20-
/// Parse the major and minor version from `ld.lld --version` output.
21-
///
22-
/// Typical formats:
23-
/// "LLD 18.1.3 (compatible with GNU linkers)"
24-
/// "LLD 19.1.0"
25-
fn system_lld_version() -> Option<LldVersion> {
26-
let output = Command::new("ld.lld").arg("--version").output().ok()?;
27-
if !output.status.success() {
28-
return None;
29-
}
30-
String::from_utf8_lossy(&output.stdout)
31-
.split_whitespace()
32-
.find_map(|tok| {
33-
let mut splitted = tok.split('.');
34-
let major = splitted.next()?.parse::<u32>().ok()?;
35-
let minor = splitted.next()?.parse::<u32>().ok()?;
36-
37-
Some(LldVersion { major, minor })
38-
})
39-
}
40-
41-
/// TLSDESC is supported in LLD from version 18.1.
42-
const MIN_LLD_VERSION_FOR_TLSDESC: LldVersion = LldVersion {
43-
major: 18,
44-
minor: 1,
45-
};
46-
47-
/// Validate that a suitable LLD is available for cross-language LTO.
48-
///
49-
/// Returns the rust-lld `gcc-ld/` directory if found; `None` means the system
50-
/// `ld.lld` will be used instead. Panics with a clear message when the
51-
/// requirements are not met.
52-
fn resolve_lld_for_inline(target_arch: &str) -> Option<PathBuf> {
53-
if let Some(dir) = find_rust_lld_dir() {
54-
return Some(dir);
55-
}
56-
57-
match system_lld_version() {
58-
Some(v) if target_arch != "x86_64" || v >= MIN_LLD_VERSION_FOR_TLSDESC => None,
59-
Some(v) => panic!(
60-
"LIBDD_OTEL_THREAD_CTX_INLINE requires LLD >= {MIN_LLD_VERSION_FOR_TLSDESC} on \
61-
x86-64 (for -mllvm -enable-tlsdesc), but system ld.lld is version {v}. \
62-
Install a newer LLD or use a Rust toolchain that bundles rust-lld."
63-
),
64-
None => panic!(
65-
"LIBDD_OTEL_THREAD_CTX_INLINE requires LLD for cross-language LTO, but neither \
66-
rust-lld nor a system ld.lld was found."
67-
),
68-
}
69-
}
6+
use std::env;
707

718
fn main() {
729
generate_and_configure_header("otel-thread-ctx.h");
@@ -76,11 +13,7 @@ fn main() {
7613
return;
7714
}
7815

79-
println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE");
80-
81-
let inline_mode = env::var("LIBDD_OTEL_THREAD_CTX_INLINE").is_ok_and(|v| v == "1");
8216
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
83-
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
8417

8518
// Export the TLSDESC thread-local variable to the dynamic symbol table so external readers
8619
// (e.g. the eBPF profiler) can discover it. Rust's cdylib linker applies a version script with
@@ -93,32 +26,10 @@ fn main() {
9326
// Merging multiple version scripts is not supported by GNU ld, so we need lld. We prefer the
9427
// toolchain's bundled rust-lld (LLD 19+ since Rust 1.84) over the system lld (if it even
9528
// exists). If rust-lld is not found we fall back to whatever `lld` the system provides.
96-
97-
// If `LIBDD_OTEL_THREAD_CTX_INLINE` is set to `1`, we try to inline the C shim. See the README
98-
// for more details.
99-
if inline_mode {
100-
let rust_lld_dir = resolve_lld_for_inline(&target_arch);
101-
102-
// Emit link args for ALL link types (not just cdylib) so that test binaries also link
103-
// correctly when RUSTFLAGS sets clang as the linker (in practice we should only build/care
104-
// about the shared object file in inline mode).
105-
if let Some(dir) = rust_lld_dir {
106-
println!("cargo:rustc-link-arg=-B{}", dir.display());
107-
}
108-
println!("cargo:rustc-link-arg=-fuse-ld=lld");
109-
110-
// On x86-64, tell the LLVM backend to use TLSDESC during LTO codegen.
111-
// On aarch64 TLSDESC is the default and the only model.
112-
if target_arch == "x86_64" {
113-
println!("cargo:rustc-link-arg=-Wl,-mllvm,-enable-tlsdesc");
114-
}
115-
} else {
116-
// Default mode: only the cdylib needs lld (for the version script).
117-
if let Some(gcc_ld_dir) = find_rust_lld_dir() {
118-
println!("cargo:rustc-cdylib-link-arg=-B{}", gcc_ld_dir.display());
119-
}
120-
println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld");
29+
if let Some(gcc_ld_dir) = find_rust_lld_dir() {
30+
println!("cargo:rustc-cdylib-link-arg=-B{}", gcc_ld_dir.display());
12131
}
32+
println!("cargo:rustc-cdylib-link-arg=-fuse-ld=lld");
12233

12334
println!(
12435
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={manifest_dir}/tls-dynamic-list.txt"

libdd-otel-thread-ctx/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,3 @@ publish = false
1616
crate-type = ["lib"]
1717
bench = false
1818

19-
[build-dependencies]
20-
build_common = { path = "../build-common" }
21-
cc = "1.1.31"

libdd-otel-thread-ctx/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Linux only for now.
1515

1616
## TLS
1717

18-
The C shim (`src/tls_shim.c`) is required because `rustc` does not yet support
19-
the TLSDESC TLS dialect required by the spec to export `otel_thread_ctx_v1`.
20-
Since the reader and the writer must agree on the TLS dialect/model, we rely on
21-
the C compiler to emit the right access pattern.
18+
The TLS symbol `otel_thread_ctx_v1` and its TLSDESC accessor are defined
19+
directly in Rust using `global_asm!` and `asm!` (both stable since Rust 1.65 /
20+
1.59). This avoids a C build dependency while guaranteeing the TLSDESC dialect
21+
on both x86-64 and aarch64 as required by the spec.
2222

2323
## Usage
2424

libdd-otel-thread-ctx/build.rs

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
22
// SPDX-License-Identifier: Apache-2.0
3-
extern crate build_common;
43

54
use std::env;
6-
use std::process::Command;
7-
8-
fn clang_is_available() -> bool {
9-
Command::new("clang")
10-
.arg("--version")
11-
.output()
12-
.map(|o| o.status.success())
13-
.unwrap_or(false)
14-
}
155

166
fn main() {
177
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
@@ -27,43 +17,4 @@ fn main() {
2717
target_arch
2818
)
2919
}
30-
31-
println!("cargo:rerun-if-env-changed=LIBDD_OTEL_THREAD_CTX_INLINE");
32-
println!("cargo:rerun-if-changed=src/tls_shim.c");
33-
34-
// The otel-thread-ctx FFI crate has a special flag to inline the C shim inside the final
35-
// library. This setup has additional requirements for the build of this crate, which are
36-
// enforced below when the flag is set.
37-
let inline_mode = env::var_os("LIBDD_OTEL_THREAD_CTX_INLINE").is_some_and(|v| v == "1");
38-
39-
let mut build = cc::Build::new();
40-
41-
if inline_mode {
42-
assert!(
43-
clang_is_available(),
44-
"LIBDD_OTEL_THREAD_CTX_INLINE is set but `clang` was not found. \
45-
Cross-language LTO requires clang as the C compiler."
46-
);
47-
build.compiler("clang");
48-
build.flag("-flto=thin");
49-
50-
// Any binary linking this crate in inline mode (including test
51-
// binaries) needs lld, because -Clinker-plugin-lto passes LTO plugin
52-
// options that only lld understands.
53-
if let Some(dir) = build_common::find_rust_lld_dir() {
54-
println!("cargo:rustc-link-arg=-B{}", dir.display());
55-
}
56-
println!("cargo:rustc-link-arg=-fuse-ld=lld");
57-
58-
// Note: in the inline setup, TLS dialect selection is handled by the linker and is taken
59-
// care of by the build script of otel-thread-ctx-ffi
60-
} else if target_arch == "x86_64" {
61-
// - On aarch64, TLSDESC is already the only dynamic TLS model so no flag is needed.
62-
// - On x86-64, we use `-mtls-dialect=gnu2` (supported since GCC 4.4 and Clang 19+) to force
63-
// the use of TLSDESC as mandated by the spec. If it's not supported, this build will
64-
// fail.
65-
build.flag("-mtls-dialect=gnu2");
66-
}
67-
68-
build.file("src/tls_shim.c").compile("tls_shim");
6920
}

0 commit comments

Comments
 (0)