refactor: purge CHandle from module internals (M14 R5)

Module-internal object references are Rust types now (values, Arc,
Mutex); CHandle remains only at the oakengine C-ABI boundary:

- oakundo: the global stack holds UndoStack/UndoCommand values
  directly (stack token is the static's address)
- oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands
  share the same allocation through Arc clones (readers in oakengine
  stubs and the app's graphops updated to lock)
- oaktask/oakstorage: sessions, write-through bindings and the
  database backend pass ProjectArc; the Session drops its manual
  release bookkeeping; nodeutil keeps the CHandle<->Arc boundary
  conversion (release_project restored for the app)
- oakcodec: handle.rs deleted outright (no facade entry needed it);
  texture/block placeholders are unit structs
- oakrender: copier's project handle is an identity u64; alive-count
  machinery removed; handle.rs is make_owned/get/get_mut only
- oakplugin: the instance registry is gone (its unregister key never
  matched, leaking weak entries); handle.rs is the RefBox boundary type
- oaknode/oakcommon: only dead guard/borrow helpers removed; external
  payload handles (texture/processor) documented as the boundary

Flake hunts landed along the way: the audio recording test serializes
on the shared manager lock with a normalized state; the autocacher
cancel test uses a slow producer so cancellation is deterministic.
This commit is contained in:
2026-08-17 16:40:15 +08:00
parent ede03d0bfe
commit b36cbd6b6f
53 changed files with 1260 additions and 2369 deletions
+16 -12
View File
@@ -32,15 +32,20 @@ use oakcore_rs::{Rational, TimeRange};
use crate::footagedescription::FootageDescription;
use crate::frame::Frame;
/// `OakRenderTexture` — refcounted GPU texture handle (an oakrender type,
/// opaque to oakcodec). The codec crate cannot depend on oakrender (the
/// dependency cycle), so it only ever produces an empty handle — the
/// shared [`crate::handle::CHandle`] carries that value unchanged.
pub type OakRenderTexture = crate::handle::CHandle;
/// `OakRenderTexture` — GPU texture token (an oakrender type, opaque to
/// oakcodec). The codec crate cannot depend on oakrender (dependency
/// cycle), so it never constructs a real texture: [`Decoder::retrieve_video`]
/// only reports whether the decode succeeded and returns this unit token
/// (the former empty `CHandle`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OakRenderTexture;
/// `OakNodeBlock` — opaque node-block handle owned elsewhere; the codec
/// only stores and forwards it (borrowed, never dereferenced).
pub type OakNodeBlock = crate::handle::CHandle;
/// `OakNodeBlock` — opaque timeline-block token owned elsewhere; the codec
/// only stores and forwards it (borrowed, never dereferenced). No module
/// ever constructs one (the former `CHandle` was only ever `None`), so the
/// type is an empty marker kept for API compatibility.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OakNodeBlock;
/// `oakcodec_video_stream_info` — POD probe output describing one video
/// stream; see `include/codec/decoder.h`.
@@ -162,8 +167,8 @@ pub enum RetrieveState {
/// `Decoder::CodecStream` — identifies one (filename, stream) pair plus an
/// optional associated timeline block.
///
/// The block is an opaque `OakNodeBlock` handle that codec only stores and
/// compares, never dereferences or retains (borrowed pointer).
/// The block is an opaque [`OakNodeBlock`] token that codec only stores and
/// compares, never dereferences or retains (borrowed).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodecStream {
filename: String,
@@ -231,8 +236,7 @@ impl CodecStream {
///
/// Implementations are [`crate::ffmpeg::FFmpegDecoder`] and
/// [`crate::oiio::OIIODecoder`]. The trait surface mirrors the C++
/// abstract base; the refcounted handle that backs the public API wraps an
/// `Arc<dyn Decoder>`.
/// abstract base; the public API hands out `Arc<dyn Decoder>` values.
pub trait Decoder: Send + Sync {
/// Unique decoder id ("ffmpeg"/"oiio").
fn id(&self) -> String;
+4 -3
View File
@@ -29,7 +29,8 @@ use oakcore_rs::{PixelFormat, SampleFormat};
use crate::encodingparams::EncodingParams;
use crate::frame::Frame;
/// `olive::Encoder` — encoder trait. Backs the refcounted encoder handle.
/// `olive::Encoder` — encoder trait. Instances are handed out as
/// `Arc<dyn Encoder>`.
pub trait Encoder: Send + Sync {
/// Unique encoder id.
fn id(&self) -> String;
@@ -129,8 +130,8 @@ pub fn set_test_encoders(list: Vec<Arc<dyn Encoder>>) {
/// WebM, SRT → FFmpeg; OpenEXR, PNG, TIFF → OIIO; anything else → `None`).
/// A non-empty test-injected list (see [`set_test_encoders`]) wins over the
/// built-in mapping. The concrete implementations are dylib stubs whose
/// `open()` fails with a clear message, so an initialized encoder handle is
/// always constructible for a recognized format.
/// `open()` fails with a clear message, so an initialized encoder is always
/// constructible for a recognized format.
pub fn create_from_params(params: &EncodingParams) -> Option<Arc<dyn Encoder>> {
if let Some(store) = TEST_ENCODERS.get() {
let injected = store.lock().unwrap();
+2 -1
View File
@@ -32,7 +32,8 @@ pub const OAKCODEC_E_NOMEM: i32 = -50005;
/// The operation was cancelled.
pub const OAKCODEC_E_CANCELLED: i32 = -50006;
/// Current ABI version stamped into every oakcodec handle.
/// The frozen C-ABI `abi_version` tag (`include/codec/decoder.h`); kept for
/// parity with the handle structs, no longer stamped at runtime.
pub const OAKCODEC_ABI_VERSION: u32 = 1;
/// Crate-internal result type; the FFI layer maps it to the codes.
+3 -8
View File
@@ -38,8 +38,8 @@
//! path or loop mode).
//! * [`RetrieveVideoParams`] drops `renderer`, `divider` and
//! `maximum_format` (the Rust trait surface), so [`Decoder::retrieve_video`]
//! can only produce textures through an empty renderer handle and there
//! is no preview-divider scaling.
//! only reports decode success — it returns a unit [`OakRenderTexture`]
//! token, never a real texture — and there is no preview-divider scaling.
//! * Probing uses stream parameters (no second decode pass), so `is_still`
//! is always false and interlacing always progressive.
//! * Subtitle streams are counted but not added as subtitle entries.
@@ -325,12 +325,7 @@ impl Decoder for FFmpegDecoder {
return Err(fail("decoder is not open on a video stream"));
}
let _ = state.retrieve_frame(&p.time, p.time == crate::decoder::k_any_timecode(), None)?;
Ok(OakRenderTexture {
ctx: std::ptr::null_mut(),
addref: None,
release: None,
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
})
Ok(OakRenderTexture)
}
fn retrieve_audio(
-281
View File
@@ -1,281 +0,0 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakplugin
//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather
//! than shared — each module DLL must run its own addref/release code
//! (the function pointers in a handle always point into the DLL that
//! created the object).
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use crate::error::{self, OAKCODEC_E_FAILED};
/// Number of boxed handle objects currently alive (leak/debug checking).
///
/// Mirrors `oakcodec::g_alive_count` in `src/codec/c_api/frame.cpp`: every
/// `make_owned` box increments it and `box_release` decrements it when the
/// last reference drops. `oakcodec_debug_alive_count` reports it.
static ALIVE: AtomicI32 = AtomicI32::new(0);
/// ABI version stamped into every handle.
pub const OAKCODEC_ABI_VERSION: u32 = 1;
/// Heap box behind a handle's `ctx`.
pub struct RefBox<T: ?Sized> {
/// Atomic reference count.
pub refs: AtomicU32,
/// Boxed value.
pub value: T,
}
/// `#[repr(C)]` mirror of the public handle structs
/// (`{ctx, addref, release, abi_version}`).
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
/// source-compatible.
pub use oakcore_rs::handle::CHandle;
/// Increment the reference count of a boxed `RefBox<T>`.
///
/// # Safety
/// `ptr` must point to a live `RefBox<T>` previously created by this module.
unsafe extern "C" fn box_addref<T: Send + 'static>(ptr: *mut std::ffi::c_void) {
if ptr.is_null() {
return;
}
let boxed = unsafe { &*(ptr as *const RefBox<T>) };
boxed.refs.fetch_add(1, Ordering::SeqCst);
}
/// Decrement the reference count; destroys the box at zero.
///
/// # Safety
/// `ptr` must point to a live `RefBox<T>` previously created by this module.
unsafe extern "C" fn box_release<T: Send + 'static>(ptr: *mut std::ffi::c_void) {
if ptr.is_null() {
return;
}
let boxed = unsafe { &*(ptr as *const RefBox<T>) };
if boxed.refs.fetch_sub(1, Ordering::SeqCst) == 1 {
// The last reference: the box is destroyed and the alive count
// drops with it (mirrors `alive_dec` in c_api/frame.cpp).
ALIVE.fetch_sub(1, Ordering::SeqCst);
unsafe { drop(Box::from_raw(ptr as *mut RefBox<T>)) };
}
}
/// Owned handle with count 1; empty on allocation failure.
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
let boxed = Box::new(RefBox {
refs: AtomicU32::new(1),
value,
});
let ctx = Box::into_raw(boxed) as *mut std::ffi::c_void;
// Every boxed handle counts toward `oakcodec_debug_alive_count`
// (mirrors `alive_inc` in c_api/frame.cpp).
ALIVE.fetch_add(1, Ordering::SeqCst);
CHandle {
ctx,
addref: Some(box_addref::<T>),
release: Some(box_release::<T>),
abi_version: OAKCODEC_ABI_VERSION,
}
}
/// Borrowed handle for an object owned elsewhere.
///
/// Takes ownership of the boxed `T` already allocated at `ptr` (e.g. one
/// passed in from C++). The resulting handle's release drops that box.
///
/// # Safety
/// Caller guarantees `ptr` was allocated with `Box::new` and is not used
/// after this call.
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
// Move ownership into a RefBox so addref/release and get() behave
// uniformly with owned handles.
let value = unsafe { *Box::from_raw(ptr) };
make_owned(value)
}
/// Typed view into a handle; `None` for empty handles.
///
/// # Safety
/// `T` must be the boxed type.
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
if h.is_null() {
return None;
}
let boxed = unsafe { &*(h.ctx as *const RefBox<T>) };
Some(&boxed.value)
}
/// Panic-catching FFI wrapper for i32-returning exports.
pub fn guard<F: FnOnce() -> error::Result<()>>(f: F) -> i32 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => error::OAKCODEC_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKCODEC_E_FAILED,
}
}
/// Panic-catching FFI wrapper for handle-returning exports.
pub fn guard_handle<F: FnOnce() -> error::Result<CHandle>>(f: F) -> CHandle {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
Ok(Err(_)) | Err(_) => CHandle::null(),
}
}
/// Panic-catching FFI wrapper for void exports.
pub fn guard_void<F: FnOnce()>(f: F) {
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// Panic-catching FFI wrapper for exports that return a raw `i32` code
/// directly (neither `Result` nor a handle). On panic, `OAKCODEC_E_FAILED`.
pub fn guard_raw<F: FnOnce() -> i32>(f: F) -> i32 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(code) => code,
Err(_) => OAKCODEC_E_FAILED,
}
}
/// Panic-catching FFI wrapper for exports that return a raw `i64` directly
/// (e.g. `oakcodec_decoder_get_image_sequence_index`). On panic,
/// `OAKCODEC_E_FAILED`.
pub fn guard_i64<F: FnOnce() -> i64>(f: F) -> i64 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(v) => v,
Err(_) => OAKCODEC_E_FAILED as i64,
}
}
/// Number of live boxed handle objects (see [`ALIVE`]).
pub fn alive_count() -> i32 {
ALIVE.load(Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::OAKCODEC_E_INVALID;
#[test]
fn make_owned_lifecycle_tracks_alive_count() {
// The shared test lock serializes the crate's `alive_count`
// assertions against every other test that creates handles.
let _g = crate::lock_tests();
let before = alive_count();
let h = make_owned(42u32);
assert!(!h.is_null());
assert_eq!(alive_count(), before + 1);
// addref/release cycle keeps the box alive.
let addref = h.addref.unwrap();
let release = h.release.unwrap();
// SAFETY: `h.ctx` is a live RefBox<u32>.
unsafe { addref(h.ctx) };
// SAFETY: second reference released; box stays (refs 2 -> 1).
unsafe { release(h.ctx) };
assert_eq!(alive_count(), before + 1);
// Release the owned reference: box destroyed.
// SAFETY: last reference.
unsafe { release(h.ctx) };
assert_eq!(alive_count(), before);
}
#[test]
fn make_borrowed_null_is_null_handle() {
let h = unsafe { make_borrowed::<u32>(std::ptr::null_mut()) };
assert!(h.is_null());
}
#[test]
fn make_borrowed_takes_ownership() {
let _g = crate::lock_tests();
let before = alive_count();
let raw = Box::into_raw(Box::new(7u32));
let h = unsafe { make_borrowed(raw) };
assert!(!h.is_null());
assert_eq!(alive_count(), before + 1);
assert_eq!(unsafe { *get::<u32>(&h).unwrap() }, 7);
unsafe { h.release.unwrap()(h.ctx) };
assert_eq!(alive_count(), before);
}
#[test]
fn addref_on_null_ctx_is_noop() {
// A handle with function pointers but a null ctx: both thunks no-op.
let h = CHandle {
ctx: std::ptr::null_mut(),
addref: Some(box_addref::<u32>),
release: Some(box_release::<u32>),
abi_version: OAKCODEC_ABI_VERSION,
};
// SAFETY: ctx is null; the thunks guard on it.
unsafe { h.addref.unwrap()(h.ctx) };
// SAFETY: ctx is null; the thunks guard on it.
unsafe { h.release.unwrap()(h.ctx) };
}
#[test]
fn guard_maps_results_and_panics() {
assert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);
assert_eq!(
guard(|| Err(crate::error::Error::Invalid)),
OAKCODEC_E_INVALID
);
assert_eq!(guard(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
let ok = guard_handle(|| Ok(make_owned(1u32)));
assert!(!ok.is_null());
assert!(guard_handle(|| Err::<CHandle, _>(crate::error::Error::Invalid)).is_null());
assert!(guard_handle(|| panic!("boom")).is_null());
assert_eq!(guard_raw(|| 5), 5);
assert_eq!(
guard_raw(|| panic!("boom")),
crate::error::OAKCODEC_E_FAILED
);
assert_eq!(guard_i64(|| 5), 5);
assert_eq!(
guard_i64(|| panic!("boom")),
crate::error::OAKCODEC_E_FAILED as i64
);
let mut called = false;
guard_void(|| called = true);
assert!(called);
guard_void(|| panic!("boom"));
}
#[test]
fn null_handle_helpers() {
let h = CHandle::null();
assert!(h.is_null());
// The shared `null()` stamps no ABI version (single-lib
// unification; `make_owned` stamps the crate version).
assert_eq!(h.abi_version, 0);
}
}
+10 -9
View File
@@ -18,13 +18,15 @@
//!
//! Reimplements the C++ oakcodec module behind its frozen C ABI
//! (`include/codec/*.h`). See README.md for the architectural mapping
//! (inheritance → traits, shared_ptr → refcounted handles, etc.).
//! (inheritance → traits, shared_ptr → `Arc`, etc.).
//!
//! ## FFI discipline
//! ## Structure
//!
//! Identical to the oaknode/oakplugin crates: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
//! state behind `Mutex`.
//! Single-lib unification: the module crates are called directly from
//! other module crates and the oakengine facade (`crates/oakengine`), so
//! no C-ABI export layer remains here (the `include/codec/*.h` contracts
//! are served by the facade). Shared state lives behind `Mutex`, decoder
//! instances behind `Arc<dyn Decoder>`.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
@@ -44,7 +46,6 @@ pub mod ffmpeg;
pub mod footagedescription;
pub mod frame;
pub mod framemanager;
pub mod handle;
pub mod oiio;
pub mod oiioframebridge;
pub mod planarfiledevice;
@@ -57,9 +58,9 @@ pub mod timecodemetadata;
mod realmedia_tests;
/// Process-wide test lock: serializes every test that reads or mutates
/// crate-global state (the injected decoder registry, the handle alive
/// count). One lock for the whole crate — tests race only with each
/// other, never with production code.
/// crate-global state (the injected decoder/encoder registries). One lock
/// for the whole crate — tests race only with each other, never with
/// production code.
#[cfg(test)]
static TEST_LOCK: Mutex<()> = Mutex::new(());