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
+18 -201
View File
@@ -14,213 +14,30 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! 引用计数句柄脚手架
//! 句柄机制的历史遗留:`RefBox<T>` 容器
//!
//! 对应 C 侧布局(`include/plugin/instance.h`,与 oak 全项目约定一致):
//! 单库化(M14 R5)后 crate 内部不再传 CHandle——原 CHandle 装拆
//! `make_owned`/`make_borrowed`/`get`)、panic 兜底(`guard`/
//! `guard_handle`/`guard_void`)与身份注册表(`Registry`)均已删除:
//! 前者只被测试使用,后者在 src 无读取方(param 桥实际走
//! [`crate::suites::param`] 的 props 地址映射与 [`crate::node`] 的
//! 身份注册表)。
//!
//! ```c
//! typedef struct OakPluginInstance {
//! void *ctx;
//! void (*addref)(void *ctx);
//! void (*release)(void *ctx);
//! uint32_t abi_version;
//! } OakPluginInstance;
//! ```
//!
//! 句柄按值传;`ctx` 指向本 crate 堆上的 [`RefBox<T>`]。`addref`/
//! `release` 函数指针永远指向本 crate 的代码(所有权不出 DLL)。
//! 仅 [`RefBox`] 保留:它是 [`crate::host::Host::create_instance`] 的
//! 返回值容器(`Arc<RefBox<Instance>>`),该类型被 oakengine
//! test_support 显式标注消费,是本 crate 在 facade 边界的公开类型。
use std::any::Any;
use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::sync::atomic::AtomicU32;
use crate::error::OAKPLUGIN_E_FAILED;
/// 当前 ABI 版本,写进每个句柄的 `abi_version` 字段。
pub const OAKPLUGIN_ABI_VERSION: u32 = 1;
/// 句柄背后的堆盒子。`owns == false` 的盒子(借用包装)在计数归零时
/// 只释放盒子本身,不销毁内含对象。
/// 边界盒:`Arc<RefBox<Instance>>` 的承载类型。
///
/// 原为 C 句柄(`{ctx, addref, release, abi_version}`)背后的堆盒子,
/// addref/release thunk 经 `refs` 计数;装拆删除后 `refs` 不再被读写,
/// 仅以固定值 1 构造("单个拥有者"语义),生命周期完全由外层
/// `Arc` 管理。
pub struct RefBox<T: ?Sized> {
/// 引用计数(原子;release 可在任意线程发生)。
/// 引用计数(句柄时代遗留,现无 thunk 读写)。
pub refs: AtomicU32,
/// 内含对象。
pub value: T,
}
/// C 句柄的 Rust 镜像。`#[repr(C)]`,与 C 头文件布局一致。
///
/// 生命周期:`*_init`/`*_create` 返回计数 1 的拥有型句柄;
/// 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 handle scaffolding stays source-compatible.
/// `Send + Sync` come from the shared type.
pub use oakcore_rs::handle::CHandle;
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
/// 的寿命,不延长被借用对象。
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
unsafe {
let rb = ctx as *const RefBox<T>;
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
(*rb).refs.fetch_add(1, Ordering::Relaxed);
}
}
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
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:归零这一侧要能看见最后一次引用前的全部写(含对象
// 析构所需的内部状态)。
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);
}
}
}
/// 为 `T` 制作拥有型句柄(计数 1)。分配失败返回空句柄并销毁对象。
///
/// 注:Rust 默认分配失败(OOM)直接 abort,不会走到"返回空句柄"
/// 路径;此处语义保留给未来接入自定义分配器的场景。
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
let rb = Box::into_raw(Box::new(RefBox {
refs: AtomicU32::new(1),
value,
}));
CHandle {
ctx: rb as *mut std::ffi::c_void,
addref: Some(refbox_addref::<T>),
release: Some(refbox_release_owned::<T>),
abi_version: OAKPLUGIN_ABI_VERSION,
}
}
/// 为已有对象制作借用句柄(计数归零只释放盒子)。`ptr` 必须在本
/// 句柄被释放前保持有效。
///
/// 语义:按位拷贝("借用拷贝",如纹理句柄的快照);被借用对象
/// 的析构完全由调用方负责,盒子从不碰它。拷贝即快照——借出后
/// 修改 `*ptr` 不会反映到句柄内。
///
/// # Safety
/// 调用方保证 `ptr` 的生命周期覆盖所有派生句柄,且其值在借用期内
/// 不被 move/析构。
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: OAKPLUGIN_ABI_VERSION,
}
}
/// 取回盒子内对象的不可变引用;空句柄返回 `None`。
///
/// # Safety
/// 调用方必须保证 `T` 与创建句柄时的类型一致。
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
if h.is_null() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// FFI 兜底:捕获 panic,把 `Result<i32>` 映射为对外错误码
/// [`crate::error`])。所有返回 i32 的导出函数必须经它。
///
/// panic 路径返回 `OAKPLUGIN_E_FAILED`panic 详情暂不落日志
/// message 桥接入后补 TODO)。
pub fn guard<F>(f: F) -> i32
where
F: FnOnce() -> crate::error::Result<()>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKPLUGIN_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKPLUGIN_E_FAILED,
}
}
/// 指针/句柄返回值版本的 [`guard`]panic 或 Err 时返回空句柄
/// (指针类返回 NULL)。
pub fn guard_handle<F>(f: F) -> CHandle
where
F: FnOnce() -> crate::error::Result<CHandle>,
{
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
Ok(Err(_)) | Err(_) => CHandle::null(),
}
}
/// 无返回值版本:panic 被吞(日志回调待 message 出口接入后补)。
pub fn guard_void<F>(f: F)
where
F: FnOnce(),
{
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// 句柄身份注册表:`usize` 身份 ↔ 弱引用。供 param↔node 等需要
/// "按身份找回对象"的桥使用(替代 M9 C++ 版的
/// `oaknode_node_identity()` 注册表)。
pub struct Registry<T: Any + Send> {
map: Mutex<HashMap<usize, Weak<RefBox<T>>>>,
}
impl<T: Any + Send> Registry<T> {
/// 空注册表。
pub fn new() -> Self {
Self {
map: Mutex::new(HashMap::new()),
}
}
/// 登记对象,返回其身份(地址语义,进程内唯一)。
pub fn register(&self, obj: &Arc<RefBox<T>>) -> usize {
// Arc 分配地址即身份:同一 RefBox 恒稳定,进程内唯一。
let id = Arc::as_ptr(obj) as *const () as usize;
lock(&self.map).insert(id, Arc::downgrade(obj));
id
}
/// 按身份取对象;对象已销毁或身份未知返回 `None`。
pub fn lookup(&self, id: usize) -> Option<Arc<RefBox<T>>> {
lock(&self.map).get(&id).and_then(|w| w.upgrade())
}
/// 摘除身份(对象销毁路径调用)。未知身份是 no-op。
pub fn unregister(&self, id: usize) {
lock(&self.map).remove(&id);
}
}
/// 取锁。毒锁(本 crate 代码持锁时 panic)时接管内部状态继续——
/// 一次 panic 不级联成后续所有 FFI 调用失败。
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
+2 -11
View File
@@ -47,7 +47,7 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use crate::descriptor::EffectDescriptor;
use crate::handle::{RefBox, Registry};
use crate::handle::RefBox;
use crate::instance::Instance;
use crate::property::{PropertySet, Value};
use crate::suites::status;
@@ -790,14 +790,6 @@ pub struct Host {
pub(crate) instances: Mutex<Vec<std::sync::Weak<RefBox<Instance>>>>,
}
/// 实例身份注册表(param 桥按身份反查;见 [`crate::handle::Registry`])。
static INSTANCE_REGISTRY: OnceLock<Registry<Instance>> = OnceLock::new();
/// 实例身份注册表入口。
pub(crate) fn instance_registry() -> &'static Registry<Instance> {
INSTANCE_REGISTRY.get_or_init(Registry::new)
}
impl Host {
/// 进程单例。首次调用构建宿主属性集(能力宣告在此写入)。
pub fn global() -> &'static Host {
@@ -919,7 +911,7 @@ impl Host {
)));
}
// 登记实例表 + param→instance 回写表。
// 登记活跃实例表(泄漏断言用)+ param→instance 回写表。
self.instances
.lock()
.unwrap_or_else(|e| e.into_inner())
@@ -929,7 +921,6 @@ impl Host {
let p_addr = &p.props as *const PropertySet as usize;
crate::suites::param::register_param_owner(p_addr, inst_props);
}
instance_registry().register(&arc);
Ok(arc)
}
+8 -8
View File
@@ -86,8 +86,9 @@ pub struct RenderScale {
pub y: f64,
}
/// 插件实例。`Arc<RefBox<Instance>>` 管理生命周期;身份注册见
/// [`crate::handle::Registry`]param 桥按身份反查)。
/// 插件实例。`Arc<RefBox<Instance>>` 管理生命周期`RefBox` 为 facade
/// 边界类型,见 [`crate::handle`]);param 桥按 props 地址映射反查
/// [`crate::suites::param`]),节点绑定经 [`crate::node`] 身份注册表。
///
/// `#[repr(C)]` + props 在偏移 0(句柄约定,见 [`crate::suites::tag`]
/// 实例期 effect/param-set handle 即 `&props`)。
@@ -123,8 +124,9 @@ pub struct Instance {
pub render_lock: std::sync::Mutex<()>,
}
/// 实例销毁路径:先通知 destroyInstance action,再摘除身份登记。
/// RefBox 归零时 Drop 触发——action 通知必须在对象析构前发出。)
/// 实例销毁路径:先通知 destroyInstance action,再摘除 param 回写登记。
/// `Arc<RefBox<Instance>>` 归零时 Drop 触发——action 通知必须在
/// 对象析构前发出。)
impl Drop for Instance {
fn drop(&mut self) {
if !self
@@ -134,8 +136,6 @@ impl Drop for Instance {
self.notify_destroy();
}
crate::suites::param::unregister_params_of(&self.props as *const _ as usize);
crate::host::instance_registry()
.unregister(&self.props as *const crate::property::PropertySet as usize);
}
}
@@ -954,8 +954,8 @@ impl Instance {
Ok(())
}
/// 销毁(destroyInstance action)。析构由 RefBox 驱动;
/// 此处只做 action 通知,幂等([`Instance::drop`] 的
/// 销毁(destroyInstance action)。析构由 `Arc<RefBox<Instance>>`
/// 归零驱动;此处只做 action 通知,幂等([`Instance::drop`] 的
/// `destroyed` 门保证只发一次)。
pub(crate) fn notify_destroy(&self) {
use crate::host::ACTION_DESTROY_INSTANCE;