Skip to content

Implement dav1d-rs's Rust API#1439

Open
leo030303 wants to merge 69 commits intomemorysafety:mainfrom
leo030303:add-rust-api
Open

Implement dav1d-rs's Rust API#1439
leo030303 wants to merge 69 commits intomemorysafety:mainfrom
leo030303:add-rust-api

Conversation

@leo030303
Copy link
Contributor

@leo030303 leo030303 commented Jul 6, 2025

I've copy pasted the PR from #1362 and updated it with some of the suggestions made on that pull request. The main changes are:

  • Have all the public-facing Rust API code in its own file.
  • Used some enums from rav1d instead of redefining new ones, and added in doc comments from the original dav1d-rs library to a few items.
  • Removed as much unsafe code as I could and replaced it with the Rust methods from rav1d as much as possible.

It currently works as a drop-in replacement for dav1d-rs; adding in use rav1d as dav1d; to my fork of image makes everything work fine.

The only functional changes I made are I removed the unsafe impls of Send and Sync for InnerPicture so Picture is no longer Sync or Send. I looked through the code and I don't believe DisjointMut<Rav1dPictureDataComponentInner>, which is a field of one of its children, is thread safe, though I'm open to correction there; I'm pretty unfamiliar with unsafe Rust.

I also don't have safety comments on the two unsafe blocks in rust_api.rs; I'm unsure what these would look like, so open to suggestions there. These are mostly taken verbatim from the old pull request.

Copy link
Collaborator

@kkysen kkysen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the much safer implementation! I'm still taking a closer look at the unsafes, as the few remaining are still quite critical, but I wanted to give some initial feedback in general first.

@kkysen kkysen changed the title Implement Rust API Implement dav1d-rs's Rust API Jul 7, 2025
kkysen added a commit that referenced this pull request Jul 7, 2025
Pulled out the docs additions from #1439 into its own PR.
@leo030303 leo030303 requested a review from kkysen July 8, 2025 22:23
kkysen added a commit that referenced this pull request Jul 15, 2025
…1442)

Pulled out the `Rav1dError` changes from #1439 into a separate PR.
kkysen added a commit that referenced this pull request Jul 15, 2025
…e `u32` (#1443)

Pulled out type changes from here #1439 into its own PR.
Copy link
Collaborator

@kkysen kkysen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leo030303, could you rebase this on main? There are a lot of other commits in here now, so it's harder to review.

leo030303 and others added 19 commits July 19, 2025 12:29
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
kkysen added a commit that referenced this pull request Feb 12, 2026
…rap_c` (#1470)

This can be used by #1439 in `fn send_data` to safely create a
`Rav1dData` without a second extra allocation. We'll still need one copy
with the way the APIs are right now, but this should keep things
obviously safe and without a second extra allocation. Next, we can work
on redesigning the API to allow zero-copy usage.
kkysen added a commit that referenced this pull request Feb 13, 2026
…API (#1471)

This does some API simplification for #1439 while also supporting other
`impl AsRef` types. It separates `CRef` from `CBox`. `CBox` is the pure
C abstraction over an owned C reference/"Box". `CRef` is an `enum` over
`CBox` and other Rust-native `impl AsRef`s. I tried using `dyn AsRef`
first, but we make some safety guarantees about reference stability that
we can't make about any `impl AsRef`, and that would force an extra
`Box`. Moving the `dyn AsRef` one level higher to `Arc<dyn AsRef<T>>`
would be super nice, but in order to send the `Arc`s through FFI
boundaries, they can't wrap an unsized type, as we can't send unsized
pointers over FFI boundaries (the ptr metadata API is still very
unstable).

Adding support for non `'static` `&T`s would be extremely useful and
would allow us to eliminate a data copy, but that is much more tricky,
as we'd have to thread the lifetime through a lot of places and probably
rearrange some of the `Rav1dContext`/`Rav1dState` API.
leo030303 and others added 6 commits February 14, 2026 13:54
Co-authored-by: Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
Co-authored-by: Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
@leo030303
Copy link
Contributor Author

This all looks good so far to me, only note is I had to remove

static_assertions::assert_impl_all!(Decoder: Send, Sync);

Since CRef isn't Send or Sync, if its trivial to add that back in then we should but I'm not sure what safety assumptions you made on the C backend so I didn't want to go messing with that, otherwise I don't think remove the assertion is a very big deal since it just restricts the API a bit

@leo030303 leo030303 requested review from Shnatsel and kkysen February 14, 2026 14:26
Copy link
Collaborator

@kkysen kkysen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I added Rc to CRef, which isn't Send or Sync. I can remove that.

Copy link
Collaborator

@kkysen kkysen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since CRef isn't Send or Sync, if its trivial to add that back in then we should

I've added it back in #1472 now. Once that merges you can rebase/merge.

@leo030303 leo030303 requested a review from kkysen February 21, 2026 11:45
@leo030303
Copy link
Contributor Author

Think that's most of the outstanding issues wrapped up!

@plops
Copy link

plops commented Mar 1, 2026

I can't wait for this to be finished!

@leo030303
Copy link
Contributor Author

@kkysen if fixing the API for that to never be null isn't straightforward then it could probably be left for a follow up pr since I think the core of this pr should be good to go

@Shnatsel
Copy link

I've opened a proof-of-concept PR for image to use rav1d via this PR: image-rs/image#2849

It's not quite a drop-in replacement but the conversion was fairly straightforward.

I've wired everything up to wondermagick and it seems to work great!

Comment on lines +347 to +391
pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] {
let height = match component {
PlanarImageComponent::Y => self.height(),
_ => match self.pixel_layout() {
PixelLayout::I420 => self.height().div_ceil(2),
PixelLayout::I422 | PixelLayout::I444 => self.height(),
PixelLayout::I400 => return &[], // grayscale images don't have color components
},
};

let stride = self.stride(component);

// Get a raw pointer to the plane data of the `component` for the decoded frame.
let index: usize = component.into();
let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index]
.as_byte_mut_ptr()
.cast_const();

