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

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.

1
AWK script calls greet("World") — the evaluator does not recognise greet as built-in.
2
Host encodes the call via WIT canonical ABI and invokes wawk:plugins/external-functions#call on each loaded plugin.
3
Plugin matches the function name, builds a greeting, and returns some("Hello, World!").
4
Host decodes the result and feeds it back to the AWK evaluator as the function return value.

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; }

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);
# Cargo.toml [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "0.42" # Build $ cargo build --target wasm32-unknown-unknown --release

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; } }
# 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

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() {}
# 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

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();
# Generate AssemblyScript bindings $ wit-bindgen assemblyscript wit/world.wit \ --out-dir generated/ # Build with AssemblyScript compiler $ npx asc assembly/plugin.ts \ --target release \ --outFile plugin.wasm

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 CLI — load plugin via --plugin flag $ wawk --plugin ./my_plugin.wasm 'BEGIN { print greet("World") }' Hello, World! # Node.js — copy to plugins/ directory $ cp my_plugin.wasm plugins/ $ node run/run.js 'BEGIN { print greet("World") }' Hello, World! # Pipe input data $ echo "AWK" | wawk --plugin ./my_plugin.wasm '{ print greet($0) }' Hello, AWK!

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 # Output: wawk_hello.wasm

Run with wawk

# Basic greeting $ wawk --plugin ./wawk_hello.wasm 'BEGIN { print greet("World") }' Hello, World! # Multi-language greeting $ wawk --plugin ./wawk_hello.wasm 'BEGIN { print greet_lang("Alice") }' Hola, Alice! # Process input data $ echo -e "Alice\nBob\nCharlie" | \ wawk --plugin ./wawk_hello.wasm '{ print greet($1) }' Hello, Alice! Hello, Bob! Hello, Charlie!

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

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.