Rust
Introduction
Beginning with 1.30 Rust can generate %.wasm
files instead of
architecture-specific binaries through three targets:
wasm32-unknown-emscripten
: mostly for browser (JavaScript) use.wasm32-unknown-unknown
: for standalone use in or outside the browser.wasm32-wasi
: for use outside the browser.
This document is maintained by wazero, which is a WebAssembly runtime that
embeds in Go applications. Hence, our notes focus on targets used outside the
browser, tested by wazero: wasm32-unknown-unknown
and wasm32-wasi
.
This document also focuses on rustc
as opposed to cargo
, for precision and
brevity.
Overview
When Rust compiles a %.rs
file with a wasm32-*
target, the output %.wasm
depends on a subset of features in the [WebAssembly 1.0 Core specification]
(https://deploy-preview-1151--wazero.netlify.app/specs/#core). The wasm32-wasi
target depends on [WASI]
(https://deploy-preview-1151--wazero.netlify.app/specs/#wasi) host functions as well.
Unlike some compilers, Rust also supports importing custom host functions and exporting functions back to the host.
Here’s a basic example of source in Rust:
#[no_mangle]
pub extern "C" fn add(x: i32, y: i32) -> i32 {
x + y
}
The following is the minimal command to build a Wasm file.
rustc -o lib.wasm --target wasm32-unknown-unknown --crate-type cdylib lib.rs
The resulting Wasm exports the add
function so that the embedding host can
call it, regardless of if the host is written in Rust or not.
Digging Deeper
There are a few things to unpack above, so let’s start at the top.
The rust source includes #[no_mangle]
and extern "C"
. Add these to
functions you want to export to the WebAssembly host. You can read more about
this in The Embedded Rust Book.
Next, you’ll notice the flag --crate-type cdylib
passed to rustc
. This
compiles the source as a library, e.g. without a main
function.
Disclaimer
This document includes notes contributed by the wazero community. While wazero includes Rust examples, the community is less familiar with Rust. For more help, consider the Rust and WebAssembly book.
Meanwhile, please help us maintain this document and star our GitHub repository, if it is helpful. Together, we can make WebAssembly easier on the next person.
Constraints
Please read our overview of WebAssembly and constraints. In short, expect limitations in both language features and library choices when developing your software.
The most common constraint is which crates you can depend on. Please refer to the Which Crates Will Work Off-the-Shelf with WebAssembly? page in the Rust and WebAssembly book for more on this.
Memory
When Rust compiles rust into Wasm, it configures the WebAssembly linear memory to an initial size of 17 pages (1.1MB), and marks a position in that memory as the heap base. All memory beyond that is used for the Rust heap.
Allocations within Rust (compiled to %.wasm
) are managed as one would expect.
The global_allocator
can allocate pages (alloc_pages
) until memory.grow
on the host returns -1.
Host Allocations
Sometimes a host function needs to allocate memory directly. For example, to write JSON of a given length before invoking an exported function to parse it.
The below snippet is a realistic example of a function exported to the host, who needs to allocate memory first.
#[no_mangle]
pub unsafe extern "C" fn configure(ptr: u32, len: u32) {
let json = &ptr_to_string(ptr, len);
}
Note: WebAssembly uses 32-bit memory addressing, so a uintptr
is 32-bits.
The general flow is that the host allocates memory by calling an allocation
function with the size needed. Then, it writes data, in this case JSON, to the
memory offset (ptr
). At that point, it can call a host function, ex
configure
, passing the ptr
and size
allocated. The guest Wasm (compiled
from Rust) will be able to read the data. To ensure no memory leaks, the host
calls a free function, with the same ptr
, afterwards and unconditionally.
Note: wazero includes an example project that shows this.
To allow the host to allocate memory, you need to define your own malloc
and
free
functions:
(func (export "malloc") (param $size i32) (result (;$ptr;) i32))
(func (export "free") (param $ptr i32) (param $size i32))
The below implements this in Rustlang:
use std::mem::MaybeUninit;
use std::slice;
unsafe fn ptr_to_string(ptr: u32, len: u32) -> String {
let slice = slice::from_raw_parts_mut(ptr as *mut u8, len as usize);
let utf8 = std::str::from_utf8_unchecked_mut(slice);
return String::from(utf8);
}
#[no_mangle]
pub extern "C" fn alloc(size: u32) -> *mut u8 {
// Allocate the amount of bytes needed.
let vec: Vec<MaybeUninit<u8>> = Vec::with_capacity(size as usize);
// into_raw leaks the memory to the caller.
Box::into_raw(vec.into_boxed_slice()) as *mut u8
}
#[no_mangle]
pub unsafe extern "C" fn free(ptr: u32, size: u32) {
// Retake the pointer which allows its memory to be freed.
let _ = Vec::from_raw_parts(ptr as *mut u8, 0, size as usize);
}
As you can see, the above code is quite technical and should be kept separate from your business logic as much as possible.
System Calls
Please read our overview of WebAssembly and System Calls. In short, WebAssembly is a stack-based virtual machine specification, so operates at a lower level than an operating system.
For functionality the operating system would otherwise provide, you must use
the wasm32-wasi
target. This imports host functions in
WASI.
For example, rustc -o hello.wasm --target wasm32-wasi hello.rs
compiles the
below main
function into a WASI function exported as _start
.
fn main() {
println!("Hello World!");
}
Note: wazero includes an example WASI project including source code
that implements cat
without any WebAssembly-specific code.
Concurrency
Please read our overview of WebAssembly and concurrency. In short, the current WebAssembly specification does not support parallel processing.
Optimizations
Below are some commonly used configurations that allow optimizing for size or performance vs defaults. Note that sometimes one sacrifices the other.
Binary size
Those with %.wasm
binary size constraints can change their source or set
rustc
flags to reduce it.
Source changes:
- wee_alloc: Smaller, WebAssembly-tuned memory allocator.
-C debuginfo=0
: Strips DWARF, but retains the WebAssembly name section.-C opt-level=3
: Includes all size optimizations.
Those using cargo should also use the --release
flag, which corresponds to
rustc -C debuginfo=0 -C opt-level=3
.
Those using the wasm32-wasi
target should consider the cargo-wasi crate
as it dramatically reduces Wasm size.
Performance
Those with runtime performance constraints can change their source or set
rustc
flags to improve it.
-C opt-level=2
: Enable additional optimizations, frequently at the expense of binary size.
Frequently Asked Questions
Why is my %.wasm
binary so big?
Rust defaults can be overridden for those who can sacrifice features or performance for a smaller binary. After that, tuning your source code may reduce binary size further.