rav1d-safe provides a C-compatible API (dav1d_* functions) behind the c-ffi feature flag. This API mirrors the upstream dav1d C API (v7.0.0) and is the primary integration point for consumers like zenavif.
The goal: keep the C API working but make the entire codebase underneath safe Rust, with unsafe confined to a thin FFI shim layer.
Two parallel type systems exist for every API type:
| C API (FFI) | Internal (Rust) |
|---|---|
Dav1dContext = RawArc<Rav1dContext> |
Arc<Rav1dContext> |
Dav1dSettings (#[repr(C)]) |
Rav1dSettings (native Rust) |
Dav1dPicture (#[repr(C)]) |
Rav1dPicture (native Rust) |
Dav1dData (#[repr(C)]) |
Rav1dData (native Rust) |
Dav1dDataProps (#[repr(C)]) |
Rav1dDataProps (native Rust) |
Dav1dSequenceHeader (#[repr(C)]) |
Rav1dSequenceHeader (native Rust) |
Dav1dFrameHeader (#[repr(C)]) |
Rav1dFrameHeader (native Rust) |
Dav1dPicAllocator (fn ptrs) |
Rav1dPicAllocator (fn ptrs) |
Each Dav1d* type has From/TryFrom conversions to/from its Rav1d* counterpart.
These are the public dav1d_* functions that external C/Rust consumers call:
| Function | Signature | Safety |
|---|---|---|
dav1d_version |
() -> *const c_char |
safe extern "C" |
dav1d_version_api |
() -> c_uint |
safe extern "C" |
dav1d_default_settings |
(NonNull<Dav1dSettings>) |
unsafe (ptr write) |
dav1d_get_frame_delay |
(Option<NonNull<Dav1dSettings>>) -> Dav1dResult |
unsafe (ptr read) |
dav1d_open |
(Option<NonNull<Option<Dav1dContext>>>, Option<NonNull<Dav1dSettings>>) -> Dav1dResult |
unsafe (ptr read/write) |
dav1d_parse_sequence_header |
(Option<NonNull<Dav1dSequenceHeader>>, Option<NonNull<u8>>, usize) -> Dav1dResult |
unsafe (ptr read/write) |
dav1d_send_data |
(Option<Dav1dContext>, Option<NonNull<Dav1dData>>) -> Dav1dResult |
unsafe (RawArc deref, ptr read/write) |
dav1d_get_picture |
(Option<Dav1dContext>, Option<NonNull<Dav1dPicture>>) -> Dav1dResult |
unsafe (RawArc deref, ptr write) |
dav1d_apply_grain |
(Option<Dav1dContext>, Option<NonNull<Dav1dPicture>>, Option<NonNull<Dav1dPicture>>) -> Dav1dResult |
unsafe (RawArc deref, ptr read/write) |
dav1d_flush |
(Dav1dContext) |
unsafe (RawArc deref) |
dav1d_close |
(Option<NonNull<Option<Dav1dContext>>>) |
unsafe (RawArc into_arc, ptr read/write) |
dav1d_get_event_flags |
(Option<Dav1dContext>, Option<NonNull<Dav1dEventFlags>>) -> Dav1dResult |
unsafe (RawArc deref, ptr write) |
dav1d_get_decode_error_data_props |
(Option<Dav1dContext>, Option<NonNull<Dav1dDataProps>>) -> Dav1dResult |
unsafe (RawArc deref, ptr write) |
dav1d_picture_unref |
(Option<NonNull<Dav1dPicture>>) |
unsafe (ptr read/write, drop) |
dav1d_data_create |
(Option<NonNull<Dav1dData>>, usize) -> *mut u8 |
unsafe (ptr write) |
dav1d_data_wrap |
(Option<NonNull<Dav1dData>>, Option<NonNull<u8>>, usize, Option<FnFree>, ...) -> Dav1dResult |
unsafe (ptr write, fn ptr) |
dav1d_data_wrap_user_data |
(Option<NonNull<Dav1dData>>, Option<NonNull<u8>>, Option<FnFree>, ...) -> Dav1dResult |
unsafe (ptr write, fn ptr) |
dav1d_data_unref |
(Option<NonNull<Dav1dData>>) |
unsafe (ptr read/write, drop) |
dav1d_data_props_unref |
(Option<NonNull<Dav1dDataProps>>) |
unsafe (ptr read/write, drop) |
Total: 19 extern "C" functions
DAV1D_API_VERSION_MAJOR/MINOR/PATCHDAV1D_PICTURE_ALIGNMENTDAV1D_INLOOPFILTER_*constantsDAV1D_DECODEFRAMETYPE_*constantsDAV1D_EVENT_FLAG_*constants
These are the types that cross the FFI boundary and must maintain their exact C layout:
Dav1dSettings- decoder configurationDav1dPicture- decoded picture outputDav1dPictureParameters- picture metadataDav1dData- input bitstream dataDav1dDataProps- data metadata/timestampsDav1dUserData- user-attached dataDav1dSequenceHeader- AV1 sequence header (large, ~500 bytes)Dav1dFrameHeader- AV1 frame header (large)Dav1dPicAllocator- picture allocation callbacks (fn ptrs + cookie)Dav1dLogger- logging callback- All sub-structs of the above (Dav1dFilmGrainData, Dav1dSegmentationData, etc.)
- Various
type Dav1d* = c_uintenum aliases
Every extern "C" function is inherently unsafe because:
- Raw pointer parameters (
*const,*mut,NonNull) RawArclifecycle management (manual reference counting)- Type erasure through
c_void - Callback function pointers (
FnFree, allocator callbacks)
Strategy: Keep this. It's a ~960-line file. Confine ALL C API unsafe here.
RawArc<T> wraps Arc<T> as a raw pointer for C consumption. Dav1dContext = RawArc<Rav1dContext>. This is fundamentally unsafe — it's manual Arc::into_raw/Arc::from_raw.
CArc<T> wraps Arc<Pin<CBox<T>>> for C-allocated data that rav1d takes ownership of (e.g., Dav1dData wrapping a user's buffer with a free callback).
Strategy: Keep these behind c-ffi. Internal code uses Arc directly.
FFISafe<'a, T> is a type-erased wrapper used in DSP function pointer dispatch. The DSP dispatch tables store fn(*const FFISafe<Dst>, ...) pointers that are called with type-erased arguments.
This exists because:
- The original dav1d has function pointer tables for runtime dispatch (8bpc vs 16bpc, AVX2 vs SSE4, etc.)
- When
asmis enabled, these point to assembly functions with C calling convention - When safe-simd is used, these point to Rust functions wrapped in
unsafe extern "C"
Strategy: This is the hardest part. The function pointer dispatch pattern requires unsafe for type erasure. Options discussed below.
DisjointMut<T> provides interior mutability for pixel buffers where different threads write to non-overlapping regions. This is a fundamental safety primitive — it CHECKS disjointness at runtime in debug builds.
Strategy: Keep as-is. This is a sound unsafe abstraction that makes the rest of the codebase safe.
Small utility modules with inherent unsafe that provides safe APIs:
align: Aligned allocation (Align16, etc.)assume: Compiler hint for optimizersend_sync_non_null:NonNullwrapper that isSend + Sync
Strategy: Keep as-is. These are foundational primitives with small, auditable unsafe surfaces.
These still have #[allow(unsafe_code)] because they contain FFI wrapper functions (unsafe extern "C" fn) that bridge between the function pointer dispatch tables and the safe implementations.
Strategy: Move all FFI wrappers into safe_simd/ modules (already done for filmgrain, ipred, mc). The dispatch modules themselves become fully safe.
Contains the default picture allocator and picture lifecycle management. Unsafe due to raw pointer manipulation for pixel buffer allocation.
Strategy: Gate the C-compatible allocator behind c-ffi. For pure Rust use, provide a safe allocator using Vec<u8> or DisjointMut.
When built with just --features "bitdepth_8,bitdepth_16":
#![deny(unsafe_code)]applies crate-wide- Only excepted modules:
align,assume,c_arc(unused),disjoint_mut,send_sync_non_null,safe_simd - These are sound abstractions with safe public APIs
- Goal: 0 unsafe in application logic. All decode, transform, filter, prediction code is safe.
Remaining work to reach this:
- Move FFI wrappers out of
itx.rs,looprestoration.rs,refmvs.rs,lf_mask.rsintosafe_simd/ - Make
msac.rssafe (inline SIMD already uses#[target_feature]) - Make
picture.rssafe (safe allocator for non-FFI builds) - Make
internal.rssafe (may need refactoring of fn ptr tables)
When c-ffi is enabled:
src/lib.rsis allowed unsafe (the 19extern "C"functions)src/c_arc.rs,src/c_box.rsare allowed unsafe (Arc↔raw ptr)src/ffi_safe.rsis allowed unsafe (type erasure for dispatch)include/dav1d/*types maintain#[repr(C)]layout- All conversions between
Dav1d*↔Rav1d*happen at the boundary
This layer is ~1500 lines total. It's the entire unsafe surface for the C API.
When asm is enabled:
- Links to hand-written assembly via
extern "C"FFI - All DSP dispatch modules need unsafe for the FFI calls
- Not our focus — the safe-simd path replaces this entirely
The biggest challenge for full safety is the DSP dispatch tables. Currently:
// In wrap_fn_ptr! macro
pub struct FnPtr(Option<unsafe extern "C" fn(/* erased args */)>);Every DSP function (MC, ITX, CDEF, loopfilter, etc.) is called through a function pointer table. This is necessary because:
- Bitdepth dispatch: 8bpc and 16bpc have different pixel types but share the same code structure
- SIMD dispatch: Different CPU feature levels get different implementations
- ASM compatibility: ASM functions have C calling convention
Option A: Keep fn ptrs, confine unsafe to call sites (current approach)
- Each call site uses
unsafe { (table.fn_ptr)(args) } - The dispatch modules create safe wrapper functions
- Works, but leaves
unsafescattered across decode.rs, recon.rs, etc.
Option B: Trait-based dispatch (major refactor)
- Replace fn ptr tables with trait objects or enums
trait McFns { fn put_8tap(&self, ...) }- Eliminates unsafe at call sites
- Problem: huge refactor, may hurt performance, doesn't work with ASM backend
Option C: Safe fn ptr wrappers with runtime checks
- Wrap each fn ptr in a type-safe struct that validates arguments
- Call sites use safe methods:
table.mc.put_8tap(dst, src, ...) - The wrapper does the unsafe call internally
- Moderate refactor, preserves performance
Option D: Hybrid — safe wrappers for non-FFI, fn ptrs for FFI
- When
c-ffiis disabled: use direct function calls or trait dispatch - When
c-ffiis enabled: keep fn ptr tables for compatibility - Best of both worlds but duplicates dispatch logic
The wrap_fn_ptr! macro already exists. Extend it to provide safe call methods that encapsulate the unsafe extern "C" fn call. The dispatch table becomes a struct of safe-callable wrappers instead of raw fn ptrs.
For pure Rust consumers (like zenavif), we should provide a safe Rust API:
// Future safe Rust API (no unsafe needed by consumer)
pub struct Decoder { /* ... */ }
impl Decoder {
pub fn new(settings: DecoderSettings) -> Result<Self, Error>;
pub fn send_data(&mut self, data: &[u8]) -> Result<(), Error>;
pub fn get_picture(&mut self) -> Result<Option<Picture>, Error>;
pub fn flush(&mut self);
}
pub struct Picture { /* ... */ }
impl Picture {
pub fn plane(&self, index: usize) -> &[u8];
pub fn stride(&self, index: usize) -> isize;
pub fn width(&self) -> u32;
pub fn height(&self) -> u32;
pub fn bit_depth(&self) -> u8;
// ...
}This would let zenavif drop all its unsafe blocks. The internal implementation calls the same rav1d_open, rav1d_send_data, etc. functions that already exist as safe Rust functions.
The C API functions are thin unsafe wrappers around these safe internal functions. The safe Rust API would be even thinner safe wrappers — or just re-exports.
- Finish making DSP dispatch modules safe (move remaining FFI wrappers to safe_simd/)
- Make msac.rs safe (target_feature wrappers)
- Make picture.rs safe (safe allocator for non-FFI builds)
- Make internal.rs safe (safe fn ptr wrapper pattern)
- Add safe Rust API (Decoder/Picture wrapper types)
- Migrate zenavif to safe Rust API (drop all unsafe in consumer)
- The 19
dav1d_*extern "C" functions stay exactly as they are - All
Dav1d*struct layouts stay#[repr(C)] - The
Dav1d* ↔ Rav1d*conversion pattern stays - ABI version stays at 7.0.0
- Feature flag behavior:
asmfor assembly,c-ffifor C API, neither for pure safe Rust