Plugins PRO#
Plugins let you extend edg with custom functions written in Go, Rust, or Zig. A plugin is a WebAssembly (.wasm) file that exports one or more functions which become available in any expression - seed args, run args, conditionals, print statements, and user-defined expressions. Plugin functions are indistinguishable from built-ins at runtime.
Writing a plugin#
Plugins can be written in Go, Rust, or Zig. Each language has its own SDK that handles the WASM ABI, function registration, and deterministic RNG.
A Go plugin is a main package that registers functions using the edg-plugins SDK:
package main
import (
"fmt"
edgplugins "github.com/codingconcepts/edg-plugins/go"
)
func init() {
edgplugins.SetPluginName("go_example")
edgplugins.Register(edgplugins.WasmFunction{
Name: "hello",
Fn: hello,
Description: "Greet by name.",
Example: "hello('world')",
})
edgplugins.Register(edgplugins.WasmFunction{
Name: "dice",
Fn: dice,
Description: "Roll an N-sided die.",
Example: "dice(6)",
})
}
func hello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func dice(sides int) int {
return edgplugins.Rng.IntN(sides) + 1
}
func main() {}SDK#
go get github.com/codingconcepts/edg-plugins/gotype WasmFunction struct {
Name string // Function name as it appears in expressions.
Fn any // The Go function.
Description string // Shown by `edg functions`.
Example string // Usage example shown by `edg functions`.
}
func Register(f WasmFunction)
func SetPluginName(name string)
var Rng *rand.Rand // Seeded by edg; use for deterministic output with --rng-seed.Function signatures#
Plugin functions follow the same conventions as built-in functions. The expression engine (expr-lang) handles type coercion, so functions can use concrete types:
func greet(name string) string // single arg, single return
func roll(min, max int) int // multiple args
func sample(args ...any) (any, error) // variadic with errorReturn (value, error) when the function can fail - edg surfaces the error to the user with full context.
Deterministic output#
For reproducible results with --rng-seed, use edgplugins.Rng instead of math/rand:
import edgplugins "github.com/codingconcepts/edg-plugins/go"
func pick(items []any) any {
return items[edgplugins.Rng.IntN(len(items))]
}Building#
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o my-plugin.wasm ./path/to/plugin/A Rust plugin uses the edg-plugin crate and the edg_plugin! macro to register functions:
use edg_plugin::*;
fn card_mask(input: String, visible: i64) -> String {
let chars: Vec<char> = input.chars().collect();
chars
.iter()
.enumerate()
.map(|(i, &c)| {
if i < chars.len().saturating_sub(visible as usize) {
'*'
} else {
c
}
})
.collect()
}
fn initials(name: String) -> String {
name.split_whitespace()
.filter_map(|w| w.chars().next())
.map(|c| c.to_uppercase().next().unwrap_or(c))
.collect()
}
edg_plugin! {
name: "rust_example",
functions: {
card_mask(input: String, visible: i64) -> String,
"Mask all but the last N characters of a string.",
"card_mask('4111111111111111', 4)";
initials(name: String) -> String,
"Extract uppercase initials from a full name.",
"initials('Jane Doe')";
}
}SDK#
Add edg-plugin to your Cargo.toml:
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
edg-plugin = "0.1"The edg_plugin! macro handles WASM ABI exports, function dispatch, and RNG seeding. Each function entry in the macro takes the function signature, a description string, and an example string.
Building#
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/my_plugin.wasm ./my-plugin.wasmA Zig plugin uses the edg module to declare functions via edg.plugin():
const edg = @import("edg");
fn slug(input: []const u8) []const u8 {
var pos: usize = 0;
for (input) |c| {
if (pos >= edg.result_buf.len) break;
if (c >= 'A' and c <= 'Z') {
edg.result_buf[pos] = c + 32;
pos += 1;
} else if ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9')) {
edg.result_buf[pos] = c;
pos += 1;
} else if (c == ' ' or c == '_') {
if (pos > 0 and edg.result_buf[pos - 1] != '-') {
edg.result_buf[pos] = '-';
pos += 1;
}
}
}
while (pos > 0 and edg.result_buf[pos - 1] == '-') pos -= 1;
return edg.result_buf[0..pos];
}
fn rot13(input: []const u8) []const u8 {
const len = @min(input.len, edg.result_buf.len);
for (input[0..len], 0..) |c, i| {
if (c >= 'a' and c <= 'z') {
edg.result_buf[i] = 'a' + (c - 'a' + 13) % 26;
} else if (c >= 'A' and c <= 'Z') {
edg.result_buf[i] = 'A' + (c - 'A' + 13) % 26;
} else {
edg.result_buf[i] = c;
}
}
return edg.result_buf[0..len];
}
const p = edg.plugin(.{
.name = "zig_example",
.functions = .{
.{ .name = "slug", .handler = slug, .desc = "Convert a string to a URL-friendly slug.", .example = "slug('Hello World')" },
.{ .name = "rot13", .handler = rot13, .desc = "Apply ROT13 cipher to a string.", .example = "rot13('Hello')" },
},
});
export fn alloc(size: i32) i32 {
return edg.allocImpl(size);
}
export fn describe() i64 {
return p.describe();
}
export fn call(fn_id: i32, arg_ptr: i32, arg_len: i32) i64 {
return p.call(fn_id, arg_ptr, arg_len);
}
export fn seed_rng(seed: i64) void {
edg.seedRng(seed);
}SDK#
The Zig SDK is available as a Zig package. Add it as a dependency in build.zig.zon:
.dependencies = .{
.edg_plugin = .{
.url = "https://github.com/codingconcepts/edg-plugins/archive/refs/heads/main.tar.gz",
},
},Then import it in build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const edg_dep = b.dependency("edg_plugin", .{});
const exe = b.addExecutable(.{
.name = "my_plugin",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .wasi,
}),
.optimize = .ReleaseSmall,
.imports = &.{
.{ .name = "edg", .module = edg_dep.module("edg") },
},
}),
});
exe.entry = .disabled;
exe.rdynamic = true;
b.installArtifact(exe);
}The edg.plugin() comptime function generates the WASM ABI exports. Use edg.result_buf as a scratch buffer for string returns. edg.seedRng() is called by the host to seed the deterministic RNG.
Building#
cd my-plugin && zig build
cp zig-out/bin/my_plugin.wasm ../my-plugin.wasmWASM plugins are cross-platform - a plugin built on macOS works on Linux and Windows without recompilation.
Using a plugin#
Pass the --plugin flag when running any edg command. The flag is repeatable:
edg run \
--config workload.edg \
--url "$DATABASE_URL" \
--plugin ./my-plugin.wasm \
--plugin ./another-plugin.wasmPlugin functions are available everywhere built-in functions are - query args, conditionals, print statements, and user-defined expressions:
seed {
populate(type: exec_batch, count: 1000, size: 100) `INSERT INTO t (msg, roll)
__values__` (
hello(gen('firstname')),
dice(20)
)
}To verify a plugin loaded correctly, list all available functions:
edg functions --plugin ./my-plugin.wasmPlugin functions appear alongside built-ins in the output.
REPL example#
The edg repl also supports plugins. Use the --plugin flag to reference the plugin and you’ll have access to all of your functions with autocompletion. Multiple plugins can be loaded at once:
edg repl \
--plugin ./go_example.wasm \
--plugin ./rust_example.wasm \
--plugin ./zig_example.wasm
>> dice(6)
3
>> hello('Rob')
Hello, Rob!
>> card_mask('4111111111111111', 4)
************1111
>> initials('Jane Doe')
JD
>> slug('Hello World!')
hello-world
>> rot13('Hello')
UryybLimitations#
- No hot reload: Plugins are loaded once at startup. Changing a plugin requires rebuilding the
.wasmand restarting edg.
Example#
See examples/plugins/ for complete working examples in Go, Rust, and Zig using CockroachDB.