Skip to main content

· 5 min read

Today we are announcing a preview release of our new bindings generation system for Extism, XTP Bindgen. XTP Bindgen is part of the XTP toolset. It works hand-in-hand with Extism to generate plug-in bindings and types from a familiar and well-typed IDL document.

Check out this video if you want to jump straight to seeing it in action:

The problem of binding generation

Extism has been a success thanks to the "bytes-in → bytes-out" programming model that has allowed our community to scale the framework's support to over 16 host languages and 11 guest languages seamlessly across a number of backing Wasm runtimes. If you described an Extism function in TypeScript, it might look something like this:

function myExtismFunction(input: Uint8Array | null): Uint8Array | null;

This works for the same reason that HTTP has worked so well: it leaves interpretation of the payload to application logic. This approach doesn't force you into any kind of paradigm or language level restrictions. The two modules just communicate via bytes in-memory and you can decide how to interpret them. Although this may seem too simple to build complex and ergonomic things, with these primitives you can build APIs that feel idiomatic and hide all the Wasm details from the callers.

Still, this leaves our plug-in-system developers to do the work of creating these idiomatic interfaces. And in a lot of cases, this work feels tedious to do in 2024. Although incredibly tempting, we intentionally decided not to solve this problem when we created Extism. Instead, we stabilized Extism 1.0 and learned from our users.

What we learned

What we learned is not too surprising, but our egos perhaps blinded us from the truth: this problem is already solved and it just needs to be adapted for Wasm. Because the underlying programming model is borrowed from HTTP, and everyone is already familiar with these tools, we can reuse a lot from that ecosystem. So the answer ends up being:

  1. Choose a well-known and well-supported encoding
  2. Encode your interface in a web-standard IDL for HTTP APIs
  3. Provide generators that make Wasm calls instead of HTTP calls
  4. Generate code for language-level bindings

We learned you can do this with mostly out-of-the-box OpenAPI. I was able to generate an Extism plug-in system for Lago using just lightly modified OpenAPI tools. Furthermore, we're finding that developers' applications already have their domain models and interfaces they wish to extend encoded this way. And because it's a web layer for them, they already have documentation as well as code to limit and sanitize the data (which are important for running untrusted code). In a way, it's like these applications already have a next-gen, Wasm plug-in system hiding right under their nose.

XTP Bindgen

Earlier this year, we started working on XTP to make it as easy and fun as possible to add extensibility to your application using Extism. We decided we cannot ask our users to make all these choices and tweak OpenAPI or GRPC generators themselves to make their plug-in ecosystems. We want the closest thing to a one-click experience to a fully functional plug-in ecosystem for your app. So we knew that we need to make a first-class tool for this.

XTP Bindgen is a system which takes an XTP Schema (a minimal, wasm-focused extension to the OpenAPI format) and generates plug-in bindings for any of our Extism supported PDK languages. The binding generators themselves are open source and it's easy to modify them or write your own. They're shipped as Extism plug-ins themselves and run in the XTP CLI!

All you need to do is pass a schema file to the xtp plugin init command, choose a language... and voilà! Fully-typed, Extism-compatible plug-in boilerplate at your fingertips:

$ xtp plugin init --schema-file schema.yaml
Select a language

1. TypeScript
2. Go
3. Rust
> 4. C#
5. Zig
6. GitHub Template
7. Local Template

••

↑/k up • ↓/j down • q quit • ? more

