Pre-Compiling and Cross-Compiling WebAssembly Programs

Wasmtime can compile a WebAssembly program to native code on one machine, and then run it on a different machine. This has a number of benefits:

  • Faster start up: Compilation is removed from the critical path. When a new HTTP request comes into your function-as-a-service platform, for example, you do not have to wait for the associated Wasm program to compile before it can start handling the request. Similarly, when a new update for your embedded device's Wasm application logic comes in, you do not need to compile the update on the under-powered device before it can begin running new updated logic.

  • Less Memory Usage: Pre-compiled Wasm programs can be lazily mmaped from disk, only paging their code into memory as those code paths are executed. If none of the code on a page is ever executed, the OS will never make the page resident. This means that running pre-compiled Wasm programs lowers overall memory usage in the system.

  • Smaller Code Size for Embedded: Wasmtime can be built such that it can only run Wasm programs that were pre-compiled elsewhere. These builds will not include the executable code for Wasm compilation. This is done by disabling the cranelift and winch cargo features at build time. These builds are useful for embedded devices, where programs must be small and fit within the device's constrained environment.

  • Smaller Attack Surfaces: Similarly, building Wasmtime without a compiler, and with only support for running pre-compiled Wasm programs, can be useful for security-minded embeddings to reduce the potential attack surface exposed to untrusted and potentially hostile Wasm guests. Compilation, triggered by the control plane, can happen inside a Wasmtime build that can compile but not run Wasm programs. Execution, in the data plane, can happen inside a Wasmtime build that can run but not compile new Wasm programs. Exposing a minimal attack surface to untrusted code is good security practice.

Note that these benefits are applicable regardless which Wasm execution strategy you've configured: Cranelift, Winch, or Pulley.

Pre-Compile the Wasm on One Machine

This must be done with a Wasmtime build that has a Wasm execution strategy enabled, e.g. was built with the cranelift or winch cargo features. It does not require the ability to run Wasm programs, so the runtime cargo feature can be disabled at build time.

//! Pre-compiling a Wasm program.

use wasmtime::{Config, Engine, Result, Strategy};

fn main() -> Result<()> {
    // Configure Wasmtime for compiling Wasm programs to x86_64 with the
    // Cranelift compiler.
    let mut config = Config::new();
    config.strategy(Strategy::Cranelift);
    if let Err(error) = config.target("x86_64") {
        eprintln!(
            "this Wasmtime was not built with the x86_64 Cranelift backend \
             enabled: {error:?}",
        );
        return Ok(());
    }

    // Create an `Engine` with that configuration.
    let engine = match Engine::new(&config) {
        Ok(engine) => engine,
        Err(error) => {
            println!("Wasmtime build is incompatible with config: {error:?}");
            return Ok(());
        }
    };

    // Pre-compile a Wasm program.
    //
    // Note that passing the Wasm text format, like we are doing here, is only
    // supported when the `wat` cargo feature is enabled.
    let precompiled = engine.precompile_module(
        r#"
            (module
              (func (export "add") (param i32 i32) (result i32)
                (i32.add (local.get 0) (local.get 1))
              )
            )
        "#
        .as_bytes(),
    )?;

    // Write the pre-compiled program to a file.
    //
    // Note that the `.cwasm` extension is conventional for these files, and is
    // what the Wasmtime CLI will use by default, for example.
    std::fs::write("add.cwasm", &precompiled)?;

    // And we are done -- now a different Wasmtime embedding can load and run
    // the pre-compiled Wasm program from that `add.cwasm` file!
    Ok(())
}

Run the Pre-Compiled Wasm on Another Machine

This must be done with a Wasmtime build that can run pre-compiled Wasm programs, that is a Wasmtime built with the runtime cargo feature. It does not need to compile new Wasm programs, so the cranelift and winch cargo features can be disabled.

//! Running pre-compiled Wasm programs.

use wasmtime::{Config, Engine, Instance, Module, Result, Store};

fn main() -> Result<()> {
    // Create the default configuration for this host platform. Note that this
    // configuration must match the configuration used to pre-compile the Wasm
    // program. We cannot run Wasm programs pre-compiled for configurations that
    // do not match our own, therefore if you enabled or disabled any particular
    // Wasm proposals or tweaked memory knobs when pre-compiling, you should
    // make identical adjustments to this config.
    let config = Config::default();

    // Create an `Engine` with that configuration.
    let engine = Engine::new(&config)?;

    // Create a runtime `Module` from a Wasm program that was pre-compiled and
    // written to the `add.cwasm` file by `wasmtime/examples/pre_compile.rs`.
    //
    // **Warning:** Wasmtime does not (and in general cannot) fully validate
    // pre-compiled modules for safety -- only create `Module`s and `Component`s
    // from pre-compiled bytes you control and trust! Passing unknown or
    // untrusted bytes will lead to arbitrary code execution vulnerabilities in
    // your system!
    let module = match unsafe { Module::deserialize_file(&engine, "add.cwasm") } {
        Ok(module) => module,
        Err(error) => {
            println!("failed to deserialize pre-compiled module: {error:?}");
            return Ok(());
        }
    };

    // Instantiate the module and invoke its `add` function!
    let mut store = Store::new(&engine, ());
    let instance = Instance::new(&mut store, &module, &[])?;
    let add = instance.get_typed_func::<(i32, i32), i32>(&mut store, "add")?;
    let sum = add.call(&mut store, (3, 8))?;
    println!("the sum of 3 and 8 is {sum}");

    Ok(())
}

See Also