WebAssembly plugins for Typst, in Zig

While I was revisiting the typesetting system Typst, I noticed that it is capable of interfacing with plugins compiled to WebAssembly.

1. Preparation

The plugin support was a pleasant surprise, and I naturally wanted to use Zig ⚡ for this purpose.

Note: You will want to have both zig and typst installed if you are following along.

(Precompiled binaries are available for both)

I’ve made a small module (called typ) that hopefully will be useful when writing plugins for Typst.

2. Dependency

The typ module can be fetched and saved into your build.zig.zon like this;

zig fetch --save git+https://github.com/peterhellberg/typ#main

Then you will add the dependency in build.zig (under the call to b.addExecutable)

build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .wasm32,
        .os_tag = .freestanding,
    });

    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("hello.zig"),
        .strip = true,
        .target = target,
        .optimize = .ReleaseSmall,
    });

    const typ = b.dependency("typ", .{}).module("typ");

    exe.root_module.addImport("typ", typ);
    exe.entry = .disabled;
    exe.rdynamic = true;

    b.installArtifact(exe);
}

We are now ready to write a little Typst plugin in Zig.

3. Plugin

Now we should be able to use the typ module to write a plugin.

hello.zig

const typ = @import("typ");

export fn hello() i32 {
    const msg = "*Hello* from `hello.wasm` written in Zig!";

    return typ.str(msg);
}

export fn echo(n: usize) i32 {
    const data = typ.alloc(u8, n) catch
        return typ.err("alloc failed");
    defer typ.free(data);

    typ.write(data.ptr);

    return typ.ok(data);
}

Two functions are exported from this plugin;

  • The hello function, which does not take any arguments and it can not fail.
  • The echo function on the other hand takes a single (Typst bytes value) argument as input and echoes it back to the host, this function can fail on allocation (unlikely to happen for this example)

Build the zig-out/bin/hello.wasm by calling zig build and then we can proceed with writing a hello.typ using the plugin we just compiled.

hello.typ

#set page(width: 35cm, height: 22cm)
#set text(font: "Inter", size: 40pt)

#let wasm = plugin("zig-out/bin/hello.wasm")
#let gy = gradient.linear(green, yellow)

== A WebAssembly plugin for Typst

#line(length: 100%, stroke: gy + 4pt)

#emph[
  Typst is capable of interfacing
  with plugins compiled to WebAssembly.
]

#line(length: 100%, stroke: gy + 4pt)

#eval(str(wasm.hello()), mode: "markup")

#let imgdata = read("wave.png", encoding: none)

#image.decode(wasm.echo(imgdata), width: 100pt)

Important: The Typst CLI is all you need! (No requirement on using the collaborative online editor)

Running typst compile hello.typ hello.svg should, if all goes well, result in a SVG file that looks something like this;

A WebAssembly plugin for Typst

Peter Hellberg

My name is Peter Hellberg and I’m a Systems Developer in Stockholm, Sweden.