While the README document details how to build the hyperlight-wasm SDK for use, this document details how to use in a host application.
These APIs are still unstable, and may change significantly in the future as we explore alternative ways to construct/cache/etc sandboxes.
Hyperlight-Wasm currently supports running both core WebAssembly modules and WebAssembly Component Model components. While the core structures discussed below are the same in either mode, the details of registering host functions and calling guest functions are quite different.
To use a WebAssembly component in hyperlight-wasm, the binary-encoded
component type of the relevant encoding (of the form produced by
encoding a WIT package; in particular, instance imports/exports need
WIT-formatted "wit:package/instance@version" names) needs to be
available at compile-time. To enable this world, set
HYPERLIGHT_WASM_WORLD
when building hyperlight-wasm
:
wasm-tools component wit -w -o world.wasm world.wit
export HYPERLIGHT_WASM_WORLD=$(readlink -f world.wasm)
The three primary APIs with which you'll be interacting are ProtoWasmSandbox
, WasmSandbox
and [LoadedWasmSandbox
](./src/hyperlight_wasm//s
rc/loaded_wasm_sandbox.rs). These are three different Rust struct
s that provide a type-safe way to ensure the current state of the system.
One helpful way to think about these three different types is as a strongly-typed state machine that is enforced by the Rust compiler. This type-safety feature is important because it allows you, the application developer, to detect issues sooner, and without having to write as many tests (because the compiler is doing more checks for you).
We'll be detailing Hyperlight-Wasm in this document using this state machine concept.
The ProtoWasmSandbox
is the initial type to be created using a SandboxBuilder
. This type allows for registration of host functions (these are functions that the host application wants to make available to be called by code running inside the sandbox). The ProtoWasmSandbox
type is a sandbox that has been initialized with host functions,
but has no Wasm runtime loaded. Once it has been transitioned to a WasmSandbox
type, host functions can no longer be registered.
The WasmSandbox
represents a sandbox state that is not completely ready for use. While it does have the wasm_runtime
guest binary loaded into it (see Rust.md for more details on this guest binary), and the Wasm runtime initialised, it is missing a user's WebAssembly module code. The 'WasmSandbox' type is an intermediate state that is designed to be cached in a host to avoid having to pay the cost of loading the wasm_runtime
guest binary and initializing the Wasm runtime each time a new user code module is loaded.
Loading user code is the final initialization step necessary to have a ready-to-use sandbox, so moving from the WasmSandbox
state to the LoadedWasmSandbox
state requires specifying what user code to load. See the "State transitions" section below for details on making this state transition.
In the previous section, we mentioned that WasmSandbox
is an intermediate state and LoadedWasmSandbox
is a "final" state. In other words, if you have a LoadedWasmSandbox
type, you are ready to execute customer-provided workloads. Do so by calling the call_guest_function
function when using a WebAssembly module, or by using an autogenerated wrapper when using a WebAssembly component. In addition, the LoadedWasmSandbox
type can be safely transitioned back to a WasmSandbox
type. This is useful if you want cache the WasmSandbox
prior to loading a different user code module.
This library includes a standardized way to make transitions between ProtoWasmSandbox
, WasmSandbox
and LoadedWasmSandbox
.
To transition from ProtoWasmSandbox
to WasmSandbox
, use the load_runtime()
method. This method consumes the ProtoWasmSandbox
and returns a WasmSandbox
. This method is useful when you want to cache a WasmSandbox
for later use, but you don't yet have a user code module to load.
To transition from WasmSandbox
to LoadedWasmSandbox
, use the load_module()
method. This method consumes the WasmSandbox
and returns a LoadedWasmSandbox
.
To transition from LoadedWasmSandbox
to WasmSandbox
, use the unload_module()
method. This method consumes the LoadedWasmSandbox
and returns a WasmSandbox
. This method is useful when you want to reuse a WasmSandbox
for a different code module.
This entire process is type safe, and thus we enforce valid state transition orders in the type system. What this all means for you is the following:
The Rust compiler checks your code for the right "state" transitions. In other words, it's impossible for you to compile code that does the wrong thing.
Please see our Hello World example for a complete, compilable code illustrating how to do these transitions through the state machine.
Before you can transition a ProtoWasmSandbox
to a WasmSandbox
(and on to a LoadedWasmSandbox
where you can call guest functions), you need to register the "host functions" that the guest can use: the guest can call back into these functions while it is executing.
In module mode, register host functions with ProtoWasmSandbox::register_host_func_i
. In this method, the i
suffix indicates the number of parameters your host function has. Currently a number in the range from 0
to 3
(inclusive) is supported.
Please see an illustration of how to do this in the hostfunc/main.rs Rust example.
Hyperlight host functions support a limited set of types for parameters and return values. Below is a list of types supported by each:
()
(nothing): ❌ parameter type, ✅ return typeString
: ✅ parameter type, ✅ return typei32
: ✅ parameter type, ✅ return typei64
: ✅ parameter type, ✅ return typebool
: ✅ parameter type, ✅ return typeVec<u8>
: ✅ parameter type, ✅ return type
If you are using the component mode, register host functions by
generating host bindings with
hyperlight_component_macro::host_bindgen!();
and then calling the
created register_host_functions
, which will also close over an
arbitrary state structure of your design:
impl wit::package::Component for State {
// ...
}
let rt = register_host_functions(&mut proto_wasm_sandbox, state);
This will return the "resource table" that is, as described above, required when calling host functions.
Assuming you have a LoadedWasmSandbox
(using the WasmSandbox::load_module
method as described in the previous section), you can call functions exported by the WebAssembly module or component that you've loaded.
You can execute module exports use call_guest_function
, as shown in
this example:
use hyperlight_wasm::{ParameterValue, ReturnType, ReturnValue};
// Assumptions:
//
// - loaded_sbox is a LoadedWasmSandbox type
// - it has a Wasm module loaded that exports a 'max' function that accepts
// two int32 parameters and returns one containing the value of the maximum
// of the two given parameters
let guest_func_ret: ReturnValue = loaded_sbox.call_guest_function(
"my_function",
Some(vec![ParameterValue::Int(1), ParameterValue::Int(2)]),
ReturnType::Int,
)?
// at this point, we expect guest_func_ret to be a ReturnValue::Int, so
// let's check for that here
match guest_func_ret {
ReturnValue::Int(i_val) => if i_val != 2 {
panic!("got {i_val} from the guest, but expected 2");
},
other => panic!("we didn't get the expected type back from our guest call"),
};
In the above code snippet, we call a guest function named my_function
that takes two i32
parameters and returns an i32
. We expect the return value to be 2
, and we panic if it is not. Once the guest function is complete the state of the LoadedWasmSandbox
is reverted to the state as it was before the call.
There are more detailed full examples of calling module exports in these examples:
Assuming that you have registered the component imports as a host
function, as described above, and gotten a LoadedWasmSandbox
, you
will need to construct a wit::package::ComponentSandbox
structure
that contains the sandbox itself along with the rts
resource table that you
received when registering host functions:
let mut wrapped = wit::package::ComponentSandbox {
sb: loaded_wasm_sandbox,
rt: rt,
};
The resultant structure will implement a generated trait called
wit::package::ComponentExports
that allows you to extract exported
instances/functions and call them. Currently, running cargo doc --document-private-items
is the easiest way to see the trait bindings
produced for a particular world.