Files
extism/runtime

Extism runtime and rust-sdk

This repo contains the code for the Extism runtime and rust-sdk. It can be embedded in any Rust application to call Extism plug-ins.

Note

: If you're unsure what Extism is or what an SDK is see our homepage: https://extism.org.

Installation

Cargo

To use the extism crate, you can add it to your Cargo file:

[depdendencies]
extism = "*"

Environment variables

There are a few environment variables that can be used for debugging purposes:

  • EXTISM_ENABLE_WASI_OUTPUT=1: show WASI stdout/stderr
  • EXTISM_MEMDUMP=extism.mem: dump Extism linear memory to a file
  • EXTISM_COREDUMP=extism.core: write coredump to a file when a WebAssembly function traps
  • EXTISM_DEBUG=1: generate debug information
  • EXTISM_PROFILE=perf|jitdump|vtune: enable Wasmtime profiling

Getting Started

This guide should walk you through some of the concepts in Extism and the extism crate.

Creating A Plug-in

The primary concept in Extism is the plug-in. You can think of a plug-in as a code module stored in a .wasm file.

Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web:

use extism::*;

fn main() {
  let url = Wasm::url(
    "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"
  );
  let manifest = Manifest::new([url]);
  let mut plugin = Plugin::new_with_manifest(&manifest, [], true).unwrap();
  let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
  println!("{}", res);
}

Note

: See the Manifest docs as it has a rich schema and a lot of options.

Calling A Plug-in's Exports

This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: count_vowels. We can call exports using Extism::Plugin::call:

let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
println!("{}", res);
# => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}

All exports have a simple interface of bytes-in and bytes-out. This plug-in happens to take a string and return a JSON encoded string with a report of results.

The call function uses extism-convert to determine which input/output types can be used. If we wanted to use a concrete type for the count_vowels result, we could defined a struct:

#[derive(Debug, serde::Deserialize)]
struct VowelCount {
  count: usize,
  total: usize,
  vowels: String,
}

Then we can use Json to get the JSON results decoded into VowelCount:

let Json(res) = plugin.call::<&str, Json<VowelCount>>::("count_vowels", "Hello, world!").unwrap();
println!("{:?}", res);
# => VowelCount {count: 3, total: 3, vowels: "aeiouAEIOU"}

Plug-in State

Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:

let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
println!("{}", res);
# => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}

let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
println!("{}", res);
# => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}

These variables will persist until this plug-in is freed or you initialize a new one.

Configuration

Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:

let manifest = Manifest::new([url]);
let mut plugin = Plugin::new_with_manifest(&manifest, [], true);
let res = plugin.call::<&str, &str>::("count_vowels", "Yellow, world!").unwrap();
println!("{}", res);
# => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
let mut plugin = Plugin::new_with_manifest(&manifest, [], true).with_config_key("vowels", "aeiouyAEIOUY");
let res = plugin.call::<&str, &str>::("count_vowels", "Yellow, world!").unwrap();
println!("{}", res);
# => {"count": 4, "total": 4, "vowels": "aeiouyAEIOUY"}

Host Functions

Let's extend our count-vowels example a little bit: Instead of storing the total in an ephemeral plug-in var, let's store it in a persistent key-value store!

Wasm can't use our KV store on it's own. This is where Host Functions come in.

Host functions allow us to grant new capabilities to our plug-ins from our application. They are simply some Rust functions you write which can be passed down and invoked from any language inside the plug-in.

Let's load the manifest like usual but load up this count_vowels_kvstore plug-in:

let url = Wasm::url(
  "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"
);
let manifest = Manifest::new([url]);

Note

: The source code for this is here and is written in rust, but it could be written in any of our PDK languages.

Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store.

We want to expose two functions to our plugin, kv_write(key: String, value: Bytes) which writes a bytes value to a key and kv_read(key: String) -> Bytes which reads the bytes at the given key.

use extism::*;

// pretend this is redis or something :)
type Map = std::collections::BTreeMap<String, Vec<u8>>;
type KVStore = std::sync::Arc<std::sync::Mutex<Map>>;

// When an untyped first argument is provided to `host_fn` it is used as the 
// variable name for the `UserData` parameter 
host_fn!(kv_read(user_data, key: String) -> u32 {
    let kv = user_data.get::<KVStore>().unwrap();
    let value = kv
        .lock()
        .unwrap()
        .get(&key)
        .map(|x| u32::from_le_bytes(x.clone().try_into().unwrap()))
        .unwrap_or_else(|| 0u32);
    Ok(value)
});

host_fn!(kv_write(user_data, key: String, value: u32) {
    let kv = user_data.get_mut::<KVStore>().unwrap();
    kv.lock().unwrap().insert(key, value.to_le_bytes().to_vec());
    Ok(())
});

fn main() {
    let kv_store = KVStore::default();

    // Wrap kv_read function
    let kv_read = Function::new(
        "kv_read",
        [ValType::I64],
        [ValType::I64],
        Some(UserData::new(kv_store.clone())),
        kv_read,
    );

    // Wrap kv_write function
    let kv_write = Function::new(
        "kv_write",
        [ValType::I64, ValType::I64],
        [],
        Some(UserData::new(kv_store.clone())),
        kv_write,
    );

    let url = Wasm::url(
        "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm",
    );
    let manifest = Manifest::new([url]);
    let mut plugin = Plugin::new_with_manifest(&manifest, [kv_read, kv_write], true);
    let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
    println!("{}", res);
}

Note

: In order to write host functions you should get familiar with the methods on the Extism::CurrentPlugin and Extism::CurrentPlugin types.

Now we can invoke the event:

let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
println!("{}", res);
# => Read from key=count-vowels"
# => Writing value=3 from key=count-vowels"
# => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}

let res = plugin.call::<&str, &str>::("count_vowels", "Hello, world!").unwrap();
println!("{}", res);
# => Read from key=count-vowels"
# => Writing value=6 from key=count-vowels"
# => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}