XTP will generate the skeleton of an Extism plug-in for your system along with all the binding level code. E.g. if the user chooses TypeScript, they will only interact with TypeScript types (even though it's running in Wasm).

By writing our own generators, we've ensured that everything works smoothly with standard Core Wasm Modules. We only generate types where we can, and when we do generate code, we make sure it looks hand-written so it's easy to modify or debug. We also generate things like doc comments about types and properties defined in the schema. This ensures that your plug-in developers get a first-class experience in their IDEs. They can't tell it's not hand-written!

Try it out

We are excited to hear what you think, so please dive in and make bindings for your Wasm projects with XTP and Extism:

1. Install the xtp CLI

curl https://static.dylibso.com/cli/install.sh -s | bash

2. Write an XTP Schema

See examples and documentation here: https://docs.xtp.dylibso.com/docs/concepts/xtp-schema

3. Generate bindings

xtp plugin init --schema-file schema.yaml

And as always, please join us on Discord to share feedback, get help, or just say hello.

· 5 min read

In our last post we showed how to call ChatGPT from inside an Extism plug-in. Although it’s a useful capability for plug-in developers, there isn’t anything specially suited for Wasm here. So we’ve been thinking about what role Wasm might be able to play in this LLM enabled future. In this post we will explore one idea around code generation.

Code Generation

One key touted feature of LLMs has been their ability to generate code. Many programmers have interacted with this through tools like Copilot or by just asking ChatGPT programming questions. Programmers generally copy-paste these code snippets or review it in some way before they integrate it into their apps. Directly running code generated by an LLM has some of the same risks that Extism deals with. The code should be considered untrusted. Not only could it be wrong, your use case might expose you to malicious actors and exploits through prompt injection.

Unix Utility Maker

Suppose you want to create a bot that generates unix text processing utilities. You ask ChatGPT through the API to generate some bash, then you pipe some standard input to that bash. The interface might look like this:

util-bot "Make a tool that counts the number of vowels in the input text" \ 
> count_vowels.sh
echo "Hello World" | sh count_vowels.sh

# => 3

The bash it generates could do anything on your computer. So this feels like a scary idea. Here the worst case scenario is likely that your script is wrong and destroys some state on your machine, but you could see if you had a use case that allowed untrusted input into this code generator, someone could easily craft some malicious actions.

Prompt injection is an ongoing research topic and some believe it will be impossible to prevent. This is perhaps where Wasm can provide some relief. Wasm can create a sandbox for the generated code and you can give it only the capabilities it needs to get the job done.

info

A Wasm sandbox cannot completely save you from all prompt injection attacks if you need to trust the output. But it can prevent the code from accessing capabilities or data that it should not have access to.

Utility Maker in Extism

Let’s build this using Extism. We’ll use LangChain to generate the code based on a description, and we’ll use the Extism Python SDK to orchestrate the Wasm module for executing the code. We are going to ask it to generate JavaScript code and run it in a sandbox we create with the JavaScript PDK.

info

If you want to jump straight to runnable code, we packaged it up here: https://github.com/extism/func-gen

And demo video can be seen here: https://www.loom.com/share/d956147a1a7d449391ec0778ebe12918

Let’s start with our plug-in. We’ll create a simple JavaScript module that has 2 functions: one to store the code for a function in a plug-in variable and one to read it and invoke it.

// sandbox.js

function register_function() {
let code = Host.inputString();
Var.set("code", code);
}

function invoke() {
let input = Host.inputString();
let code = Var.getString("code");
let func = eval(code);
Host.outputString(func(input).toString());
}

module.exports = {register_function, invoke}
info

This simple plug-in can be re-used in many situations where you want to sandbox some JavaScript code and it works from any host that Extism supports, including the browser!

Let’s start the host side now. First we need some imports:

# host.py

import re
import sys
import json
import pathlib
from extism import Context
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)

In order to generate runnable code, we need to coerce ChatGPT into generating a CJS module with a single function as an export. As an example, here is the code we expect from the prompt:

Write a function that takes a string of comma separated integers and
returns the sum as an integer

This prompt should generate the following code:

module.exports = function sumCommaSeparatedIntegers(str) {
return str.split(',').reduce((acc, curr) => acc + parseInt(curr), 0);
}

This allows us to easily eval it into a function and call it in the plug-in code:

let func = eval(code)
let result = func(inputStr)

This process of coercion is colloquially called prompt engineering. LangChain gives us some tools to do this. Let’s write the function that generates the code from a description:

MARKDOWN_RE = re.compile("\`\`\`javascript([^\`]*)\`\`\`")
chat = ChatOpenAI(temperature=0)

def generate_code(description):
messages = [
SystemMessage(content="Your goal is to write a Javascript function."),
SystemMessage(content="You must export the function as the default export, using the CommonJS module syntax"),
SystemMessage(content="You must not include any comments, explanations, or markdown. The response should be JavaScript only."),
HumanMessage(content=description),
]

response = chat(messages)
code = response.content
# sometimes the LLM wraps the code in markdown, this removes it
# if it does
m = MARKDOWN_RE.match(code)
if m and m.group(1):
code = m.group(1)
return code

