id: ffi-10 original_id: P.UNS.FFI.10 level: P impact: CRITICAL
Exported Rust Functions Must Be Designed for Thread-Safety
Summary
Functions exported to C with #[no_mangle] extern "C" may be called from multiple threads. Ensure they are thread-safe.
Rationale
- C code doesn't know about Rust's thread safety guarantees
- C may call your function from any thread
- Global state must be synchronized
- Race conditions are undefined behavior
Bad Example
// DON'T: Unsynchronized global state
static mut COUNTER: i32 = 0;
#[no_mangle]
pub extern "C" fn increment() -> i32 {
unsafe {
COUNTER += 1; // Data race if called from multiple threads!
COUNTER
}
}
// DON'T: Thread-local assuming single thread
thread_local! {
static CONFIG: RefCell<Config> = RefCell::new(Config::default());
}
#[no_mangle]
pub extern "C" fn set_config(value: i32) {
// Different threads get different configs!
// Is that what the C caller expects?
CONFIG.with(|c| c.borrow_mut().value = value);
}
// DON'T: Non-Send types in globals
static mut HANDLE: Option<Rc<Data>> = None; // Rc is not Send!
Good Example
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Mutex, OnceLock};
// DO: Use atomics for simple counters
static COUNTER: AtomicI32 = AtomicI32::new(0);
#[no_mangle]
pub extern "C" fn increment() -> i32 {
COUNTER.fetch_add(1, Ordering::SeqCst) + 1
}
// DO: Use Mutex for complex state
static CONFIG: OnceLock<Mutex<Config>> = OnceLock::new();
fn get_config() -> &'static Mutex<Config> {
CONFIG.get_or_init(|| Mutex::new(Config::default()))
}
#[no_mangle]
pub extern "C" fn set_config_value(value: i32) -> i32 {
match get_config().lock() {
Ok(mut config) => {
config.value = value;
0 // Success
}
Err(_) => -1 // Lock poisoned
}
}
// DO: Document thread safety requirements
/// Initializes the library. NOT thread-safe.
/// Must be called once from main thread before any other function.
#[no_mangle]
pub extern "C" fn init() -> i32 {
// One-time initialization
0
}
/// Processes data. Thread-safe.
/// May be called from multiple threads concurrently.
#[no_mangle]
pub extern "C" fn process(data: *const u8, len: usize) -> i32 {
// Uses only local state or synchronized globals
0
}
// DO: Make non-thread-safe APIs explicit
/// Handle for single-threaded use only.
///
/// # Thread Safety
///
/// This handle must only be used from the thread that created it.
struct SingleThreadHandle {
data: *mut Data,
_not_send: std::marker::PhantomData<*const ()>, // !Send
}
Synchronization Patterns
| Pattern | Use Case |
|---|---|
AtomicT | Simple counters, flags |
Mutex<T> | Complex shared state |
RwLock<T> | Read-heavy shared state |
OnceLock<T> | Lazy one-time init |
thread_local! | Per-thread state (document!) |
Checklist
- Does my exported function access global state?
- Is that state properly synchronized?
- Have I documented thread-safety guarantees?
- Are any types !Send/!Sync exposed across FFI?
Related Rules
ptr-01: Don't share raw pointers across threadssafety-05: Send/Sync implementation safety
你可能感兴趣的文章
Do Not Expose Raw Pointers in Public APIs
Unsafe Code Authors Must Verify Safety Invariants
Ensure Consistent Data Layout for Custom Types
Add Safety Section in Docs for Public Unsafe Functions
Do Not Use Union Variants Across Different Lifetimes
Use MaybeUninit
Ensure C-ABI Compatibility for Strings Between Rust and C
Do Not Dereference Pointers Cast to Misaligned Types
Use Dedicated Opaque Type Pointers Instead of c_void for C Opaque Types