refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/ oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain Rust, CHandle marshalling shrinks to the oakengine boundary, and tests call the Rust APIs directly (pure C-ABI wrapper tests removed where the domain layer already covers the behavior). exporter.h family implemented: oakengine_export_render (CLI contract), oakengine_export_render_with_params (was a stub), last_error and progress callback; synchronous path reuses task_create_export + start_sync. Fixes on the way: oaktask video ticket self-deadlock, audio params dropped on the export path, codec encoder AAC slicing and H.264 time base. Real-mp4 tests cover both entry points, progress and the illegal-argument matrix. Also: oakstorage session maps null project handles to None (version- info path), configstore test double literal 3.14 -> 3.15 (clippy PI lint), oakaudio output callback scratch buffer + env-aware P1 test, cli media round-trip test uses a generated 16-frame clip (no more minute-long debug runs).
This commit is contained in:
@@ -1,427 +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/>.
|
||||
|
||||
//! C ABI export layer: implements `include/undo/*.h` verbatim.
|
||||
//!
|
||||
//! Organization: one submodule per public header. The authoritative
|
||||
//! function list is the header itself; each submodule below carries a
|
||||
//! complete inventory comment plus export stubs. Bodies only unwrap
|
||||
//! handles, call the safe Rust domains, and map results through
|
||||
//! [`crate::handle::guard*`]. `include/undo/error.h` exports macros
|
||||
//! only, so it is folded into this module's preamble instead of getting
|
||||
//! its own submodule.
|
||||
//!
|
||||
//! Handles cross the boundary by field copy between the public
|
||||
//! [`OakUndoCommand`]/[`OakUndoStack`] structs and the crate-internal
|
||||
//! [`CHandle`]. Stacks live behind a `Mutex` (shared state), commands in
|
||||
//! a `CommandBox`.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{guard, guard_handle, guard_void, make_owned, CHandle};
|
||||
use crate::undocommand::{
|
||||
command_from_borrowed, command_from_owned, command_take, command_to_mut, command_to_ref,
|
||||
OakUndoCommandVtable, UndoCommand,
|
||||
};
|
||||
use crate::undostack::UndoStack;
|
||||
|
||||
/// `include/undo/error.h` — macros only, no exported functions.
|
||||
///
|
||||
/// `OAKUNDO_OK` and the `OAKUNDO_E_*` codes are mirrored as
|
||||
/// [`crate::error`] constants; `OAKUNDO_ABI_VERSION` lives in
|
||||
/// [`crate::handle`].
|
||||
|
||||
/// `include/undo/undocommand.h` handle type — `OakUndoCommand`
|
||||
/// (`{ctx, addref, release, abi_version}`). Single-lib unification:
|
||||
/// aliases the shared [`CHandle`] (identical layout; the C ABI is
|
||||
/// unchanged).
|
||||
pub type OakUndoCommand = CHandle;
|
||||
|
||||
/// `include/undo/undostack.h` handle type — `OakUndoStack`. Single-lib
|
||||
/// unification: aliases the shared [`CHandle`].
|
||||
pub type OakUndoStack = CHandle;
|
||||
|
||||
/// 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).
|
||||
fn with_stack<R>(stack: OakUndoStack, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
|
||||
// Keep a local handle so `get`'s returned reference lives for the
|
||||
// whole call (its lifetime ties to the `&CHandle` argument).
|
||||
let ch = stack;
|
||||
let m = unsafe { crate::handle::get::<Mutex<UndoStack>>(&ch) }.ok_or(Error::Invalid)?;
|
||||
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
|
||||
f(&mut guard)
|
||||
}
|
||||
|
||||
/// 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() }
|
||||
}
|
||||
}
|
||||
|
||||
/// `include/undo/undocommand.h` exports (complete inventory):
|
||||
/// command_init / command_init_multi / command_multi_add_child /
|
||||
/// command_multi_child_count / command_multi_child / command_redo_now /
|
||||
/// command_undo_now / command_free.
|
||||
pub mod command {
|
||||
use super::*;
|
||||
|
||||
/// `oakundo_command_init`: vtable-backed command, refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_init(
|
||||
vtable: *const OakUndoCommandVtable,
|
||||
userdata: *mut c_void,
|
||||
) -> OakUndoCommand {
|
||||
guard_handle(|| unsafe {
|
||||
if vtable.is_null() {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
let table = *vtable;
|
||||
Ok(command_from_owned(UndoCommand::from_vtable(
|
||||
table, userdata,
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_init_multi`: empty multi command, refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_init_multi() -> OakUndoCommand {
|
||||
guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) })
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_add_child` (stack takes one child ref).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_multi_add_child(
|
||||
multi: OakUndoCommand,
|
||||
child: OakUndoCommand,
|
||||
) -> 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(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_child_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_multi_child_count(
|
||||
multi: OakUndoCommand,
|
||||
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(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_multi_child` (returned handle carries own ref).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_multi_child(
|
||||
multi: OakUndoCommand,
|
||||
index: c_int,
|
||||
out_child: *mut OakUndoCommand,
|
||||
) -> 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(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_redo_now`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_redo_now(command: OakUndoCommand) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
|
||||
c.redo_now();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_undo_now`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_undo_now(command: OakUndoCommand) -> c_int {
|
||||
guard(|| unsafe {
|
||||
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
|
||||
c.undo_now();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_command_free`: NULL/empty no-op; clears `command->ctx`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_command_free(command: *mut OakUndoCommand) {
|
||||
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();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `include/undo/undostack.h` exports (complete inventory):
|
||||
/// undostack_init / undostack_free / undostack_push /
|
||||
/// undostack_push_pre_executed / undostack_undo / undostack_redo /
|
||||
/// undostack_jump / undostack_clear / undostack_can_undo /
|
||||
/// undostack_can_redo / undostack_count / undostack_index /
|
||||
/// undostack_command_text / undostack_command_is_done.
|
||||
pub mod undostack {
|
||||
use super::*;
|
||||
|
||||
/// `oakundo_undostack_init`: fresh stack, refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_init() -> OakUndoStack {
|
||||
guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new()))))
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_free`: NULL/empty no-op; clears `stack->ctx`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_free(stack: *mut OakUndoStack) {
|
||||
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();
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_push`: redo then record; drops redoable tail.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_push(
|
||||
stack: OakUndoStack,
|
||||
command: OakUndoCommand,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_push_pre_executed`: record without redoing.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_push_pre_executed(
|
||||
stack: OakUndoStack,
|
||||
command: OakUndoCommand,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_undo`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_undo(stack: OakUndoStack) -> c_int {
|
||||
guard(|| with_stack(stack, |s| s.undo()))
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_redo`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_redo(stack: OakUndoStack) -> c_int {
|
||||
guard(|| with_stack(stack, |s| s.redo()))
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_jump` (clamped to 0; `index` is i64).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_jump(stack: OakUndoStack, index: i64) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(stack, |s| {
|
||||
s.jump(index);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_clear`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_clear(stack: OakUndoStack) -> c_int {
|
||||
guard(|| {
|
||||
with_stack(stack, |s| {
|
||||
s.clear();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_can_undo`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_can_undo(
|
||||
stack: OakUndoStack,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_can_redo`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_can_redo(
|
||||
stack: OakUndoStack,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_count(
|
||||
stack: OakUndoStack,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_index`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_index(
|
||||
stack: OakUndoStack,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_command_text` (two-stage string getter).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_command_text(
|
||||
stack: OakUndoStack,
|
||||
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(_) => crate::error::OAKUNDO_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakundo_undostack_command_is_done`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakundo_undostack_command_is_done(
|
||||
stack: OakUndoStack,
|
||||
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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod handle;
|
||||
pub mod undocommand;
|
||||
pub mod undostack;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! ## Command boxes
|
||||
//!
|
||||
//! Commands cross the FFI boundary through a dedicated [`CommandBox`]
|
||||
//! 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
|
||||
@@ -36,12 +36,18 @@
|
||||
//! 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_void;
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::handle::{CHandle, OAKUNDO_ABI_VERSION};
|
||||
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
|
||||
@@ -348,7 +354,7 @@ pub(crate) struct CommandBox {
|
||||
///
|
||||
/// # Safety
|
||||
/// The returned handle owns `cmd`; release it with `command_release`.
|
||||
pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
|
||||
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,
|
||||
@@ -367,7 +373,7 @@ pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
|
||||
///
|
||||
/// # Safety
|
||||
/// `cmd` must outlive the returned handle.
|
||||
pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
|
||||
pub unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
|
||||
let boxed = Box::into_raw(Box::new(CommandBox {
|
||||
command: cmd,
|
||||
owns: false,
|
||||
@@ -385,7 +391,7 @@ pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
|
||||
pub unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return None;
|
||||
@@ -402,7 +408,7 @@ pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCom
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
|
||||
pub unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return None;
|
||||
@@ -420,7 +426,7 @@ pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut Und
|
||||
///
|
||||
/// # Safety
|
||||
/// `ctx` must come from a valid, live `CommandBox`.
|
||||
pub(crate) unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
|
||||
pub unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
|
||||
unsafe {
|
||||
if ctx.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
@@ -470,3 +476,113 @@ pub(crate) unsafe extern "C" fn command_release(ctx: *mut c_void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 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 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 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 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();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,9 +23,13 @@
|
||||
//! `can_undo` is false at the bottom (see `undostack.cpp::clear`).
|
||||
|
||||
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, Result};
|
||||
use crate::undocommand::UndoCommand;
|
||||
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};
|
||||
|
||||
/// Maximum number of retained history rows (`k_max_undo_commands`).
|
||||
pub const K_MAX_UNDO_COMMANDS: usize = 200;
|
||||
@@ -218,3 +222,204 @@ 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