Course contentsShow
← Rust
Lesson 1623 of 1,91014. System Integration and EcosystemFree lesson

Handling Global State in Libraries

You will synchronize global C state using Rust's concurrency primitives like Mutex and OnceCell.

In this lesson, you will learn how to safely wrap legacy C global state within a thread-safe, idiomatic Rust interface using synchronization primitives.

When building libraries that interface with C code, you often encounter global mutable variables—such as a C-based configuration struct or a logging buffer—that lack built-in thread safety. Exposing these directly via FFI is a recipe for data races. To bridge this, we use once_cell::sync::Lazy (or std::sync::OnceLock) combined with std::sync::Mutex to gate access.

The Synchronization Pattern

The core strategy is to treat the C global state as an "opaque asset" that only exists inside a Rust-managed lock. By wrapping the global variable in a Mutex, you ensure that even if multiple threads call your Rust library functions simultaneously, the underlying C function receives serialized, sequential access to the state.

Consider a legacy C library with a void c_update_config(int value) function that modifies a global variable. In Rust, we wrap it like this:

use std::sync::Mutex;
use once_cell::sync::Lazy;

// The mutex ensures only one thread interacts with the C global state at a time
static C_STATE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

pub fn update_library_config(val: i32) {
    // Lock the mutex before touching the C FFI
    let _guard = C_STATE_LOCK.lock().unwrap();
    
    unsafe {
        // Now it is safe to call the C function that modifies its global state
        ffi::c_update_config(val);
    }
}

Why this is necessary

Even if your Rust code is safe, the C library does not know about Rust’s borrow checker. If two threads call ffi::c_update_config at the same time, the C library might experience an internal race condition or memory corruption.

By using Lazy to initialize the Mutex at runtime and a guard to scope the access, you turn an unsafe global variable into a controlled resource. The OnceCell/Lazy construct ensures the lock itself is initialized exactly once, while the Mutex enforces the memory synchronization required to make those FFI calls sound.

Key Takeaway: Always wrap mutable FFI globals in a Mutex to serialize access and protect the underlying C state from concurrent modification.