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/go
type 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 error

Return (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.wasm

A 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.wasm

WASM 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.wasm

Plugin 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.wasm

Plugin 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')
Uryyb

Limitations#

  • No hot reload: Plugins are loaded once at startup. Changing a plugin requires rebuilding the .wasm and restarting edg.

Example#

See examples/plugins/ for complete working examples in Go, Rust, and Zig using CockroachDB.