How can threads calling FFI code in Rust communicate with other threads or modify shared state?

In the following example, I've got application state in my main thread and a UI running on another thread. The UI is implemented in another language and calls back to Rust over FFI. I'd like to use the Rust channels mechanism to communicate between the two threads. I've seen the Rust examples on channels and threading, but in all examples the thread is implemented in a closure and has access to the application state. With FFI, however, you only have C-like functions that do not have access to the application state, and therefore no way to access the channel that was created earlier.

use std::thread;
use std::sync::mpsc;

extern {
	fn ui_create_and_run();
}

struct AppState {
	foo: i32
}

enum Msg {
    SetFoo(i32),
}

fn main() {
	let mut state = AppState{ foo: 0 };
    let (tx, rx) = mpsc::channel();

	let ui_thr_handle = thread::spawn(|| {
		unsafe { ui_create_and_run(); }
	});

    for msg in rx {
        match msg {
            Msg::SetFoo(x) => state.foo = x;
        }
    }
	ui_thr_handle.join().unwrap();
}

#[no_mangle]
pub extern "C" fn set_foo(newFoo: i32) {
    // PROBLEM: tx NOT AVAILABLE HERE!
    tx.send(Msg::SetFoo(newFoo)).unwrap();
}

Is it possible to give my extern "C" functions access to some global channel tx without passing all kinds of nasty pointers around? I've tried

thread_local!(static CHANNEL: RefCell<(mpsc::Sender<Msg>, mpsc::Receiver<Msg>)> = RefCell::new(mpsc::channel()));

but can't seem to wrap my head around borrowing issues that this creates.

the common pattern in FFI is to pass state as pointer in argument...

Having it global is fine too, but initializing it in library is harder unless you use std::sync::Once or lazy_static
thread local is not the best place, cuz it has to be initialized per thread

1 Like

This sounds like a design flaw in the FFI code. Normally when accepting a callback you should also accept some void * pointer to some user-provided data so the callback gets access to application state for exactly this reason.

When the code accepting a callback function doesn't accept a void *user_data parameter your only real option is to use global variables.

3 Likes

Right. Let's say hypothetically that the FFI code is someone else's library and it doesn't offer a void* argument, can you give a quick example of how one might create a global variable out of a call to mpsc::channel()?

Simply creating static CHANNEL: (mpsc::Sender<Msg>, mpsc::Receiver<Msg>) = mpsc::channel(); gives multiple errors:

error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
error[E0277]: `std::sync::mpsc::Sender<Msg>` cannot be shared between threads safely
error[E0277]: `std::sync::mpsc::Receiver<Msg>` cannot be shared between threads safely

te-clipboard/rt.rs at master · DoumanAsh/te-clipboard · GitHub simple example with lazy_static

I would use std::sync::Once normally though, but it requires some unsafe code

1 Like

@DoumanAsh Can you show a simple example of how you would accomplish this using std::sync::Once? I'd prefer not to pull in any third party crates.

Trying to create this:

static mut CHANNEL: (mpsc::Sender<Msg>, mpsc::Receiver<Msg>) = mpsc::channel();

gives the error:

calls in statics are limited to constant functions, tuple structs and tuple variants

Which I'm guessing is because of some heap allocation when creating a channel.

Is there an idiomatic way of creating this channel using std::sync::Once? The channel does not have some kind of empty or default constructor, so I'm afraid I'm at a loss on how to make it work. The only thing that comes to mind is wrapping it in an Option, but that would require unwrap()ing it everywhere that I use it, which seems a little ridiculous.

This sort of thing is rather easy to get wrong, and lazy_static may as well be part of std. Personally, I like the simpler crate once_cell. (Disclaimer: I have contributed to once_cell)

With once_cell

use once_cell::sync::Lazy;

static CHANNEL: Lazy<(mspc::SyncSender<Msg>, Mutex<Option<mspc::Reciever<Msg>>>)> = Lazy::new(|| {
    let (sender, reciever) = mspc::sync_channel();

    (sender, Mutex::new(Some(reciever)))
});

fn main() {
    let reciever = CHANNEL.1.lock().unwrap()
        .take().expect("Reciver was already aquired");
}

The Lazy allows you to initialize the static lazily with the given closure. (That closure will be called at most once, and will initialize the Lazy). The Mutex is needed to syncronize access to the Receiver because mpsc::Reciever is not Sync. But let's not pay that cost more than we need to, by using an Option. This allows us to take the Reciever out and use it without going through the Mutex everytime. If you don't mind the Mutex then you don't need the Option.

If you are willing to use just one more crate, use parking_lot. This gives you reentrant mutexes which allieviates most of the downsides of normal mutexes (they lock per thread, not per scope). This means that they turn things that are Send but not Sync into Send + Sync with very little cost. This is exactly what we need for mspc.

use once_cell::sync::Lazy;
use parking_lot::ReentrantMutex;

static CHANNEL: Lazy<(mspc::SyncSender<Msg>, ReentrantMutex<mspc::Reciever<Msg>>)> = Lazy::new(|| {
    let (sender, reciever) = mspc::sync_channel();

    (sender, ReentrantMutex::new(reciever))
});

The final option that I will offer is to give up on std::mspc. It is a wart on the language, and is superceded in almost every way by crossbeam's mpmc channel. This is the method that I highly reccomend.

crossbeam is a fundemental crate that contains a number of synchronization primitives.

use once_cell::sync::Lazy;
use crossbeam::channel::{Sender, Reciever, bounded, unbounded};

static CHANNEL: Lazy<(Sender<Msg>, Reciever<Msg>)> = Lazy::new(unbounded);
4 Likes

Rust is pretty annoying in that regard because there is rule of no life before main, so any non-const functions cannot be used to initialize static/const variable

Here simple example how safely to initialize such static without need for third party dependency Rust Playground

Well, sorry for late response, but I hope it will help you to avoid unecessary dependencies

3 Likes

Thanks everyone! These are all wonderful answers with a lot of thought put into them. I'm marking @DoumanAsh's answer as the solution because it was accomplished without any additional dependencies.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.