Morphir Extension Development Guide
This guide describes how to develop Morphir extensions using various programming languages. Morphir extensions are WebAssembly (Wasm) components that follow the The Elm Architecture (TEA) pattern.
Prerequisites
Regardless of the language you choose, you will need the following tools:
- Wasmtime (Runtime)
- wit-bindgen (Binding generator)
- wit-component (Component encoder)
The WIT definitions for Morphir extensions are located in crates/morphir-ext-core/wit.
WASI Compatibility and the Component Model
Morphir uses a hybrid architecture to balance modern features with toolchain compatibility:
- Host (Preview 2): The Morphir Daemon provides a WASI Preview 2 (P2) environment. This is based on the WebAssembly Component Model, which allows us to use WIT for high-level interfaces.
- Guests (Preview 1 / Unknown):
- Most language toolchains (Rust
wasm32-wasip1, TinyGo) still generate WASI Preview 1 (P1) code. These can be run in our host using a “compatibility adapter” (shim). - For extensions that don’t need system access (like pure logic or string transformations), we recommend the
wasm32-unknown-unknowntarget. This produces a “pure” Wasm module with no system dependencies, making it faster to load and easier to distribute.
- Most language toolchains (Rust
1. Rust
Rust provides the most mature support for Wasm Components.
Setup
Add wit-bindgen to your Cargo.toml:
[dependencies]
wit-bindgen = "0.35.0"
Implementation
Use the bindgen! macro to generate host and guest bindings.
wit_bindgen::generate!({
world: "extension",
path: "wit", // Path to the .wit files
});
struct MyExtension;
impl Guest for MyExtension {
fn init(init_data: Envelope) -> (Envelope, Envelope) {
// ... implementation
}
fn update(msg: Envelope, model: Envelope) -> (Envelope, Envelope) {
// ... implementation
}
// ... other TEA methods
}
export!(MyExtension);
Build
Target wasm32-wasip1 or wasm32-unknown-unknown, then encode into a component.
2. TypeScript / JavaScript
TypeScript extensions are built using jco and componentize-js.
Setup
Install the necessary tools:
npm install -g @bytecodealliance/jco @bytecodealliance/componentize-js
Implementation
Create a module that matches the exported program interface.
export const program = {
init(initData) {
const initialModel = { /* ... */ };
return [initialModel, []];
},
update(msg, model) {
// ... implementation
return [newModel, commands];
},
subscriptions(model) {
return [];
},
info() {
return { content: "My TypeScript Extension" };
}
};
Build
Componentize the JavaScript using jco:
jco componentize app.js --wit wit --world extension -o extension.wasm
3. Python
Python extensions are built using componentize-py.
Setup
Install componentize-py:
pip install componentize-py
Implementation
Implement the Extension world using Python class-based or module-based exports.
import extension
class MyExtension(extension.Extension):
def init(self, init_data: extension.Envelope) -> (extension.Envelope, extension.Envelope):
# ...
return (model, commands)
def update(self, msg: extension.Envelope, model: extension.Envelope) -> (extension.Envelope, extension.Envelope):
# ...
return (new_model, commands)
Build
componentize-py -d wit -w extension componentize my_app -o extension.wasm
4. Go
Go extensions use TinyGo for small Wasm binaries and wit-bindgen-go.
Setup
Install TinyGo and wit-bindgen-go.
Implementation
Generate bindings and implement the methods.
package main
import (
"morphir/ext/program"
"morphir/ext/envelope"
)
type MyExtension struct{}
func (e *MyExtension) Init(initData envelope.Envelope) (envelope.Envelope, envelope.Envelope) {
// ...
}
func (e *MyExtension) Update(msg envelope.Envelope, model envelope.Envelope) (envelope.Envelope, envelope.Envelope) {
// ...
}
func main() {
program.SetProgram(&MyExtension{})
}
Build
tinygo build -o extension.wasm -target=wasi main.go
# Then use wit-component to encode
Appendix: Low-Level ABI Specification
If you are developing for a language that doesn’t yet have high-level Component Model toolchains, you can implement the Core Wasm ABI directly.
Memory Convention
All structured data (strings, bytes, JSON) is passed using a (pointer, length) pair where both values are i32.
Guest Exports
The extension must export the following functions:
| Function | Signature | Purpose |
|---|---|---|
init | (hdr_p, hdr_l, ct_p, ct_l, m_p, m_l) -> i32 | Initializes the extension. Returns a program ID. |
update | (id, hdr_p, hdr_l, ct_p, ct_l, m_p, m_l) -> void | Sends a message to a specific program instance. |
Host Imports
The host provides the following functions in the env module:
| Function | Signature | Purpose |
|---|---|---|
log | (level, ptr, len) -> void | Logs a message. Level: 0=Trace, 1=Debug, 2=Info, 3=Warn, 4=Error. |
get_env_var | (k_p, k_l, o_p, o_l) -> void | Retrieves an environment variable as JSON. |
set_env_var | (k_p, k_l, v_p, v_l) -> void | Stores an environment variable as JSON. |
Data Structures
- Header: Passed as a JSON string:
{"seqnum": u64, "session_id": "uuid", "kind": "hint"}. - Envelope Content: Passed as raw bytes.
- Environment Value: Passed as a JSON serialized
EnvValueenum.
For further details, explore the implementation in crates/morphir-ext-core/src/abi.rs.