# Zig

> The one generated SDK that keeps the document in the open, because an arena is how Zig already works.

The Zig package links `libotio`, the C ABI built from the Rust core, and is
generated from that ABI, so it carries the whole data model: the schemas, the
composition algorithms, the ten edit operations and the file-format adapters.
Zig has no upstream binding to copy, so what the binding *is* was decided
here, and it differs from the other SDKs in one large way: the document is
visible, held the way a Zig programmer holds any other arena. It needs Zig
0.16 or newer.

```zig
const otio = @import("otio");
```

There is no `@cImport` and no header is read at build time. The declarations
are written out directly from the same description the rest of the SDK is
generated from, so the package builds wherever Zig builds and cross-compiles
with nothing but the static library.

## Install

Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases)
carries the package with the static library for every target it is built for,
under `lib/<target>/`, so one fetch covers them all:

```sh
zig fetch --save https://github.com/alchemist-editor/otio-rust/releases/download/v0.1.0/otio-zig-0.1.0.tar.gz
```

```zig
const otio = b.dependency("otio", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("otio", otio.module("otio"));
```

The targets are `x86_64-linux-gnu`, `aarch64-linux-gnu`, `aarch64-macos`,
`x86_64-macos`, `x86_64-windows-msvc`, `aarch64-windows-msvc` and
`x86_64-windows-gnu`.

To build from the repository instead, put a static library built from the Rust
core in `lib/<target>/` or in `lib/` itself:

```sh
cargo build -p otio-capi --release
cp target/release/libotio.a sdk/zig/lib/

cd sdk/zig
zig build test
```

