# C

> libotio and otio.h, the C ABI every generated SDK is built on, with handles, out-parameters and status codes.

The C ABI is `libotio`, plus the header in `crates/otio-capi/include/otio.h`.
It is the layer every language binding other than Python sits on: Go, Swift,
Zig, C++, C# and Objective-C link it, and TypeScript is the same ABI compiled
to WebAssembly. From C you use it directly, which means the document is
explicit, values are copied out into buffers you free, and every call that can
fail returns a status.

## Install

```sh
cargo build -p otio-capi --release
```

That writes `libotio.so`, `libotio.dylib` or `otio.dll` alongside the static
`libotio.a`, under `target/release/`. Compile against `include/otio.h` and link
`otio`.

Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases)
carries a prebuilt `libotio-<version>-<target>.tar.gz` per target, holding
`include/otio.h`, `lib/` (static, and shared where built) and the licence. 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`, the last static only. A `SHA256SUMS` file covers every
asset.

## Your first program

```c
#include <stdio.h>
#include <stdlib.h>

#include "otio.h"

int 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. */
    OtioReadOptions options;
    options.struct_size = sizeof options;
    otio_read_options_init(&options);
    options.rate = 24;

    /* A call that can fail says why in the buffer passed last, which is
     * yours to free. Pass NULL where the status is all you want. */
    OtioDocument *document = NULL;
    OtioBuffer error;
    if (otio_read_from_file(OTIO_FORMAT_CMX_3600, "cut.edl", &options, &document, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }

    OtioNode root;
    otio_document_root(document, &root, NULL);

    /* A list call is asked twice: once with no buffer, to learn the count,
     * and once to fill one. */
    size_t count = 0;
    otio_node_find_clips(document, root, NULL, 0, &count, NULL);

    OtioNode *clips = malloc(count * sizeof *clips);
    otio_node_find_clips(document, root, clips, count, &count, NULL);

    for (size_t index = 0; index < count; index += 1) {
        OtioBuffer name;
        if (otio_node_name(document, clips[index], &name, NULL) == OTIO_STATUS_OK) {
            printf("%s\n", name.data);
            otio_buffer_free(name);
        }
    }

    free(clips);
    otio_document_free(document);
    return 0;
}
```

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

Most of what the C ABI asks of a caller is in that program. The format is an
`OtioFormat`, and the options are sized and then filled with their defaults
by `otio_read_options_init()`; the rate is the one an EDL cannot do without (see [CMX 3600 EDL](https://otio-rust-docs.vercel.app/docs/formats/edl/)).
A list is asked for twice, once with a capacity of zero to learn the count and
once to fill a buffer of that size. A string comes back as an `OtioBuffer` the
caller frees with `otio_buffer_free`.

## Building a timeline

```c
#include <stdio.h>

#include "otio.h"

int main(void) {
    OtioDocument *document = otio_document_new();

    OtioNode timeline, stack, track;
    otio_timeline_new(document, "Cut", &timeline, NULL);
    otio_stack_new(document, "tracks", &stack, NULL);
    otio_track_new(document, "V1", "Video", &track, NULL);
    otio_timeline_set_tracks(document, timeline, stack, NULL);
    otio_composition_append_child(document, stack, track, NULL);

    const char *names[] = {"A", "B", "C"};
    for (size_t index = 0; index < 3; index += 1) {
        OtioNode clip;
        otio_clip_new(document, names[index], &clip, NULL);

        OtioTimeRange span;
        span.start_time.value = (double)index * 24.0;
        span.start_time.rate = 24.0;
        span.duration.value = 24.0;
        span.duration.rate = 24.0;
        otio_item_set_source_range(document, clip, span, NULL);

        otio_composition_append_child(document, track, clip, NULL);
    }

    otio_document_set_root(document, timeline, NULL);

    /* Three seconds of picture, written as canonical OpenTimelineIO JSON. */
    OtioRationalTime duration;
    otio_item_duration(document, track, &duration, NULL);
    printf("%g\n", otio_rational_time_to_seconds(duration));

    otio_document_write_to_file(document, "cut.otio", otio_default_indent(), NULL);
    otio_document_free(document);
    return 0;
}
```

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

`otio_document_new` makes a document, and every constructor, such as
`otio_clip_new`, takes the document to build the object in. The root is set
explicitly with `otio_document_set_root`, and `otio_document_free` releases the
document and everything in it.

## How objects and documents work here

**An object is a handle, not a pointer.** `OtioNode` is the index of a slot in
the document's arena and the generation of that slot. Nothing hands out a
pointer into a document, so an edit that moves objects around cannot leave a
caller holding a dangling one, and a handle to something that has been removed
fails a lookup instead of reaching whatever took the slot. Within one document,
two `OtioNode` values compare equal when they name the same object, and that
is the whole of object identity here. A handle is scoped to the document that
issued it: in a copy made by `otio_document_clone` the same handle names the
copy of the same object, and a handle passed to some other document is a
programming error the library cannot always catch.

**A document is explicit.** There is one document, created and freed when the
caller says. Each document owns its objects. `otio_document_absorb` moves
every object of one document into another and hands back each object's old
and new handle; otherwise, build in one document, or write and read.

**A value is copied out, never borrowed.** Times are plain structs passed by
value; strings and whole written files come back as owned `OtioBuffer`s.
Returning a pointer into the document would dangle the moment anything was
edited.

**Threads.** A document is not internally synchronized. Several threads may
read one at the same time; a thread that edits one must be the only thread
touching it. Two threads working on two documents never interfere.

## Errors and missing values

Every call that can fail returns an `OtioStatus`, delivers its result through
out-parameters, and takes one more out-parameter last: `OtioBuffer *out_error`,
where it writes the sentence saying what went wrong. `OTIO_STATUS_OK` is zero,
so `if (otio_...(...))` reads as "if it failed". Pass `NULL` to get only the
status. When it is not `NULL` it is written on every return, empty after
success, so a caller can free it unconditionally.

The message comes back from the call that failed rather than from a second
call asking about "the last failure", so it does not matter which thread asks.

`OTIO_STATUS_NO_VALUE` means the question was answered and the answer is
"nothing", which is what an item with no source range reports. It is not an
error, but it still carries a message saying what had no value. A panic in the
Rust core is caught at the boundary and reported as `OTIO_STATUS_PANIC` rather
than unwinding into C.

`OTIO_STATUS_WRONG_KIND` means the call was asked of the wrong kind of thing:
a clip asked for its children or a track's kind, a metadata list stepped into
by key, or a metadata string read as a number. That is almost always a mistake
in the calling code, so it is kept apart from `OTIO_STATUS_CORE_ERROR`, which
is the document itself refusing, such as a clip with no media asked for its
available range.

## Editing and algorithms

```c
#include <stdio.h>
#include <stdlib.h>

