I'm making a Turing-complete DSL in Rust for the web using wasm-bindgen. I want the ability to download arbitrary WASM code from the web and then use functions from that file in my DSL. Some sort of dynamic linking with an equivalent of dlopen
is what I have in mind.
I have no idea how to actually achieve this though.
From reading the WebAssembly docs I get the impression that it should indeed be possible but I'm not knowledgeable enough to understand the details of the process from this document.
There is a chapter in the wasm-bindgen reference detailing how to instantiate WebAssembly modules from inside WebAssembly modules!, but this seems to do it via JavaScript which seems suboptimal and not what the WebAssembly doc describes.
In js-sys it is possible to create JavaScript functions from arbitrary strings, but this essentially calls Function(/* some arbitrary string */)
from the JavaScript side which again seems suboptimal and not what the WebAssembly doc describes.
Is it possible or is there some other more suitable way to achieve my goal?
The State of WebAssembly 2022 survey of Wasm developers shows Rust on top, with Blazor and Python on the rise.
💯 Performance advantages of Rust and WebAssembly Rust is 2x (200%) faster, but uses only 1% of the memory compared with Java. Rust is 150x (15,000%) faster, and uses about the same amount of memory compared with Python.
Dynamic linking support in llvm/lld for WebAssembly is still a work in progress. I imagine that dynamic linking in Rust is currently blocked on dynamic linking support in in llvm/lld more generally.
Don't use this in production code (it's a house of cards), I am just sharing my research for others who are also tinkering around this topic. This will let you change the bindings arbitrarily at runtime. Appears to work correctly today for every optimization level, but who can know if it will work tomorrow. For real support, see sbc100's answer.
/// If at any point you call this function directly, it will probably do exactly what its
/// implementation here is, as it should compile to a "call" instruction when you directly call.
/// That instruction does not appear to be impacted by changes in the function table.
pub fn replace_me() {
// The function body must be unique, otherwise the optimizer will deduplicate
// it and you create unintended impacts. This is never called, it's just unique.
force_call_indirect_for_function_index(replace_me as u32);
}
/// We'll replace the above function with this function for all "call indirect" instructions.
pub fn return_50() -> u64 {
50
}
/// This allows us to force "call indirect". Both no_mangle and inline(never) seem to be required.
/// You could simply strip every invocation of this function from your final wasm binary, since
/// it takes one value and returns one value. It's here to stop optimizations around the function
/// invocation by index.
#[inline(never)]
#[no_mangle]
fn force_call_indirect_for_function_index(function_index: u32) -> u32 {
function_index
}
/// Inline this or make it generic or whatever you want for ease of use, this is your calling code.
/// Note that the function index you use does not need to have the same signature as the function it
/// is replaced with.
///
/// This seems to compile to:
/// i32.const, call force_call_indirect_for_function_index, call indirect.
///
/// So stripping force_call_indirect_for_function_index invocations would make this as efficient
/// as possible for a dynamically linked wasm call I think.
fn call_replace_me_indirectly() -> u64 {
unsafe {
std::mem::transmute::<u32, fn() -> u64>(force_call_indirect_for_function_index(
replace_me as u32,
))()
}
}
/// Replaces replace_me with return_50 in the wasm function table. I've tested that this works with
/// Functions exported from other wasm modules. For this example, I'll use a function defined in
/// this module (return_50).
fn replace_replace_me() {
let function_table: js_sys::WebAssembly::Table = wasm_bindgen::function_table()
.dyn_into::<js_sys::WebAssembly::Table>()
.expect("I'm going to find you...");
let function = function_table
.get(return_50 as u32)
.expect("I know you're in there...");
function_table
.set(replace_me as u32, &function)
.expect("It's not unsafe, but is it undefined behavior?");
}
/// Mangles "replace_me" call indirection invocations, and returns 50.
pub fn watch_me() -> u64 {
replace_replace_me();
call_replace_me_indirectly()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With