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
+26 -80
View File
@@ -14,18 +14,25 @@
// 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 oakplugin crate
//! (`src/plugin/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).
//! Refcounted-handle scaffolding for the oakengine facade boundary.
//!
//! Single-lib unification made module-to-module calls plain Rust; the
//! facade (oakengine) is the only remaining consumer of `CHandle`s in
//! this crate — it boxes oaknode domain objects (`Project`,
//! `NodeRef`) and small ABI payloads behind [`CHandle`]s so the frozen
//! C API keeps working unchanged, and oakstorage reuses the same boxes
//! for the write-through session. This module is that surface:
//! [`make_owned`]/[`make_owned_with`] create the boxes, [`get`] borrows
//! their payloads, [`RefBox`] is the box layout.
//!
//! The crate's own object references never travel through handles, and
//! the panic-catching `guard*` wrappers from the old FFI era were
//! removed together with the crate's C exports (oakengine has its own
//! guard layer).
use std::any::Any;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
use crate::error::OAKNODE_E_FAILED;
/// ABI version stamped into every handle.
pub const OAKNODE_ABI_VERSION: u32 = 1;
@@ -40,45 +47,34 @@ pub struct RefBox<T: ?Sized> {
/// 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.
/// here so the facade's handle scaffolding stays source-compatible.
pub use oakcore_rs::handle::CHandle;
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
/// 的寿命,不延长被借用对象。
/// addref implementation: atomic +1. Shared by owned and facade boxes —
/// a borrowed copy only extends the box's lifetime, never the borrowed
/// object's.
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *const RefBox<T>;
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
// Caller guarantees the handle is alive (ctx non-null and not
// released) for the duration of the call.
(*rb).refs.fetch_add(1, Ordering::Relaxed);
}
}
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
/// release implementation (owned): atomic -1, frees the box and destroys
/// the boxed value at zero.
unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
// AcqRel:归零这一侧要能看见最后一次引用前的全部写(含对象
// 析构所需的内部状态)。
// AcqRel: the zeroing side must observe every write from the last
// reference (including state the destructor needs).
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(rb));
}
}
}
/// release 的实现(借用型,[`make_borrowed`] 的产物):归零时只回收
/// 盒子内存,把内含对象原样忘掉——其所有权仍在借用方手里。
unsafe extern "C" fn refbox_release_borrowed<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *mut RefBox<T>;
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
// 部分 move:把 value 移出临时 Box,Box 析构只释放分配;
// value 用 forget 放弃析构(double-free 防线)。
std::mem::forget((Box::from_raw(rb)).value);
}
}
}
/// Owned handle with count 1; empty on allocation failure.
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
@@ -94,7 +90,7 @@ pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
}
/// Owned handle with count 1 and a caller-provided release routine
/// (used by the ffi layer's alive-counted node/project boxes, where the
/// (used by the facade for the alive-counted project boxes, where the
/// release must also update the debug counter).
pub fn make_owned_with<T: Any + Send>(
value: T,
@@ -112,32 +108,6 @@ pub fn make_owned_with<T: Any + Send>(
}
}
/// Borrowed handle for an object owned elsewhere (release frees only
/// the box).
///
/// Semantics: bitwise copy ("borrowed copy"); the borrowed object's
/// destructor is entirely the caller's responsibility — the box never
/// touches it.
///
/// # Safety
/// Caller guarantees `ptr` outlives every derived handle, and that its
/// value is not moved or destroyed for the borrow's lifetime.
pub unsafe fn make_borrowed<T: Any + Send>(ptr: *mut T) -> CHandle {
if ptr.is_null() {
return CHandle::null();
}
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value: unsafe { std::ptr::read(ptr) },
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_borrowed::<T>),
abi_version: OAKNODE_ABI_VERSION,
}
}
/// Typed view into a handle; `None` for empty handles.
///
/// # Safety
@@ -148,27 +118,3 @@ pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// Panic-catching FFI wrapper for i32-returning exports.
///
/// Panics map to [`OAKNODE_E_FAILED`].
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKNODE_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKNODE_E_FAILED,
}
}
/// Panic-catching FFI wrapper for handle-returning exports.
pub fn guard_handle<F: FnOnce() -> crate::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));
}
+9 -4
View File
@@ -20,11 +20,16 @@
//! (`include/node/*.h`). See README.md for the architectural mapping
//! (inheritance → arena + trait objects, etc.).
//!
//! ## FFI discipline
//! ## Handle discipline
//!
//! Identical to the oakplugin crate: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
//! state behind `Mutex`.
//! Post-single-lib, this crate has no C exports: module-to-module calls
//! are plain Rust types, and object references never travel through
//! handles. The remaining `CHandle`s are (a) the facade-facing handle
//! scaffolding in [`handle`] (oakengine/oakstorage box `Project` /
//! `NodeRef` behind it) and (b) opaque cross-module payloads the crate
//! cannot name as Rust types — oakrender textures/frames/caches and
//! oaktimeline markers/work areas, whose owning crates depend on
//! oaknode. Shared state lives behind `Mutex`.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
+6
View File
@@ -426,6 +426,12 @@ impl NodeCore {
/// The node's oakrender caches (frame/thumbnail/audio/waveform),
/// owned handles released with the node.
///
/// Cross-module payloads: the cache objects live behind opaque
/// oakrender handles created lazily by the facade (oakengine reads
/// `caches.video` directly through the C ABI), and oakrender depends on
/// oaknode, so no Rust type is nameable here — the handle is the
/// boundary representation.
#[derive(Clone)]
pub struct NodeCaches {
/// Video frame hash cache.
@@ -90,7 +90,9 @@ impl GeneratorWithMerge {
/// The Rust model has no shader-job payload (see
/// [`crate::nodes::mathbase`]): the merged case pushes a null
/// texture handle marking a renderer-deferred `"mrg"` shader job,
/// and the un-merged case pushes `job` itself.
/// and the un-merged case pushes `job` itself. `job` is an opaque
/// oakrender texture handle (cross-module payload; null in the
/// deferred-job model) — see [`crate::value::NodeValue::Texture`].
pub fn push_mergable_job(
inputs: &crate::value::NodeValueRow,
job: crate::handle::CHandle,
+4 -2
View File
@@ -35,9 +35,11 @@ pub const TRACK_INPUT_FORMAT: &str = "track_in_%1";
pub struct SequenceBehavior {
/// Track list node ids (video then audio, C++ order).
pub track_lists: Vec<NodeId>,
/// Timeline markers handle (oaktimeline, owned).
/// Timeline markers handle (oaktimeline, owned; created lazily by
/// the facade through the C ABI).
pub markers: crate::handle::CHandle,
/// Work area handle (oaktimeline, owned).
/// Work area handle (oaktimeline, owned; created lazily by the
/// facade through the C ABI).
pub workarea: crate::handle::CHandle,
/// Length cache (C++ last_length_).
pub last_length: oakcore_rs::Rational,
+3 -1
View File
@@ -22,7 +22,9 @@
//! value and releases on drop, which keeps the ownership chain inside
//! the refcount discipline instead of the C++ shared_ptr-in-Variant
//! model (the one documented exception of the C++ tree; it does not
//! exist here).
//! exist here). Textures specifically are oakrender objects, and
//! oakrender depends on oaknode, so the payload must stay an opaque
//! [`crate::handle::CHandle`] at this boundary.
use std::ffi::c_int;
+8 -37
View File
@@ -21,7 +21,7 @@
use oakcore_rs::{Rational, TimeRange};
use oaknode::error::{Error, OAKNODE_E_FAILED, OAKNODE_E_INVALID};
use oaknode::error::{Error, OAKNODE_E_INVALID};
use oaknode::handle::{self, CHandle, RefBox};
use oaknode::id::NodeId;
use oaknode::input::{flags, Input, ValueHint};
@@ -851,51 +851,22 @@ fn ops_category_and_copy_inputs() {
assert!(ops::copy_inputs(&mut g, src, NodeId::INVALID, false).is_err());
}
/// handle.rs: null/is_null/guards + refcount discipline.
/// handle.rs: null/is_null + owned-box refcount discipline (the
/// facade-facing surface; the guard* wrappers and make_borrowed were
/// removed with the crate's C exports).
#[test]
fn handle_helpers_and_guards() {
use oaknode::error::OAKNODE_OK;
fn handle_boxing_discipline() {
let null = CHandle::null();
assert!(null.is_null());
assert!(unsafe { handle::get::<u32>(&null) }.is_none());
// guard: Ok -> OK; Err -> mapped code; panic -> E_FAILED.
assert_eq!(handle::guard(|| Ok(())), OAKNODE_OK);
assert_eq!(handle::guard(|| Err(Error::Invalid)), OAKNODE_E_INVALID);
assert_eq!(
handle::guard(|| -> Result<(), Error> { panic!("boom") }),
OAKNODE_E_FAILED
);
// guard_handle: Ok -> handle; Err/panic -> empty.
let h = handle::guard_handle(|| Ok(handle::make_owned(5u32)));
assert!(!h.ctx.is_null());
assert!(
handle::guard_handle(|| -> Result<CHandle, Error> { Err(Error::NotFound) })
.ctx
.is_null()
);
assert!(
handle::guard_handle(|| -> Result<CHandle, Error> { panic!("x") })
.ctx
.is_null()
);
// guard_void swallows panics.
handle::guard_void(|| panic!("swallowed"));
// make_owned / make_owned_with / make_borrowed round-trip.
// make_owned / make_owned_with round-trip.
let owned = handle::make_owned(7u32);
let rb = owned.ctx as *const RefBox<u32>;
unsafe {
assert_eq!((*rb).refs.load(std::sync::atomic::Ordering::Relaxed), 1);
}
let value = 9u32;
let borrowed = unsafe { handle::make_borrowed(&value as *const u32 as *mut u32) };
assert!(!borrowed.ctx.is_null());
assert_eq!(unsafe { handle::get::<u32>(&borrowed) }, Some(&9u32));
assert!(unsafe { handle::make_borrowed::<u32>(std::ptr::null_mut()) }.is_null());
assert_eq!(unsafe { handle::get::<u32>(&owned) }, Some(&7u32));
// make_owned_with uses a custom release.
unsafe extern "C" fn custom_release(ctx: *mut std::ffi::c_void) {
@@ -913,7 +884,7 @@ fn handle_helpers_and_guards() {
);
// Release everything (single release each).
for h in [owned, borrowed, custom] {
for h in [owned, custom] {
unsafe { (h.release.unwrap())(h.ctx) };
}
}