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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user