Here we use a series of SystemMessages to pre-prompt the model with its goals. We put the user’s description at the end, in the HumanMessage.

info

There are likely more rules you’d want to add to this system, we are not prompt engineers.

Now we’ll write a function to execute this code in the Wasm module.

def execute_code(code, input):
wasm_file_path = pathlib.Path(__file__).parent / "sandbox.wasm"
config = {"wasm": [{"path": str(wasm_file_path)}], "memory": {"max": 5}}

with Context() as context:
plugin = context.plugin(config, wasi=True)
plugin.call("register_function", code)
return plugin.call("invoke", input)
info

You can turn your JS plug-in code into Wasm with the command: extism-js sandbox.js -o sandbox.wasm

And now a main to orchestrate it:

if __name__ == "__main__":
code = generate_code(sys.argv[1])
print(execute_code(code, sys.stdin.read()))

Let’s try it out by asking it to generate the canonical Extism count-vowels example

$ pip3 install extism langchain
$ export OPENAI_API_KEY=sk-mykeyhere # needed to query openai
$ echo "hello world" | python3 host.py "write a function that counts the number of vowels in a string"
b'3'

· 7 min read

Let's make some games

In the previous parts to this blog series (Part I and Part II), we discussed the creation of GameBox, an Elixir application implemented as an Extism host, that provides an extensible platform for multi-player, turn-based games.

In this post, we're going to explore the nuts and bolts of writing some GameBox games! The best part about this is we can harness the power of the Extism Plug-in Development Kits (PDKs) to write our games in any number of languages that can be compiled into WebAssembly, even though GameBox itself is implemented in Elixir. This gives you, the game creator, the flexibility needed to unleash your creative power expressed in the programming language that best suits your passion and skills, all while ensuring the game platform itself remains secure, and the games themselves are lightweight and portable.

The Game API

As the game creator, you need only implement and export four basic functions that GameBox will call during the course of the gameplay. Let’s demonstrate this by creating a “game” which has a single button and shows the log of events coming into the game:

GameBox Screenshot

We’re going to write this in JavaScript for simplicity. Our JavaScript PDK still has a fairly primitive API for defining Extism functions. Different languages come with various levels of sugar to make it prettier. But this should be the easiest to follow.

Helpers

Let’s start with some helper functions. Currently GameBox expects you, the game programmer, to store the state of the game yourself. This can be accomplished with plug-in variables. So let’s make some helpers to get and set the state into a plug-in variable called “state”:

function set_state(state) {
Var.set('state', JSON.stringify(state))
}

function get_state() {
return JSON.parse(Var.getString('state'))
}
info

The variable doesn’t need to be JSON, but this is the easiest way to consistently store any JS object into bytes in this language.

This uses Extism’s Var.set and Var.getString to set and get the variable, and the data will persist between calls.

Interface Functions

Now with our helper functions ready to go, let's move on to the interactions with GameBox itself.

The first function that GameBox will invoke, get_constraints, provides it with some metadata about the constraints you want to apply as the game creator. Currently, these constraints are related to the minimum and maximum number of players, specified as integers, but additional constraints could be added in the future.

get_constraints(void) -> GameConstraints

function get_constraints() {
const constraints ={
min_players: 2,
max_players: 10,
}
Host.outputString(JSON.stringify(constraints))
}

Once all players are ready to start the game, GameBox will call init_game, passing in information about the players and allowing your game to allocate state, memory, and anything else required. Here we are only interested in the player_ids that are joining the game. We define our state and persist it with our set_state helper:

init_game(GameConfig) -> void

function init_game() {
const game_config = JSON.parse(Host.inputString())
const state = {
player_ids: game_config.player_ids,
events: [],
version: 0,
}
set_state(state)
}
info

The version property here will be incremented every time the game state changes. This will be used to tell the system to re-render the clients.

Next, the render function is called each time the game board needs to be rendered. It's called for each user watching or playing the game and each time the state version changes. You can render the game depending on who is viewing it by looking at the metadata on the user's socket. This is passed in as an object called assigns. For example, you will render the game differently based on who's turn it is and which screen it's being rendered on. By default the assigns will always have the player_id, but you can attach whatever you want to it. Take note this may be called for people who are only viewing the game and not playing, so you may want to conditionally render things like controls for those people.