#include "otio.h"

/* One second of picture, named. */
static OtioNode second(OtioDocument *document, const char *name) {
    OtioNode clip;
    otio_clip_new(document, name, &clip, NULL);

    OtioTimeRange span;
    span.start_time.value = 0.0;
    span.start_time.rate = 24.0;
    span.duration.value = 24.0;
    span.duration.rate = 24.0;
    otio_item_set_source_range(document, clip, span, NULL);
    return clip;
}

static void show(OtioDocument *document, OtioNode track) {
    size_t count = 0;
    otio_node_children(document, track, NULL, 0, &count, NULL);

    OtioNode *children = malloc(count * sizeof *children);
    otio_node_children(document, track, children, count, &count, NULL);

    for (size_t index = 0; index < count; index += 1) {
        OtioBuffer name;
        if (otio_node_name(document, children[index], &name, NULL) == OTIO_STATUS_OK) {
            printf("%s%s", index == 0 ? "" : " ", name.data);
            otio_buffer_free(name);
        }
    }
    free(children);

    OtioRationalTime duration;
    otio_item_duration(document, track, &duration, NULL);
    printf(" - %g frames\n", duration.value);
}

int main(void) {
    OtioDocument *document = otio_document_new();

    OtioNode track;
    otio_track_new(document, "V1", "Video", &track, NULL);

    const char *names[] = {"A", "B", "C"};
    for (size_t index = 0; index < 3; index += 1) {
        otio_composition_append_child(document, track, second(document, names[index]), NULL);
    }
    show(document, track);

    /* Insert makes room: everything from the insertion point onwards moves
     * later, and the track gets longer. */
    OtioRationalTime at = {24.0, 24.0};
    otio_edit_insert(document, second(document, "D"), track, at, false, otio_node_none(), NULL);
    show(document, 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. */
    OtioTimeRange over;
    over.start_time.value = 48.0;
    over.start_time.rate = 24.0;
    over.duration.value = 24.0;
    over.duration.rate = 24.0;
    otio_edit_overwrite(document, second(document, "E"), track, over, false, otio_node_none(), NULL);
    show(document, track);

    otio_document_free(document);
    return 0;
}
```

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

The ten edit operations are `otio_edit_*` calls, such as `otio_edit_insert` and
`otio_edit_overwrite`, and the algorithms are `otio_algorithm_*`, such as
`otio_algorithm_flatten_stack`. [Editing a timeline](https://otio-rust-docs.vercel.app/docs/guides/editing/) says
what each edit does.

## Formats

Every format goes through the same four calls, `otio_read_from_bytes`,
`otio_read_from_file`, `otio_write_to_bytes` and `otio_write_to_file`, picked
by an `OtioFormat`: OTIO JSON, ALE, CMX 3600 EDL, both FCP XML flavours, AAF,
and the two bundle formats, `.otioz` and `.otiod`. A bundle is read and written
through a path only; the calls that take bytes refuse one with
`OTIO_STATUS_UNSUPPORTED`. What is written is the document's root, which for a
bundle has to be a timeline.

```c
#include <stdio.h>

#include "otio.h"

int 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. */
    OtioReadOptions options;
    options.struct_size = sizeof options;
    otio_read_options_init(&options);
    options.rate = 24;

    /* A call that can fail says why in the buffer passed last, which is
     * yours to free. Pass NULL where the status is all you want. */
    OtioDocument *document = NULL;
    OtioBuffer error;
    if (otio_read_from_file(OTIO_FORMAT_CMX_3600, "cut.edl", &options, &document, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        return 1;
    }

    /* 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. */
    if (otio_write_to_file(OTIO_FORMAT_FCPX_XML, document, "cut.fcpxml", NULL, &error) != OTIO_STATUS_OK) {
        fprintf(stderr, "%s\n", error.data);
        otio_buffer_free(error);
        otio_document_free(document);
        return 1;
    }

    otio_document_free(document);
    return 0;
}
```

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

A few adapter options are not exposed yet: ALE's explicit column order, AAF's
`transcribe_log`, and upstream's bundle `dry_run`. 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/).

Metadata is named by a path rather than a key, such as `"cmx_3600.reel"` or
`"comments[0]"`, read with calls such as `otio_metadata_get_string`. A key
that holds a `.`, a `[` or a `\` writes it after a `\`: `"com\.avid\.reel"`
is the single key `com.avid.reel`. The header states the full grammar under
METADATA PATHS.

## Options structs

`OtioReadOptions` and `OtioWriteOptions` gain fields as formats gain options,
so each opens with `size_t struct_size`. Set it to the struct's size, call
`otio_read_options_init()` or `otio_write_options_init()` to fill in every
field's default, and change only the fields you need. The library reads only
the fields that size covers and takes its defaults for the rest, so a program
built against an older header keeps working with a newer library. A struct
zeroed by hand has no size and is refused with `OTIO_STATUS_INVALID_ARGUMENT`.

The defaults are filled into the caller's struct rather than returned, because
a struct returned by value has the size the library was built with. A newer
library returning its larger struct would write past the end of an older
program's.

## Media linkers and hooks

`otio_register_media_linker` and `otio_register_hook_script` take an
`OtioPluginFn`, a `void *context` handed to every call, and an
`OtioPluginReleaseFn` the library calls with that context once nothing will
call the plugin again. The function is handed the document, the clip or the
hook's target, an object whose metadata is the arguments, a place for its
answer, and room for a message. Return a status other than `OTIO_STATUS_OK`,
with the reason written into `message`, to fail the read; the caller sees
`OTIO_STATUS_PLUGIN_ERROR`. A linker that leaves `out_result` alone leaves
the clip as it was.

A read names its linker in `OtioReadOptions.media_linker` and carries the
argument maps as JSON in `media_linker_arguments` and `hook_arguments`. The
document is the library's for the length of the call: answer with an object
in it, and keep no handle past the call. See
[Media linkers and hooks](https://otio-rust-docs.vercel.app/docs/guides/media-linkers-and-hooks/).

## Platforms

CI compiles and runs the C ABI's own C test program on Linux and macOS. The
library builds on Windows and its struct layouts are asserted from the Rust
side, but no C program is linked there in CI.

A WebAssembly build for the browser has no file system, so every call that
takes a path answers `OTIO_STATUS_UNSUPPORTED` there; the calls that take
bytes work everywhere. A WASI build has a file system and keeps the path
calls.

## Versions

`otio_version()` is the release. `OTIO_ABI_VERSION`, from the header, and
`otio_abi_version()`, from the library, are the version of the binary
interface; check at startup that they agree:

```c
if (otio_abi_version() != OTIO_ABI_VERSION) {
    fprintf(stderr, "libotio %s is not the one this program was built for\n",
            otio_version());
    return 1;
}
```

The ABI version is 0 until the ABI is declared stable, so for now any release
may change it; insist on the exact `otio_version()` you built against too.
From version 1, a release names the shared library for its ABI version
(`libotio.so.1`, `libotio.1.dylib`, `otio1.dll`) so that two can be installed
side by side.

Every enum is open: a later release may add values, such as a new
`OtioStatus` or `OtioNodeKind`, without changing the ABI version. Handle a
value you do not know with a `default:` rather than assuming the set is
closed. `OTIO_NODE_KIND_OTHER` and `OTIO_VALUE_OTHER` exist so that a newer
core reports something honest to a caller built against an older header.
That promise is to C callers: the generated SDKs are made from one release's
header and are meant to run with that release's library, which they ship
with or pin.
`otio_node_kind` reports the most specific kind it knows: a clip is
`OTIO_NODE_KIND_CLIP`, never `OTIO_NODE_KIND_ITEM`.

## Reference

- [`crates/otio-capi/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/crates/otio-capi/README.md):
  the four decisions the ABI makes, and how the header is kept honest.
- [The reference](https://otio-rust-docs.vercel.app/reference/), generated from the same description as every
  SDK, with each declaration lifted verbatim from `otio.h`.
- [How the SDKs are made](https://otio-rust-docs.vercel.app/docs/internals/how-the-sdks-are-made/).

---

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