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