render(Assigns) → String

function ui(state, assigns) {
const version = `<h1>Version: ${state.version}</h1>`
const youare = `<h1>You are ${assigns.player_id}`
const button = `<button phx-click="cell-clicked" phx-value-cell="0">Click me</button>`
// here we will just stringify the events we have received and log them in a list
const events = state.events.map(e => `<li>${JSON.stringify(e)}</li>`)

return `${version} ${youare} ${button} ${events}`
}

function render() {
const assigns = JSON.parse(Host.inputString())
Host.outputString(ui(get_state(), assigns))
}
info

You can attach any assigns you want to the user’s socket in handle_event , but by default it will always contain the version and the player_id

info

For simplicity sake we are just rendering the html using string interpolation. How you render the html is up to you. In tic-tac-toe for example, we use tera templates.

info

It's important to learn about LiveView to understand how the system works and how a game can be created.

And finally, during the course of the game lifecycle, GameBox will proxy any events (e.g. a player making a move, answering a trivia question, etc.) it receives to your game through handle_event and accept a return value that contains some player-specific information.

handle_event(LiveEvent) -> Assigns

function handle_event() {
const event = JSON.parse(Host.inputString())
const state = get_state()
state.events.push(event) // we're just pushing the event into this log
state.version += 1 // tell the system we need to re-render
set_state(state)
// for Assigns here, we only need the version
Host.outputString(JSON.stringify({ version: state.version }))
}

The shape of the input LiveEvent will depend on what Phoenix binding the user interacts with. In our case, with the button we defined, it will look something like this:

{
"event_name":"cell-clicked",
"player_id":"BEN",
"value":{
"cell": "0"
}
}

The return value, Assigns, can be anything you want it to be, but at a minimum it should contain the version. Any assigns you return will get attached to the user’s socket and passed back to you in render.

info

Your game should be viewed as a state machine that receives these events and updates the state until a state transition occurs and the rules change. See some of the example games to understand how this idea can be applied.


And that’s all folks! Of course, your game can be as simple or complex as you like, but the interactions between GameBox and your game are streamlined and simple.

For more details on the API check out the GameBox repo on Github, which also includes a few example games for reference.

What’s Next

While this is all fun and games, the implications are staggering when one thinks about the opportunity for user-generated content that becomes possible across a wide variety of applications when WebAssembly is in the mix. One need only look at Shopify functions as an example of an extendable eCommerce platform, or data platforms such as SingleStore that enable users to write their own data transformations that run right alongside the host code to further see the possibilities.

Whether you want to create an extensible platform of your own, or write plug-ins that extend another platform, Extism, the universal plug-in system, provides you with a powerful facilitation agent to orchestrate the magic on both sides.

Feeling inspired? We’d love to hear about your ideas. Join us on Discord!

· 4 min read

In Part I, we wrote a special Elixir GenServer that allows us to replace any GenServer in our Elixir apps with an Extism Plug-in. Our goal was to build a version of this turn-based game system that allows users of the platform to create and upload their own games.

Today we’re announcing the result, GameBox!

GameBox Screenshot

GameBox

GameBox is hosted on Fly.io and you can start creating and uploading games today. You can write your game in any language where we have PDK support. So far we’ve written games in JavaScript, TypeScript, and Rust. And we have people attempting games in Zig and Go.

If you want to see GameBox in action, we played a live game of Trivia with the entire audience at Wasm/IO in Barcelona this year. See the first few minutes of Steve’s talk here:

How it works

The primary technical concept behind GameBox is, as lifted from the previous post, about replacing a key GenServer implementation with an Extism plug-in. Like its inspiration, GameBox is built with the Phoenix framework and the GenServer we targeted to extend is LiveView. The LiveView module has a pretty simple API:

It's important to learn about LiveView to understand how the system works and how a game can be created.

LiveView LifeCycle

mount/3 is hand coded for the most part, but handle_info/3 and render/1 are proxied to the game plug-in to handle. You can think of the game as a single state machine. Events come in through handle_info and may or may not mutate the state. When the system needs to be redrawn, render is called and the game outputs some HTML.

Events are defined and triggered on the client using Phoenix bindings. The game programmer doesn’t need to code up a javascript front end, they just need to use the HTML bindings provided by Phoenix. For example, in tic tac toe, a button looks like this:

