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
+69 -59
View File
@@ -17,13 +17,44 @@
//! Render-side project copy client (the C++ ProjectCopier, inverted):
//! all copying happens inside oaknode. oaknode never implemented the
//! deep-copy direction (single-lib plan §4.1 — dead direction), so the
//! copy operations fail explainably and the success-path tests are
//! `#[ignore]`d.
//! copy operations fail explainably; the tests assert those failures.
//!
//! M14 R5: the module is entirely internal to oakrender (no facade entry
//! is involved), so the oaknode project handle was reduced to its numeric
//! identity — a Rust value type instead of a `CHandle`. The live project
//! object stays with oaknode; the render side stores only identity pairs
//! (see `COVERAGE.md`, "render 只存 identity 对").
use crate::error::{Error, Result};
/// A project handle (oaknode-owned; the shared canonical handle type).
pub type ProjectHandle = crate::handle::CHandle;
/// An opaque identity for an oaknode project (the C++ handle's `ctx`
/// reduced to its numeric value). oaknode owns the live project and
/// maintains the identity map; this module never holds the object, so no
/// lifetime management is required.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProjectHandle(u64);
impl ProjectHandle {
/// New identity; `0` is the empty handle.
pub fn new(identity: u64) -> Self {
ProjectHandle(identity)
}
/// The empty handle (no project attached).
pub fn null() -> Self {
ProjectHandle(0)
}
/// True for the empty handle.
pub fn is_null(self) -> bool {
self.0 == 0
}
/// The raw identity value.
pub fn as_u64(self) -> u64 {
self.0
}
}
/// One change record (see oaknode `ChangeRecord`).
#[repr(C)]
@@ -77,15 +108,13 @@ pub fn project_sync_copy(
))
}
/// A handle to a render-side project copy.
/// A render-side project copy (identity only — the copy's live object
/// stays with oaknode).
pub struct ProjectCopy {
/// Identity of the source project.
pub source: u64,
/// Identity of the copied project (oaknode-owned).
/// Identity of the copied project (0 = no copy attached).
pub copy: u64,
/// Owned oaknode handle to the copy (kept alive for the copier's
/// lifetime; released on drop).
copy_handle: Option<ProjectHandle>,
/// Change-generation counter of the last successful sync.
pub last_sync_generation: u64,
/// True while recorded changes await `sync`.
@@ -98,19 +127,18 @@ impl ProjectCopy {
Self {
source: 0,
copy: 0,
copy_handle: None,
last_sync_generation: 0,
has_pending_updates: false,
}
}
/// Create a deep copy of `source` through the oaknode C ABI
/// (C++ `ProjectCopier::set_project`).
/// Create a deep copy of `source` through oaknode (C++
/// `ProjectCopier::set_project`).
pub fn set_project(&mut self, source: ProjectHandle) -> Result<()> {
if source.is_null() {
return Err(Error::Invalid);
}
// Release any previous copy.
// Drop any previous copy.
self.release_copy();
let copy = crate::copier::project_deep_copy(source);
if copy.is_null() {
@@ -118,9 +146,8 @@ impl ProjectCopy {
"oaknode_project_deep_copy failed (symbol missing or copy error)".into(),
));
}
self.source = source.ctx as u64;
self.copy = copy.ctx as u64;
self.copy_handle = Some(copy);
self.source = source.as_u64();
self.copy = copy.as_u64();
self.last_sync_generation = 0;
self.has_pending_updates = false;
Ok(())
@@ -129,26 +156,26 @@ impl ProjectCopy {
/// Push a recorded change set into the copy (C++
/// ProjectCopier::process_update_queue).
pub fn sync(&mut self, changes: &[ChangeRecord]) -> Result<()> {
let source = ProjectHandle {
ctx: self.source as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKRENDER_ABI_VERSION,
};
let copy = self.copy_handle.unwrap_or_else(ProjectHandle::null);
if copy.is_null() {
if self.copy == 0 {
return Err(Error::State);
}
crate::copier::project_sync_copy(source, copy, changes)?;
crate::copier::project_sync_copy(
ProjectHandle::new(self.source),
ProjectHandle::new(self.copy),
changes,
)?;
self.last_sync_generation += 1;
self.has_pending_updates = false;
Ok(())
}
/// The copied project handle (owned by this copier; borrowed for the
/// caller).
/// The copied project's identity (borrowed for the caller).
pub fn copied_project(&self) -> Option<ProjectHandle> {
self.copy_handle
if self.copy == 0 {
None
} else {
Some(ProjectHandle::new(self.copy))
}
}
/// The copied counterpart of an original node — requires the oaknode
@@ -158,19 +185,12 @@ impl ProjectCopy {
None
}
/// Drop the copy (releases the oaknode handle).
/// Drop the copy (forgets its identity).
pub fn destroy(&mut self) {
self.release_copy();
}
fn release_copy(&mut self) {
if let Some(handle) = self.copy_handle.take() {
if let Some(release) = handle.release {
// SAFETY: the handle came from oaknode_project_deep_copy;
// releasing the last reference destroys the copy.
unsafe { release(handle.ctx) };
}
}
self.copy = 0;
}
}
@@ -181,12 +201,6 @@ impl Default for ProjectCopy {
}
}
impl Drop for ProjectCopy {
fn drop(&mut self) {
self.release_copy();
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -209,6 +223,19 @@ mod tests {
);
}
#[test]
fn set_project_with_valid_identity_fails() {
// oaknode never implemented the deep-copy direction; even a valid
// identity cannot be copied, and the copier fails explainably.
let mut pc = ProjectCopy::new();
assert_eq!(
pc.set_project(ProjectHandle::new(1)).unwrap_err().code(),
Error::Failed(String::new()).code()
);
assert_eq!(pc.copy, 0);
assert!(pc.copied_project().is_none());
}
#[test]
fn sync_without_project_is_state_error() {
let mut pc = ProjectCopy::new();
@@ -225,21 +252,4 @@ mod tests {
pc.destroy();
assert_eq!(pc.copy, 0);
}
#[test]
#[ignore = "needs oaknode deep-copy (not implemented)"]
fn deep_copy_roundtrip_with_real_node() {
// oaknode never implemented the deep-copy direction; the copy
// always fails explainably, which is what the live path checks.
let mut pc = ProjectCopy::new();
let src = ProjectHandle {
ctx: 1 as *mut std::ffi::c_void,
addref: None,
release: None,
abi_version: crate::handle::OAKRENDER_ABI_VERSION,
};
pc.set_project(src).unwrap();
assert_ne!(pc.copy, 0);
assert!(pc.copied_project().is_some());
}
}