Plugin Development
Build Your First Wawk Plugin
Wawk plugins extend AWK with custom functions using the WIT Component Model. A plugin is a standalone .wasm file that exports the wawk:plugins/external-functions interface. 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
Plugin size
~60 KB typical
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.
1AWK script calls greet("World") — the evaluator does not recognise greet as built-in.
2Host encodes the call via WIT canonical ABI and invokes wawk:plugins/external-functions#call on each loaded plugin.
3Plugin matches the function name, builds a greeting, and returns some("Hello, World!").
4Host 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.
// wit/world.wit
package wawk:plugins;
interface external-functions {
/// Dispatch a function call with string arguments.
/// Return some(result) if handled, none to pass through.
call: func(name: string, args: list<string>) -> option<string>;
}
world wawk-plugin {
export external-functions;
}
Step 2 — Rust
Implement in Rust
Use wit-bindgen to generate exports from the WIT definition. Implement the Guest trait — match on function names and return Some(result) or None.
// src/lib.rs
wit_bindgen::generate!({
world: "wawk-plugin",
path: "wit",
});
struct Component;
impl exports::wawk::plugins::external_functions::Guest for Component {
fn call(name: String, args: Vec<String>) -> Option<String> {
match name.as_str() {
"greet" => {
let who = args.first()
.map(|s| s.as_str())
.unwrap_or("World");
Some(format!("Hello, {}!", who))
}
_ => None,
}
}
}
export!(Component);
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.42"
$ cargo build --target wasm32-unknown-unknown --release
Step 2 — C
Implement in C
Use wit-bindgen C code generation to produce header and source files from the WIT definition. Implement the generated C function signatures directly.
// plugin.c
// Generated by: wit-bindgen c wit/world.wit
#include "external-functions.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
void exports_wawk_plugins_external_functions_call(
wawk_string_t *name,
wawk_list_string_t *args,
wawk_option_string_t *ret
) {
if (strncmp(name->ptr, "greet", name->len) == 0) {
const char *who = (args->len > 0)
? args->ptr[0].ptr : "World";
char buf[256];
int n = snprintf(buf, sizeof(buf),
"Hello, %s!", who);
ret->is_some = 1;
ret->val.ptr = malloc(n);
memcpy(ret->val.ptr, buf, n);
ret->val.len = n;
} else {
ret->is_some = 0;
}
}
$ wit-bindgen c wit/world.wit --out-dir generated/
$ clang --target=wasm32-unknown-unknown \
-I generated/ \
-c plugin.c generated/external-functions.c \
generated/world.c -o plugin.o
$ wasm-ld --no-entry --export-all plugin.o -o plugin.wasm
Step 2 — Go
Implement in Go
Use wit-bindgen Go code generation, then compile with TinyGo (the standard Go compiler does not yet target wasm32-unknown-unknown with the component model).
// plugin.go
// Generated by: wit-bindgen go wit/world.wit
package main
import (
"fmt"
"example.com/generated/wawk/plugins"
)
type WawkPlugin struct{}
func (WawkPlugin) Call(name string, args []string) *string {
if name == "greet" {
who := "World"
if len(args) > 0 {
who = args[0]
}
result := fmt.Sprintf("Hello, %s!", who)
return &result
}
return nil
}
func main() {}
$ wit-bindgen go wit/world.wit --out-dir generated/
$ tinygo build -target=wasm-unknown \
-o plugin.wasm plugin.go
Step 2 — TypeScript
Implement in TypeScript
Use AssemblyScript with the @bytecodealliance/assemblyscript plugin to generate TypeScript bindings from WIT, then compile to WebAssembly.
// assembly/plugin.ts
import { ExternalFunctions } from "../generated/external-functions.js";
class WawkPlugin implements ExternalFunctions {
call(name: string, args: string[]): string | null {
if (name === "greet") {
const who = args.length > 0 ? args[0] : "World";
return `Hello, ${who}!`;
}
return null;
}
}
export new WawkPlugin();
$ wit-bindgen assemblyscript wit/world.wit \
--out-dir generated/
$ npx asc assembly/plugin.ts \
--target release \
--outFile plugin.wasm
Step 3
Use in AWK Scripts
Once built, load the plugin .wasm using the --plugin flag (wawk CLI) or place it in the plugins/ directory (Node.js). Your functions are then available in AWK scripts — regardless of which language the plugin was written in.
$ wawk --plugin ./my_plugin.wasm 'BEGIN { print greet("World") }'
Hello, World!
$ cp my_plugin.wasm plugins/
$ node run/run.js 'BEGIN { print greet("World") }'
Hello, World!
$ echo "AWK" | wawk --plugin ./my_plugin.wasm '{ print greet($0) }'
Hello, AWK!
Example
wawk-hello: A Complete Example
wawk-hello is a minimal example plugin (written in Rust) that demonstrates the wawk plugin architecture. It provides two functions:
| 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
Run with wawk
$ wawk --plugin ./wawk_hello.wasm 'BEGIN { print greet("World") }'
Hello, World!
$ wawk --plugin ./wawk_hello.wasm 'BEGIN { print greet_lang("Alice") }'
Hola, Alice!
$ echo -e "Alice\nBob\nCharlie" | \
wawk --plugin ./wawk_hello.wasm '{ print greet($1) }'
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Comparison
Language Comparison
All four languages produce functionally identical plugins. Choose based on your team's existing expertise and toolchain preferences.
| Language |
Toolchain |
Build Command |
Typical Size |
| Rust |
cargo + wasm-bindgen |
cargo build --target wasm32-unknown-unknown |
~60 KB |
| C |
clang + wasm-ld |
clang --target=wasm32 ... |
~30 KB |
| Go |
TinyGo |
tinygo build -target=wasm-unknown |
~80 KB |
| TypeScript |
AssemblyScript (asc) |
npx asc plugin.ts --target release |
~20 KB |
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 |
| 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 place in plugins/ for Node.js. No manifest or registration code needed.
- Standard WIT — uses the WIT Component Model canonical ABI. No proprietary protocol.
- Return
None for unknown functions — the host tries the next plugin before reporting "unknown function".
- 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.