How to Use librav1e C API in Non-Rust Projects

This article explains how librav1e—the AV1 video encoder library written in Rust—extends its compatibility to non-Rust environments. It explores how the library’s C-compatible Application Programming Interface (API) bridges the language gap, allowing developers to seamlessly integrate high-performance AV1 encoding into projects written in C, C++, Python, and other languages.

The Role of the Foreign Function Interface (FFI)

At the core of librav1e’s cross-language compatibility is Rust’s Foreign Function Interface (FFI). Rust is designed to coexist with C-based systems. By utilizing the extern "C" keyword and the #[no_mangle] attribute in its source code, librav1e prevents the Rust compiler from altering function names during compilation. This ensures that the compiled binary exports symbols in a standard format that any C-compatible linker can recognize and resolve.

Header Generation and C Types

To allow C and C++ compilers to understand the library’s interfaces, librav1e provides a C header file (rav1e.h). This header file is often generated automatically using tools like cbindgen.

The header maps Rust’s native data structures to standard C equivalents: * Opaque Pointers: Complex Rust structures, such as the encoder configuration (RaContext and RaConfig), are exposed to C as opaque pointers. The host application handles these as void pointers, preventing non-Rust code from directly manipulating Rust’s memory layout. * Standard Data Types: Standard integer types, floats, and character arrays are mapped directly to their C counterparts (e.g., uint8_t, size_t). * Enums and Structs: Configuration options, error codes, and frame metadata are converted into standard C enums and structures.

Compilation and Linking

When librav1e is compiled for non-Rust use, the Cargo build system generates standard system libraries rather than Rust-specific library files (.rlib). Depending on the target system and requirements, it compiles into: * Static Libraries: .a (Unix) or .lib (Windows) * Dynamic Libraries: .so (Linux), .dylib (macOS), or .dll (Windows)

Non-Rust projects link against these compiled libraries just as they would with any traditional C library. For example, a C project can compile using gcc or clang by specifying the path to the rav1e header and linking the library via the -lrav1e flag.

Memory Management Across the Boundary

Because Rust and C manage memory differently, librav1e exposes specific allocation and deallocation functions to prevent memory leaks and undefined behavior.

  1. Allocation: The C host application requests the creation of an encoder context using rav1e_config_new() and rav1e_context_new(). This allocates memory on the heap within the Rust runtime.
  2. Deallocation: Because C cannot automatically clean up Rust objects, the host application must explicitly release this memory using designated destructor functions, such as rav1e_config_unref() and rav1e_context_unref().

Enabling Bindings for Other Languages

By exposing a standard C API, librav1e automatically gains compatibility with almost every modern programming language. Higher-level languages use their own C FFI libraries to wrap the librav1e C API. For example, Python uses ctypes or cffi, Swift uses Clang importer, and Go uses cgo. These languages can call the C functions exported by librav1e to perform AV1 encoding without needing to interact with Rust directly.