<button
phx-click="cell-clicked"
phx-value-cell="3"
class="cell">
</button>

When the user clicks this button, an event is sent to the handle_info callback of this shape:


{
"player_id": "benjamin",
"event_name": "cell-clicked",
"value": {
"cell": "3"
}
}

The game logic can interpret this message and mutate the state of the board by placing the player’s character (X or O) at cell index 3.

Game Logic

As mentioned before, the game developer creates a game by satisfying these callbacks. We provide an interface, a number of functions, that the game must export to plug-in to the GameBox system. Thanks to Extism, this interface can be implemented in any language we support.

Jump over to Part III of this series to get the details on how to make a game as a plug-in.

Credits

We got a lot of help from the community on this project and we want to thank all the individuals who put so much work into it:

  • Brian Berlin When we came up with the idea, We were a bit overwhelmed jumping back into Phoenix, but luckily Brian's expertise with Elixir got us far. After a week of hacking we had a solid protoype.
  • Revelry When we decided we wanted to step this up and make this a professional app, we turned to Revelry for help. In a few short weeks they were able to polish it into something we can be proud of.
  • Amy Murphy We also want to callout Amy on the Revelry team who went above and beyond making what are the coolest games on the platform.

· 3 min read

We’ve seen a few people try to create Extism plug-ins that make use of ChatGPT. The simplest way you can accomplish this is to use Extism’s built-in HTTP host function. A more advanced option would be to create your own host function.

Let’s go the first route and create the simplest possible plug-in that utilizes ChatGPT. We’ll do this in Rust, but the same is possible from our other plug-in languages.

Project Setup

First let’s initialize our rust project:

mkdir chatgpt-plugin
cd chatgpt-plugin
cargo init --lib
cargo add serde serde_json extism-pdk

Now tell cargo we wish to build a shared library by putting this in Cargo.toml

[lib]
crate_type = ["cdylib"]

Writing lib.rs

First we import the code we need into lib.rs:

use std::str::from_utf8;
use extism_pdk::*;
use serde::{Deserialize};
use serde_json::json;

Then let’s create some structs for the response body. We’re just putting what we need for this example. See the completion docs for the whole response.

#[derive(Deserialize)]
struct ChatMessage {
content: String,
}

#[derive(Deserialize)]
struct ChatChoice {
message: ChatMessage,
}

#[derive(Deserialize)]
struct ChatResult {
choices: Vec<ChatChoice>,
}

Now let’s write our code. We’re going to skip error handling and edge cases as an exercise for the user:

#[plugin_fn]
pub fn call_chat_gpt(input: String) -> FnResult<String> {
// get API key from the plug-in config key open_ai_key
let api_key = config::get("open_ai_key").expect("Could not find config key 'open_ai_key'");
let req = HttpRequest::new("https://api.openai.com/v1/chat/completions")
.with_header("Authorization", format!("Bearer {}", api_key))
.with_header("Content-Type", "application/json")
.with_method("POST");

// We could make our own structs for the body
// this is a quick way to make some unstructured JSON
let req_body = json!({
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"messages": [
{
"role": "user",
"content": input,
}
],
});

let res = http::request::<String>(&req, Some(req_body.to_string()))?;
let body = res.body();
let body = from_utf8(&body)?;
let body: ChatResult = serde_json::from_str(body)?;

Ok(body.choices[0].message.content.clone())
}
caution

Think carefully about how you handle secrets with plug-ins. You should always follow best practices like keeping secrets encrypted at rest and with plug-ins, you must take extra care not to leak secrets across users. Currently there are two ways you can handle secrets:

  1. You can use plug-in configs and the runtime function to update them. If you do this you’ll probably want to assume all configs are secrets and take extra care not to leak configs across customers.
  2. You provide some kind of host function for the plug-in programmer to fetch and decrypt their secrets from a secret provider / manager.

In this example we are going to pass the key from the host via the config object and leaving safely managing it as an exercise to the reader.

Compiling and Running

We can compile and run this plug-in like we always do:

cargo build --release --target wasm32-unknown-unknown
info

Note we do not need a wasi target since we are using Extism’s HTTP host function.

Now we can use the extism CLI to test.

