refactor(oakundo): replace the CHandle vtable layer with owned trait objects
With the C ABI facade (oakengine) retired, the frozen-ABI rationale is gone. UndoCommand now boxes a Send Command trait (new/from_closures/ multi), dropping OakUndoCommandVtable, the userdata trampolines, the refcount shell, the handle module, and all undostack_* handle exports. The global facade loses its raw-pointer out-params (can_undo/can_redo return bool, command_name returns String). oaktimeline/oaknode/ oakplugin/oaktask construct commands directly via UndoCommand::new. oakundo src is now free of unsafe; behavior (ordering, idempotence, done flags, groups, observers, 200-row cap) is unchanged and pinned by the rewritten tests.
This commit is contained in:
+63
-200
@@ -15,31 +15,27 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The process-wide undo stack, undo groups and the command-success
|
||||
//! observer hook (M14 R1: sunk from the engine facade's `undo.rs`).
|
||||
//! observer hook.
|
||||
//!
|
||||
//! The facade used to own the process-wide stack (the module-00 analogue
|
||||
//! of `EngineCore::undo_stack()`), the open undo group and the
|
||||
//! write-through notification as facade state; all of it is process state,
|
||||
//! so this module holds it and the facade forwards. The observer registry
|
||||
//! The retired engine facade used to own the process-wide stack (the
|
||||
//! module-00 analogue of `EngineCore::undo_stack()`), the open undo group
|
||||
//! and the write-through notification as facade state; all of it is
|
||||
//! process state, so this module holds it directly. The observer registry
|
||||
//! lets downstream modules (the oakstorage write-through session manager)
|
||||
//! subscribe to "a command was recorded" notifications without a facade
|
||||
//! round-trip.
|
||||
//! subscribe to "a command was recorded" notifications.
|
||||
//!
|
||||
//! M14 R5: everything in this module is Rust-typed — the process-wide
|
||||
//! stack is a plain `static Mutex<UndoStack>` and the open group holds an
|
||||
//! [`UndoCommand`] value. The only remaining `CHandle` is the
|
||||
//! [`push_or_run`] facade entry, which converts the incoming module
|
||||
//! command handle into an [`UndoCommand`] value at the boundary.
|
||||
//! Everything here is Rust-typed: the process-wide stack is a plain
|
||||
//! `static Mutex<UndoStack>` and the open group holds an
|
||||
//! [`UndoCommand`] value. The former `CHandle`-marshalling entry point
|
||||
//! (`push_or_run`) and the raw-pointer out-parameter queries
|
||||
//! (`can_undo(out)` / `command_text(buf, size)` / `command_is_done(out)`)
|
||||
//! were deleted with the C ABI — the same operations are exposed as
|
||||
//! value-typed functions below.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::error::{Error, OAKUNDO_E_FAILED, Result};
|
||||
use crate::handle::CHandle;
|
||||
use crate::undocommand::{
|
||||
command_free, command_from_owned, command_redo_now, command_take, UndoCommand,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use crate::undocommand::UndoCommand;
|
||||
use crate::undostack::UndoStack;
|
||||
|
||||
/// The process-wide undo stack, created lazily on first use and kept for
|
||||
@@ -49,13 +45,6 @@ fn global_stack() -> &'static Mutex<UndoStack> {
|
||||
STACK.get_or_init(|| Mutex::new(UndoStack::new()))
|
||||
}
|
||||
|
||||
/// Stable opaque token for the engine's `oakengine_undo_handle` export:
|
||||
/// the stack's address (never dereferenced by callers; lives for the
|
||||
/// process).
|
||||
pub fn stack_token() -> *mut c_void {
|
||||
global_stack() as *const Mutex<UndoStack> as *mut c_void
|
||||
}
|
||||
|
||||
/// Run `f` on the process-wide stack; a poisoned mutex is recovered (its
|
||||
/// inner value is still valid).
|
||||
fn with_stack<R>(f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
|
||||
@@ -121,7 +110,7 @@ fn group_lock() -> std::sync::MutexGuard<'static, Option<OpenGroup>> {
|
||||
pub fn group_begin(name: &str) -> Result<()> {
|
||||
let mut g = group_lock();
|
||||
if g.is_some() {
|
||||
return Err(Error::State);
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
*g = Some(OpenGroup {
|
||||
multi: UndoCommand::multi(),
|
||||
@@ -135,7 +124,7 @@ pub fn group_begin(name: &str) -> Result<()> {
|
||||
/// when no group is open.
|
||||
pub fn group_end() -> Result<()> {
|
||||
let mut g = group_lock();
|
||||
let open = g.take().ok_or(Error::State)?;
|
||||
let open = g.take().ok_or(crate::error::Error::State)?;
|
||||
let multi = open.multi;
|
||||
let name = open.name;
|
||||
drop(g);
|
||||
@@ -155,7 +144,7 @@ pub fn group_end() -> Result<()> {
|
||||
/// no group is open. No observers fire (nothing was recorded).
|
||||
pub fn group_abort() -> Result<()> {
|
||||
let mut g = group_lock();
|
||||
let open = g.take().ok_or(Error::State)?;
|
||||
let open = g.take().ok_or(crate::error::Error::State)?;
|
||||
let mut multi = open.multi;
|
||||
drop(g);
|
||||
// The multi command itself is never marked done (each child was
|
||||
@@ -170,79 +159,29 @@ pub fn group_abort() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push `command` onto the stack and execute its redo (or add it to the
|
||||
/// open group). `command` is consumed: the stack/multi takes the command
|
||||
/// value, leaving the caller's handle as a non-owning shell that still
|
||||
/// needs its own release. Returns 0 on success, a module error code
|
||||
/// otherwise. Command observers fire only when the command was recorded on
|
||||
/// the STACK — a group child joins the group, and the group itself
|
||||
/// notifies at [`group_end`].
|
||||
pub fn push_or_run(command: CHandle, name: &str) -> c_int {
|
||||
/// Push `command` onto the process-wide stack (redo then record), or into
|
||||
/// the open group as an already-done child. On a stack record the command
|
||||
/// observers fire (the oakstorage write-through persists the edit).
|
||||
pub fn push(command: UndoCommand, name: &str) -> Result<()> {
|
||||
let mut g = group_lock();
|
||||
if let Some(group) = g.as_mut() {
|
||||
// The group takes the command value (command_take), so the eager
|
||||
// redo must happen on the still-owned handle FIRST — the group
|
||||
// receives an already-done command (C++ semantics: add_child +
|
||||
// redo_now, net effect identical for the group's reverse-order
|
||||
// undo).
|
||||
let rc = command_redo_now(command);
|
||||
if rc != 0 {
|
||||
return rc;
|
||||
}
|
||||
// SAFETY: `command` is a live module command handle (facade
|
||||
// contract); `command_take` moves the value out of its box and
|
||||
// marks the shell non-owning.
|
||||
return match unsafe { command_take(command.ctx) } {
|
||||
Ok(cmd) => {
|
||||
group.multi.multi_add_child(cmd);
|
||||
0
|
||||
}
|
||||
Err(e) => e.code(),
|
||||
};
|
||||
// The group takes the command as an already-done child: the eager
|
||||
// redo must run BEFORE the child joins the group (C++ semantics:
|
||||
// add_child + redo_now, net effect identical for the group's
|
||||
// reverse-order undo).
|
||||
let mut command = command;
|
||||
command.redo_now();
|
||||
group.multi.multi_add_child(command);
|
||||
return Ok(());
|
||||
}
|
||||
// No group open: take the command value and push it onto the
|
||||
// process-wide stack (redo then record).
|
||||
// SAFETY: as above, `command` is a live module command handle.
|
||||
let cmd = match unsafe { command_take(command.ctx) } {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => return e.code(),
|
||||
};
|
||||
drop(g);
|
||||
let mut guard = global_stack().lock().unwrap_or_else(|e| e.into_inner());
|
||||
guard.push(cmd, name);
|
||||
guard.push(command, name);
|
||||
drop(guard);
|
||||
// The stack took the command; its redo already ran (plan M13 D2):
|
||||
// persist the write-through subscribers.
|
||||
// The stack took the command; its redo already ran: persist the
|
||||
// write-through subscribers.
|
||||
notify_observers();
|
||||
0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Safe value-typed surface (M14 R3)
|
||||
//
|
||||
// The direct-rlib frontends (the app) hold module [`UndoCommand`] values,
|
||||
// not CHandles. The functions below are the safe twins of the handle-based
|
||||
// API above; the handle marshalling they perform stays inside this crate.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Push `command` onto the process-wide stack (redo then record), or into
|
||||
/// the open group. On a stack record the command observers fire (the
|
||||
/// oakstorage write-through persists the edit).
|
||||
pub fn push(command: UndoCommand, name: &str) -> Result<()> {
|
||||
// SAFETY: `command_from_owned` boxes the value; `push_or_run` takes the
|
||||
// value out of the handle (stack push or group child), and
|
||||
// `command_free` releases the remaining non-owning shell (dropping the
|
||||
// command itself when nobody took it).
|
||||
let mut handle = unsafe { command_from_owned(command) };
|
||||
if handle.is_null() {
|
||||
return Err(Error::NoMem);
|
||||
}
|
||||
let rc = push_or_run(handle, name);
|
||||
unsafe{command_free(&mut handle)};
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from_code(rc))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Step the process-wide stack back one entry (no-op at the bottom). On
|
||||
@@ -261,14 +200,12 @@ pub fn redo() -> Result<()> {
|
||||
|
||||
/// Whether the process-wide stack has an entry to undo.
|
||||
pub fn undoable() -> bool {
|
||||
let mut v: c_int = 0;
|
||||
unsafe{can_undo(&mut v).is_ok() && v != 0}
|
||||
can_undo()
|
||||
}
|
||||
|
||||
/// Whether the process-wide stack has an entry to redo.
|
||||
pub fn redoable() -> bool {
|
||||
let mut v: c_int = 0;
|
||||
unsafe{can_redo(&mut v).is_ok() && v != 0}
|
||||
can_redo()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -285,30 +222,14 @@ pub fn index() -> Result<i64> {
|
||||
with_stack(|s| Ok(s.done_count()))
|
||||
}
|
||||
|
||||
/// Whether an undo is possible (1/0 via `out_value`; a module error code
|
||||
/// otherwise).
|
||||
pub unsafe fn can_undo(out_value: *mut c_int) -> Result<()> {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let value = with_stack(|s| Ok(if s.can_undo() { 1 } else { 0 }))?;
|
||||
// SAFETY: `out_value` points to at least one writable `c_int`, per the
|
||||
// facade contract.
|
||||
unsafe { *out_value = value };
|
||||
Ok(())
|
||||
/// Whether an undo is possible.
|
||||
pub fn can_undo() -> bool {
|
||||
with_stack(|s| Ok(s.can_undo())).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether a redo is possible (1/0 via `out_value`; a module error code
|
||||
/// otherwise).
|
||||
pub unsafe fn can_redo(out_value: *mut c_int) -> Result<()> {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let value = with_stack(|s| Ok(if s.can_redo() { 1 } else { 0 }))?;
|
||||
// SAFETY: `out_value` points to at least one writable `c_int`, per the
|
||||
// facade contract.
|
||||
unsafe { *out_value = value };
|
||||
Ok(())
|
||||
/// Whether a redo is possible.
|
||||
pub fn can_redo() -> bool {
|
||||
with_stack(|s| Ok(s.can_redo())).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Undo/redo until the done-command count equals `index`. On success the
|
||||
@@ -331,58 +252,15 @@ pub fn clear() -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Two-stage label getter for the row at `row` (see
|
||||
/// [`crate::undostack::undostack_command_text`]): returns the required
|
||||
/// size including the NUL, or a module error code.
|
||||
pub unsafe fn command_text(row: i64, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
|
||||
with_stack(|s| {
|
||||
if row < 0 || row >= s.command_count() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let name = s.command_name(row)?;
|
||||
let required = (name.len() + 1) as i32;
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let copy_len = name.len().min((buf_size as usize).saturating_sub(1));
|
||||
let bytes = name.as_bytes();
|
||||
// SAFETY: `buf` points to `buf_size` writable bytes and we
|
||||
// write at most `copy_len` (+ one NUL) of them.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
|
||||
*buf.add(copy_len) = 0;
|
||||
}
|
||||
}
|
||||
Ok(required)
|
||||
})
|
||||
}));
|
||||
match result {
|
||||
Ok(Ok(required)) => required,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKUNDO_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the row at `row` is done (1/0 via `out_value`; a module error
|
||||
/// code otherwise — `-20004` for an out-of-range row).
|
||||
pub unsafe fn command_is_done(row: i64, out_value: *mut c_int) -> Result<()> {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let done = with_stack(|s| s.command_is_done(row))?;
|
||||
// SAFETY: `out_value` points to at least one writable `c_int`, per the
|
||||
// facade contract.
|
||||
unsafe { *out_value = if done { 1 } else { 0 } };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The user-visible label of the row at `row` (the safe twin of the
|
||||
/// two-stage [`command_text`]; the history panel's row query).
|
||||
/// The user-visible label of the row at `row` (the safe replacement for
|
||||
/// the C-ABI two-stage `command_text(buf, size)`; the history panel's row
|
||||
/// query). `NotFound` for an out-of-range row.
|
||||
pub fn command_name(row: i64) -> Result<String> {
|
||||
with_stack(|s| s.command_name(row).map(|n| n.to_string()))
|
||||
}
|
||||
|
||||
/// Whether the row at `row` is done (the safe twin of
|
||||
/// [`command_is_done`]; the history panel's gray-row query).
|
||||
/// Whether the row at `row` is done (the safe replacement for the C-ABI
|
||||
/// `command_is_done(out)`; the history panel's gray-row query).
|
||||
pub fn command_done(row: i64) -> Result<bool> {
|
||||
with_stack(|s| s.command_is_done(row))
|
||||
}
|
||||
@@ -405,21 +283,12 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The stack/group/observer state is process-wide: every test here runs
|
||||
/// serially under this lock, mirroring the facade's GLOBAL_STACK_LOCK
|
||||
/// pattern.
|
||||
/// serially under this lock.
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn vtable_command() -> CHandle {
|
||||
use crate::undocommand::{command_init, OakUndoCommandVtable};
|
||||
unsafe{
|
||||
command_init(
|
||||
&OakUndoCommandVtable {
|
||||
redo: None,
|
||||
undo: None,
|
||||
free_fn: None,
|
||||
},
|
||||
std::ptr::null_mut(),
|
||||
)}
|
||||
/// A no-op command value.
|
||||
fn noop_command() -> UndoCommand {
|
||||
UndoCommand::from_closures(|| {}, || {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -431,23 +300,18 @@ mod tests {
|
||||
assert!(clear().is_ok());
|
||||
assert_eq!(count().unwrap(), 1);
|
||||
assert_eq!(index().unwrap(), 1);
|
||||
let mut v: c_int = 1;
|
||||
assert!(unsafe{can_undo(&mut v).is_ok()});
|
||||
assert_eq!(v, 0);
|
||||
assert!(!can_undo());
|
||||
|
||||
// Push fires the observer once.
|
||||
let cmd = vtable_command();
|
||||
assert_eq!(push_or_run(cmd, "alpha"), 0);
|
||||
push(noop_command(), "alpha").unwrap();
|
||||
assert_eq!(count().unwrap(), 2);
|
||||
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// Group begin/end fires the observer once at end.
|
||||
assert!(group_begin("grouped").is_ok());
|
||||
assert!(group_begin("again").is_err()); // State
|
||||
let c1 = vtable_command();
|
||||
let c2 = vtable_command();
|
||||
assert_eq!(push_or_run(c1, "c1"), 0);
|
||||
assert_eq!(push_or_run(c2, "c2"), 0);
|
||||
push(noop_command(), "c1").unwrap();
|
||||
push(noop_command(), "c2").unwrap();
|
||||
// Children joined the group: no observer fire yet.
|
||||
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
assert_eq!(count().unwrap(), 2);
|
||||
@@ -457,8 +321,7 @@ mod tests {
|
||||
|
||||
// Abort fires nothing.
|
||||
assert!(group_begin("abort").is_ok());
|
||||
let c3 = vtable_command();
|
||||
assert_eq!(push_or_run(c3, "c3"), 0);
|
||||
push(noop_command(), "c3").unwrap();
|
||||
assert!(group_abort().is_ok());
|
||||
assert_eq!(count().unwrap(), 3);
|
||||
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 2);
|
||||
@@ -470,9 +333,9 @@ mod tests {
|
||||
assert!(clear().is_ok());
|
||||
}
|
||||
|
||||
/// The safe value-typed surface (M14 R3): `push` redoes and records a
|
||||
/// closure command, `undo`/`redo` step the stack, and the observers
|
||||
/// fire on every recorded mutation.
|
||||
/// The value-typed surface: `push` redoes and records a closure
|
||||
/// command, `undo`/`redo` step the stack, and the observers fire on
|
||||
/// every recorded mutation.
|
||||
#[test]
|
||||
fn value_push_and_undo_redo() {
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -511,9 +374,9 @@ mod tests {
|
||||
assert!(clear().is_ok());
|
||||
}
|
||||
|
||||
/// The safe row getters answer like the C-ABI twins: labels survive an
|
||||
/// undo (undone rows stay labeled) and `command_done` flips with the
|
||||
/// stack pointer.
|
||||
/// The row getters answer like the C-ABI twins: labels survive an undo
|
||||
/// (undone rows stay labeled) and `command_done` flips with the stack
|
||||
/// pointer.
|
||||
#[test]
|
||||
fn value_command_name_and_done() {
|
||||
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// 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 oaknode/oakcodec
|
||||
//! crates (`src/node/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).
|
||||
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// ABI version stamped into every handle.
|
||||
pub const OAKUNDO_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,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub use oakcore_rs::handle::CHandle;
|
||||
|
||||
/// Owned handle with count 1; empty on allocation failure.
|
||||
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
|
||||
let boxed = RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value,
|
||||
};
|
||||
let ctx = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
|
||||
CHandle {
|
||||
ctx,
|
||||
addref: Some(addref_owned::<T>),
|
||||
release: Some(release_owned::<T>),
|
||||
abi_version: OAKUNDO_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed handle for an object owned elsewhere (release frees only
|
||||
/// the box).
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller guarantees `ptr` outlives every derived handle.
|
||||
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
|
||||
let boxed = RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value: ptr,
|
||||
};
|
||||
let ctx = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
|
||||
CHandle {
|
||||
ctx,
|
||||
addref: Some(addref_borrowed::<T>),
|
||||
release: Some(release_borrowed::<T>),
|
||||
abi_version: OAKUNDO_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed view into a handle; `None` for empty handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type.
|
||||
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
|
||||
unsafe {
|
||||
if h.ctx.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(&(*(h.ctx as *mut RefBox<T>)).value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for i32-returning exports.
|
||||
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(())) => crate::error::OAKUNDO_OK,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => crate::error::OAKUNDO_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,
|
||||
_ => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for void exports.
|
||||
pub fn guard_void<F: FnOnce()>(f: F) {
|
||||
let _ = catch_unwind(AssertUnwindSafe(f));
|
||||
}
|
||||
|
||||
/// Owned addref: bump the box's refcount.
|
||||
unsafe extern "C" fn addref_owned<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut RefBox<T>;
|
||||
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned release: drop the box when the refcount hits zero.
|
||||
unsafe extern "C" fn release_owned<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut RefBox<T>;
|
||||
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(Box::from_raw(boxed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed addref: bump the shell's refcount.
|
||||
unsafe extern "C" fn addref_borrowed<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut RefBox<*mut T>;
|
||||
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed release: free only the shell, never the pointee.
|
||||
unsafe extern "C" fn release_borrowed<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut RefBox<*mut T>;
|
||||
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(Box::from_raw(boxed));
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-10
@@ -16,22 +16,28 @@
|
||||
|
||||
//! # oakundo — the undo/redo history module (Rust)
|
||||
//!
|
||||
//! Reimplements the C++ oakundo module behind its frozen C ABI
|
||||
//! (`include/undo/*.h`). See README.md for the architectural mapping —
|
||||
//! most notably the **vtable-command pattern**: C++ subclassing of
|
||||
//! `olive::UndoCommand` becomes a callback table + `userdata` pointer.
|
||||
//! Reimplements the C++ oakundo module. See README.md for the
|
||||
//! architectural mapping — most notably the **trait-object command
|
||||
//! pattern**: C++ subclassing of `olive::UndoCommand` becomes a boxed
|
||||
//! [`undocommand::Command`] (closure-backed or a
|
||||
//! [`undocommand::MultiUndoCommand`] composite) behind a plain owned
|
||||
//! [`undocommand::UndoCommand`] value.
|
||||
//!
|
||||
//! ## FFI discipline
|
||||
//! ## Consumers
|
||||
//!
|
||||
//! Identical to the oaknode/oakcodec crates: every export goes through
|
||||
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
|
||||
//! state behind `Mutex`.
|
||||
//! Every consumer is in-process Rust: the app's edit layer
|
||||
//! (`oakui::graphops` / `oakui::real`) drives the process-wide stack
|
||||
//! through [`global`], `oaktimeline`/`oaktask`/`oaknode`/`oakplugin`
|
||||
//! build commands as [`undocommand::UndoCommand`] values, and
|
||||
//! `oakstorage` subscribes to the command-success observers in
|
||||
//! [`global`]. The former frozen C ABI (`include/undo/*.h`), the
|
||||
//! refcounted [`CHandle`] layer and the callback-table vtable commands
|
||||
//! were deleted with the engine facade — the crate is pure owned-value
|
||||
//! Rust with no `unsafe`.
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod error;
|
||||
pub mod global;
|
||||
pub mod handle;
|
||||
pub mod undocommand;
|
||||
pub mod undostack;
|
||||
|
||||
@@ -15,66 +15,42 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `olive::UndoCommand` / `olive::MultiUndoCommand` — the undoable
|
||||
//! operation and its composite. Mirrors `src/undo/src/undocommand.h`
|
||||
//! and `include/undo/undocommand.h`.
|
||||
//! operation and its composite. Mirrors `src/undo/src/undocommand.h`.
|
||||
//!
|
||||
//! The C++ base is subclassed by other modules (`redo()`/`undo()`
|
||||
//! overrides). Rust has no inheritance, so the C ABI models the same
|
||||
//! polymorphism with a callback table + `userdata` pointer
|
||||
//! ([`OakUndoCommandVtable`]); this module's [`UndoCommand`] holds a
|
||||
//! [`CommandKind`] that is either that vtable-backed command or a
|
||||
//! [`MultiUndoCommand`] composite. This vtable-command pattern is the
|
||||
//! centerpiece of the oakundo architecture (see README.md).
|
||||
//!
|
||||
//! ## Command boxes
|
||||
//!
|
||||
//! Commands cross crate boundaries through a dedicated [`CommandBox`]
|
||||
//! (not the generic [`crate::handle::RefBox`]): it holds the raw
|
||||
//! `*mut UndoCommand`, an `owns` flag (mirroring the C++
|
||||
//! `OakUndoCommandBox`) and a refcount. An **owning** box owns and
|
||||
//! destroys its command at refcount zero; a **borrowed** box (a
|
||||
//! reference into a `MultiUndoCommand` or an `UndoStack`) owns only its
|
||||
//! shell. `take_command` moves the command value out of an owning box,
|
||||
//! turning it into a non-owning shell (the C++ `mark_container_owned`).
|
||||
//!
|
||||
//! The handle-level functions below (`command_init` ... `command_free`)
|
||||
//! were sunk from the former C ABI export layer: they keep the
|
||||
//! `CHandle`-based signatures so the facade bridges and integration
|
||||
//! tests can drive the command machinery directly. The `CHandle` layer
|
||||
//! itself is removed in a later milestone.
|
||||
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
//! overrides); the Rust equivalent of that virtual dispatch is a
|
||||
//! trait-object command. [`UndoCommand`] boxes a [`Command`] (implemented
|
||||
//! by closure-backed commands and by the [`MultiUndoCommand`] composite)
|
||||
//! plus the command's state flags. This replaces the former
|
||||
//! `OakUndoCommandVtable` + `userdata` callback-table model, which only
|
||||
//! existed to cross the frozen C ABI (see README.md). With every consumer
|
||||
//! in-process there is no callback table, no `extern "C"` trampoline and
|
||||
//! no refcounted `CHandle` — commands are owned values.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{guard, guard_handle, guard_void, CHandle, OAKUNDO_ABI_VERSION};
|
||||
|
||||
/// `oakundo_command_vtable` — the caller-defined redo/undo/free
|
||||
/// callback table. Any entry may be `None`; a `None` redo/undo makes
|
||||
/// that direction a no-op, `None` free_fn skips userdata release.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakUndoCommandVtable {
|
||||
/// Execute the operation.
|
||||
pub redo: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Reverse the operation.
|
||||
pub undo: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Release `userdata` when the command is destroyed.
|
||||
pub free_fn: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
}
|
||||
/// A single undoable operation (the C++ virtual `redo()`/`undo()` pair).
|
||||
///
|
||||
/// Implementations must be `Send`: the owning [`UndoCommand`] lives in
|
||||
/// stacks shared behind a `Mutex`, so it may move across threads (exactly
|
||||
/// as the C++ commands did when the owning stack was shared).
|
||||
pub trait Command: Send {
|
||||
/// Apply the change.
|
||||
fn redo(&mut self);
|
||||
/// Revert the change.
|
||||
fn undo(&mut self);
|
||||
|
||||
/// What backs an [`UndoCommand`]: a caller-defined vtable command or a
|
||||
/// composite of children. The direct analog of C++ virtual dispatch.
|
||||
pub enum CommandKind {
|
||||
/// Caller-defined command: callback table plus opaque userdata.
|
||||
Vtable {
|
||||
/// The copied callback table.
|
||||
vtable: OakUndoCommandVtable,
|
||||
/// Opaque caller state; owned by the command.
|
||||
userdata: *mut c_void,
|
||||
},
|
||||
/// Composite command (`olive::MultiUndoCommand`).
|
||||
Multi(MultiUndoCommand),
|
||||
/// `Some` for composite commands; used by [`UndoCommand::multi_*`].
|
||||
/// Defaults to `None` (a plain command is never a composite).
|
||||
fn as_multi(&self) -> Option<&MultiUndoCommand> {
|
||||
None
|
||||
}
|
||||
|
||||
/// `Some` for composite commands; used by [`UndoCommand::multi_*`].
|
||||
/// Defaults to `None` (a plain command is never a composite).
|
||||
fn as_multi_mut(&mut self) -> Option<&mut MultiUndoCommand> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `olive::UndoCommand` — an undoable operation.
|
||||
@@ -84,13 +60,13 @@ pub enum CommandKind {
|
||||
/// *callbacks* are not part of the C ABI and are intentionally omitted
|
||||
/// (see README.md decision 2); only the flag snapshot is retained.
|
||||
///
|
||||
/// `prepared` mirrors the C++ `prepared_` flag (the vtable has no
|
||||
/// `prepared` mirrors the C++ `prepared_` flag (the command trait has no
|
||||
/// `prepare()` callback, so it starts `true` and never un-prepares);
|
||||
/// `is_empty` marks the invariant bottom-of-stack command so the stack
|
||||
/// can keep it out of `can_undo`.
|
||||
pub struct UndoCommand {
|
||||
/// The operation (vtable or composite).
|
||||
kind: CommandKind,
|
||||
/// The operation (closure-backed or composite).
|
||||
inner: Box<dyn Command>,
|
||||
/// Whether the command has been executed (`done_`).
|
||||
done: bool,
|
||||
/// Project-dirty flag snapshot recorded at the last redo.
|
||||
@@ -105,17 +81,13 @@ pub struct UndoCommand {
|
||||
is_empty: bool,
|
||||
}
|
||||
|
||||
/// A command's userdata is caller-controlled; like the C++ object it
|
||||
/// may be moved across threads when the owning stack is shared behind a
|
||||
/// `Mutex`. Callbacks must be thread-safe with respect to the caller's
|
||||
/// own locking, exactly as in the C++ implementation.
|
||||
unsafe impl Send for UndoCommand {}
|
||||
|
||||
impl UndoCommand {
|
||||
/// New vtable-backed command; takes ownership of `userdata`.
|
||||
pub fn from_vtable(vtable: OakUndoCommandVtable, userdata: *mut c_void) -> Self {
|
||||
/// Wrap any [`Command`] as an undoable command value. This is the
|
||||
/// construction path for whole-struct commands (the app's timeline
|
||||
/// and task commands); one-off edits use [`UndoCommand::from_closures`].
|
||||
pub fn new(command: impl Command + 'static) -> Self {
|
||||
UndoCommand {
|
||||
kind: CommandKind::Vtable { vtable, userdata },
|
||||
inner: Box::new(command),
|
||||
done: false,
|
||||
modified: false,
|
||||
prepared: true,
|
||||
@@ -123,89 +95,57 @@ impl UndoCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// New empty composite command (`olive::MultiUndoCommand`).
|
||||
pub fn multi() -> Self {
|
||||
UndoCommand {
|
||||
kind: CommandKind::Multi(MultiUndoCommand::new()),
|
||||
done: false,
|
||||
modified: false,
|
||||
prepared: true,
|
||||
is_empty: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// New closure-backed command (M14 R3): `redo`/`undo` are plain Rust
|
||||
/// closures, boxed behind the vtable machinery. This is the safe way
|
||||
/// for direct-rlib consumers (the app) to build composite edit
|
||||
/// commands without writing their own `extern "C"` trampolines.
|
||||
/// New closure-backed command: `redo` runs on redo, `undo` on undo.
|
||||
/// The safe way for direct-rlib consumers (the app) to build edit
|
||||
/// commands without writing any callback machinery.
|
||||
pub fn from_closures(
|
||||
redo: impl FnMut() + Send + 'static,
|
||||
undo: impl FnMut() + Send + 'static,
|
||||
) -> Self {
|
||||
let state = Box::into_raw(Box::new(ClosureCommand {
|
||||
UndoCommand::new(ClosureCommand {
|
||||
redo: Box::new(redo),
|
||||
undo: Box::new(undo),
|
||||
}));
|
||||
UndoCommand::from_vtable(
|
||||
OakUndoCommandVtable {
|
||||
redo: Some(closure_redo),
|
||||
undo: Some(closure_undo),
|
||||
free_fn: Some(closure_free),
|
||||
},
|
||||
state as *mut c_void,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// The invariant bottom-of-stack command ("New/Open Project"); all
|
||||
/// callbacks are no-ops and it is never undoable.
|
||||
/// New empty composite command (`olive::MultiUndoCommand`).
|
||||
pub fn multi() -> Self {
|
||||
UndoCommand::new(MultiUndoCommand::new())
|
||||
}
|
||||
|
||||
/// The invariant bottom-of-stack command ("New/Open Project"); a
|
||||
/// no-op in both directions and never undoable.
|
||||
pub(crate) fn empty() -> Self {
|
||||
UndoCommand {
|
||||
kind: CommandKind::Vtable {
|
||||
vtable: OakUndoCommandVtable {
|
||||
redo: None,
|
||||
undo: None,
|
||||
free_fn: None,
|
||||
},
|
||||
userdata: std::ptr::null_mut(),
|
||||
},
|
||||
done: false,
|
||||
modified: false,
|
||||
prepared: true,
|
||||
is_empty: true,
|
||||
}
|
||||
let mut command = UndoCommand::from_closures(|| {}, || {});
|
||||
command.is_empty = true;
|
||||
command
|
||||
}
|
||||
|
||||
/// Add `child` to a composite command (takes one reference).
|
||||
pub fn multi_add_child(&mut self, child: UndoCommand) {
|
||||
match &mut self.kind {
|
||||
CommandKind::Multi(m) => m.add_child(child),
|
||||
CommandKind::Vtable { .. } => {
|
||||
panic!("multi_add_child on a non-multi command")
|
||||
}
|
||||
match self.inner.as_multi_mut() {
|
||||
Some(m) => m.add_child(child),
|
||||
None => panic!("multi_add_child on a non-multi command"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of children of a composite command.
|
||||
/// Number of children of a composite command (0 for plain commands).
|
||||
pub fn multi_child_count(&self) -> usize {
|
||||
match &self.kind {
|
||||
CommandKind::Multi(m) => m.child_count(),
|
||||
CommandKind::Vtable { .. } => 0,
|
||||
}
|
||||
self.inner.as_multi().map_or(0, |m| m.child_count())
|
||||
}
|
||||
|
||||
/// Reference to the child at `index` of a composite command.
|
||||
pub fn multi_child(&self, index: usize) -> Result<&UndoCommand> {
|
||||
match &self.kind {
|
||||
CommandKind::Multi(m) => m.child(index),
|
||||
CommandKind::Vtable { .. } => Err(Error::Invalid),
|
||||
}
|
||||
self.inner
|
||||
.as_multi()
|
||||
.map_or(Err(Error::Invalid), |m| m.child(index))
|
||||
}
|
||||
|
||||
/// Mutable reference to the child at `index` of a composite command.
|
||||
pub fn multi_child_mut(&mut self, index: usize) -> Result<&mut UndoCommand> {
|
||||
match &mut self.kind {
|
||||
CommandKind::Multi(m) => m.child_mut(index),
|
||||
CommandKind::Vtable { .. } => Err(Error::Invalid),
|
||||
match self.inner.as_multi_mut() {
|
||||
Some(m) => m.child_mut(index),
|
||||
None => Err(Error::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +179,8 @@ impl UndoCommand {
|
||||
self.undo_now();
|
||||
}
|
||||
|
||||
/// `has_prepared`: whether `prepare()` has run (vtable commands have
|
||||
/// no prepare; always true).
|
||||
/// `has_prepared`: whether `prepare()` has run (commands have no
|
||||
/// prepare; always true).
|
||||
pub fn has_prepared(&self) -> bool {
|
||||
self.prepared
|
||||
}
|
||||
@@ -267,49 +207,17 @@ impl UndoCommand {
|
||||
|
||||
/// Whether this is a composite (`MultiUndoCommand`) command.
|
||||
pub(crate) fn is_multi(&self) -> bool {
|
||||
matches!(self.kind, CommandKind::Multi(_))
|
||||
self.inner.as_multi().is_some()
|
||||
}
|
||||
|
||||
/// Run the operation (virtual dispatch).
|
||||
fn redo(&mut self) {
|
||||
match &mut self.kind {
|
||||
CommandKind::Vtable { vtable, userdata } => {
|
||||
if let Some(f) = vtable.redo {
|
||||
// SAFETY: `userdata` is caller-supplied and outlives
|
||||
// the command (owned by it). The callback is the one
|
||||
// registered by the caller.
|
||||
unsafe { f(*userdata) }
|
||||
}
|
||||
}
|
||||
CommandKind::Multi(m) => m.redo(),
|
||||
}
|
||||
self.inner.redo();
|
||||
}
|
||||
|
||||
/// Reverse the operation (virtual dispatch).
|
||||
fn undo(&mut self) {
|
||||
match &mut self.kind {
|
||||
CommandKind::Vtable { vtable, userdata } => {
|
||||
if let Some(f) = vtable.undo {
|
||||
// SAFETY: as in `redo`.
|
||||
unsafe { f(*userdata) }
|
||||
}
|
||||
}
|
||||
CommandKind::Multi(m) => m.undo(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UndoCommand {
|
||||
/// Invokes the vtable `free_fn` on `userdata` (composites free
|
||||
/// children transitively).
|
||||
fn drop(&mut self) {
|
||||
if let CommandKind::Vtable { vtable, userdata } = &self.kind {
|
||||
if let Some(f) = vtable.free_fn {
|
||||
// SAFETY: `userdata` is owned by the command; this is the
|
||||
// final release.
|
||||
unsafe { f(*userdata) }
|
||||
}
|
||||
}
|
||||
self.inner.undo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,43 +229,14 @@ struct ClosureCommand {
|
||||
undo: Box<dyn FnMut() + Send>,
|
||||
}
|
||||
|
||||
/// `redo` trampoline for closure commands.
|
||||
///
|
||||
/// # Safety
|
||||
/// `userdata` must be the `Box<ClosureCommand>` produced by
|
||||
/// [`UndoCommand::from_closures`]; the owning command keeps it alive.
|
||||
unsafe extern "C" fn closure_redo(userdata: *mut c_void) {
|
||||
if userdata.is_null() {
|
||||
return;
|
||||
impl Command for ClosureCommand {
|
||||
fn redo(&mut self) {
|
||||
(self.redo)();
|
||||
}
|
||||
// SAFETY: per the from_closures contract; the command owns the box.
|
||||
let state = unsafe { &mut *(userdata as *mut ClosureCommand) };
|
||||
(state.redo)();
|
||||
}
|
||||
|
||||
/// `undo` trampoline for closure commands.
|
||||
///
|
||||
/// # Safety
|
||||
/// As [`closure_redo`].
|
||||
unsafe extern "C" fn closure_undo(userdata: *mut c_void) {
|
||||
if userdata.is_null() {
|
||||
return;
|
||||
fn undo(&mut self) {
|
||||
(self.undo)();
|
||||
}
|
||||
// SAFETY: per the from_closures contract; the command owns the box.
|
||||
let state = unsafe { &mut *(userdata as *mut ClosureCommand) };
|
||||
(state.undo)();
|
||||
}
|
||||
|
||||
/// `free` trampoline for closure commands: drops the boxed closures.
|
||||
///
|
||||
/// # Safety
|
||||
/// As [`closure_redo`]; called at most once (the command's final release).
|
||||
unsafe extern "C" fn closure_free(userdata: *mut c_void) {
|
||||
if userdata.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: per the from_closures contract; this is the final release.
|
||||
unsafe { drop(Box::from_raw(userdata as *mut ClosureCommand)) };
|
||||
}
|
||||
|
||||
/// `olive::MultiUndoCommand` — a composite of child commands.
|
||||
@@ -369,9 +248,6 @@ pub struct MultiUndoCommand {
|
||||
children: Vec<UndoCommand>,
|
||||
}
|
||||
|
||||
/// See `UndoCommand::Send`.
|
||||
unsafe impl Send for MultiUndoCommand {}
|
||||
|
||||
impl MultiUndoCommand {
|
||||
/// New empty composite.
|
||||
pub fn new() -> Self {
|
||||
@@ -421,250 +297,20 @@ impl Default for MultiUndoCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// Heap box behind a command handle's `ctx`. Mirrors the C++
|
||||
/// `OakUndoCommandBox` (`{command, owns, refs}`).
|
||||
pub(crate) struct CommandBox {
|
||||
/// Raw pointer to the command value.
|
||||
command: *mut UndoCommand,
|
||||
/// Whether this box owns (and must destroy) `command`.
|
||||
owns: bool,
|
||||
/// Atomic reference count.
|
||||
refs: AtomicU32,
|
||||
}
|
||||
impl Command for MultiUndoCommand {
|
||||
fn redo(&mut self) {
|
||||
MultiUndoCommand::redo(self);
|
||||
}
|
||||
|
||||
/// Owned handle for a fresh command value (refcount 1, owns `cmd`).
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned handle owns `cmd`; release it with `command_release`.
|
||||
pub unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
|
||||
let boxed = Box::into_raw(Box::new(CommandBox {
|
||||
command: Box::into_raw(Box::new(cmd)),
|
||||
owns: true,
|
||||
refs: AtomicU32::new(1),
|
||||
})) as *mut c_void;
|
||||
CHandle {
|
||||
ctx: boxed,
|
||||
addref: Some(command_addref),
|
||||
release: Some(command_release),
|
||||
abi_version: OAKUNDO_ABI_VERSION,
|
||||
fn undo(&mut self) {
|
||||
MultiUndoCommand::undo(self);
|
||||
}
|
||||
|
||||
fn as_multi(&self) -> Option<&MultiUndoCommand> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn as_multi_mut(&mut self) -> Option<&mut MultiUndoCommand> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed handle for a command owned elsewhere (release frees only
|
||||
/// the shell).
|
||||
///
|
||||
/// # Safety
|
||||
/// `cmd` must outlive the returned handle.
|
||||
pub unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
|
||||
let boxed = Box::into_raw(Box::new(CommandBox {
|
||||
command: cmd,
|
||||
owns: false,
|
||||
refs: AtomicU32::new(1),
|
||||
})) as *mut c_void;
|
||||
CHandle {
|
||||
ctx: boxed,
|
||||
addref: Some(command_addref),
|
||||
release: Some(command_release),
|
||||
abi_version: OAKUNDO_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only view of the command behind `ctx`; `None` for a null handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
let boxed = ctx as *mut CommandBox;
|
||||
if (*boxed).command.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(&*(*boxed).command)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable view of the command behind `ctx`; `None` for a null handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
let boxed = ctx as *mut CommandBox;
|
||||
if (*boxed).command.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(&mut *(*boxed).command)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the command value out of an owning box, turning it into a
|
||||
/// non-owning shell (the C++ `mark_container_owned` transfer).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let boxed = ctx as *mut CommandBox;
|
||||
if !(*boxed).owns || (*boxed).command.is_null() {
|
||||
return Err(Error::State);
|
||||
}
|
||||
let value = Box::from_raw((*boxed).command);
|
||||
(*boxed).command = std::ptr::null_mut();
|
||||
(*boxed).owns = false;
|
||||
Ok(*value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment a command box's refcount (the `addref` function pointer).
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid `CommandBox`.
|
||||
pub(crate) unsafe extern "C" fn command_addref(ctx: *mut c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut CommandBox;
|
||||
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement a command box's refcount; destroys at zero. Owned boxes
|
||||
/// free their command first, then the shell; borrowed boxes free only
|
||||
/// the shell.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid `CommandBox`.
|
||||
pub(crate) unsafe extern "C" fn command_release(ctx: *mut c_void) {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = ctx as *mut CommandBox;
|
||||
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
if (*boxed).owns && !(*boxed).command.is_null() {
|
||||
drop(Box::from_raw((*boxed).command));
|
||||
}
|
||||
drop(Box::from_raw(boxed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle-level command API (sunk from the former C ABI export layer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create a vtable-backed command handle (refcount 1); a `NULL` vtable
|
||||
/// yields an empty handle (`oakundo_command_init`).
|
||||
pub unsafe fn command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle {
|
||||
guard_handle(|| unsafe {
|
||||
if vtable.is_null() {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
let table = *vtable;
|
||||
Ok(command_from_owned(UndoCommand::from_vtable(table, userdata)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an empty multi command handle (refcount 1)
|
||||
/// (`oakundo_command_init_multi`).
|
||||
pub fn command_init_multi() -> CHandle {
|
||||
guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) })
|
||||
}
|
||||
|
||||
/// Add `child` to a multi command handle (the stack/multi takes one
|
||||
/// child reference); `E_INVALID` for an empty handle or a non-multi
|
||||
/// parent (`oakundo_command_multi_add_child`).
|
||||
pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?;
|
||||
if !parent.is_multi() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let child_cmd = command_take(child.ctx)?;
|
||||
parent.multi_add_child(child_cmd);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of children of a multi command handle; `E_INVALID` for an
|
||||
/// empty handle or a non-multi parent (`oakundo_command_multi_child_count`).
|
||||
pub unsafe fn command_multi_child_count(multi: CHandle, out_count: *mut c_int) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_count.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
|
||||
if !parent.is_multi() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
*out_count = parent.multi_child_count() as c_int;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a borrowed handle to the child at `index` of a multi command
|
||||
/// (the returned handle carries its own shell ref); `E_NOT_FOUND` for
|
||||
/// an out-of-range index (`oakundo_command_multi_child`).
|
||||
pub unsafe fn command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_child.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
|
||||
if !parent.is_multi() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
if index < 0 {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let child = parent.multi_child(index as usize)?;
|
||||
let ptr = child as *const UndoCommand as *mut UndoCommand;
|
||||
*out_child = command_from_borrowed(ptr);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a command handle's redo (no-op when already done)
|
||||
/// (`oakundo_command_redo_now`).
|
||||
pub fn command_redo_now(command: CHandle) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
|
||||
c.redo_now();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a command handle's undo (no-op when not done)
|
||||
/// (`oakundo_command_undo_now`).
|
||||
pub fn command_undo_now(command: CHandle) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
|
||||
c.undo_now();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Release a command handle in place: run the release callback, then
|
||||
/// clear `ctx`. `NULL` / empty handles are no-ops
|
||||
/// (`oakundo_command_free`).
|
||||
pub unsafe fn command_free(command: *mut CHandle) {
|
||||
guard_void(|| unsafe {
|
||||
if command.is_null() || (*command).ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = (*command).release {
|
||||
release((*command).ctx);
|
||||
}
|
||||
(*command).ctx = std::ptr::null_mut();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `olive::UndoStack` — the undo/redo history stack. Mirrors
|
||||
//! `src/undo/src/undostack.h` and `include/undo/undostack.h`.
|
||||
//! `src/undo/src/undostack.h`.
|
||||
//!
|
||||
//! Two deques: `commands` (done commands, oldest at the front) and
|
||||
//! `undone` (undone commands, most-recently-undone at the front). The
|
||||
//! fresh stack holds a single "New/Open Project" empty command so that
|
||||
//! `can_undo` is false at the bottom (see `undostack.cpp::clear`).
|
||||
//!
|
||||
//! The former handle-level exports (`undostack_init` … `undostack_free`)
|
||||
//! are gone with the C ABI: the stack is a plain owned value, shared
|
||||
//! process-wide through the [`crate::global`] module.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::{c_char, c_int, CStr};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::error::{Error, OAKUNDO_E_FAILED, Result};
|
||||
use crate::handle::{get, guard, guard_handle, guard_void, make_owned, CHandle};
|
||||
use crate::undocommand::{command_take, UndoCommand};
|
||||
use crate::error::Result;
|
||||
use crate::undocommand::UndoCommand;
|
||||
|
||||
/// Maximum number of retained history rows (`k_max_undo_commands`).
|
||||
pub const K_MAX_UNDO_COMMANDS: usize = 200;
|
||||
@@ -67,9 +67,6 @@ pub struct UndoStack {
|
||||
undone: VecDeque<CommandEntry>,
|
||||
}
|
||||
|
||||
/// See `UndoCommand::Send`.
|
||||
unsafe impl Send for UndoStack {}
|
||||
|
||||
impl UndoStack {
|
||||
/// New stack; contains a single empty "New/Open Project" command.
|
||||
pub fn new() -> Self {
|
||||
@@ -198,7 +195,7 @@ impl UndoStack {
|
||||
/// Whether the row at `row` is currently done.
|
||||
pub fn command_is_done(&self, row: i64) -> Result<bool> {
|
||||
if row < 0 || row >= self.command_count() {
|
||||
return Err(Error::NotFound);
|
||||
return Err(crate::error::Error::NotFound);
|
||||
}
|
||||
Ok(row < self.commands.len() as i64)
|
||||
}
|
||||
@@ -206,7 +203,7 @@ impl UndoStack {
|
||||
/// Label of the history row at `row`.
|
||||
pub fn command_name(&self, row: i64) -> Result<&str> {
|
||||
if row < 0 || row >= self.command_count() {
|
||||
return Err(Error::NotFound);
|
||||
return Err(crate::error::Error::NotFound);
|
||||
}
|
||||
let row = row as usize;
|
||||
if row < self.commands.len() {
|
||||
@@ -222,204 +219,3 @@ impl Default for UndoStack {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle-level stack API (sunk from the former C ABI export layer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Read a NUL-terminated C string; `NULL` yields an empty string
|
||||
/// (mirrors the C++ `name ? name : ""`).
|
||||
fn read_name(name: *const c_char) -> String {
|
||||
if name.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
// SAFETY: `name` is a valid NUL-terminated string supplied by the
|
||||
// caller, or NULL (already handled).
|
||||
unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an
|
||||
/// empty handle. A poisoned mutex is recovered (its inner value is still
|
||||
/// valid).
|
||||
pub fn with_stack<R>(stack: &CHandle, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
|
||||
// SAFETY: the stack handle always boxes a `Mutex<UndoStack>` (created
|
||||
// by `undostack_init`).
|
||||
let m = unsafe { get::<Mutex<UndoStack>>(stack) }.ok_or(Error::Invalid)?;
|
||||
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
/// Create a fresh stack handle (refcount 1) (`oakundo_undostack_init`).
|
||||
pub fn undostack_init() -> CHandle {
|
||||
guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new()))))
|
||||
}
|
||||
|
||||
/// Release a stack handle in place; `NULL` / empty handles are no-ops
|
||||
/// (`oakundo_undostack_free`).
|
||||
pub fn undostack_free(stack: *mut CHandle) {
|
||||
guard_void(|| unsafe {
|
||||
if stack.is_null() || (*stack).ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = (*stack).release {
|
||||
release((*stack).ctx);
|
||||
}
|
||||
(*stack).ctx = std::ptr::null_mut();
|
||||
})
|
||||
}
|
||||
|
||||
/// Push a command handle onto the stack (redo then record; the stack
|
||||
/// takes ownership of the command value, leaving a non-owning shell)
|
||||
/// (`oakundo_undostack_push`).
|
||||
pub fn undostack_push(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(&stack, |s| unsafe {
|
||||
let cmd = command_take(command.ctx)?;
|
||||
let name = read_name(name);
|
||||
s.push(cmd, &name);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Push an already-executed command handle (redo skipped)
|
||||
/// (`oakundo_undostack_push_pre_executed`).
|
||||
pub fn undostack_push_pre_executed(stack: CHandle, command: CHandle, name: *const c_char) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(&stack, |s| unsafe {
|
||||
let cmd = command_take(command.ctx)?;
|
||||
let name = read_name(name);
|
||||
s.push_pre_executed(cmd, &name);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Undo one command on the stack (`oakundo_undostack_undo`).
|
||||
pub fn undostack_undo(stack: CHandle) -> c_int {
|
||||
guard(|| with_stack(&stack, |s| s.undo()))
|
||||
}
|
||||
|
||||
/// Redo one command on the stack (`oakundo_undostack_redo`).
|
||||
pub fn undostack_redo(stack: CHandle) -> c_int {
|
||||
guard(|| with_stack(&stack, |s| s.redo()))
|
||||
}
|
||||
|
||||
/// Jump to a done-command index (clamped to 0; `index` is i64)
|
||||
/// (`oakundo_undostack_jump`).
|
||||
pub fn undostack_jump(stack: CHandle, index: i64) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(&stack, |s| {
|
||||
s.jump(index);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear the stack back to the empty bottom command
|
||||
/// (`oakundo_undostack_clear`).
|
||||
pub fn undostack_clear(stack: CHandle) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(&stack, |s| {
|
||||
s.clear();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Write whether an undo is possible (`oakundo_undostack_can_undo`).
|
||||
pub fn undostack_can_undo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
with_stack(&stack, |s| {
|
||||
*out_value = if s.can_undo() { 1 } else { 0 };
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Write whether a redo is possible (`oakundo_undostack_can_redo`).
|
||||
pub fn undostack_can_redo(stack: CHandle, out_value: *mut c_int) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
with_stack(&stack, |s| {
|
||||
*out_value = if s.can_redo() { 1 } else { 0 };
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the total history row count (`oakundo_undostack_count`).
|
||||
pub fn undostack_count(stack: CHandle, out_count: *mut i64) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_count.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
with_stack(&stack, |s| {
|
||||
*out_count = s.command_count();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the done-command count (`oakundo_undostack_index`).
|
||||
pub fn undostack_index(stack: CHandle, out_index: *mut i64) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_index.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
with_stack(&stack, |s| {
|
||||
*out_index = s.done_count();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Two-stage label getter for the row at `row`: returns the required
|
||||
/// size (including NUL), or an error code; copies (truncating) when a
|
||||
/// buffer is supplied (`oakundo_undostack_command_text`).
|
||||
pub fn undostack_command_text(stack: CHandle, row: i64, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
|
||||
with_stack(&stack, |s| {
|
||||
if row < 0 || row >= s.command_count() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let name = s.command_name(row)?;
|
||||
let required = (name.len() + 1) as i32;
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let copy_len = name.len().min((buf_size as usize).saturating_sub(1));
|
||||
let bytes = name.as_bytes();
|
||||
// SAFETY: `buf` points to `buf_size` writable bytes and we
|
||||
// write at most `copy_len` (+ one NUL) of them.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
|
||||
*buf.add(copy_len) = 0;
|
||||
}
|
||||
}
|
||||
Ok(required)
|
||||
})
|
||||
}));
|
||||
match result {
|
||||
Ok(Ok(required)) => required,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKUNDO_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write whether the row at `row` is done (`oakundo_undostack_command_is_done`).
|
||||
pub fn undostack_command_is_done(stack: CHandle, row: i64, out_value: *mut c_int) -> c_int {
|
||||
guard(|| unsafe {
|
||||
if out_value.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
with_stack(&stack, |s| {
|
||||
*out_value = if s.command_is_done(row)? { 1 } else { 0 };
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user