Skip to content

Latest commit

 

History

History
168 lines (118 loc) · 9.57 KB

RustDev.md

File metadata and controls

168 lines (118 loc) · 9.57 KB

Using the Rust Hyperlight-Wasm SDK

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.

WebAssembly Component vs WebAssembly Module APIs

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)

ProtoWasmSandbox vs WasmSandbox vs LoadedWasmSandbox

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 structs 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.

More about ProtoWasmSandbox

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.

More about WasmSandbox

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.

More about LoadedWasmSandbox

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.

Making transitions between ProtoWasmSandbox WasmSandbox and LoadedWasmSandbox

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.

Registering host functions

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.

Registering host functions in module mode

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.

Parameter and return types

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 type
  • String: ✅ parameter type, ✅ return type
  • i32: ✅ parameter type, ✅ return type
  • i64: ✅ parameter type, ✅ return type
  • bool: ✅ parameter type, ✅ return type
  • Vec<u8>: ✅ parameter type, ✅ return type

Registering host functions in component mode

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.

Calling guest 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.

Calling WebAssembly module exports with call_guest_function

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:

Calling WebAssembly component exports with generated bindings

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.