# C++

> Header-only C++17 over libotio, shaped like upstream's own C++ API, with objects as small values over an arena handle.

The C++ SDK is a set of C++17 headers over `libotio`, the C ABI built from the
Rust core. It 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. Its shape is upstream OpenTimelineIO's own C++ API: a
class per schema deriving as the schemas derive, `snake_case` members, values
as structs, and no document in the surface. Everything lives in namespace
`otio`.

```cpp
#include <opentimelineio/otio.hpp>
```

## Install

The headers are header-only. They call a static `libotio`, which the CMake
target expects to find in `sdk/cpp/lib/`. The library is not checked in, so
build it from the Rust core first:

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

cmake -S sdk/cpp -B sdk/cpp/build
cmake --build sdk/cpp/build
ctest --test-dir sdk/cpp/build --output-on-failure
```

Each [GitHub release](https://github.com/alchemist-editor/otio-rust/releases)
also carries the C++ SDK's sources, laid out as in the repository with an
empty `lib/`, and a prebuilt `libotio-<version>-<target>.tar.gz` per target
whose `lib/` holds the library to put there.

## Your first program

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    // 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.
    otio::ReadOptions options = otio::read_options_default();
    options.rate = 24;

    // Reading hands back the file's root object; there is no container
    // around it to hold.
    const otio::SerializableObject root =
        otio::read_from_file(otio::Format::CMX_3600, "cut.edl", options);

    for (const otio::SerializableObject &node : root.find_clips()) {
        if (std::optional<otio::Clip> clip = node.as<otio::Clip>()) {
            std::cout << clip->name() << "\n";
        }
    }

    return 0;
}
```

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

Reading hands back the file's root object, with no container around it to
hold. `otio::open` infers the format from the filename instead of taking one,
and `otio::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/).

What an object really is, the library knows rather than the compiler:
`node.is<otio::Clip>()` asks, and `node.as<otio::Clip>()` hands back an
`std::optional<otio::Clip>` holding the clip where the answer is yes. They
take the place of `dynamic_cast`, because an object here is a value rather
than a retained pointer.

## Building a timeline

```cpp
#include <iostream>

#include <opentimelineio/otio.hpp>

int main() {
    // Each object is made on its own and joins a timeline when you put it
    // into one. Nothing has to exist before the thing it goes into.
    otio::Timeline timeline = otio::Timeline::create("Cut");
    otio::Stack stack = otio::Stack::create("tracks");
    otio::Track track = otio::Track::create("V1", "Video");

    timeline.set_tracks(stack);
    stack.append_child(track);

    const std::vector<std::string> names = {"A", "B", "C"};
    for (std::size_t index = 0; index < names.size(); ++index) {
        otio::Clip clip = otio::Clip::create(names[index]);
        const otio::RationalTime start(static_cast<double>(index) * 24, 24);
        clip.set_source_range(otio::TimeRange(start, otio::RationalTime(24, 24)));
        track.append_child(clip);
    }

    // Three seconds of picture, written as canonical OpenTimelineIO JSON.
    // Objects keep their timeline alive between them, so there is nothing
    // to close.
    std::cout << track.duration().to_seconds() << "\n";
    otio::save(timeline, "cut.otio");
    return 0;
}
```

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

Each object is made on its own with a static `create` and joins a timeline
when you put it in one. `RationalTime` and `TimeRange` are structs.

## How objects and documents work here

An object is a small value over an arena handle. Underneath, the core keeps a
timeline's objects in an arena; the SDK does that bookkeeping, and objects keep
their timeline alive between them, so there is nothing to close. `close()`
exists for releasing a large one early, and every object that lived in it then
fails with `Status::NULL_POINTER` rather than reading freed memory.

Objects made apart stay apart until one takes the other in. Appending or
inserting an object that is still a child in another timeline is refused as
the library refuses it, with `Status::CORE_ERROR` and the library's own
message, and so is placing one whose handle has gone stale, with
`Status::STALE_HANDLE`. Both are refused before that timeline is brought over,
so both timelines stay whole and releasing one leaves the other working.

## Errors and missing values

A call that can fail throws an `otio::Error` carrying a `Status`, where
upstream's C++ takes an `ErrorStatus *` out-parameter.

A call that only *names* an object, such as `detach_child`, `index_of_child`
or `has_child`, refuses one that belongs to a different timeline before
asking the library, because merging the two and failing afterwards would
already have done the damage. That refusal is an `otio::OtherTimelineError`:
an `otio::Error` with `Status::INVALID_ARGUMENT` and a type of its own, so it
can be caught apart from the library's failures with
`catch (const otio::OtherTimelineError&)`.

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 call answers an
empty `std::optional`, because that is an answer rather than a failure:

```cpp
if (std::optional<otio::TimeRange> span = clip.source_range()) {
    std::cout << span->duration.value << "\n";
}
```

## Editing and algorithms

```cpp
#include <iostream>
#include <string>

#include <opentimelineio/otio.hpp>

/// One second of picture, named.
static otio::Clip second(const std::string &name) {
    otio::Clip clip = otio::Clip::create(name);
    clip.set_source_range(
        otio::TimeRange(otio::RationalTime(0, 24), otio::RationalTime(24, 24)));
    return clip;
}

static void show(const otio::Track &track) {
    std::string names;
    for (const otio::SerializableObject &child : track.children()) {
        if (!names.empty()) names += " ";
        names += child.name();
    }
    std::cout << names << " - " << track.duration().value << " frames\n";
}

