Usage

This guide will walk you through Extism concepts using the Python SDK.

Installation

To use extism, first install it using pip:

(.venv) $ pip install extism

Getting Started

To start, let’s use a pre-existing WebAssembly module. We’ll define a Manifest that pulls a pre-compiled Wasm module down from the web. In this case, we’ll start with a vowel counting module:

 1import extism
 2import json
 3
 4manifest = {
 5    "wasm": [
 6        {
 7            "url": "https://github.com/extism/plugins/releases/download/v0.3.0/count_vowels.wasm",
 8            # Note the hash! We use this to ensure that the remote file matches what we expect.
 9            # If the remote version changes, the plugin will raise an `extism.Error` when we try
10            # to construct it.
11            "hash": "cf29364cb62d3bc4de8654b187fae9cf50174634760eb995b1745650e7a38b41"
12        }
13    ]
14}
15with extism.Plugin(manifest, wasi=True) as plugin:
16    ...

Let’s call a Wasm function from the module. Our example module exports a single guest function, count_vowels:

15with extism.Plugin(manifest, wasi=True) as plugin:
16    wasm_vowel_count = plugin.call(
17        "count_vowels",
18        "hello world",
19    )
20    print(wasm_vowel_count) # b'{"count": 3, "total": 3, "vowels": "aeiouAEIOU"}'

All Extism guest functions have a simple “bytes-in”, “bytes-out” interface. In this case, the input is a utf-8 encoded string, and the output is a utf-8 encoded string containing JSON. We can pass a parse parameter to call to automatically decode the output for us:

15with extism.Plugin(manifest, wasi=True) as plugin:
16
17    wasm_vowel_count = plugin.call(
18        "count_vowels",
19        "hello world",
20        parse = lambda output: json.loads(
21          bytes(output).decode('utf-8')
22        )
23    )
24    print(wasm_vowel_count) # {'count': 3, 'total': 3, 'vowels': 'aeiouAEIOU'}

State and Configuration

Plugins may keep track of their own state:

15with extism.Plugin(manifest, wasi=True) as plugin:
16    [print(plugin.call(
17        "count_vowels",
18        "hello world",
19        parse = lambda output: json.loads(
20          bytes(output).decode('utf-8')
21        )
22    )['total']) for _ in range(0, 3)] # prints 3, 6, 9
23
24# if we re-instantiate the plugin, however, we reset the count:
25with extism.Plugin(manifest, wasi=True) as plugin:
26    [print(plugin.call(
27        "count_vowels",
28        "hello world",
29        parse = lambda output: json.loads(
30          bytes(output).decode('utf-8')
31        )
32    )['total']) for _ in range(0, 3)] # prints 3, 6, 9 all over again

We can also statically configure values for use by the guest:

15with extism.Plugin(manifest, wasi=True) as plugin:
16    print(plugin.call(
17        "count_vowels",
18        "hello world",
19        config = {'vowels': 'h'}
20        parse = lambda output: json.loads(
21          bytes(output).decode('utf-8')
22        )
23    )['count']) # 1

This example:

  1. Downloads Wasm from the web,

  2. Verifies that the Wasm matches a hash before running it,

  3. And executes a function exported from that module from Python.

Host Functions

What if you want your guest plugin to communicate with your host? Extism has your back.

Let’s take a look at a slightly more advanced example. This time, we’re going to call a guest function in a locally defined module in WebAssembly Text Format (AKA “WAT” format), that guest function is going to call a host function, and then the guest function will return the result.

Lets take a look at a new example.

host.py
 1from extism import host_fn, Plugin
 2
 3# Our host function. Note the type annotations -- these allow extism to
 4# automatically generate Wasm type bindings for the function!
 5@host_fn()
 6def hello_world(input: str) -> str:
 7    print(input)
 8    return "Hello from Python!"
 9
10with Plugin(open("guest.wat", "rb").read()) as plugin:
11    plugin.call("my_func", b"")

We’ve defined a host function that takes a string and returns a string. If you’re interested in the guest plugin code, check it out below.

The extism.host_fn() decorator registers our Python function with extism, inferring the name we want to use from the function name. Because hello_world has type annotations, extism.host_fn() automatically created appropriate low-level Wasm bindings for us. We can take this a step further, though: if we expect to be called with JSON data, we can indicate as much:

host.py
 1from extism import host_fn, Plugin, Json
 2from typing import Annotated
 3
 4# Our host function. Note the type annotations -- these allow extism to
 5# automatically generate Wasm type bindings for the function!
 6@host_fn()
 7def hello_world(input: Annotated[dict, Json]) -> str:
 8    print(input['message'])
 9    return "Hello from Python!"
10
11with Plugin(open("guest.wat", "rb").read()) as plugin:
12    plugin.call("my_func", b"")

Guest Code

Tip

If the WebAssembly here looks intimidating, check out Extism Plugin Dev Kits (PDKs), which allow you to write code in your language of choice.

guest.wat
 1(module
 2    (; code between winking-emoji parenthesis is a WAT comment! ;)
 3
 4    (import "env" "hello_world" (func $hello (param i64) (result i64)))
 5    (import "env" "extism_alloc" (func $extism_alloc (param i64) (result i64)))
 6    (import "env" "extism_store_u8" (func $extism_store_u8 (param i64 i32)))
 7    (import "env" "extism_output_set" (func $extism_output_set (param i64 i64)))
 8    (import "env" "extism_length" (func $extism_length (param i64) (result i64)))
 9
10    (; store a string to send to the host. ;)
11    (memory $memory (export "mem")
12      (data "{\"message\": \"Hello from WAT!\"}\00")
13    )
14    (func $my_func (result i64)
15      (local $result i64)
16      (local $offset i64)
17      (local $i i32)
18      (local.set $offset (call $extism_alloc (i64.const 15)))
19
20      (;
21        You can read this as:
22
23        for(i=0; memory[i] != 0; ++i) {
24          extism_store_u8(offset + i, memory[i])
25        }
26
27        We're copying our local string into extism memory to transport it to the host.
28      ;)
29      (block $end
30        (loop $loop
31          (br_if $end (i32.eq (i32.const 0) (i32.load8_u (local.get $i))))
32
33          (call $extism_store_u8
34            (i64.add
35              (local.get $offset)
36              (i64.extend_i32_u (local.get $i))
37            )
38            (i32.load8_u (local.get $i))
39          )
40
41          (local.set $i (i32.add (i32.const 1) (local.get $i)))
42          br $loop
43        )
44      )
45
46      (; call the host and store the resulting offset into extism memory in a local variable. ;)
47      local.get $offset
48      (local.set $result (call $hello (local.get $offset)))
49
50      (;
51        now tell extism we want to use that offset as our output memory. extism tracks the extent
52        of that memory, so we can call extism_length() to get that data.
53      ;)
54      (call $extism_output_set
55        (local.get $result)
56        (call $extism_length (local.get $result))
57      )
58    )
59    (export "my_func" (func $my_func))
60)