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

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.

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>; } interface format-handler { detect: func(input: string) -> string; parse: func(input: string) -> string; serialize: func(tree-json: string) -> string; } world wawk-plugin { export external-functions; export format-handler; }

Implement in Your Language

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", }); struct Component; // External functions: custom AWK-callable functions impl exports::wawk::plugins::external_functions::Guest for Component { fn call(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::Guest for Component { fn detect(_input: String) -> String { String::new() } fn parse(_input: String) -> String { r#"{"error":"not a format plugin"}"#.into() } fn serialize(_tree: String) -> String { r#"{"error":"not a format plugin"}"#.into() } } export!(Component);
# Cargo.toml [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "0.42" # Build $ cargo build --target wasm32-unknown-unknown --release

Use in AWK Scripts

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.

CLI: --plugin Flag

# wawk CLI — load plugin via --plugin flag $ wawk --plugin ./wawk_hello.wasm 'BEGIN { print greet("World") }' Hello, World! # Pipe input data $ echo -e "Alice\nBob\nCharlie" | \ wawk --plugin ./wawk_hello.wasm '{ print greet($1) }' Hello, Alice! Hello, Bob! Hello, Charlie!

Script: @plugin Directive

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.

# script.awk — load wawk-hello plugin inline @plugin "wawk_hello.wasm" { print greet($1) } END { print "Processed", NR, "records" }
# Run the script with plugin $ echo -e "Alice\nBob" | wawk -f script.awk Hello, Alice! Hello, Bob! Processed 2 records

@include Directive

@include "file.awk" expands included files inline (gawk-compatible, max 16 levels nesting). Use it to share utility functions across scripts.

# utils.awk — shared helper functions function titlecase(s) { return toupper(substr(s,1,1)) substr(s,2) } # main.awk — include the module @include "utils.awk" { print titlecase($1) }

Node.js / Browser

# Node.js — copy to plugins/ directory $ cp wawk_hello.wasm plugins/ $ node run/run.js 'BEGIN { print greet("World") }' Hello, World! # Browser: register JS dispatch function $ import { set_plugin_dispatch } from './wawk/wawk.js'; set_plugin_dispatch((name, args) => { if (name === 'greet') return `Hello, ${args[0]}!`; return null; });

wawk-hello: A Complete Example

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

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

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.