19

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Emma
  • 294
  • 1
  • 4
  • 11
  • 3
    I have no experience in Rust on WASM but pretty much in C (Emscripten). As you guessed WebAssembly designed with dynamic linking in mind, by Import and Export. However, the tooling is not as mature as you can easily dynamically link libraries without a hack yet. The good news is easy dynamic linking is definitely in their goal list. – Bumsik Kim Jan 24 '19 at 15:11

2 Answers2

10

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
sbc100
  • 2,619
  • 14
  • 11
  • 2
    So the answer is basically: _It's reasonable to assume it will be possible in the future, but not currently_. Would that be a correct interpretation of your answer? – Emma Jan 29 '19 at 09:14
4

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()
}
djozis
  • 69
  • 5
  • I used transmute here, but I could've made `replace_me` an `fn() -> u64` instead, and have `force_call_indirect_for_function_index` take/return a `fn() -> u64`, and done all of this with just safe Rust, in theory? Crossing between wasm/Rust seems really dangerous in some areas. – djozis Oct 20 '19 at 19:53