if stride == 0 || raw_plane_data_pointer.is_null() {
return &[];
}
let data_length = (stride as usize)
.checked_mul(height as usize)
.expect("The product of stride and height exceeded usize::MAX");
// SAFETY: The following invariants are upheld:
// 1. Pointer validity: Checked above - if null or stride is 0, we return &[].
// 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte)
// alignment (see Rav1dPictureDataComponentInner::new), which exceeds any
// primitive type's alignment requirements.
// 3. Allocated size: The allocator guarantees the buffer is at least stride * height
// bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul
// ensures this calculation doesn't overflow.
// 4. Initialization: The allocator is required to initialize the data per the
// alloc_picture_callback safety requirements.
// 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData>
// alive for the duration of the borrow.
// 6. No mutable aliases: The Picture is only returned after decoding is complete,
// so the decoder no longer writes to this buffer. The public API only exposes
// shared (&[u8]) access, and no &mut references to this data can exist once
// the Picture is handed to the user.
//
// Past dav1d-rs PRs relevant to this line:
// https://github.com/rust-av/dav1d-rs/pull/121
// https://github.com/rust-av/dav1d-rs/pull/123
unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] {
let height = match component {
PlanarImageComponent::Y => self.height(),
_ => match self.pixel_layout() {
PixelLayout::I420 => self.height().div_ceil(2),
PixelLayout::I422 | PixelLayout::I444 => self.height(),
PixelLayout::I400 => return &[], // grayscale images don't have color components
},
};
let stride = self.stride(component);
// Get a raw pointer to the plane data of the `component` for the decoded frame.
let index: usize = component.into();
let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index]
.as_byte_mut_ptr()
.cast_const();
if stride == 0 || raw_plane_data_pointer.is_null() {
return &[];
}
let data_length = (stride as usize)
.checked_mul(height as usize)
.expect("The product of stride and height exceeded usize::MAX");
// SAFETY: The following invariants are upheld:
// 1. Pointer validity: Checked above - if null or stride is 0, we return &[].
// 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte)
// alignment (see Rav1dPictureDataComponentInner::new), which exceeds any
// primitive type's alignment requirements.
// 3. Allocated size: The allocator guarantees the buffer is at least stride * height
// bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul
// ensures this calculation doesn't overflow.
// 4. Initialization: The allocator is required to initialize the data per the
// alloc_picture_callback safety requirements.
// 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData>
// alive for the duration of the borrow.
// 6. No mutable aliases: The Picture is only returned after decoding is complete,
// so the decoder no longer writes to this buffer. The public API only exposes
// shared (&[u8]) access, and no &mut references to this data can exist once
// the Picture is handed to the user.
//
// Past dav1d-rs PRs relevant to this line:
// https://github.com/rust-av/dav1d-rs/pull/121
// https://github.com/rust-av/dav1d-rs/pull/123
unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) }
pub fn plane_data(
&self,
component: PlanarImageComponent,
) -> DisjointImmutGuard<'_, Rav1dPictureDataComponentInner, [u8]> {
let data = &self.inner.data.as_ref().unwrap().data;
let component = &data[usize::from(component)];
// SAFETY: `.slice` is not marked `unsafe`,
// but that's because it is not meant to be used outside of `rav1d`,
// and inside `rav1d` we collectively uphold the `DisjointMut` safety requirements.
// Here, though, we expose this publicly, so it must be sound regardless of the caller.
// It's safe because a [`Picture`] is only created in [`Decoder::get_picture`],
// which calls [`rav1d_get_picture`], which is safe for all callers.
// Once this [`Picture`] has been created, there aren't anymore
// potentially mutable references to it while it's being decoded,
// as it's already fully decoded.
component.slice::<BitDepth8, _>(..)
}

