咨询:通过Rust FFI从C调用OpenTelemetry时保留Trace/Span引用的方法
解决Rust FFI中跨调用保留OpenTelemetry Trace/Span的问题
针对你遇到的跨FFI调用无法保留Trace/Span的问题,核心矛盾是OpenTelemetry的Tracer和Span属于Rust复杂类型,无法直接在C侧传递或存储。以下是三种实用的解决方案,兼顾内存安全和易用性:
方案1:用不透明指针包装实例
将Rust侧的Tracer和Span打包成*mut c_void(不透明指针)传递给C,C只需保存指针,后续FFI调用时回传即可。Rust侧负责类型转换和内存管理。
Rust侧代码
use opentelemetry::{trace::{Tracer, Span}, global}; use std::os::raw::c_void; use std::ptr; // 创建并返回Tracer的不透明指针 #[no_mangle] pub extern "C" fn otel_create_tracer() -> *mut c_void { let tracer = global::tracer("my-ffi-tracer"); Box::into_raw(Box::new(tracer)) as *mut c_void } // 通过Tracer指针启动Span,返回Span的不透明指针 #[no_mangle] pub extern "C" fn otel_start_span(tracer_ptr: *mut c_void, name: *const u8) -> *mut c_void { if tracer_ptr.is_null() || name.is_null() { return ptr::null_mut(); } // 临时取出Tracer,用完放回堆内存(避免提前释放) let tracer = unsafe { Box::from_raw(tracer_ptr as *mut dyn Tracer) }; let span_name = unsafe { std::ffi::CStr::from_ptr(name).to_str().unwrap_or("unknown") }; let span = tracer.start(span_name); let _ = Box::into_raw(tracer); Box::into_raw(Box::new(span)) as *mut c_void } // 结束Span并释放内存 #[no_mangle] pub extern "C" fn otel_end_span(span_ptr: *mut c_void) { if !span_ptr.is_null() { let mut span = unsafe { Box::from_raw(span_ptr as *mut dyn Span) }; span.end(); // Span会在此处自动drop释放 } } // 销毁Tracer并释放内存 #[no_mangle] pub extern "C" fn otel_destroy_tracer(tracer_ptr: *mut c_void) { if !tracer_ptr.is_null() { let _tracer = unsafe { Box::from_raw(tracer_ptr as *mut dyn Tracer) }; // Tracer会在此处自动drop释放 } }
C侧调用示例
#include <stdio.h> void* otel_create_tracer(); void* otel_start_span(void* tracer, const char* name); void otel_end_span(void* span); void otel_destroy_tracer(void* tracer); int main() { void* tracer = otel_create_tracer(); void* parent_span = otel_start_span(tracer, "main_process"); // 创建嵌套Span void* child_span = otel_start_span(tracer, "sub_task"); // 执行业务逻辑... otel_end_span(child_span); otel_end_span(parent_span); otel_destroy_tracer(tracer); return 0; }
方案2:全局存储+ID映射
在Rust侧维护全局哈希表,将Tracer和Span映射为整数ID,C侧只需传递ID即可。适合不想让C管理指针的场景。
Rust侧代码
use opentelemetry::{trace::{Tracer, Span}, global}; use lazy_static::lazy_static; use std::collections::HashMap; use std::sync::Mutex; use std::os::raw::c_int; // 全局线程安全存储 lazy_static! { static ref TRACERS: Mutex<HashMap<c_int, Box<dyn Tracer>>> = Mutex::new(HashMap::new()); static ref SPANS: Mutex<HashMap<c_int, Box<dyn Span>>> = Mutex::new(HashMap::new()); static ref NEXT_ID: Mutex<c_int> = Mutex::new(1); } // 生成唯一ID fn get_next_id() -> c_int { let mut id = NEXT_ID.lock().unwrap(); let current = *id; *id += 1; current } #[no_mangle] pub extern "C" fn otel_create_tracer() -> c_int { let tracer = global::tracer("my-ffi-tracer"); let id = get_next_id(); TRACERS.lock().unwrap().insert(id, Box::new(tracer)); id } #[no_mangle] pub extern "C" fn otel_start_span(tracer_id: c_int, name: *const u8) -> c_int { let span_name = unsafe { std::ffi::CStr::from_ptr(name).to_str().unwrap_or("unknown") }; let mut tracers = TRACERS.lock().unwrap(); if let Some(tracer) = tracers.get_mut(&tracer_id) { let span = tracer.start(span_name); let span_id = get_next_id(); SPANS.lock().unwrap().insert(span_id, Box::new(span)); span_id } else { -1 // 返回错误标识 } } #[no_mangle] pub extern "C" fn otel_end_span(span_id: c_int) { let mut spans = SPANS.lock().unwrap(); if let Some(mut span) = spans.remove(&span_id) { span.end(); } } #[no_mangle] pub extern "C" fn otel_destroy_tracer(tracer_id: c_int) { TRACERS.lock().unwrap().remove(&tracer_id); }
C侧调用示例
#include <stdio.h> int otel_create_tracer(); int otel_start_span(int tracer_id, const char* name); void otel_end_span(int span_id); void otel_destroy_tracer(int tracer_id); int main() { int tracer_id = otel_create_tracer(); int parent_id = otel_start_span(tracer_id, "main"); int child_id = otel_start_span(tracer_id, "nested_task"); // 执行操作... otel_end_span(child_id); otel_end_span(parent_id); otel_destroy_tracer(tracer_id); return 0; }
方案3:线程本地存储(TLS)
如果C代码是单线程或每个线程的Trace/Span独立,可使用Rust的thread_local!宏存储当前线程的Trace/Span上下文,无需C传递任何标识。
Rust侧代码
use opentelemetry::{trace::{Tracer, Span}, global}; use std::cell::RefCell; // 线程本地存储当前Tracer和Span thread_local! { static CURRENT_TRACER: RefCell<Option<Box<dyn Tracer>>> = RefCell::new(None); static CURRENT_SPAN: RefCell<Option<Box<dyn Span>>> = RefCell::new(None); } #[no_mangle] pub extern "C" fn otel_init_thread_tracer() { CURRENT_TRACER.with(|t| { *t.borrow_mut() = Some(Box::new(global::tracer("my-ffi-tracer"))); }); } #[no_mangle] pub extern "C" fn otel_start_current_span(name: *const u8) { let span_name = unsafe { std::ffi::CStr::from_ptr(name).to_str().unwrap_or("unknown") }; CURRENT_TRACER.with(|t| { if let Some(tracer) = t.borrow_mut().as_mut() { let span = tracer.start(span_name); CURRENT_SPAN.with(|s| { *s.borrow_mut() = Some(Box::new(span)); }); } }); } #[no_mangle] pub extern "C" fn otel_end_current_span() { CURRENT_SPAN.with(|s| { if let Some(mut span) = s.borrow_mut().take() { span.end(); } }); }
关键注意事项
- 内存安全:无论哪种方案,都必须确保资源被正确释放——指针方案要调用销毁函数,ID映射要移除无用条目,TLS会随线程退出自动清理,但长期运行线程需手动管理。
- 线程安全:多线程场景下,全局哈希表必须加锁;TLS方案天然支持多线程隔离。
- 上下文传递:若需要跨线程传递Trace/Span,可将OpenTelemetry的
Context也包装成不透明指针或ID,在目标线程恢复上下文。
内容的提问来源于stack exchange,提问作者MB14
相关产品推荐
相关产品推荐