int main() {
    otio::Track track = otio::Track::create("V1", "Video");
    for (const char *name : {"A", "B", "C"}) {
        track.append_child(second(name));
    }
    show(track);

    // Insert makes room: everything from the insertion point onwards moves
    // later, and the track gets longer.
    otio::insert(second("D"), track, otio::RationalTime(24, 24), false);
    show(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.
    otio::overwrite(
        second("E"), track,
        otio::TimeRange(otio::RationalTime(48, 24), otio::RationalTime(24, 24)),
        false);
    show(track);

    return 0;
}
```

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

The ten edit operations and the algorithms are free functions in `otio`, such
as `otio::insert`, `otio::overwrite` and `otio::flatten_tracks`, 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.

## Formats

`otio::read_from_file` and `otio::write_to_file` take an `otio::Format`:
`OTIO_JSON`, `ALE`, `CMX_3600`, `FCP7_XML`, `FCPX_XML`, `AAF`, and the two
bundles, `OTIOZ` and `OTIOD`, which are read and written through a path only.

```cpp
#include <opentimelineio/otio.hpp>

int main() {
    // 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.
    otio::ReadOptions options = otio::read_options_default();
    options.rate = 24;

    const otio::SerializableObject root =
        otio::read_from_file(otio::Format::CMX_3600, "cut.edl", options);

    // 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.
    otio::write_to_file(otio::Format::FCPX_XML, root, "cut.fcpxml");

    return 0;
}
```

<sub>Source: https://github.com/alchemist-editor/otio-rust/blob/main/site/content/samples/convert-a-format/cpp.cpp</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/).

## Media linkers and hooks

```cpp
#include <cstdint>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

#include <opentimelineio/otio.hpp>

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

int main() {
    // 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.
    // An empty optional leaves the clip as it was.
    otio::register_media_linker(
        "proxies",
        [](const otio::Clip &clip, const otio::Metadata &arguments)
            -> std::optional<otio::MediaReference> {
            const std::string root = arguments.get_string("root");
            return otio::ExternalReference::create(
                clip.name(), root + "/" + clip.name() + ".mov");
        });

    // A hook script is handed the whole result, and answers with what the
    // read goes on with: here the same object, stamped. An exception thrown
    // from either fails the read with Status::PLUGIN_ERROR and its message.
    otio::register_hook_script(
        "stamp", [](const otio::SerializableObject &target, const otio::Metadata &arguments) {
            otio::Metadata(target).set_string("read_by", arguments.get_string("who"));
            return target;
        });
    otio::attach_hook_script("post_adapter_read", "stamp");

    // The read names the linker, and carries both sets of arguments as JSON.
    otio::ReadOptions options = otio::read_options_default();
    options.media_linker = "proxies";
    options.media_linker_arguments = R"({"root": "/proxies"})";
    options.hook_arguments = R"({"who": "the conform"})";
    const otio::SerializableObject track = otio::read_from_bytes(
        otio::Format::OTIO_JSON, std::vector<std::uint8_t>(cut.begin(), cut.end()), options);

    std::cout << "read by " << otio::Metadata(track).get_string("read_by") << "\n";
    for (const otio::SerializableObject &node : track.find_clips()) {
        const otio::Clip clip = *node.as<otio::Clip>();
        const otio::ExternalReference media =
            *clip.media_reference()->as<otio::ExternalReference>();
        std::cout << clip.name() << " -> " << media.target_url() << "\n";
    }

    // The registry is the whole process's, so take them out again.
    otio::detach_hook_script("post_adapter_read", "stamp");
    otio::unregister_hook_script("stamp");
    otio::unregister_media_linker("proxies");
    return 0;
}
```

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

A media linker and a hook script are `std::function`s, registered by name
with `otio::register_media_linker` and `otio::register_hook_script`. A linker
is handed each clip a read produced and the read's
`media_linker_arguments` as `otio::Metadata`, and answers an
`std::optional<otio::MediaReference>`: the reference the clip should use, or
empty to leave the clip alone. A hook script is handed what the hook runs on
and the `hook_arguments`, and answers the object to go on with, which may be
the one it was handed or another built fresh. Hook scripts run only at the
hooks `otio::attach_hook_script` attaches them to; `run_hook` runs a hook of
your own on any object.

An exception a linker or a script throws stops the read or write there, which
then throws an `otio::Error` with `Status::PLUGIN_ERROR` and the exception's
`what()`; nothing unwinds into the library. The registry is the whole
process's, so a name registered again replaces what was there, and the
`std::function` and whatever it captured are released when the name is
replaced or unregistered. A read may run on any thread, so a plugin must be
safe to call from any of them.

What a plugin is handed belongs to the timeline being read, and is valid only
for the call: keep nothing from it. That timeline is the library's to free
while the call runs, even where it is your own, as it is for `run_hook`. So
moving it into another, by appending what the plugin was handed to a track
built inside the call, is refused, and closing it does nothing.

## Platforms

CI builds and tests the SDK on Linux and macOS. Windows is left out, as it is
for the other SDKs that link the Rust static library through the platform's
C/C++ toolchain.

## Reference

- [`sdk/cpp/README.md`](https://github.com/alchemist-editor/otio-rust/blob/main/sdk/cpp/README.md).
- The [C ABI reference](https://otio-rust-docs.vercel.app/reference/) the headers are generated from, and
  [ADR 0003](https://github.com/alchemist-editor/otio-rust/blob/main/docs/adr/0003-sdk-generation.md)
  for every deliberate departure from upstream's C++ API.

---

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