This would make things fully safe (although there's a hidden unsafety, but one that's much more obviously sound). Judging by image-rs/image#2849, it seems like returning a guard from this instead of the &[u8] directly should still work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could do this:

Suggested change
pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] {
let height = match component {
PlanarImageComponent::Y => self.height(),
_ => match self.pixel_layout() {
PixelLayout::I420 => self.height().div_ceil(2),
PixelLayout::I422 | PixelLayout::I444 => self.height(),
PixelLayout::I400 => return &[], // grayscale images don't have color components
},
};
let stride = self.stride(component);
// Get a raw pointer to the plane data of the `component` for the decoded frame.
let index: usize = component.into();
let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index]
.as_byte_mut_ptr()
.cast_const();
if stride == 0 || raw_plane_data_pointer.is_null() {
return &[];
}
let data_length = (stride as usize)
.checked_mul(height as usize)
.expect("The product of stride and height exceeded usize::MAX");
// SAFETY: The following invariants are upheld:
// 1. Pointer validity: Checked above - if null or stride is 0, we return &[].
// 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte)
// alignment (see Rav1dPictureDataComponentInner::new), which exceeds any
// primitive type's alignment requirements.
// 3. Allocated size: The allocator guarantees the buffer is at least stride * height
// bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul
// ensures this calculation doesn't overflow.
// 4. Initialization: The allocator is required to initialize the data per the
// alloc_picture_callback safety requirements.
// 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData>
// alive for the duration of the borrow.
// 6. No mutable aliases: The Picture is only returned after decoding is complete,
// so the decoder no longer writes to this buffer. The public API only exposes
// shared (&[u8]) access, and no &mut references to this data can exist once
// the Picture is handed to the user.
//
// Past dav1d-rs PRs relevant to this line:
// https://github.com/rust-av/dav1d-rs/pull/121
// https://github.com/rust-av/dav1d-rs/pull/123
unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) }
pub fn plane_data<'a>(&'a self, component: PlanarImageComponent) -> &'a [u8] {
let data = &self.inner.data.as_ref().unwrap().data;
let component = &data[usize::from(component)];
let guard = component.slice::<BitDepth8, _>(..);
// SAFETY: [`Picture`] is only created after decoding is complete
// (in [`Decoder::get_picture`], which calls [`rav1d_get_picture`]).
// Thus, the decoder no longer writes to this data,
// and there are no other safe public ways to have a `&mut` to this data.
unsafe { guard.unchecked_disjoint_mut() }

If that seems better, than I can open a PR to add this fn unchecked_disjoint_mut:

impl<'a, T: ?Sized + AsMutPtr, V: ?Sized> DisjointImmutGuard<'a, T, V> {
    // # Safety
    //
    // This is no longer checked at runtime to be legitimately disjoint mut.
    pub unsafe fn unchecked_disjoint_mut(&self) -> &'a V {
        self.slice
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once this is done, the PR should be all good to go.

Also, you could revert back to the original Plane API so that it's more of a drop-in replacement. That should be easy enough for either of these APIs I suggested above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide Rust API

7 participants