# copy/paste Open AI secret key in environment variable
read -s -p "Enter OpenAI API Key:" OPEN_AI_KEY

# optionally just use export:
# export OPEN_AI_KEY=<past-key-here>

extism call \
target/wasm32-unknown-unknown/release/chatgpt_plugin.wasm \
call_chat_gpt \
--set-config="{\"open_ai_key\": \"$OPEN_AI_KEY\"}" \
--input="Please write me a haiku about Wasm"

Wasm code compiled,
Runs in browser, fast and light,
Web apps flourish bright.

· 5 min read

Part 1: Extism in Elixir

Since I built Elixir support into Extism, I've been thinking about how to demonstrate the power of combining the two.

I wanted to explore wrapping a Wasm module in an Erlang process and see what kind of interesting things you could create when you leverage OTP. Along the way, I discovered that the programming model of an Extism plugin maps almost perfectly to the GenServer behavior.

This has some powerful implications! In this two-part post, I'm going to show why. Our goal is to extend this awesome idea of a turn-based game engine created by Fly.io. With just a couple invocations of the Extism library, we will allow users to create and upload their own turn based games.

Part 1 of this post will be explaining how to use Extism in Elixir and how to create this magic Extism GenServer.

Setting up Extism

Install Depdendencies

All you need to get started is rustup and elixir. If you have these installed you can skip to the next section.

Use Docker

If you want a temporary Dockerized environment, run these commands:

mkdir /tmp/elixir-blogpost
cd /tmp/elixir-blogpost
curl https://extism.org/data/elixir-blogpost/Dockerfile > Dockerfile
docker build -t game_box .
docker run -it game_box bash

Create an Elixir project

Now let's create a new Elixir project with mix:

mix new game_box
cd game_box

Add Extism as a dependency in mix.exs

  defp deps do
[
# ...
{:extism, "~> 0.1.0"},
]
end

Fecth the deps and compile:

mix do deps.get, compile

This should kick off a rust build of the Extism package and runtime and it should end something like this:

Finished release [optimized] target(s) in 3m 39s
Generated extism app
Getting Support

If you do run into a problem at any point please file an issue or reach out on Discord.

Running a plugin

Let's test this out in a repl. Before we do that, let's pull down a compiled wasm plugin to execute: This is our "count-vowels" example plugin:

curl https://raw.githubusercontent.com/extism/extism/main/wasm/code.wasm > code.wasm

Now open an IEx repl:

iex -S mix

Paste in this code. This creates an Extism Context and loads the Plugin. Then it calls the count_vowels function on the plugin with a test string and returns the output of the plugin (which in this case is a JSON encoded result).

ctx = Extism.Context.new
manifest = %{ wasm: [ %{ path: "./code.wasm" } ]}
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
JSON.decode!(output)

If all is working you'll see {"count" => 4} in the output string:

iex(5)> JSON.decode!(output)
%{"count" => 4}

Utilizing OTP

Okay, how do we turn this into a GenServer? There would be two approaches:

  1. Wrap the Extism.Context in the GenServer
    • Store the Context as the state
    • Provide callbacks to load and unload plugins
    • Provide callbacks to lookup and call those plugins
  2. Wrap the Extism.Plugin in the GenServer
    • Store both the context and the plugin as state
    • Provide callbacks to load and reload the plugin code
    • Provide callbacks to call functions on the plugin
What is a Context?

You can think of a Context as an arena of plugins. When a plugin is loaded, the context owns that plugin and is responsible for freeing it. Freeing a context frees all its plugins.

I went with #2 here because I think it's more granular and allows for more flexibility and concurrency. There isn't much overhead to have one context per plugin so it's okay to do it this way.

With that in mind, I implemented this barebones GenServer in lib/game_box.ex. Go ahead and copy paste it into that file:

defmodule GameBox.PluginServer do
use GenServer

@impl true
def init(_init_arg) do
ctx = Extism.Context.new()
# as our state we will store a {Extism.Context, Extism.Plugin} tuple
{:ok, {ctx, nil}}
end

# This special call is for loading or reloading a plugin given a manifest
@impl true
def handle_call({:new, manifest, wasi}, _from, {ctx, plugin}) do
# if we have an existing Plugin let's free it
if plugin do
Extism.Plugin.free(plugin)
end
# Load a new plugin given the manifest and store it in the new state
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, wasi)
{:reply, {:ok, plugin}, {ctx, plugin}}
end

