Skip to main content

Crate holochain_wasmer_guest

Crate holochain_wasmer_guest 

Source
Expand description

Guest-side runtime for wasm modules running under holochain_wasmer_host. Provides the macros and helper functions a wasm zome (or similar wasm-as-plugin) needs to talk to a Holochain host: declaring extern host functions, decoding host inputs, calling back into the host, and returning serializable values across the host↔guest boundary.

Most users do not depend on this crate directly — the Holochain HDK hides the host/guest plumbing entirely and zome authors should reach for it instead. The contents below are for HDK maintainers, for the macros’ implementations, and for anyone embedding Holochain wasms outside of an HDK context.

§Conceptual model

Wasm has only four primitive types (i32, i64, f32, f64) and a single shared linear memory. There are no strings, sequences, structs or other complex types in the wasm ABI itself. To pass anything richer between host and guest, both sides agree on a byte-level protocol: the sender writes serialized bytes into the shared linear memory, the receiver reads them out at a known pointer and length.

This crate’s job is to make that protocol invisible at the call site. You write what looks like a Rust function with serializable inputs and outputs, and the macros and helpers below take care of the leak-and-pointer dance underneath.

Constraints to keep in mind:

  • The host has full access to guest memory; the guest has none of the host’s. The only way the guest can read host data is to call an imported host function and have the host write the response into shared linear memory.
  • When the host calls into the guest, the host cannot call back into the same guest instance from inside that call. The guest call must complete (or trap) before the host can re-enter.
  • Wasm linear memory pages can be added but never removed. A guest that allocates aggressively will hold that memory for its lifetime, so be conservative about copying large payloads.
  • The serialization format must round-trip cleanly (we use holochain_serialized_bytes, which wraps messagepack). It is the caller’s responsibility to ensure types implement Serialize / DeserializeOwned consistently on both sides.

All code samples in the rest of this documentation are // ignore-marked rustdoc blocks. They are not run as doctests because guest code is meant to be compiled to the wasm32-unknown-unknown target with the __hc__allocate_1 / __hc__deallocate_1 exports and the host’s imported functions linked in — none of which are present when rustdoc compiles a doctest as a host-side binary. Treat them as the canonical shape to copy from rather than as runnable snippets; the test-crates/wasms/ directory in the repository contains the same patterns inside real wasm crates that CI exercises against a real host on every PR.

§Declaring host functions you want to call

The host exposes a set of imported functions to each guest instance. Use the host_externs! macro to declare the ones you intend to call. The macro takes pairs of name:version and generates extern "C" declarations of the form __hc__<name>_<version>:

use holochain_wasmer_guest::*;

host_externs!(test_process_string:2, debug:1);
// Generates:
//   extern "C" fn __hc__test_process_string_2(ptr: usize, len: usize) -> DoubleUSize;
//   extern "C" fn __hc__debug_1(ptr: usize, len: usize) -> DoubleUSize;

The version suffix lets the host evolve a function’s signature without breaking older guests; new guests opt in to the new version by bumping the literal.

§Writing functions the host can call

Every function the host can call into must have the signature extern "C" fn(guest_ptr: usize, len: usize) -> DoubleUSize. The #[no_mangle] attribute keeps the symbol name stable so the host can look it up by string:

use holochain_wasmer_guest::*;

#[no_mangle]
pub extern "C" fn process_string(guest_ptr: usize, len: usize) -> DoubleUSize {
    let input: String = match host_args(guest_ptr, len) {
        Ok(v) => v,
        Err(err_ptr) => return err_ptr,
    };
    return_ptr(format!("guest: {}", input))
}

The (guest_ptr, len) -> DoubleUSize shape is forced by what stable Rust’s extern "C" ABI can express on wasm32-unknown-unknown. Multi-value returns would let us return a (ptr, len) tuple directly, but they require nightly’s extern "wasm". Instead we pack both u32s into a single u64 (a DoubleUSize on a 32-bit target) and split it again on the host side.

§Receiving input with host_args

host_args takes the (guest_ptr, len) pair the host passed in and tries to deserialize it into your input type:

use holochain_wasmer_guest::*;

#[no_mangle]
pub extern "C" fn foo(guest_ptr: usize, len: usize) -> DoubleUSize {
    let input: MyInputType = match host_args(guest_ptr, len) {
        Ok(v) => v,
        Err(err_ptr) => return err_ptr,
    };
    // ... use input ...
}

If deserialization fails, host_args returns Err(DoubleUSize) — a pointer to a serialized WasmError that the host knows how to read. The guest must immediately return that pointer; trying to recover or call further host functions after a deserialization failure leaves the guest in an inconsistent state and risks corrupting memory.

§Calling host functions with host_call

Unlike host_args, host_call returns a native Rust Result, so it works anywhere — including outside of an extern function. Pass it the extern declared by host_externs! and a serializable input; it deserializes the host’s response into the type you ask for:

use holochain_wasmer_guest::*;

host_externs!(test_process_string:2);

fn call_host() -> Result<String, WasmError> {
    let input = String::from("hello");
    let output: String = host_call(__hc__test_process_string_2, &input)?;
    Ok(output)
}

