1

Everything in this question is specific to the Wasmtime runtime for running WebAssembly modules. I am trying to build a Rust program that would be able to do a very basic form of dynamic linking between WebAssembly modules, i.e. providing Rust implementations for host_dlopen(), host_dlsym() functions that would be converted to a WASM Function and be called whenever necessary. My whole problem is being able to correctly (this would be trivial in another language but turns out to be very hard in Rust) pass the Linker into the host_dlopen() I am trying to define.

I have this struct where I defined all objects of interest:

struct GlobalWasmCtx {
    engine: Engine,
    store:  Store<WasiP1Ctx>,
    linker: Linker<WasiP1Ctx>,
}

this constructor:

impl GlobalWasmCtx {
    fn new() -> Self {
      
        let engine = Engine::default();
        let wasi   = WasiCtxBuilder::new().build_p1();
        let mut store  = Store::new(&engine, wasi);
        let mut linker = Linker::new(&engine);
        Self { engine, store, linker }
    }
}

and this declaration:

thread_local! {
    static GLOBAL_OBJECTS: RefCell<GlobalWasmCtx> = RefCell::new(GlobalWasmCtx::new());
}

and I am running my whole main function inside a wrapper:

GLOBAL_OBJECTS.with(|ctx| {
      ...
      let dlopen_func = Func::wrap(&mut store, |caller: Caller<'_, WasiP1Ctx>, ptr: i32, flags: i32| {
            
            // Use linker here
           
            Ok(())
        });
     ....
    })

This is just a start of a very long error trace:

`NonNull<GlobalWasmCtx>` cannot be shared between threads safely [E0277]
Help: within `RefMut<'_, GlobalWasmCtx>`, the trait `Sync` is not implemented for `NonNull<GlobalWasmCtx>`
Note: required because it appears within the type `RefMut<'_, GlobalWasmCtx>`
Note: required for `&RefMut<'_, GlobalWasmCtx>` to implement `std::marker::Send`
Note: required because it's used within this closure
Note: required for `{closure@wasm_launcher.rs:104:50: 104:103}` to implement `IntoFunc<WasiP1Ctx, (Caller<'_, WasiP1Ctx>, i32, i32), std::result::Result<(), anyhow::Error>>`
Note: required by a bound in `wasmtime::Func::wrap`

There are a few blocks like this and each one ends with "cannot be shared between threads safely". If my Linker and Store were not generic over WasiP1Ctx and were something like Linker<()> and Store<()> this code would work. But WasiP1Ctx introduces the constraint of implementing the Sync trait. And I really need to use this type otherwise I have to implement system calls and argument passing on my own.

I have tried other things as well, wrapping my fields in a RefCell and then in a RwLock like so:

store:  RwLock<RefCell<Store<WasiP1Ctx>>>,
linker: RwLock<RefCell<Linker<WasiP1Ctx>>>,

, taking the GlobalWasmCtx outside ofRefCell, but nothing seems to work.

I do not know what are my options to make this work. I would have thought that creating the struct inside thread_local!() is enough. I am a beginner in Rust so probably my approach is fundamentally wrong, but I cannot think of another way to do this. Is passing a Linker object inside a closure accepted by Func::wrap doable in Rust?

2
  • wasmtime's Func::wrap requires func to be usable on other threads but how you're constructing it is trying to bind a reference to a thread-local from the creating thread, which isn't going to work. You may be able invert your nesting - i.e. do GLOBAL_OBJECTS.with inside your function instead - but you have some ...s that imply more is going on. Commented May 17 at 14:51
  • On second reading, does it make sense for there to be different GlobalWasmCtxs per thread? I'm getting the feeling this is the wrong way to go. Commented May 17 at 15:56

1 Answer 1

2

Use a Mutex.

A WasiP1Ctx can be sent to different threads (it is Send) but it cannot be used by multiple threads at the same time (it is !Sync). Using a RwLock that you mentioned you tried is not enough since otherwise multiple readers would be able to access it at the same time and violate that constraint. And remove RefCell; your attempt with RwLock<RefCell<_>> is frankly nonsense.

Then you probably just want a static variable instead of thread-locals. You can see How do I create a global, mutable singleton?. But here is my recommendation since the wasmtime objects are not const-constructible:

use std::sync::{Mutex, OnceLock};

use wasmtime::{Engine, Linker, Store};
use wasmtime_wasi::preview1::WasiP1Ctx;
use wasmtime_wasi::WasiCtxBuilder;

struct GlobalWasmCtx {
    engine: Engine,
    store: Mutex<Store<WasiP1Ctx>>,
    linker: Mutex<Linker<WasiP1Ctx>>,
}

impl GlobalWasmCtx {
    fn new() -> Self {
        let engine = Engine::default();
        let wasi = WasiCtxBuilder::new().build_p1();
        let store = Store::new(&engine, wasi);
        let linker = Linker::new(&engine);
        Self {
            engine,
            store: Mutex::new(store),
            linker: Mutex::new(linker),
        }
    }
}

fn global_objects() -> &'static GlobalWasmCtx {
    static GLOBAL_OBJECTS: OnceLock<GlobalWasmCtx> = OnceLock::new();

    GLOBAL_OBJECTS.get_or_init(|| GlobalWasmCtx::new())
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you very much! It works. I took a bit to reply because I was experimenting with your other idea of inverting the nesting of GLOBAL_OBJECTS.with().

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.