If the library lives somewhere else, say so: `zig build test -Dlibrary=/path/to/dir`.
For an MSVC target the library is `otio.lib`, built against the static C
runtime and stripped of Rust's own copy of compiler-rt with
`scripts/strip-msvc-builtins.sh`; the
[README](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/zig/README.md#windows)
has the commands.

## Your first program

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    // An EDL never says what rate its timecode is at, so this has to be
    // right: a file read at the wrong rate puts every event in the wrong
    // place rather than failing.
    const options = otio.ReadOptions{
        .rate = 24,
        .name_column = null,
        .ignore_timecode_mismatch = false,
    };

    const document = try otio.Document.readFromFile(.cmx3600, "cut.edl", options);
    defer document.deinit();

    const root = (try document.root()) orelse return error.Empty;

    // Anything the library hands back that has to be freed is copied into an
    // allocator you pass, and freed with `allocator.free`.
    const clips = try root.findClips(allocator);
    defer allocator.free(clips);

    for (clips) |node| {
        if (node.asClip()) |clip| {
            const name = try clip.name(allocator);
            defer allocator.free(name);
            std.debug.print("{s}\n", .{name});
        }
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/read-an-edl/zig.zig</sub>

Reading hands back a `Document`, and `defer document.deinit()` is how it goes.
`otio.open` infers the format from the filename instead of taking one, and
`document.save` writes back the same way. The rate is the one option an EDL
cannot do without; see [CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/).

## Building a timeline

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    // A document is an arena, and it is held in the open the way a Zig
    // programmer holds any other arena.
    const document = try otio.Document.init();
    defer document.deinit();

    const timeline = try otio.Timeline.init(document, "Cut");
    const stack = try otio.Stack.init(document, "tracks");
    const track = try otio.Track.init(document, "V1", "Video");
    try timeline.setTracks(stack.asNode());
    try stack.appendChild(track.asNode());

    for ([_][:0]const u8{ "A", "B", "C" }, 0..) |name, index| {
        const clip = try otio.Clip.init(document, name);
        try clip.setSourceRange(.{
            .start_time = .{ .value = @floatFromInt(index * 24), .rate = 24 },
            .duration = .{ .value = 24, .rate = 24 },
        });
        try track.appendChild(clip.asNode());
    }

    try document.setRoot(timeline.asNode());

    // Three seconds of picture, written as canonical OpenTimelineIO JSON.
    const duration = try track.duration();
    std.debug.print("{d}\n", .{duration.toSeconds()});
    try document.save("cut.otio");
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/build-a-timeline/zig.zig</sub>

Every object is made in a document, which each `init` takes first. An object is
a `Node`: a handle, and the document it can be resolved against. The schemas
are Zig types holding one, and each writes out the methods of every schema it
derives from, so a `Clip` answers to everything an `Item`, a `Composable` and
a `Node` answer to. Ask a node what it is with its `as` method, and cast back
up the ladder the same way:

```zig
if (node.asClip()) |clip| {
    const reference = try clip.mediaReference(null);
}
try track.appendChild(clip.asNode());
```

## How objects and documents work here

The other SDKs hide the document, so that a clip is built on its own and
adopts one when it is appended. That rests on a finalizer and a cache of live
handles, which Zig has neither of and would have to pay for with an allocator
inside every object. A Zig programmer already holds an arena and hands it to
the things that allocate from it, which is exactly what a document is.

Allocation is the caller's: nothing here allocates behind your back. Anything
the library hands back that has to be freed, such as a name, a JSON document
or a list of children, is copied into an allocator you pass, and freed with
`allocator.free`. A call that needs one takes it as its first argument after
the receiver, which is the only signal you need that it allocates:

```zig
const name = try clip.name(allocator);
defer allocator.free(name);
```

A call that answers with several things at once hands back a small record with
a `deinit` of its own:

```zig
const both = try track.rangesOfChildren(allocator);
defer both.deinit(allocator);
```

## Errors and missing values

A call that can fail answers with an error union over `Error`. A Zig error
carries no message, so the failing call's sentence is kept for the thread that
made the call and read with `otio.lastErrorMessage()`. Read it before the next
failure on that thread, which replaces it. Asking an object for something it
does not have fails rather than answering with a zero value: a clip asked for a
track's kind reports `error.WrongKind`.

An object from another document is refused with `error.ForeignObject`.

Where "there is nothing here" is one of the answers, such as an item with no
source range or a clip with no active media reference, the answer is an
optional rather than an error, because that is what Zig has optionals for:

```zig
if (try clip.sourceRange()) |span| {
    // the clip is trimmed to span
} else {
    // it is untrimmed
}
```

## Editing and algorithms

```zig
const std = @import("std");
const otio = @import("otio");

/// One second of picture, named.
fn second(document: *otio.Document, name: [:0]const u8) !otio.Clip {
    const clip = try otio.Clip.init(document, name);
    try clip.setSourceRange(.{
        .start_time = .{ .value = 0, .rate = 24 },
        .duration = .{ .value = 24, .rate = 24 },
    });
    return clip;
}

fn show(allocator: std.mem.Allocator, track: otio.Track) !void {
    // Anything the library hands back that has to be freed is copied into an
    // allocator you pass, and freed with `allocator.free`.
    const children = try track.children(allocator);
    defer allocator.free(children);

    for (children, 0..) |child, index| {
        const name = try child.name(allocator);
        defer allocator.free(name);
        if (index > 0) std.debug.print(" ", .{});
        std.debug.print("{s}", .{name});
    }

    const duration = try track.duration();
    std.debug.print(" - {d} frames\n", .{duration.value});
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    const document = try otio.Document.init();
    defer document.deinit();

    const track = try otio.Track.init(document, "V1", "Video");
    for ([_][:0]const u8{ "A", "B", "C" }) |name| {
        try track.appendChild((try second(document, name)).asNode());
    }
    try show(allocator, track);

    // Insert makes room: everything from the insertion point onwards moves
    // later, and the track gets longer.
    const inserted = try second(document, "D");
    try document.insert(inserted.asNode(), track.asNode(), .{ .value = 24, .rate = 24 }, false, null);
    try show(allocator, track);

    // Overwrite does not: it lays an item over a span and whatever was in
    // that span gives way. The track is the same length afterwards.
    const laid = try second(document, "E");
    try document.overwrite(laid.asNode(), track.asNode(), .{
        .start_time = .{ .value = 48, .rate = 24 },
        .duration = .{ .value = 24, .rate = 24 },
    }, false, null);
    try show(allocator, track);
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/edit-operations/zig.zig</sub>

The ten edit operations and the algorithms are methods of the `Document`, such
as `document.insert`, `document.overwrite` and `document.flattenTracks`,
because each is about two objects and belongs to neither.
[Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) says what each edit does.

## Media linkers and hooks

A media linker and a hook script are Zig functions the library calls back in
the middle of a read or a write. Zig has no closures, so each is registered as
a function known at compile time and a context it is handed on every call, the
shape `std.sort` takes a comparison in: a pointer, or `{}` for none. A linker
takes the context, the clip and the arguments metadata, and answers with an
optional `Node`, null leaving the clip alone. A hook script takes the context,
the object and the arguments, and answers with the object to go on with.

```zig
const std = @import("std");
const otio = @import("otio");

// A cut of two clips, as an .otio file would hold it.
const cut =
    \\{
    \\  "OTIO_SCHEMA": "Track.1",
    \\  "name": "V1",
    \\  "kind": "Video",
    \\  "children": [
    \\    {"OTIO_SCHEMA": "Clip.2", "name": "A"},
    \\    {"OTIO_SCHEMA": "Clip.2", "name": "B"}
    \\  ]
    \\}
;

// Zig has no closures, so a plugin is a function and a context it is handed
// on every call. Here the context carries the allocator.
const Plugins = struct {
    allocator: std.mem.Allocator,

    // A media linker is handed each clip as it is read, with the arguments
    // the read was given, and answers with the media the clip should use,
    // built in the clip's own document. Null leaves the clip as it was.
    fn proxies(self: *const Plugins, clip: otio.Clip, arguments: otio.Metadata) !?otio.Node {
        const name = try clip.name(self.allocator);
        defer self.allocator.free(name);
        const root = try arguments.getString(self.allocator, "root");
        defer self.allocator.free(root);
        const url = try std.fmt.allocPrintSentinel(self.allocator, "{s}/{s}.mov", .{ root, name }, 0);
        defer self.allocator.free(url);
        const proxy = try otio.ExternalReference.init(clip.node.doc.?, null, url);
        return proxy.node;
    }

    // A hook script is handed the whole result, and answers with what the
    // read goes on with: here the same object, stamped.
    fn stamp(self: *const Plugins, target: otio.Node, arguments: otio.Metadata) !otio.Node {
        const who = try arguments.getString(self.allocator, "who");
        defer self.allocator.free(who);
        const held = try self.allocator.dupeZ(u8, who);
        defer self.allocator.free(held);
        try target.metadata().setString("read_by", held);
        return target;
    }
};

pub fn main() !void {
    const allocator = std.heap.smp_allocator;
    const plugins: Plugins = .{ .allocator = allocator };

    try otio.registerMediaLinker("proxies", &plugins, Plugins.proxies);
    defer _ = otio.unregisterMediaLinker("proxies");
    try otio.registerHookScript("stamp", &plugins, Plugins.stamp);
    defer _ = otio.unregisterHookScript("stamp");
    try otio.attachHookScript("post_adapter_read", "stamp");
    defer _ = otio.detachHookScript("post_adapter_read", "stamp");

    // The read names the linker, and carries both sets of arguments as JSON.
    var options = otio.readOptionsDefault();
    options.media_linker = "proxies";
    options.media_linker_arguments = "{\"root\": \"/proxies\"}";
    options.hook_arguments = "{\"who\": \"the conform\"}";
    const document = try otio.Document.readFromBytes(.otio_json, cut, options);
    defer document.deinit();

    const track = (try document.root()) orelse return error.Empty;
    const who = try track.metadata().getString(allocator, "read_by");
    defer allocator.free(who);
    std.debug.print("read by {s}\n", .{who});

    const clips = try track.findClips(allocator);
    defer allocator.free(clips);
    for (clips) |node| {
        const clip = node.asClip() orelse continue;
        const media = (try clip.mediaReference(null)) orelse continue;
        const external = media.asExternalReference() orelse continue;
        const url = try external.targetUrl(allocator);
        defer allocator.free(url);
        const name = try clip.name(allocator);
        defer allocator.free(name);
        std.debug.print("{s} -> {s}\n", .{ name, url });
    }
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/link-media-and-run-hooks/zig.zig</sub>

The document a plugin is handed is lent for the call: build what you answer
with in it, `clip.node.doc.?`, and never free it. Freeing a lent document from
inside the call panics and absorbing one is refused, including the caller's own
document when `runHook` lends it. An answer built in a document of its own is
absorbed into the lent one, which consumes that document. An error the plugin
returns stops the read or write with `error.PluginError`, and its name is what
`lastErrorMessage()` then says. A Zig panic is not an error and still ends the
process. The context is the caller's: the library holds on to it until the
name is unregistered or registered again, and may use it from any thread that
reads or writes.

## Formats

`Document.readFromFile` and `document.writeToFile` take a `Format`:
`.otio_json`, `.ale`, `.cmx3600`, `.fcp7_xml`, `.fcpx_xml`, `.aaf`, and the
two bundles, `.otioz` and `.otiod`, which are read and written through a path
only.

```zig
const std = @import("std");
const otio = @import("otio");

pub fn main() !void {
    // An EDL never says what rate its timecode is at, so this has to be
    // right: a file read at the wrong rate puts every event in the wrong
    // place rather than failing.
    const options = otio.ReadOptions{
        .rate = 24,
        .name_column = null,
        .ignore_timecode_mismatch = false,
    };

    const document = try otio.Document.readFromFile(.cmx3600, "cut.edl", options);
    defer document.deinit();

    // Nothing happens in between. The timeline an EDL parses to is the same
    // timeline FCP X writes out, so converting is a read and a write: the
    // object model is the interchange, and the file formats are two ways of
    // spelling it.
    try document.writeToFile(.fcpx_xml, "cut.fcpxml", null);
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/zig.zig</sub>

See [Reading and writing files](https://otio-rust-docs.vercel.app/docs/guides/reading-and-writing/),
[AAF](https://otio-rust-docs.vercel.app/docs/formats/aaf/) and [Bundles](https://otio-rust-docs.vercel.app/docs/formats/bundles/).

## Platforms

Wherever Zig builds and the Rust core builds. CI runs the tests on Linux,
macOS and Windows (MSVC), and each release builds and runs a program that
depends on the released package on every target a runner can run.

The description the package is generated from carries struct layouts for
64-bit targets and `wasm32`. A target that aligns a `double` to four bytes,
such as `i386`, lays the structs out differently, and the package refuses to
compile there with a message saying so rather than getting the offsets wrong.

## Reference

- [`sdk/zig/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/zig/README.md).
- [How the SDKs are made](https://otio-rust-docs.vercel.app/docs/internals/how-the-sdks-are-made/), which says
  why Zig is the exception.
- The [C ABI reference](https://otio-rust-docs.vercel.app/reference/) the package is generated from, and
  [ADR 0003](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0003-sdk-generation.md).

---

Page: https://otio-rust-docs.vercel.app/docs/languages/zig/