Inside an extern "C" guest function — where the return type is DoubleUSize, not Result — wrap the call with the try_ptr! macro to get ?-style early return:

use holochain_wasmer_guest::*;

host_externs!(test_process_string:2);

#[no_mangle]
pub extern "C" fn process(_: usize, _: usize) -> DoubleUSize {
    let input = String::from("hello");
    let output: String = try_ptr!(
        host_call(__hc__test_process_string_2, &input),
        "test_process_string_2 failed"
    );
    return_ptr(output)
}

§Returning to the host with return_ptr / return_err_ptr

Inside an extern, the return value the host receives must be a DoubleUSize. Use return_ptr for any serializable success value and return_err_ptr for a WasmError:

use holochain_wasmer_guest::*;

#[no_mangle]
pub extern "C" fn ok_example(_: usize, _: usize) -> DoubleUSize {
    return_ptr("hello from the guest".to_string())
}

#[no_mangle]
pub extern "C" fn err_example(_: usize, _: usize) -> DoubleUSize {
    return_err_ptr(wasm_error!(WasmErrorInner::Guest("nope".into())))
}

The host treats every guest return value as an “outer” Result: Ok means the guest completed normally and any inner success or domain error is encoded in the value, while Err means the host↔guest interface itself failed (deserialization, missing extern, etc.) and the host should treat the instance as suspect.

§Worked examples

See test-crates/wasms/wasm_core/src/wasm.rs in the holochain-wasmer repository for a complete test wasm that exercises every macro and helper in this crate against a real host built from holochain_wasmer_host.

Re-exports§

pub extern crate holochain_serialized_bytes;

Modules§

allocation
result
serde_bytes
Wrapper types to enable optimized handling of &[u8] and Vec<u8>.

Macros§

holochain_serial
unidiomatic way to derive default trait implementations of TryFrom in/out of SerializedBytes
host_externs
paste
try_ptr
A simple macro to wrap return_err_ptr in an analogy to the native rust ?.
wasm_error
Helper macro for returning an error from a WASM.

Structs§

SerializedBytes
A Canonical Serialized Bytes representation for data If you have a data structure that needs a canonical byte representation use this Always round-trip through SerializedBytes via. a single TryFrom implementation. This ensures that the internal bytes of SerializedBytes are indeed canonical. The corrolary is that if bytes are NOT wrapped in SerializedBytes we can assume they are NOT canonical. Typically we need a canonical serialization when data is to be handled at the byte level by independently implemented and maintained systems.
UnsafeBytes
UnsafeBytes the only way to implement a custom round trip through bytes for SerializedBytes It is intended to be an internal implementation in TryFrom implementations The assumption is that any code using UnsafeBytes is NOT valid messagepack data This allows us to enforce that all data round-tripping through SerializedBytes is via TryFrom and also allow for custom non-messagepack canonical representations of data types.
WasmError
Wraps a WasmErrorInner with the location it was raised at.

Enums§

SerializedBytesError
WasmErrorInner
Enum of all possible ERROR states that wasm can encounter.

Traits§

Deserialize
A data structure that can be deserialized from any data format supported by Serde.
Serialize
A data structure that can be serialized into any data format supported by Serde.
TryFrom
Simple and safe type conversions that may fail in a controlled way under some circumstances. It is the reciprocal of TryInto.
TryInto
An attempted conversion that consumes self, which may or may not be expensive.

Functions§

decode
encode
host_args
Receive arguments from the host. The guest sets the type O that the host needs to match. If deserialization fails then a GuestPtr to a WasmError::Deserialize is returned. The guest should immediately return an Err back to the host. The WasmError::Deserialize enum contains the bytes that failed to deserialize so the host can unambiguously provide debug information.
host_call
Given an extern that we expect the host to provide:
merge_u32
merge_u64
merge_usize
Given 2x u32, return a DoubleUSize merged. Works via a simple bitwise shift to move the pointer to high bits then OR the length into the low bits.
return_err_ptr
Convert a WasmError to a GuestPtrLen as best we can. This is not necessarily straightforward as the serialization process can error recursively. In the worst case we can’t even serialize an enum variant, in which case we panic. The casts from usize to u32 are safe as long as the guest code is compiled for wasm32-unknown-unknown target.
return_ptr
Convert any serializable value into a GuestPtr that can be returned to the host. The host is expected to know how to consume and deserialize it.
split_u64
split_u128
split_usize
Given 2x merged usize, split out two usize. Performs the inverse of merge_usize.

Type Aliases§

DoubleUSize
GuestPtr
A WasmSize that points to a position in wasm linear memory that the host and guest are sharing to communicate across function calls.
GuestPtrLen
Enough bits to fit a pointer and length into so we can return it. The externs defined as “C” don’t support multiple return values (unlike wasm). The native Rust support for wasm externs is not stable at the time of writing.
Len
A WasmSize integer that represents the size of bytes to read/write to memory.
WasmSize
Something like usize for wasm. Wasm has a memory limit of 4GB so offsets and lengths fit in u32.

Derive Macros§

Deserialize
Serialize
SerializedBytes