Global init-once state in a library

I have a crate that produces a cdylib. Its interface is that there’s a single function exported, and it’s called with a function table, and it modifies the function table with pointers to its own (internal) functions. A copy of the original table also needs to be kept around. The exported function is only called once.

What is the proper way to deal with this? This is a simplified version of what I have with the newly-stable MaybeUninit:

use core::mem::MaybeUninit;
use core::ptr;

#[repr(C)]
#[derive(Clone, Copy)]
pub struct Table {
    pub foo: Option<extern "C" fn()>,
}

static mut TABLE: MaybeUninit<Table> = MaybeUninit::uninit();

#[no_mangle]
pub extern "C" fn init(table: *mut Table) {
    unsafe {
        if let Some(table) = table.as_mut() {
            ptr::write(TABLE.as_mut_ptr(), *table);
            table.foo = Some(foo);
        }
    }
}

extern "C" fn foo() {
    let table = unsafe { &*TABLE.as_ptr() };
    (table.foo.unwrap())();
}

Is there some undefined behavior in there as far as the compiler is concerned? As far as runtime is concerned, TABLE cannot be uninitialized when foo is called.

Edit: While here: what would be a proper way to avoid defining the table as a struct of Options to avoid the unwrap() in foo? The initial call is guaranteed to contain a full table. Even if I want to be extra careful, the init function could avoid writing to the table if it’s not complete in the first place. How do you go about that? Define two types? One with Options and one without?

As written, there is UB: calling init concurrently from different threads causes a data race.

Either init should be marked unsafe with a contract "call this exactly once, before any call to foo", or a suitable synchronization should be added. For synchronization, you can take a look at my once_cell crate: https://github.com/matklad/once_cell/, specifically, at Safe initialization of global data.

If you use Option for foo, you don’t need MaybeUninit, as there’s a reasonable default const value:

static mut TABLE: Table = Table { foo: None };

if you remove an option (which looks like you should just do perhaps?), and you go with unsafe solution without synchronization, than yes, you’ll need MaybeUninit.

1 Like