25 releases (3 stable)

2.0.0 Sep 24, 2024
1.1.0 Mar 23, 2023
1.0.0 Feb 10, 2022
0.10.1 Oct 6, 2020
0.0.4 Nov 5, 2019

#74 in WebAssembly

Download history 804/week @ 2024-07-30 1113/week @ 2024-08-06 625/week @ 2024-08-13 474/week @ 2024-08-20 465/week @ 2024-08-27 582/week @ 2024-09-03 574/week @ 2024-09-10 803/week @ 2024-09-17 1254/week @ 2024-09-24 354/week @ 2024-10-01 793/week @ 2024-10-08 351/week @ 2024-10-15 738/week @ 2024-10-22 229/week @ 2024-10-29 233/week @ 2024-11-05 241/week @ 2024-11-12

1,523 downloads per month
Used in 6 crates

Apache-2.0

46KB
608 lines

waPC

crates.io license

This is the Rust implementation of the waPC host protocol. This crate defines the WapcHost struct and the WebAssemblyEngineProvider trait to provide dynamic execution of WebAssembly modules. For information on the implementations with WebAssembly engines, check out the provider crates below:

wapc

The wapc crate provides a high-level WebAssembly host runtime that conforms to an RPC mechanism called waPC (WebAssembly Procedure Calls). waPC is designed to be a fixed, lightweight standard allowing both sides of the guest/host boundary to make method calls containing arbitrary binary payloads. Neither side of the contract is ever required to perform explicit allocation, ensuring maximum portability for wasm targets that might behave differently in the presence of garbage collectors and memory relocation, compaction, etc.

To use wapc, first you'll need a waPC-compliant WebAssembly module (referred to as the guest) to load and execute. You can find a number of these samples available in the GitHub repository.

Next, you will need to chose a runtime engine. waPC describes the function call flow required for wasm-RPC, but it does not dictate how the low-level WebAssembly function calls are made. This allows you to select whatever engine best suits your needs, whether it's a JIT-based engine or an interpreter-based one. Simply instantiate anything that implements the WebAssemblyEngineProvider trait and pass it to the WapcHost constructor and the WapcHost will facilitate all RPCs.

To make function calls, ensure that you provided a suitable host callback function (or closure) when you created your WapcHost. Then invoke the call function to initiate the RPC flow.

Example

The following is an example of synchronous, bi-directional procedure calls between a WebAssembly host runtime and the guest module.

use std::error::Error;

use wapc::WapcHost;
use wasmtime_provider::WasmtimeEngineProviderBuilder; // Or Wasm3EngineProvider

pub fn main() -> Result<(), Box<dyn Error>> {

  // Sample host callback that prints the operation a WASM module requested.
  let host_callback = |id: u64, bd: &str, ns: &str, op: &str, payload: &[u8]| {
    println!("Guest {} invoked '{}->{}:{}' with a {} byte payload",
    id, bd, ns, op, payload.len());
    // Return success with zero-byte payload.
    Ok(vec![])
  };

  let file = "../../wasm/crates/wasm-basic/build/wasm_basic.wasm";
  let module_bytes = std::fs::read(file)?;

  let engine = WasmtimeEngineProviderBuilder::new()
    .module_bytes(&module_bytes)
    .build()?;
  let host = WapcHost::new(Box::new(engine), Some(Box::new(host_callback)))?;

  let res = host.call("ping", b"payload bytes")?;
  assert_eq!(res, b"payload bytes");

  Ok(())
}

For running examples, take a look at the examples available in the individual engine provider repositories:

Notes

waPC is reactive. Hosts make requests and guests respond. During a request, guests can initiate calls back to the host and interact with the environment (via WASI). When a request is done the guest should be considered parked until the next request.

async Support

A waPC-compliant WebAssembly module can be used in an asynchronous context. This can be done using a WapcHostAsync and a provider that implements the WebAssemblyEngineProviderAsync trait. Currently only the wasmtime-provider crate provides an implementation of this trait.

