Exported Rust Functions Must Be Designed for Thread-Safety

2026-08-30 浏览 (1)


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

PatternUse Case
AtomicTSimple 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 threads
  • safety-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 for Uninitialized Memory

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

Choose Appropriate Data Layout for Struct/Tuple/Enum

  • 所属分类: AI
  • 本文标签: rust
  • 版权声明: 本文链接 https://seaxiang.com/blog/hbPKYpC8