A very experimental WebAssembly library for PostHog.
The goal is to reduce the cost of implementing new Client SDK features.
We considered compiling Rust to portable libraries, but each library has to be.
- Sidecar service (called through http):
- Compile to platform specific dlls: deployment is painful.
Compile your Rust crate for a minimal and portable WASM output:
cargo build --target wasm32-unknown-unknown --release
Rust functions must use stable names and the C calling conventions to be callable from outside:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
WASM only supports numbers — strings must be passed via memory.
In Rust:
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut u8 {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
core::mem::forget(buf);
ptr
}
- Host (e.g., C#) calls alloc() to get space
- Writes the string into WASM memory
- Rust reads it using unsafe pointer logic
Rust automatically exports memory if you use heap allocations (Vec, String, etc.).
In the host, access it like:
var memory = instance.GetMemory("memory")!;
memory.WriteBytes(ptr, bytes); // custom helper
In Rust:
extern "C" {
fn http_request(url_ptr: *const u8, url_len: usize,
method_ptr: *const u8, method_len: usize,
body_ptr: *const u8, body_len: usize) -> *const u8;
fn http_request_len() -> usize;
}
In the host (e.g., C#):
linker.Define("env", "http_request", Function.FromCallback<int, int, int, int, int, int>(store, (url_ptr, url_len, method_ptr, method_len, body_ptr, body_len) => {
// Make synchronous HTTP call, write to memory, return pointer
}));
- Must match Rust's expected (i32, i32) -> i32 signature
- Must be synchronous (avoid
Task<T>
orasync
unless using WIT/component model)
Your Rust alloc()
must return pointers inside the exported WASM linear memory, or else the host (like C#) won’t be able to write to it.
Use a compact allocator like:
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Avoid:
- wasm-bindgen
- std
- wasi
Use:
#![no_std] + alloc
- Manual memory passing
wee_alloc
for heap management
WIT (WebAssembly Interface Types) + wit-bindgen can generate bindings for Rust, C#, JS, etc. automatically — but it’s early-stage and not as portable yet as low-level FFI. We chose not to use it yet because it's not well supported.
Implementing a WASM client in Rust that's easy to call from other platforms is very doable, but challenging.
The issues:
no_std
means we can't use the standard library and write idiomatic Rust. It makes things complicated. For example, we have to stick to primitive types in exported functions:
i32
, i64
, f32
, f64
, *const u8
, usize
And pass more complex types via memory (pointers to serialized strings or structs)
-
We could use
wasm-bindgen
to bind to JS environments, but that doesn't help for non-JS platforms. -
wit-bindgen
and the Component Model are interesting, but they're not well supported yet. It would allow us to define structured interfaces and then generate bindings for different languages. -
WASM is synchronous by default, so unclear how to implement async operations.
So in-conclusion, WASM is definitely promising as a means of not having to write PHP ever again, but it's not ready for prime time yet. We should wait till there's broad support for wit-bindgen
and the Component Model.