Note: the async support relies on the tokio runtime.

Example

The following is an example of synchronous, bi-directional procedure calls between a WebAssembly host runtime and the guest module, all done inside an asynchronous context.

use std::error::Error;

use wapc::{HostCallbackAsync, WapcHostAsync};
use wasmtime_provider::WasmtimeEngineProviderBuilder;

async fn host_callback(
  id: u64,
  bd: String,
  ns: String,
  op: String,
  payload: Vec<u8>,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
  println!(
    "Guest {} invoked '{}->{}:{}' on the host with a payload of '{}'",
    id,
    bd,
    ns,
    op,
    ::std::str::from_utf8(&payload).unwrap()
  );
  Ok(vec![])
}

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn Error>> {
  let file = "../../wasm/crates/wasm-basic/build/wasm_basic.wasm";
  let module_bytes = std::fs::read(file)?;

  let engine = WasmtimeEngineProviderBuilder::new()
    .module_bytes(&module_bytes)
    .build_async()?;

  let callback: Box<HostCallbackAsync> = Box::new(move |id, bd, ns, op, payload| {
    let fut = host_callback(id, bd, ns, op, payload);
    Box::pin(fut)
  });

  let host = WapcHostAsync::new(Box::new(engine), Some(callback)).await?;

  let res = host.call("ping", b"payload bytes").await?;
  assert_eq!(res, b"payload bytes");

  Ok(())
}

RPC Exchange Flow

The following is a detailed outline of which functions are invoked and in which order to support a waPC exchange flow, which is always triggered by a consumer invoking the call function. Invoking and handling these low-level functions is the responsibility of the engine provider, while orchestrating the high-level control flow is the job of the WapcHost.

  1. Host invokes __guest_call on the WebAssembly module (via the engine provider)
  2. Guest calls the __guest_request function to instruct the host to write the request parameters to linear memory
  3. Guest uses the op_len and msg_len parameters long with the pointer values it generated in step 2 to retrieve the operation (UTF-8 string) and payload (opaque byte array)
  4. Guest performs work
  5. (Optional) Guest invokes __host_call on host with pointers and lengths indicating the binding, namespace, operation, and payload.
  6. (Optional) Guest can use __host_response and host_response_len functions to obtain and interpret results
  7. (Optional) Guest can use __host_error_len and __host_error to obtain the host error if indicated (__host_call returns 0)
    1. Steps 5-7 can repeat with as many different host calls as the guest needs
  8. Guest will call guest_error to indicate if an error occurred during processing
  9. Guest will call guest_response to store the opaque response payload
  10. Guest will return 0 (error) or 1 (success) at the end of __guest_call

Required Host Exports

List of functions that must be exported by the host (imported by the guest)

Module Function Parameters Description
wapc __host_call br_ptr: i32
bd_len: i32
ns_ptr: i32
ns_len: i32
op_ptr: i32
op_len: i32
ptr: i32
len: i32
-> i32
Invoked to initiate a host call
wapc __console_log ptr: i32, len: i32 Allows guest to log to stdout
wapc __guest_request op_ptr: i32
ptr: i32
Writes the guest request payload and operation name to linear memory at the designated locations
wapc __host_response ptr: i32 Instructs host to write the host response payload to the given location in linear memory
wapc __host_response_len -> i32 Obtains the length of the current host response
wapc __guest_response ptr: i32
len: i32
Tells the host the size and location of the current guest response payload
wapc __guest_error ptr: i32
len: i32
Tells the host the size and location of the current guest error payload
wapc __host_error ptr: i32 Instructs the host to write the host error payload to the given location
wapc __host_error_len -> i32 Queries the host for the length of the current host error (0 if none)

Required Guest Exports

List of functions that must be exported by the guest (invoked by the host)

Function Parameters Description
__guest_call op_len: i32
msg_len: i32
Invoked by the host to start an RPC exchange with the guest module

Dependencies

~0.6–7.5MB
~48K SLoC