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
+75 -58
View File
@@ -78,6 +78,7 @@ use oaknode::project::Project;
use crate::backend::{LoadResult, StorageBackend};
use crate::error::{Error, Result};
use crate::handle::CHandle;
use crate::nodeutil::ProjectArc;
use crate::uri::StorageUri;
/// Journal kind for redo commands (the D1 save path).
@@ -613,15 +614,9 @@ impl DatabaseBackend {
if file_uri.scheme != "file" {
return Err(Error::Invalid);
}
let handle = self.load_handle_by_uuid(uri, uuid)?;
let project = self.load_project_by_uuid(uri, uuid)?;
let backend = crate::backends::ove_xml::OveXmlBackend::new();
let result = backend.save(handle, file_uri, 0);
// The loaded handle is ours (refcount 1); release it regardless
// of the save outcome.
if let Some(release) = handle.release {
unsafe { release(handle.ctx) };
}
result
backend.save_project(&project, file_uri, 0)
}
/// Import a `.ove`/`.otio`/`.fcpxml` file as a new library row
@@ -634,6 +629,9 @@ impl DatabaseBackend {
return Err(Error::Invalid);
}
let backend = crate::registry::Registry::global().resolve(file_uri)?;
// The file backend's `load` returns its project as a handle (the
// facade-facing trait form); convert it to the boxed project the
// crate uses internally, and release the temporary handle.
let result = backend.load(file_uri)?;
let handle = result.project;
if handle.is_null() {
@@ -643,12 +641,15 @@ impl DatabaseBackend {
)));
}
let uuid = {
let arc = unsafe { crate::nodeutil::project_arc(&handle)? };
let fresh = new_uuid();
arc.lock().map_err(|_| Error::State)?.uuid = fresh.clone();
unsafe { crate::nodeutil::project_arc(&handle)? }
.lock()
.map_err(|_| Error::State)?
.uuid = fresh.clone();
fresh
};
let outcome = self.save(handle, uri, 0).map(|()| uuid.clone());
let arc = unsafe { crate::nodeutil::project_arc(&handle)? };
let outcome = self.save_project(&arc, uri, 0).map(|()| uuid.clone());
if let Some(release) = handle.release {
unsafe { release(handle.ctx) };
}
@@ -683,11 +684,14 @@ impl DatabaseBackend {
/// persistent undo history (plan §0): a snapshot at or before `seq`
/// is replayed forward with the journal rows up to `seq`. `seq` 0 is
/// the empty project. E_INVALID when `seq` is out of range.
///
/// Returns the project as an owned oaknode handle (the facade-facing
/// form — the engine's integration tests consume it this way).
pub fn load_at(&self, uri: &StorageUri, uuid: &str, seq: i64) -> Result<CHandle> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
let uuid = uuid.to_string();
self.run(key, move |conn| async move {
let project = self.run(key, move |conn| async move {
let model = project::Entity::find()
.filter(project::Column::Uuid.eq(&uuid))
.one(&conn)
@@ -698,30 +702,22 @@ impl DatabaseBackend {
return Err(Error::Invalid);
}
let xml = assemble_at(&conn, model.id, &model.uuid, seq).await?;
Ok(crate::nodeutil::make_project_owned(
crate::nodeutil::serializer_load(&xml)?,
))
})
crate::nodeutil::serializer_load(&xml)
})?;
Ok(crate::nodeutil::make_project_owned(project))
}
/// The manager stats of a library project (plan §4): the head state
/// is replayed and the stats derived from the node graph.
pub fn project_stats(&self, uri: &StorageUri, uuid: &str) -> Result<ProjectStats> {
let handle = self.load_handle_by_uuid(uri, uuid)?;
let stats = (|| -> Result<ProjectStats> {
let arc = unsafe { crate::nodeutil::project_arc(&handle)? };
let guard = arc.lock().map_err(|_| Error::State)?;
Ok(derive_stats(&guard))
})();
if let Some(release) = handle.release {
unsafe { release(handle.ctx) };
}
stats
let project = self.load_project_by_uuid(uri, uuid)?;
let guard = project.lock().map_err(|_| Error::State)?;
Ok(derive_stats(&guard))
}
/// Load the project payload of the library row `uuid` as an owned
/// handle (the head state).
fn load_handle_by_uuid(&self, uri: &StorageUri, uuid: &str) -> Result<CHandle> {
/// Load the project payload of the library row `uuid` as the boxed
/// project (the head state).
fn load_project_by_uuid(&self, uri: &StorageUri, uuid: &str) -> Result<ProjectArc> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
let uuid = uuid.to_string();
@@ -733,11 +729,50 @@ impl DatabaseBackend {
.map_err(db_err)?
.ok_or(Error::NotFound)?;
let xml = assemble_at(&conn, model.id, &model.uuid, model.command_seq).await?;
Ok(crate::nodeutil::make_project_owned(
crate::nodeutil::serializer_load(&xml)?,
))
crate::nodeutil::serializer_load(&xml)
})
}
/// Load the head state of the library's selected project (the
/// `?project=` uuid, or the most recently modified row) as the boxed
/// project. The Rust-typed inner load: the facade-facing trait `load`
/// wraps the result in an owned handle.
pub fn load_project(&self, uri: &StorageUri) -> Result<ProjectArc> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
let project = target.project().map(str::to_string);
self.run(key, move |conn| async move {
let model = pick_project(&conn, project.as_deref()).await?;
let xml = assemble_at(&conn, model.id, &model.uuid, model.command_seq).await?;
crate::nodeutil::serializer_load(&xml)
})
}
/// Save a project (already read out of its handle) to the library,
/// journaling the diff. The Rust-typed inner save: the facade-facing
/// trait `save` converts the project handle and forwards here.
pub fn save_project(
&self,
project: &ProjectArc,
uri: &StorageUri,
_options: u32,
) -> Result<()> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
// Serialize under the project lock (the same per-node writer the
// `.ove` backend uses — one serialization truth).
let (uuid, name, nodes, settings_xml, settings_map) = {
let guard = project.lock().map_err(|_| Error::State)?;
let uuid = guard.uuid.clone();
let name = project_display_name(&guard);
let (nodes, settings_xml, settings_map) = serialize_project_state(&guard)?;
(uuid, name, nodes, settings_xml, settings_map)
};
self.run(key, move |conn| async move {
save_tx(&conn, &uuid, &name, &nodes, &settings_xml, &settings_map).await
})?;
Ok(())
}
}
impl Default for DatabaseBackend {
@@ -763,35 +798,17 @@ impl StorageBackend for DatabaseBackend {
}
fn load(&self, uri: &StorageUri) -> Result<LoadResult> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
let project = target.project().map(str::to_string);
self.run(key, move |conn| async move {
let model = pick_project(&conn, project.as_deref()).await?;
let xml = assemble_at(&conn, model.id, &model.uuid, model.command_seq).await?;
let handle =
crate::nodeutil::make_project_owned(crate::nodeutil::serializer_load(&xml)?);
Ok(LoadResult::success(handle))
})
// Facade boundary: box the loaded project into an owned handle.
Ok(LoadResult::success(crate::nodeutil::make_project_owned(
self.load_project(uri)?,
)))
}
fn save(&self, project: CHandle, uri: &StorageUri, _options: u32) -> Result<()> {
let target = parse_target(uri)?;
let key = db_key_of(&target);
// Serialize under the project lock (the same per-node writer the
// `.ove` backend uses — one serialization truth).
fn save(&self, project: CHandle, uri: &StorageUri, options: u32) -> Result<()> {
// Facade boundary: convert the project handle to the boxed
// project, then run the Rust-typed save.
let arc = unsafe { crate::nodeutil::project_arc(&project)? };
let (uuid, name, nodes, settings_xml, settings_map) = {
let guard = arc.lock().map_err(|_| Error::State)?;
let uuid = guard.uuid.clone();
let name = project_display_name(&guard);
let (nodes, settings_xml, settings_map) = serialize_project_state(&guard)?;
(uuid, name, nodes, settings_xml, settings_map)
};
self.run(key, move |conn| async move {
save_tx(&conn, &uuid, &name, &nodes, &settings_xml, &settings_map).await
})?;
Ok(())
self.save_project(&arc, uri, options)
}
}
+45 -30
View File
@@ -67,6 +67,7 @@ use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType};
use crate::backend::LoadResult;
use crate::nodeutil as node;
use crate::nodeutil::ProjectArc;
use crate::error::{Error, Result};
use crate::uri::StorageUri;
@@ -81,6 +82,46 @@ impl OtioBackend {
pub fn new() -> Self {
OtioBackend
}
/// Save a project (already read out of its handle) to the URI. The
/// Rust-typed inner path: the facade-facing trait `save` converts the
/// project handle and forwards here.
pub fn save_project(
&self,
project: &ProjectArc,
uri: &StorageUri,
_options: u32,
) -> Result<()> {
let path = uri.local_path().ok_or(Error::Invalid)?.to_string();
let ext = uri.extension().ok_or(Error::Invalid)?;
let guard = project.lock().map_err(|_| Error::State)?;
let timelines = project_to_timelines(&guard);
match ext.as_str() {
"otio" => {
let root = if timelines.len() == 1 {
Serializable::Timeline(timelines.into_iter().next().unwrap())
} else if timelines.is_empty() {
// Nothing to export; a single empty timeline is the
// friendliest shape for a fresh import.
Serializable::Timeline(Timeline::new("Timeline"))
} else {
let children: Vec<Serializable> = timelines
.into_iter()
.map(Serializable::Timeline)
.collect();
Serializable::SerializableCollection(SerializableCollection::new(
"oak",
children,
))
};
root.to_json_file(&path).map_err(|e| Error::Io(e.to_string()))
}
"fcpxml" => oakotio::to_fcpxml_file(&timelines, &path)
.map_err(|e| Error::Io(e.to_string())),
_ => Err(Error::Invalid),
}
}
}
impl Default for OtioBackend {
@@ -149,38 +190,12 @@ impl crate::backend::StorageBackend for OtioBackend {
&self,
project: crate::handle::CHandle,
uri: &StorageUri,
_options: u32,
options: u32,
) -> Result<()> {
let path = uri.local_path().ok_or(Error::Invalid)?.to_string();
let ext = uri.extension().ok_or(Error::Invalid)?;
// Facade boundary: convert the project handle to the boxed
// project, then run the Rust-typed save.
let arc = unsafe { node::project_arc(&project)? };
let guard = arc.lock().map_err(|_| Error::State)?;
let timelines = project_to_timelines(&guard);
match ext.as_str() {
"otio" => {
let root = if timelines.len() == 1 {
Serializable::Timeline(timelines.into_iter().next().unwrap())
} else if timelines.is_empty() {
// Nothing to export; a single empty timeline is the
// friendliest shape for a fresh import.
Serializable::Timeline(Timeline::new("Timeline"))
} else {
let children: Vec<Serializable> = timelines
.into_iter()
.map(Serializable::Timeline)
.collect();
Serializable::SerializableCollection(SerializableCollection::new(
"oak",
children,
))
};
root.to_json_file(&path).map_err(|e| Error::Io(e.to_string()))
}
"fcpxml" => oakotio::to_fcpxml_file(&timelines, &path)
.map_err(|e| Error::Io(e.to_string())),
_ => Err(Error::Invalid),
}
self.save_project(&arc, uri, options)
}
}
+28 -13
View File
@@ -32,6 +32,7 @@
use crate::backend::LoadResult;
use crate::error::{Error, OAKSTORAGE_OK, OAKSTORAGE_TOO_NEW, OAKSTORAGE_TOO_OLD, OAKSTORAGE_UNKNOWN_VERSION};
use crate::nodeutil::ProjectArc;
use crate::uri::StorageUri;
use oaknode::serializer::XmlRead;
@@ -82,6 +83,29 @@ impl OveXmlBackend {
pub fn new() -> Self {
OveXmlBackend
}
/// Save a project (already read out of its handle) to the URI. The
/// Rust-typed inner path: the facade-facing trait `save` converts the
/// project handle and forwards here.
pub fn save_project(
&self,
project: &ProjectArc,
uri: &StorageUri,
_options: u32,
) -> crate::error::Result<()> {
let path = uri
.local_path()
.ok_or(Error::Invalid)?
.to_string();
let xml = {
let guard = project
.lock()
.map_err(|_| Error::State)?;
crate::nodeutil::serializer_save(&guard)?
};
std::fs::write(&path, xml).map_err(|e| Error::Io(e.to_string()))?;
Ok(())
}
}
impl Default for OveXmlBackend {
@@ -135,20 +159,11 @@ impl crate::backend::StorageBackend for OveXmlBackend {
&self,
project: crate::handle::CHandle,
uri: &StorageUri,
_options: u32,
options: u32,
) -> crate::error::Result<()> {
let path = uri
.local_path()
.ok_or(Error::Invalid)?
.to_string();
// Facade boundary: convert the project handle to the boxed
// project, then run the Rust-typed save.
let arc = unsafe { crate::nodeutil::project_arc(&project)? };
let xml = {
let guard = arc
.lock()
.map_err(|_| Error::State)?;
crate::nodeutil::serializer_save(&guard)?
};
std::fs::write(&path, xml).map_err(|e| Error::Io(e.to_string()))?;
Ok(())
self.save_project(&arc, uri, options)
}
}
+10 -158
View File
@@ -14,164 +14,16 @@
// 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 other crates;
//! duplicated on purpose — handle function pointers must run code from
//! the creating DLL).
//! The `CHandle` 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
//! so the crate's signatures stay source-compatible with the shared type.
//!
//! `CHandle` is the canonical shared ABI value-handle type from
//! `oakcore-rs` (single-lib unification, see
//! `docs/zh/plans/riir/single-lib.md`), so a handle returned by
//! `oakstorage_*` is structurally interchangeable with one created by
//! `oaknode_*`: `oakstorage_save` consumes an `OakNodeProject*` handle
//! and `oakstorage_project_take_project` hands one back.
use std::any::Any;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
//! M14 R5: oakstorage never creates its own boxes — every project handle
//! it produces goes through `oaknode::handle` (see [`crate::nodeutil`]),
//! so the per-DLL RefBox machinery is not needed here and was removed;
//! only the shared `CHandle` type remains, for the facade-facing surface
//! (project handles in [`crate::backend::LoadResult`] and the
//! `StorageBackend` trait, the write-through binding entries).
pub use oakcore_rs::handle::CHandle;
use crate::error::OAKSTORAGE_E_FAILED;
/// ABI version stamped into every handle.
pub const OAKSTORAGE_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,
}
/// addref implementation: atomic +1 (owned and borrowed handles alike —
/// a borrow only extends the box's life, not 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>;
// The caller guarantees the handle is live for the borrow period.
(*rb).refs.fetch_add(1, Ordering::Relaxed);
}
}
/// release implementation (owned): atomic -1; at zero, reclaim the box
/// and destroy the contained value.
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: the zeroing side sees every write that preceded the
// last reference (including the state the destructor needs).
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(rb));
}
}
}
/// release implementation (borrowed, from [`make_borrowed`]): at zero,
/// free only the box memory and forget the contained value — ownership
/// stays with the borrowing side.
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 {
// Partial move: move the value out of the temporary Box (its
// destructor then only frees the allocation); `forget` skips
// the value's destructor (double-free defense).
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 {
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: OAKSTORAGE_ABI_VERSION,
}
}
/// Owned handle with count 1 and a caller-provided release routine
/// (used by the ffi layer's alive-counted session boxes, whose release
/// must also update the debug counter).
pub fn make_owned_with<T: Any + Send>(
value: T,
release: unsafe extern "C" fn(*mut std::ffi::c_void),
) -> 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(release),
abi_version: OAKSTORAGE_ABI_VERSION,
}
}
/// 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: OAKSTORAGE_ABI_VERSION,
}
}
/// Typed view into a handle; `None` for empty handles.
///
/// # Safety
/// `T` must be the boxed type.
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
if h.ctx.is_null() {
return None;
}
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
}
/// Panic-catching FFI wrapper for i32-returning exports.
///
/// Panics map to [`OAKSTORAGE_E_FAILED`].
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKSTORAGE_OK,
Ok(Err(e)) => e.code(),
Err(_) => OAKSTORAGE_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));
}
+23 -28
View File
@@ -19,8 +19,11 @@
//!
//! The ove-xml/otio backends need oaknode's project + serializer
//! families. Graph (de)serialization stays oaknode's own serializer
//! (`load`/`save`); these helpers box the resulting `Arc<Mutex<Project>>`
//! into the canonical handle form the storage session API moves around.
//! (`load`/`save`); these helpers box the resulting
//! `Arc<Mutex<Project>>` into the canonical handle form only where a
//! project crosses the facade boundary (backends' `load` results and
//! `save` inputs) — inside the crate, projects travel as the plain
//! [`ProjectArc`] alias.
use std::sync::{Arc, Mutex};
@@ -31,36 +34,15 @@ use crate::handle::CHandle;
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
/// Box an existing project as an owned handle (refcount 1).
///
/// This is the facade-boundary conversion: backends hand loaded projects
/// to the caller (through [`crate::backend::LoadResult`] / the database
/// backend's `load_at`) as an oaknode project handle; everything inside
/// the crate works with the boxed [`ProjectArc`] instead.
pub fn make_project_owned(project: ProjectArc) -> CHandle {
oaknode::handle::make_owned(project)
}
/// Release one owned reference of a project handle produced by
/// [`make_project_owned`] (or the database backend's load). The handle is
/// dead afterwards; the write-through binding keeps its own reference, so
/// releasing the caller's copy never tears a bound project down.
pub fn release_project(mut h: CHandle) {
if let Some(release) = h.release {
// SAFETY: `h` is an owned handle from this module; the release runs
// the box's own destructor once per owned reference.
unsafe { release(h.ctx) };
}
h.ctx = std::ptr::null_mut();
}
/// The boxed project of a handle produced by [`make_project_owned`] or the
/// database backend's load (both box a `ProjectArc`). `None` for an empty
/// handle.
///
/// The handle ABI carries no type tag, so the caller must only pass handles
/// from those two producers; the unsafe downcast is contained here instead
/// of spread across every direct-rlib consumer.
pub fn project_arc_of(h: &CHandle) -> Option<ProjectArc> {
// SAFETY: the documented precondition — handles from this module's
// producers box a `ProjectArc`.
unsafe { oaknode::handle::get::<ProjectArc>(h) }.cloned()
}
/// Read the boxed project of a project handle.
///
/// # Safety
@@ -72,6 +54,19 @@ pub unsafe fn project_arc(h: &CHandle) -> Result<ProjectArc> {
.ok_or(crate::error::Error::Invalid)
}
/// Release a project handle created by [`make_project_owned`] (its box's
/// release callback drops one reference). NULL is a no-op.
pub fn release_project(h: CHandle) {
if h.is_null() {
return;
}
if let Some(release) = h.release {
// SAFETY: `h` was produced by `make_project_owned`; the callback
// owns the box and nulls nothing else.
unsafe { release(h.ctx) };
}
}
/// Load a project from XML text via the oaknode serializer.
pub fn serializer_load(xml: &str) -> Result<ProjectArc> {
oaknode::serializer::load(xml).map_err(|e| crate::error::Error::Format(e.to_string()))
+20 -31
View File
@@ -15,29 +15,31 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The project session (M10 §2.2 `OakStorageProject`): wraps a loaded
//! project plus its source URI. `take` transfers the project handle
//! out, leaving an empty shell that must still be freed.
//! project plus its source URI. `take` transfers the project out, leaving
//! an empty shell that must still be freed.
//!
//! M14 R5: the session holds the boxed project directly
//! ([`crate::nodeutil::ProjectArc`]) instead of a `CHandle` — the handle
//! form is only produced at the backend `load` boundary
//! ([`crate::backend::LoadResult`]); `open` converts it before wrapping.
use crate::handle::CHandle;
use crate::nodeutil::ProjectArc;
use crate::uri::StorageUri;
/// An open project session.
pub struct Session {
/// Source URI.
uri: StorageUri,
/// The project handle (None after [`Session::take`]).
project: Option<CHandle>,
/// The boxed project (None after [`Session::take`]).
project: Option<ProjectArc>,
}
impl Session {
/// Wrap a freshly loaded project. A null handle (the version-info
/// path: TOO_OLD/TOO_NEW/UNKNOWN_VERSION carries no project) maps to
/// `None`, not `Some(null)`.
pub fn new(uri: StorageUri, project: CHandle) -> Self {
Session {
uri,
project: (!project.is_null()).then_some(project),
}
/// Wrap a freshly loaded project. `None` (the version-info path:
/// TOO_OLD/TOO_NEW/UNKNOWN_VERSION carries no project) leaves an empty
/// session.
pub fn new(uri: StorageUri, project: Option<ProjectArc>) -> Self {
Session { uri, project }
}
/// Source URI.
@@ -45,27 +47,14 @@ impl Session {
&self.uri
}
/// Borrowed project handle (None after take).
pub fn project(&self) -> Option<&CHandle> {
/// Borrowed project (None after take).
pub fn project(&self) -> Option<&ProjectArc> {
self.project.as_ref()
}
/// Transfer the project out (C++ take_project semantics); the
/// session becomes an empty shell, and the caller owns the returned
/// handle (release it with `oaknode_project_free`).
pub fn take(&mut self) -> Option<CHandle> {
/// Transfer the project out (C++ take_project semantics); the session
/// becomes an empty shell, and the caller owns the returned reference.
pub fn take(&mut self) -> Option<ProjectArc> {
self.project.take()
}
}
impl Drop for Session {
fn drop(&mut self) {
// Release the still-held project handle (the `take` path already
// removed it).
if let Some(h) = self.project.take() {
if let Some(f) = h.release {
unsafe { f(h.ctx) };
}
}
}
}
+53 -48
View File
@@ -27,20 +27,22 @@
//!
//! ## Binding model
//!
//! The map is keyed by the project handle's `ctx` pointer (the module
//! `RefBox` identity — one per in-memory project instance), so several
//! projects can be bound at once (multi-project, plan §3) without
//! confusing their library rows. Every undo-path operation re-saves ALL
//! bound projects ([`note_command`]): the oakstorage backend diffs each
//! project against its own library head, so untouched projects are
//! no-op touches and only the project the command actually changed
//! advances its journal. (The plan's "current project" phrasing maps to
//! this — the undo stack is cleared on every project switch, so at most
//! one project's graph changes per command; a bound-but-untouched
//! project can gain its import row this way, which reflects its true
//! state.) Closing a project ([`unbind_project`], hooked from the
//! facade's `project_free`) flushes its pending writes and drops the
//! binding.
//! The map is keyed by the project box's identity (`Arc::as_ptr` — one
//! per in-memory project instance), so several projects can be bound at
//! once (multi-project, plan §3) without confusing their library rows.
//! Every undo-path operation re-saves ALL bound projects
//! ([`note_command`]): the oakstorage backend diffs each project against
//! its own library head, so untouched projects are no-op touches and
//! only the project the command actually changed advances its journal.
//! (The plan's "current project" phrasing maps to this — the undo stack
//! is cleared on every project switch, so at most one project's graph
//! changes per command; a bound-but-untouched project can gain its
//! import row this way, which reflects its true state.) Closing a
//! project ([`unbind_project`], hooked from the facade's `project_free`)
//! flushes its pending writes and drops the binding. The binding holds
//! the boxed project ([`ProjectArc`]) directly — the `CHandle` facade
//! entries convert at the boundary — so a bound project outlives the
//! caller's own handle reference.
//!
//! The library is selected from the `Storage` config group (all defaults
//! are config-driven, plan §5):
@@ -79,27 +81,22 @@
use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::{Condvar, Mutex, OnceLock};
use std::sync::{Arc, Condvar, Mutex, OnceLock};
use std::time::Duration;
use oaknode::project::Project;
use oakundo::global;
use crate::backend::StorageBackend;
use crate::backends::database::DatabaseBackend;
use crate::handle::CHandle;
use crate::nodeutil::ProjectArc;
use crate::uri::StorageUri;
/// The boxed project payload behind an oaknode project handle (the
/// engine's `crate::handle::domain::ProjectArc` equivalent).
type ProjectArc = std::sync::Arc<std::sync::Mutex<Project>>;
/// One bound project: its session (library uri + row uuid) plus the
/// write state.
struct Binding {
/// The project handle (addref'd at bind, released at unbind; keeps
/// the project alive past the caller's own handle).
project: CHandle,
/// The boxed project (kept alive here past the caller's own handle
/// reference).
project: ProjectArc,
/// Library uri (`oakdb+sqlite:///…`).
uri: String,
/// Library row uuid.
@@ -123,7 +120,7 @@ pub fn backend() -> &'static DatabaseBackend {
BACKEND.get_or_init(DatabaseBackend::new)
}
/// project identity (handle `ctx` pointer) -> binding.
/// project identity (boxed project allocation) -> binding.
fn bindings() -> &'static Mutex<HashMap<usize, Binding>> {
static BINDINGS: OnceLock<Mutex<HashMap<usize, Binding>>> = OnceLock::new();
BINDINGS.get_or_init(|| Mutex::new(HashMap::new()))
@@ -167,12 +164,20 @@ fn ensure_command_observer() {
/// backend is disabled by config, the project cannot be addressed (no
/// uuid), or it is already bound. No database write happens here — the
/// first undo-path operation creates the library row.
///
/// Facade entry: takes the project as a `CHandle` (the engine's project
/// handle form) and converts it to the boxed project at the boundary;
/// everything from here on works with the [`ProjectArc`].
pub fn bind_project(project: CHandle) {
let _ = catch_unwind(AssertUnwindSafe(|| {
if project.is_null() {
return;
}
let key = project.ctx as usize;
// SAFETY: facade project handles box a `ProjectArc`.
let Ok(arc) = (unsafe { crate::nodeutil::project_arc(&project) }) else {
return;
};
let key = Arc::as_ptr(&arc) as usize;
{
let g = bindings().lock().unwrap_or_else(|e| e.into_inner());
if g.contains_key(&key) {
@@ -186,28 +191,19 @@ pub fn bind_project(project: CHandle) {
return;
};
let uuid = {
let arc = unsafe { oaknode::handle::get::<ProjectArc>(&project) };
match arc {
Some(a) => a.lock().unwrap_or_else(|e| e.into_inner()).uuid.clone(),
None => return,
}
let guard = arc.lock().unwrap_or_else(|e| e.into_inner());
guard.uuid.clone()
};
if uuid.is_empty() {
return;
}
// Addref the handle so the binding owns a reference independent of
// the caller's.
let owned = project;
if let Some(addref) = owned.addref {
unsafe { addref(owned.ctx) };
}
bindings()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(
key,
Binding {
project: owned,
project: arc,
uri,
uuid,
dirty: false,
@@ -224,14 +220,15 @@ pub fn bind_project(project: CHandle) {
/// project). No-op when not bound.
pub fn unbind_project(project: CHandle) {
let _ = catch_unwind(AssertUnwindSafe(|| {
let key = project.ctx as usize;
// SAFETY: facade project handles box a `ProjectArc`.
let Ok(arc) = (unsafe { crate::nodeutil::project_arc(&project) }) else {
return;
};
let key = Arc::as_ptr(&arc) as usize;
flush_one(key);
let mut g = bindings().lock().unwrap_or_else(|e| e.into_inner());
if let Some(b) = g.remove(&key) {
if let Some(release) = b.project.release {
unsafe { release(b.project.ctx) };
}
}
// Dropping the binding releases the project reference.
g.remove(&key);
}));
}
@@ -240,10 +237,14 @@ pub fn is_bound(project: CHandle) -> bool {
if project.is_null() {
return false;
}
// SAFETY: facade project handles box a `ProjectArc`.
let Ok(arc) = (unsafe { crate::nodeutil::project_arc(&project) }) else {
return false;
};
bindings()
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains_key(&(project.ctx as usize))
.contains_key(&(Arc::as_ptr(&arc) as usize))
}
/// The last write-through / snapshot error of `project` (empty when none
@@ -252,10 +253,14 @@ pub fn last_error(project: CHandle) -> Option<String> {
if project.is_null() {
return None;
}
// SAFETY: facade project handles box a `ProjectArc`.
let Ok(arc) = (unsafe { crate::nodeutil::project_arc(&project) }) else {
return None;
};
bindings()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&(project.ctx as usize))
.get(&(Arc::as_ptr(&arc) as usize))
.and_then(|b| b.last_error.clone())
}
@@ -286,7 +291,7 @@ fn write_through(key: usize) {
let (project, uri, _uuid) = {
let mut g = bindings().lock().unwrap_or_else(|e| e.into_inner());
match g.get_mut(&key) {
Some(b) => (b.project, b.uri.clone(), b.uuid.clone()),
Some(b) => (b.project.clone(), b.uri.clone(), b.uuid.clone()),
None => return,
}
};
@@ -297,7 +302,7 @@ fn write_through(key: usize) {
return;
}
};
match backend().save(project, &parsed, 0) {
match backend().save_project(&project, &parsed, 0) {
Ok(()) => {
let mut g = bindings().lock().unwrap_or_else(|e| e.into_inner());
if let Some(b) = g.get_mut(&key) {