Is `external "C"` required for plugin system?

I'm following this tutorial about implementing a plugin system in Rust.

In the tutorial, the main function of the plugin is marked external "C".

// random/src/lib.rs

plugins_core::export_plugin!(register);

extern "C" fn register(registrar: &mut dyn PluginRegistrar) {
    registrar.register_function("random", Box::new(Random));
}

I wonder if this is really required. Since the application which loads the plugin and calls this function is made in Rust.
Also, I tried to remove the external "C" and it's seems to work. So is the tutorial too old (2019) or am I missing something ?

Without extern "C", plugins compiled by one version of the Rust compiler are unlikely to work with a host application compiled by a different version.

3 Likes

Yes, it's required. Rust doesn't have its own stable ABI yet, and its unlikely to have one in the near future.

There's abi_stable crate that helps using more advanced types with extern "C"/#[repr(C)] interfaces.

3 Likes

Where does "write plugins in wasm" fit in?

Does it: (1) not count as a plugin system or (2) considers the Rust/wasmtime boundary as "extern C" ?

AFAIK, to call WASM function from external code (including the one which drives the runtime), this function have to be extern "C", too.

FFI through WASM is not special. Functions exported to/imported from WASM must also be extern "C".

There's a reason we need the whole wasm_bindgen baggage. JS and anything non-Rust looks just like C from Rust's point of view, the memory model is not Rust native (obviously), so we have to interface with it by translating between the set of primitives allowed in FFI and Rust's high-level types.

Just out of curiosity, where is the extern 'C' hiding in the example at: wasmtime - Rust

Quoting the example:

use anyhow::Result;
use wasmtime::*;

fn main() -> Result<()> {
    // Modules can be compiled through either the text or binary format
    let engine = Engine::default();
    let wat = r#"
        (module
            (import "host" "hello" (func $host_hello (param i32)))

            (func (export "hello")
                i32.const 3
                call $host_hello)
        )
    "#;
    let module = Module::new(&engine, wat)?;

    // All wasm objects operate within the context of a "store". Each
    // `Store` has a type parameter to store host-specific data, which in
    // this case we're using `4` for.
    let mut store = Store::new(&engine, 4);
    let host_hello = Func::wrap(&mut store, |caller: Caller<'_, u32>, param: i32| {
        println!("Got {} from WebAssembly", param);
        println!("my host state is: {}", caller.data());
    });

    // Instantiation of a module requires specifying its imports and then
    // afterwards we can fetch exports by name, as well as asserting the
    // type signature of the function with `get_typed_func`.
    let instance = Instance::new(&mut store, &module, &[host_hello.into()])?;
    let hello = instance.get_typed_func::<(), (), _>(&mut store, "hello")?;

    // And finally we can call the wasm!
    hello.call(&mut store, ())?;

    Ok(())
}

Intuitively, your argument makes sense; practically, I am just curious how the above manages how hide the extern "C" away.

The essence is not the literal "extern C" syntax. That's a means of telling the Rust compiler what to find in an external module and what ABI that has to follow. If you JIT or interpret your own wasm using an engine that provides an API, then of course the internals of that engine have to make sure they generate code that correctly follows the ABI.

Incidentally, you could try returning complex, Rust-only types (e.g. Vec<NonReprCStruct>) from your wasm function. It probably won't work – it might not even compile (if Wasmtime pre-bakes a set of allowed types via traits), or it might cause UB.

If it used a Wasm Interpreter instead of a Wasm JIT, would you still be claiming that it is using "extern C" ?

It probably wouldn't, although at that point I'm not sure it's useful to consider that as "FFI".

I was thinking about the original question of whether a plugin system would imply "extern C"; and although I agree with the logical arguments for wasm-interpreter and wasm-jit, I do find it a bit weird that JIT-ing suddenly makes it "extern C" territory.

When jitting you are interacting between two different language. For this you need a stable abi. The only stable abi that exists on multiple platforms is the C abi.

2 Likes