fix(avhw): add SAFETY comments to all undocumented unsafe blocks

Close #7

- Add // SAFETY: comments to 19 undocumented unsafe blocks and impls
- Add nb_streams/null guard on stream array dereference (drain_encoder)
- Add clippy undocumented_unsafe_blocks = warn lint to prevent regression

avhw.rs now has 0 clippy unsafe documentation warnings.
This commit is contained in:
dailz
2026-06-06 15:54:09 +08:00
parent 226768c3e3
commit 7c1c9b2e19
2 changed files with 55 additions and 4 deletions

View File

@@ -31,3 +31,6 @@ dirs = "6"
[dev-dependencies]
tempfile = "3.27.0"
[lints.clippy]
undocumented_unsafe_blocks = "warn"

View File

@@ -23,12 +23,17 @@ pub struct AvHwDevCtx {
ptr: *mut ffi::AVBufferRef,
}
// SAFETY: AvHwDevCtx wraps an FFmpeg AVBufferRef which is not Send by default,
// but we guarantee exclusive access through &mut self. The underlying VAAPI
// device context is thread-safe for the operations we perform.
unsafe impl Send for AvHwDevCtx {}
impl AvHwDevCtx {
pub fn new_vaapi(drm_device: &Path) -> Result<Self> {
let device_cstr = CString::new(drm_device.to_str().unwrap())?;
let mut p: *mut ffi::AVBufferRef = ptr::null_mut();
// SAFETY: device_cstr is a valid C string for the duration of the call;
// p is a valid out-pointer that FFmpeg initializes on success.
let ret = unsafe {
ffi::av_hwdevice_ctx_create(
&mut p,
@@ -74,10 +79,15 @@ pub struct AvHwFrameCtx {
ptr: *mut ffi::AVBufferRef,
}
// SAFETY: AvHwFrameCtx wraps an FFmpeg AVBufferRef to an AVHWFramesContext.
// It is only accessed through &mut self, ensuring no concurrent mutation.
// The underlying hardware frames pool is thread-safe for the send/receive pattern.
unsafe impl Send for AvHwFrameCtx {}
impl AvHwFrameCtx {
fn new_inner(hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel) -> Result<Self> {
// SAFETY: hw_dev is a live AVHWDeviceContext; FFmpeg returns either a valid
// frames context ref or null (checked below).
let mut p = unsafe { ffi::av_hwframe_ctx_alloc(hw_dev.as_ptr()) };
if p.is_null() {
bail!("av_hwframe_ctx_alloc returned null");
@@ -92,6 +102,8 @@ impl AvHwFrameCtx {
(*fc).height = h as i32;
(*fc).initial_pool_size = 4;
}
// SAFETY: p is a valid AVHWFramesContext ref configured above and not yet
// transferred or freed.
let ret = unsafe { ffi::av_hwframe_ctx_init(p) };
if ret < 0 {
// SAFETY: p is valid but init failed; clean up.
@@ -211,6 +223,9 @@ pub unsafe fn import_dma_buf_to_vaapi(
}
let mut dst = ff::frame::Video::empty();
// SAFETY: frames_ctx is guaranteed by this unsafe function's contract to be a
// valid initialized VAAPI frames context; we set format/hw_frames_ctx on a
// freshly allocated dst frame.
unsafe {
let dp = dst.as_mut_ptr();
(*dp).format = ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
@@ -219,6 +234,8 @@ pub unsafe fn import_dma_buf_to_vaapi(
bail!("av_buffer_ref(frames_ctx) returned null");
}
}
// SAFETY: src and dst are initialized AVFrames; dst has a valid hw_frames_ctx
// ref and av_hwframe_map fills dst from src.
let ret = unsafe {
ffi::av_hwframe_map(
dst.as_mut_ptr(),
@@ -244,6 +261,8 @@ unsafe extern "C" fn cleanup_drm_descriptor(_opaque: *mut c_void, data: *mut u8)
fn av_err_to_string(err: i32) -> String {
let mut buf = vec![0u8; 128];
// SAFETY: buf points to 128 writable bytes and lives for the duration of
// av_strerror.
unsafe {
ffi::av_strerror(err, buf.as_mut_ptr() as *mut i8, buf.len());
}
@@ -308,6 +327,8 @@ impl EncState {
)?;
let mut sink_ctx = video_filter.get("out").unwrap();
// SAFETY: sink_ctx is a live buffersink; the returned hw_frames_ctx is
// borrowed, so av_buffer_ref creates an owned reference.
let sink_hw_frames = unsafe {
let raw = ffi::av_buffersink_get_hw_frames_ctx(sink_ctx.as_mut_ptr());
if raw.is_null() {
@@ -320,6 +341,8 @@ impl EncState {
hw_ref
};
// SAFETY: sink_hw_frames is an owned AVBufferRef to an AVHWFramesContext
// returned by the validated filter graph.
unsafe {
let fc = (*sink_hw_frames).data as *mut ffi::AVHWFramesContext;
let actual_w = (*fc).width as u32;
@@ -523,6 +546,8 @@ impl EncState {
match filter_sink.frame(&mut filtered) {
Ok(()) => {
let start_ts = self.starting_timestamp.unwrap_or(0);
// SAFETY: filtered is a valid VAAPI frame drained from the
// filter graph; enc_video is an opened encoder.
let ret = unsafe {
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr())
};
@@ -569,9 +594,14 @@ impl EncState {
// Rescale timestamps from encoder time_base to stream time_base
let enc_tb = self.enc_video.time_base();
// SAFETY: octx was created with stream 0 during muxer setup; streams
// is non-null and stream 0 remains owned by the format context.
let stream_tb = unsafe {
let streams = (*self.octx.as_ptr()).streams;
let st = *streams.add(0);
let fmt = *self.octx.as_ptr();
if fmt.nb_streams == 0 || fmt.streams.is_null() {
bail!("no streams in output context");
}
let st = *fmt.streams.add(0);
ff::Rational::from((*st).time_base)
};
pkt.rescale_ts(enc_tb, stream_tb);
@@ -763,6 +793,8 @@ impl SwEncState {
}
}
// SAFETY: Sending a null frame flushes the opened software encoder;
// no frame data is dereferenced. enc_video is exclusively borrowed via &mut self.
unsafe {
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
if ret < 0 && ret != ffi::AVERROR_EOF {
@@ -791,6 +823,8 @@ impl SwEncState {
return Ok(());
}
}
// SAFETY: av_frame_alloc returns a newly allocated AVFrame or null,
// which is checked below.
let mut sw_nv12 = unsafe { ffi::av_frame_alloc() };
if sw_nv12.is_null() {
bail!("av_frame_alloc failed for NV12 transfer frame");
@@ -863,9 +897,14 @@ impl SwEncState {
match self.output {
Some(FrameOutput::Muxer(ref mut octx)) => {
let enc_tb = self.enc_video.time_base();
// SAFETY: muxer output was created with stream 0 during setup;
// streams is non-null and stream 0 remains owned by the format context.
let stream_tb = unsafe {
let streams = (*octx.as_ptr()).streams;
let st = *streams.add(0);
let fmt = *octx.as_ptr();
if fmt.nb_streams == 0 || fmt.streams.is_null() {
bail!("no streams in output context");
}
let st = *fmt.streams.add(0);
ff::Rational::from((*st).time_base)
};
pkt.rescale_ts(enc_tb, stream_tb);
@@ -883,6 +922,9 @@ impl SwEncState {
self.frames_written = true;
}
Some(FrameOutput::Channel(ref tx)) => {
// SAFETY: pkt is a valid AVPacket just filled by
// avcodec_receive_packet; this copies fields for
// read-only inspection before pkt is dropped.
let raw = unsafe { *pkt.as_mut_ptr() };
if raw.size > 0 && !raw.data.is_null() {
// SAFETY: `pkt` is a valid AVPacket just filled by a successful
@@ -994,6 +1036,8 @@ fn build_swenc_filter_graph(
);
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
// SAFETY: av_buffersrc_parameters_alloc returns newly allocated parameters
// or null, which is checked below.
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
if par.is_null() {
bail!("av_buffersrc_parameters_alloc returned null");
@@ -1233,6 +1277,8 @@ fn create_software_h264_encoder(
enc.set_max_b_frames(0);
if codec_name == "libx264" {
// SAFETY: priv_data belongs to the unopened encoder context; each
// CString lives for the duration of its av_opt_set call.
unsafe {
let key = CString::new("preset").unwrap();
let val = CString::new("ultrafast").unwrap();
@@ -1345,6 +1391,8 @@ fn build_filter_graph(
Transform::Normal => unreachable!(),
};
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
// SAFETY: trans_ctx is a live transpose_vaapi filter context;
// scale_vaapi/transpose_vaapi keep a ref-counted device context.
unsafe {
(*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
}