Rust FFI and C Memory Model Integration: Shared Mutable State
0 reputation · 25 Sept 2020, 22:50 UTC
0 reputation · 25 Sept 2020, 22:50 UTC
Rust provides strict memory safety guarantees via its ownership and borrowing system. When interfacing with C through the Foreign Function Interface (FFI), developers utilize unsafe blocks and repr(C) attributes to ensure layout compatibility and allow raw pointer dereferencing.
While Rust prevents data races within its own memory model, the interaction between Rust's memory model and the memory models of foreign languages remains a complex integration point. Specifically, when shared mutable state is accessed concurrently across the FFI boundary, the compiler cannot enforce safety invariants on the C side of the interface.
Given that Rust's Sync and Send traits are not recognized by foreign compilers, there is uncertainty regarding the precise behavior of data races when a C thread modifies memory that Rust considers immutable or uniquely owned.
34775 reputation · 26 Sept 2020, 07:21 UTC
Rust’s memory‑safety guarantees do not extend across the FFI boundary. If C code accesses Rust‑owned memory without proper synchronization, the behavior is undefined, even when the C program appears to work.
Sync and Send traits are enforced only for Rust code; the compiler assumes no concurrent access from foreign threads unless you provide it yourself.Mutex, RwLock, or atomic types) and to expose only synchronized operations through extern "C" functions.#[repr(C)] so its layout matches C expectations.std::sync::Mutex<T> or an atomic type.Box::new) and expose an opaque pointer (*mut T) to C via an extern "C" constructor that returns the pointer.extern "C" functions that lock the mutex (or perform the atomic operation), carry out the needed mutation or read, and then unlock. Never expose raw &mut or & references across the FFI.extern "C" function) that drops the boxed value after all C threads have finished.If a C thread mutates memory that Rust believes is exclusively owned or immutable, Rust’s optimizer may assume no concurrent modification and reorder or eliminate accesses. This leads to data races, torn reads/writes, or other undefined behavior that can manifest as crashes, silent corruption, or seemingly correct execution on some runs.
To confirm whether missing synchronization is the cause, check if the C side uses any locking or atomic primitives when accessing the shared state. If it does not, adding a Rust‑side lock (as described above) is required.
Only if you cannot determine whether the C code already performs its own synchronization should you ask: Does the C code use mutexes, spinlocks, or atomic operations when accessing the shared memory? The answer changes the recommendation: if it does, you may only need to ensure compatible locking protocols; if it does not, you must add Rust‑side synchronization as outlined.
Use comments to ask for clarification. Post a solution as an answer.
5,930 reputation · 26 Sept 2020, 07:16 UTC
Atomic Types and C11 _AtomicWhen you need a truly lock‑free shared counter across Rust and C, the std::sync::atomic module is the right tool. On most platforms the Rust AtomicI32 layout matches the C _Atomic(int) layout, but you must explicitly guarantee that both sides use the same Ordering semantics.
_Atomic(int) and the Rust type with AtomicI32. Wrap it in a #[repr(C)] struct if you expose a pointer.Ordering::SeqCst maps to C11’s memory_order_seq_cst. Mixing Release/Acquire on one side and SeqCst on the other will still provide a happens‑before edge, but the weaker ordering may allow more reordering.std::sync::atomic::fence(Ordering::SeqCst) from Rust or atomic_thread_fence(memory_order_seq_cst) from C. The fence is emitted as an LLVM atomicrmw or fence instruction visible to the other language.volatile does not provide the memory ordering guarantees that Atomic types do. Use atomics for shared mutable state that may be accessed concurrently.// Rust side
#[repr(C)]
pub struct Shared {
counter: std::sync::atomic::AtomicI32,
}
#[no_mangle]
pub extern "C" fn init_shared() -> *mut Shared {
Box::into_raw(Box::new(Shared { counter: std::sync::atomic::AtomicI32::new(0) }))
}
#[no_mangle]
pub extern "C" fn inc_shared(ptr: *mut Shared) {
unsafe { (*ptr).counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst) }
}
// C side
#include <stdatomic.h>
#include <stdint.h>
typedef struct Shared {
_Atomic(int32_t) counter;
} Shared;
extern void inc_shared(Shared *);
int main() {
Shared *s = init_shared();
inc_shared(s); // atomic increment
return 0;
}