# this is a generic way to proxy messages to the underlying Extism.Plugin module
# we're mostly going to use `call` here:
# e.g. call_details = {:call, "count_vowels", "this is a test"}
@impl true
def handle_call(call_details, _from, {ctx, plugin}) do
[func_name | args] = Tuple.to_list(call_details)
response = apply(Extism.Plugin, func_name, [plugin | args])
{:reply, response, {ctx, plugin}}
end
end

Okay, let's try the count vowels example again but now with a GenServer. Start the repl again:

$ iex -S mix

iex(1)> {:ok, pid} = GenServer.start_link(GameBox.PluginServer, nil)
{:ok, #PID<0.220.0>}
iex(2)> GenServer.call(pid, {:new, %{wasm: [%{path: "./code.wasm"}]}, false})
{:ok,
%Extism.Plugin{
ctx: %Extism.Context{ptr: #Reference<0.584822572.876216322.21881>},
plugin_id: 0
}}
iex(3)> GenServer.call(pid, {:call, "count_vowels", "this is a test"})
{:ok, "{\"count\": 4}"}

Now we're doing the same thing as before, but instead of invoking a function on the plugin, we're invoking it on a pid. What does this inversion give us exactly? Well, because it's a pid, it can live anywhere in your cluster. And it can be supervised and registered by OTP.

This also means we can replace any GenServer in our application, or in a framework like Phoenix, with a Wasm module. This would allow your customers to extend your application or maybe allow you to write part of your Elixir application in a language better suited to the task of the GenServer you are replacing.

Check out part II of this blog series where we show how this PluginServer can be used like a Phoenix.LiveView module.

We also invite you to get in on the action! Join us on Discord.

· 7 min read

Hello, World!

Today, we are excited to officially announce Extism, the universal plug-in system. Over the past few months, we have focused on building an embeddable, safe, performant runtime that is as easy to use from as many programs as possible. Extism brings extensibility to software of all shapes and sizes in a way which we believe to have been previously unacheivable.

Extism's goal is simple: make all software programmable. We've released the project under the permissive BSD-3 license, and you can see all of its components on GitHub.

Extism, universal plug-in system

Being "Universal"

The power of a plug-in system is that it enables software end-users to dream up new capabilities and functionality of a program, which the original authors couldn't foresee, or didn't want to add to the core of a product. Keeping programs as simple as possible often leads to higher quality, better performance, and easier maintainability. It's important to us that software creators using any language have the same opportunity to have these benefits, and also be able to give their users rich extensibility.

As of today, Extism can be easily embedded into 13+ languages, using our official SDKs:

  • Browser
  • C
  • C++
  • Elixir
  • Go
  • Haskell
  • Java
  • .NET
  • Node
  • OCaml
  • PHP
  • PHP
  • Python
  • Ruby
  • Rust
  • Zig

Embed Extism into any of your projects: web apps (using our JavaScript SDK), databases, API servers, command-line apps, smart TVs, IoT, SaaS... you name it! Extism meets your code wherever it's at.

Extism, at its core, is a code runtime built in Rust. Underneath the hood, we run WebAssembly code as the plug-in execution format. This makes it safe to execute untrusted, 3rd party plug-in code even while it's directly embedded within the same OS process as your program. WebAssembly has a battle-tested sandbox isolation architecture, and is in use across the software industry from browsers, edge platforms, cloud environments, and more. In addition to its security benefits, WebAssembly is a compilation target that is already supported by many different programming languages. This enables plug-in authors to use the language that they prefer to write their plug-in, and as of today, Extism plug-ins can be written in 5 languages, using our official plug-in development kits (PDKs):

  • Rust
  • JavaScript
  • Go
  • Haskell
  • AssemblyScript
  • C
  • Zig
  • .NET

We plan to add more language support to our SDKs and PDKs over the coming months & years, so if you don't see your favorite language listed above, please join our Discord or file and issue and we can prioritize it or help you contribute!

How We Built It

Staying true to our goal to make all software programmable, we knew Extism would need to be embedded into several languages. It needs to be able to go anywhere. Other projects with similar goals like sqlite or openssl, are low-level system components exposed to a multitude of language environments through FFI, the foreign function interface. Extism is no different. Choosing Rust was an obvious choice to help build a reliable and performance-sensitive core runtime, and with Rust's fantastic FFI support, we could expose runtime APIs which can be called from almost any language used today.

All of our official SDKs provide idiomatic wrappers over bindings to Extism, so users will feel right at home working within a language they know and love.

The runtime is only half of the equation though. What about plug-in authors? What kind of features and functionality should they get from the runtime? How do we expose these features to the plug-in environment? This is where WebAssembly really shines. Providing a standard ABI to WebAssembly modules is very straightforward thanks to its simple import/export architecture. Extism defines a set of functions which are linked from the host runtime to the .wasm module when it is instantiated, and our PDKs provide idiomatic wrappers over bindings to these native functions. From a .wasm module's perspective, these functions are "imports", provided to it from its host. Some of these include the ability for plug-ins to make network calls via HTTP, persist variables in between plug-in invocations, read host configuration data, and most importantly read & write complex data between the host and plug-in.

In addition to the imports provided by Extism, a host can elect to enable WASI and offer plug-ins a rich POSIX-like system interface, which allows many existing codebases and libraries to be compiled to .wasm modules and used within a plug-in. It's important to keep safety and security in mind, and as such, we've decided to hold off on enabling direct disk/filesystem access from plug-ins, and instead opt for a more explicit requirement to pass file data in and out of a plug-in directly. We're experimenting with approaches here and would appreciate your feedback.

Let's See Some Code

Head over to the SDK documentation for dozens of examples of embedding Extism into all of our supported host languages, or if you're interested in compiling plug-ins to WebAssembly, check out many examples in the PDK QuickStart for plug-in code in each supported language.

As a fully open-source project, we also invite you to head to our GitHub repository and see how everything works. In the main repository, you will find all of the runtime code as well as each of the host SDKs. Each PDK is split into its own repository within the Extism GitHub Organization.

For a quick glance at some simple examples, see below:

Node.js Host SDK Example

index.js
const { withContext, Context } = require('@extism/extism');
const { readFileSync } = require('fs');

withContext(async function (context) {
// get plug-in code from anywhere (disk, network, cache, database etc.)
let wasm = readFileSync('../wasm/code.wasm');
// construct a plug-in, to use WASI, pass `true` to constructor (see docs for more options)
let plugin = context.plugin(wasm);

// simple call any function from the plug-in and pass it any complex data,
// many SDKs provide options to pass strings, raw bytes, etc.
let buf = await plugin.call('count_vowels', 'this could be any data!');

// parse or decode the returned data from the plug-in however your app needs to
console.log(buf.toString());

// plug-ins will be automatically freed where possible, but you can ensure cleanup is done
p.free();
});

Go Plugin PDK Example

main.go
package main

import (
"fmt"

"github.com/extism/go-pdk"
)

//export count_vowels
func count_vowels() int32 {
// read input from the host and use it from within the plug-in
input := pdk.Input()

count := 0
for _, a := range input {
switch a {
case 'A', 'I', 'E', 'O', 'U', 'a', 'e', 'i', 'o', 'u':
count++
default:
}
}

// for demonstration, use persisted variables which can be accessed
// between plug-in invocations
if pdk.GetVar("a") == nil {
pdk.SetVar("a", []byte("this is var a"))
}
varA := pdk.GetVar("a")

// for demonstration, access key-value based configuration data
// provided by the host
thing, ok := pdk.GetConfig("thing")
if !ok {
thing = "<unset by host>"
}

// prepare some data to write back to the host
output := fmt.Sprintf("Counted %d vowels!", count)
mem := pdk.AllocateString(output)

// zero-copy output to host
pdk.OutputMemory(mem)

// most PDKs return a status code to hosts, `0` here indicates success.
return 0
}

What's Next?

Over the coming weeks and months, we plan on working with interested users and incorporating feedback, adding more language support wherever possible, and expanding our documentation and demos for users to learn from. Your input is critical and always appreciated, so please join us on Discord where we hang out and chat. Use each repository's issue tracker to file any experience report or bug you encounter. No feedback is too small, and we thank you for the time it takes to help us make Extism as great as it can be!

Spread the word!