Files
oak-editor/crates/oakundo/src/ffi.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

428 lines
12 KiB
Rust

// 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(())
})
})
}
}