Wawk plugins extend AWK with custom functions using the WIT Component Model. A plugin is a standalone .wasm file that exports two interfaces: wawk:plugins/external-functions (custom functions) and wawk:plugins/format-handler (format detection and parsing). Build once — the same plugin works in wasmtime, Node.js, and the browser with zero changes.
Plugins can be written in any language that compiles to WebAssembly: Rust, C, C++, Go (via TinyGo), or TypeScript (via AssemblyScript). The WIT interface is the same regardless of language.
Build target
wasm32-unknown-unknown
Configuration
--plugin flag
Languages
Rust, C, Go, TS
Architecture
How Plugins Work
When the AWK evaluator encounters a function call it does not recognise as built-in or user-defined, it iterates through loaded plugins. Each plugin receives the function name and arguments via the WIT canonical ABI. A plugin returns some(result) if it handles the function, or none to pass to the next plugin.
Plugin lifecycle: When a plugin is loaded, the host first calls __init__ to initialize it, then __meta__ to retrieve metadata (name, version, dependencies). During execution, function calls are dispatched via the external-functions interface.
1
Host calls __init__ and __meta__ to initialize the plugin and read its metadata.
2
AWK script calls greet("World") — the evaluator does not recognise greet as built-in.
3
Host encodes the call via WIT canonical ABI and invokes wawk:plugins/external-functions#call on each loaded plugin.
4
Plugin matches the function name, builds a greeting, and returns some("Hello, World!").
5
Host decodes the result and feeds it back to the AWK evaluator as the function return value.
Step 1
Define the WIT Interface
Copy the canonical WIT world into your plugin's wit/ directory. This is the contract between your plugin and every wawk host — it is the same regardless of which language you implement in.
Use wit-bindgen to generate bindings from the WIT definition. Implement the function dispatch logic for your language — match on function names and return results. The pattern is the same across all languages.
// src/lib.rs
wit_bindgen::generate!({
world: "wawk-plugin",
path: "wit",
});
structComponent;
// External functions: custom AWK-callable functionsimpl exports::wawk::plugins::external_functions::GuestforComponent {
fncall(name: String, args: Vec<String>) -> Option<String> {
match name.as_str() {
"__init__" => Some("ok".into()),
"__meta__" => Some(r#"{"name":"my-plugin","version":"0.1.0"}"#.into()),
"greet" => {
let who = args.first()
.map(|s| s.as_str())
.unwrap_or("World");
Some(format!("Hello, {}!", who))
}
_ => None,
}
}
}
// Format handler: required by WIT world (stub if not a format plugin)impl exports::wawk::plugins::format_handler::GuestforComponent {
fndetect(_input: String) -> String { String::new() }
fnparse(_input: String) -> String { r#"{"error":"not a format plugin"}"#.into() }
fnserialize(_tree: String) -> String { r#"{"error":"not a format plugin"}"#.into() }
}
export!(Component);
# Generate C bindings from WIT$ wit-bindgen c wit/world.wit --out-dir generated/# Compile with clang to wasm32$ clang --target=wasm32-unknown-unknown \ -I generated/ \ -c plugin.c generated/external-functions.c \ generated/world.c -o plugin.o# Link into a .wasm component$ wasm-ld --no-entry --export-all plugin.o -o plugin.wasm
// plugin.go// Generated by: wit-bindgen go wit/world.witpackage main
import (
"fmt""example.com/generated/wawk/plugins"
)
type WawkPlugin struct{}
func (WawkPlugin) Call(name string, args []string) *string {
if name == "greet" {
who := "World"iflen(args) > 0 {
who = args[0]
}
result := fmt.Sprintf("Hello, %s!", who)
return &result
}
returnnil
}
funcmain() {}
# Generate Go bindings from WIT$ wit-bindgen go wit/world.wit --out-dir generated/# Build with TinyGo$ tinygo build -target=wasm-unknown \ -o plugin.wasm plugin.go
Once built, load the plugin .wasm using the --plugin flag (wawk CLI) or the @plugin preprocessor directive. Your functions are then available in AWK scripts — regardless of which language the plugin was written in.
Use @plugin "path" at the top of your AWK script to load plugins inline. The preprocessor rewrites function calls to use the plugin prefix automatically. Combined with @include for code modules, this enables modular AWK programming.
wawk-hello is a minimal example plugin (written in Rust) that demonstrates the full wawk plugin architecture. It provides two custom functions callable via @plugin or --plugin:
Function
Args
Description
greet(name)
1
Returns "Hello, {name}!"
greet_lang(name)
1
Returns a greeting in one of 10 languages
Clone and Build
$ git clone https://github.com/ailurlabs/wawk-hello.git$ cd wawk-hello$ ./build.sh# Output: wawk_hello.wasm
All four languages produce functionally identical plugins. Choose based on your team's existing expertise and toolchain preferences.
Language
Toolchain
Build Command
Rust
cargo + wasm-bindgen
cargo build --target wasm32-unknown-unknown
C
clang + wasm-ld
clang --target=wasm32 ...
Go
TinyGo
tinygo build -target=wasm-unknown
TypeScript
AssemblyScript (asc)
npx asc plugin.ts --target release
Specification
WIT Canonical ABI Reference
The wit-bindgen toolchain generates core WebAssembly exports that follow the WIT canonical ABI. Hosts (Node.js, browser, wasmtime) call these exports directly — no additional tooling required at runtime.
Export
Purpose
wawk:plugins/external-functions#call
String ABI — receives function name and string arguments, returns optional string result
wawk:plugins/format-handler#detect
Returns format name if detected, empty string otherwise
wawk:plugins/format-handler#parse
Parses input into JSON-encoded PropertyTree
wawk:plugins/format-handler#serialize
Serializes JSON-encoded PropertyTree into output format
cabi_realloc
Standard WIT memory allocation (old_ptr, old_size, align, new_size) → new_ptr
cabi_post_*#call
Cleanup function called by the host after reading the result
Key design principles
Build once, run anywhere — the same .wasm file works under wasmtime, Node.js, and in the browser.
Explicit loading — load plugins via --plugin flag in the CLI, or use set_plugin_dispatch() in JS hosts. No manifest or registration code needed.
Standard WIT — uses the WIT Component Model canonical ABI. No proprietary protocol.
Dual interface — every plugin exports both external-functions (custom functions) and format-handler (format detection/parsing). Non-format plugins return stub responses for format-handler.
Return None for unknown functions — the host tries the next plugin before reporting "unknown function".
Plugin lifecycle — __init__ and __meta__ are called on load for initialization and metadata retrieval.
String ABI — the call function handles all plugin dispatch via string arguments.
Language-agnostic — Rust, C, Go, and TypeScript all produce compatible plugins from the same WIT definition.