Limit function call only in current thread

Hi, everyone

I am trying to do a ffi binding, and all the functions imported from c should only be called in current thread. So I want a compiler error in the following code sample when the functions come to another thread. Is it possible?
Thanks.

pub struct MyFFI {
    call: extern "C" fn() -> i32,
}

static mut ffi:Option<MyFFI> = None;

#[no_mangle]
pub extern "C" fn init(b:MyFFI) {
    unsafe {
        ffi = Some(b);
    }
}

pub fn call() ->i32 {
    (get_ffi().call)()
}

pub fn get_ffi() ->&'static MyFFI {
    unsafe {
        ffi.as_ref().unwrap()
    }
}

pub fn test() {
    std::thread::spawn(||{
        call();
    });
}

(Playground)

You can prevent instances from crossing a thread boundary by ensuring they're !Send. To ensure only one thread can call FFI functions, you can do something like this:

use std::marker::PhantomData;
use std::sync::Once;

#[derive(Copy,Clone)]
pub struct MyFFI {
    call_fn: extern "C" fn() -> i32,
    phantom: PhantomData<*mut ()>,  // !Send + !Sync
}

impl MyFFI {
    pub fn get()->Option<Self> {
        static INIT: Once = Once::new();
        thread_local! {
            static FFI: Option<MyFFI> = {
                let mut ffi = None;
                INIT.call_once(|| ffi = Some(MyFFI::init()));
                ffi
            }
        }
        
        FFI.with(|&ffi| ffi)
    }
    
    fn init()->Self { todo!() }
    
    pub fn call(&self)->i32 {
        (self.call_fn)()
    }
}

pub fn test() {
    let ffi = MyFFI::get().unwrap();
    std::thread::spawn(||{
        // Compile error here: MyFFI is !Send
        ffi.call();
    });
}

Playground

(NB: MyFFI::get() will return None if called in a second thread)

2 Likes
use std::marker::PhantomData;
use std::sync::Once;

#[derive(Copy,Clone)]
pub struct MyFFI {
    call_fn: extern "C" fn() -> i32,
    phantom: PhantomData<*mut ()>,  // !Send + !Sync
}

impl MyFFI {
    pub fn get()->Option<Self> {
        static INIT: Once = Once::new();
        thread_local! {
            static FFI: Option<MyFFI> = {
                let mut ffi = None;
                INIT.call_once(|| ffi = Some(MyFFI::init()));
                ffi
            }
        }
        
        FFI.with(|&ffi| ffi)
    }
    
    fn init()->Self { todo!() }
    
    pub fn call(&self)->i32 {
        (self.call_fn)()
    }
}

pub fn test() {
   
    std::thread::spawn(||{
        let ffi = MyFFI::get().unwrap();
        ffi.call();
    });
}

I don't quite understand the solution. I tried comment phantom field, and compiler complained about lifttime. So I move ffi from outside into thread closure, and it's ok now whether phantom field exists or not.

The PhantomData prevents any MyFFI object from crossing a thread boundary, and MyFFI::get() ensures that only one MyFFI instance is ever created. In your example, you're creating that instance on the spawned thread instead of the main one, which is still a form of single-threaded operation.

That's clear. Thanks for all the replies.