Files
oak-editor/crates/oakengine/src/handle.rs
T
Mike-Solar ab1a2e9c7b 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).
2026-08-16 00:33:45 +08:00

345 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/>.
//! Facade scaffolding: engine opaque pointers as thin newtype wrappers
//! around module [`CHandle`] values.
//!
//! Every `OakEngine*` opaque type from `engine/include/oakengine/*.h`
//! is a `#[repr(C)]` struct holding one [`CHandle`] (the module C ABI's
//! `{ctx, addref, release, abi_version}` value handle, see
//! `include/common/handle.h`). The C caller only ever sees an opaque
//! pointer, so the field layout is ours to choose; the wrappers exist so
//! the exported `oakengine_*` signatures match the frozen headers
//! verbatim.
//!
//! A box is created by [`box_handle`] and freed by [`free_box`]: freeing
//! calls the handle's `release` (for a module-borrowed handle that only
//! releases the handle shell, never the graph-owned object) and then
//! deallocates the box. Consuming exports (`oakengine_*_free`,
//! `oakengine_undo_push`, ...) call [`free_box`].
//!
//! String output follows the engine's buf/size convention (see
//! [`write_string`]): the return value is the required length including
//! the terminating NUL; negative values are error codes.
use std::ffi::{c_char, c_int};
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::error::{Error, Result};
/// 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
/// by every module crate, so the facade can pass a handle straight into a
/// module's `pub` Rust functions without an `extern "C"` declaration.
/// `Clone + Copy + Send + Sync` come from the shared type.
pub use oakcore_rs::handle::CHandle;
/// Engine-side boxed payloads holding the oaknode domain (single-lib
/// unification). Every `oakengine_*` node-family handle ultimately wraps
/// one of these behind a [`CHandle`]:
///
/// - projects box [`domain::ProjectArc`] (`Arc<Mutex<Project>>`);
/// - nodes, blocks, tracks, footage, sequences and folders box a
/// [`domain::NodeRef`] (`(Arc<Mutex<Project>>, NodeId)` — the
/// oaknode crate's `project::NodeRef` value type).
///
/// The box is created through `oaknode::handle::make_owned` (refcounted
/// shell + release callback), so the facade's existing
/// [`box_handle`]/[`free_box`] discipline (and the addref copies the
/// engine takes) works unchanged.
pub mod domain {
use std::sync::{Arc, Mutex};
use oaknode::id::NodeId;
use crate::handle::CHandle;
/// Engine-side boxed payload for project handles: shared ownership of
/// the oaknode domain project (its graph, settings, filename state).
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
/// Engine-side boxed payload for node/block/track/footage/sequence/
/// folder handles: a reference into a project's graph. Reuses the
/// oaknode crate's own `NodeRef` value type (project + id + owned
/// flag); a stale id fails validation instead of aliasing.
pub type NodeRef = oaknode::project::NodeRef;
/// Box a project payload behind a refcounted handle.
pub fn box_project(project: ProjectArc) -> CHandle {
oaknode::handle::make_owned(project)
}
/// Box a node reference behind a refcounted handle. `owned` marks
/// detached (factory-created) nodes so the engine's debug alive
/// counter accounts them exactly once.
pub fn box_node(project: ProjectArc, id: NodeId, owned: bool) -> CHandle {
oaknode::handle::make_owned(NodeRef::new(project, id, owned))
}
/// Borrow the project payload behind a handle.
///
/// # Safety
/// `h` must be a live handle created by [`box_project`] (or empty).
pub unsafe fn project_of(h: &CHandle) -> Option<&ProjectArc> {
// SAFETY: forwarded to the oaknode handle contract.
unsafe { oaknode::handle::get::<ProjectArc>(h) }
}
/// Borrow the node-reference payload behind a handle.
///
/// # Safety
/// `h` must be a live handle created by [`box_node`] (or empty).
pub unsafe fn node_ref_of(h: &CHandle) -> Option<&NodeRef> {
// SAFETY: forwarded to the oaknode handle contract.
unsafe { oaknode::handle::get::<NodeRef>(h) }
}
/// Mutable view of the node-reference payload (used by the graph
/// transfer paths, which rewrite the shared box in place — the
/// "write_node_ref" semantics).
///
/// # Safety
/// `h` must be a live handle created by [`box_node`]; the caller must
/// hold exclusive access to the boxed value.
pub unsafe fn node_ref_mut(h: &CHandle) -> Option<&mut NodeRef> {
// SAFETY: forwarded to the shared-box contract.
unsafe { boxed_mut::<NodeRef>(h) }
}
/// Mutable typed view into an oaknode-style `RefBox` payload (the
/// oaknode crate exposes only a read-only `get`; this mirrors its
/// box layout — `refs`/`value` are `pub` fields).
///
/// # Safety
/// `h` must be a live handle boxing `T`; the caller must hold
/// exclusive access to the boxed value.
pub unsafe fn boxed_mut<T: 'static>(h: &CHandle) -> Option<&mut T> {
if h.ctx.is_null() {
return None;
}
// SAFETY: contract above; the box is an
// `oaknode::handle::RefBox<T>`.
unsafe { Some(&mut (*(h.ctx as *mut oaknode::handle::RefBox<T>)).value) }
}
}
/// Engine opaque handle types, one per `typedef struct OakEngine*` in
/// `engine/include/oakengine/*.h`. All are thin newtype wrappers around a
/// [`CHandle`] value with a uniform extraction surface ([`EngineBox`]).
macro_rules! engine_handle {
($($name:ident),* $(,)?) => {
$(
/// Opaque engine handle: thin newtype wrapper around a module
/// [`CHandle`] value.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct $name {
/// The wrapped module handle.
pub handle: CHandle,
}
impl EngineBox for $name {
fn boxed_new(handle: CHandle) -> Self {
$name { handle }
}
fn handle(&self) -> CHandle {
self.handle
}
}
)*
};
}
engine_handle! {
OakEngineAudioBuffer,
OakEngineAudioProcessor,
OakEngineBlock,
OakEngineClip,
OakEngineClipboard,
OakEngineColorConfig,
OakEngineColorManager,
OakEngineColorProcessor,
OakEngineEncodingParams,
OakEngineFootage,
OakEngineFrame,
OakEngineFrameCache,
OakEngineKeyframe,
OakEngineMarker,
OakEngineMarkerList,
OakEngineNode,
OakEngineNodeDragger,
OakEnginePlayback,
OakEnginePlaybackCache,
OakEnginePreviewRequest,
OakEngineProject,
OakEngineRenderer,
OakEngineSequence,
OakEngineTask,
OakEngineThumbnailCache,
OakEngineTrack,
OakEngineTrackList,
OakEngineTraverseDb,
OakEngineWaveformCache,
OakEngineWorkarea,
}
/// Uniform construction/extraction surface of the engine opaque types.
pub trait EngineBox: Sized {
/// Build the wrapper from a module handle.
fn boxed_new(handle: CHandle) -> Self;
/// Extract the wrapped module handle (copy).
fn handle(&self) -> CHandle;
}
/// Allocate a heap box for a module handle and return its raw pointer.
/// The box must later be released with [`free_box`].
pub fn box_handle<T: EngineBox>(handle: CHandle) -> *mut T {
Box::into_raw(Box::new(T::boxed_new(handle)))
}
/// Dereference an engine opaque pointer and copy out its module handle.
/// Returns [`Error::Invalid`] for a NULL pointer or an empty handle.
///
/// # Safety
/// `ptr` must point to a live box created by [`box_handle`] (or be
/// NULL).
pub unsafe fn unbox<T: EngineBox>(ptr: *const T) -> Result<CHandle> {
unsafe {
if ptr.is_null() {
return Err(Error::Invalid);
}
let h = (*ptr).handle();
if h.is_null() {
return Err(Error::Invalid);
}
Ok(h)
}
}
/// Free a box created by [`box_handle`]: release the module handle (via
/// its `release` function pointer) and deallocate the box. NULL and
/// empty handles are no-ops. After the call `ptr` is dangling; the
/// caller must not use it again.
///
/// # Safety
/// `ptr` must be a pointer previously returned by [`box_handle`] (or
/// NULL) and must not be freed twice.
pub unsafe fn free_box<T: EngineBox>(ptr: *mut T) {
unsafe {
if ptr.is_null() {
return;
}
let handle = (*ptr).handle();
if let Some(release) = handle.release {
release(handle.ctx);
}
drop(Box::from_raw(ptr));
}
}
/// Panic-catching FFI wrapper for `i32`-returning exports.
pub fn guard<F: FnOnce() -> Result<()>>(f: F) -> c_int {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKENGINE_OK,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKENGINE_E_FAILED,
}
}
/// Panic-catching FFI wrapper for pointer-returning exports.
pub fn guard_ptr<T, F: FnOnce() -> Result<*mut T>>(f: F) -> *mut T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(p)) => p,
_ => std::ptr::null_mut(),
}
}
/// Panic-catching FFI wrapper for `int64_t`-returning exports
/// (`OAKENGINE_E_INVALID` sentinel on error, matching the engine's
/// "no application core exists" convention).
pub fn guard_i64<F: FnOnce() -> Result<i64>>(f: F) -> i64 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(v)) => v,
Ok(Err(_)) => crate::error::OAKENGINE_E_INVALID as i64,
Err(_) => crate::error::OAKENGINE_E_FAILED as i64,
}
}
/// Panic-catching FFI wrapper for void exports.
pub fn guard_void<F: FnOnce()>(f: F) {
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// Panic-catching FFI wrapper for exports whose return value IS the
/// result (a count, a 1/0 flag, a required string length): the closure
/// returns the positive payload, errors are returned as negative codes.
pub fn guard_int<F: FnOnce() -> Result<c_int>>(f: F) -> c_int {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(v)) => v,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKENGINE_E_FAILED,
}
}
/// Write `s` into `buf` following the engine buf/size convention and
/// return the string length **excluding** the terminating NUL (the engine
/// headers' "would-be length"; module getters report len+1 and are
/// converted with [`string_result`]). A NULL `buf` or `buf_size <= 0`
/// only reports the length. `s` is truncated to `buf_size - 1` bytes when
/// it does not fit.
///
/// # Safety
/// `buf` must point to `buf_size` writable bytes when non-NULL and
/// `buf_size > 0`.
pub unsafe fn write_string(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
unsafe {
if !buf.is_null() && buf_size > 0 {
let copy_len = s.len().min((buf_size as usize).saturating_sub(1));
std::ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
}
s.len() as c_int
}
/// Read a NUL-terminated C string; NULL yields an empty string.
///
/// # Safety
/// `s` must be a valid NUL-terminated string, or NULL.
pub unsafe fn read_cstr(s: *const c_char) -> String {
unsafe {
if s.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(s).to_string_lossy().into_owned()
}
}
}
/// Convert a module two-stage getter result to the engine convention.
/// Module getters report the required buffer size **including** the
/// terminating NUL; the engine headers' buf/size convention reports the
/// string **length** (excluding the NUL, mirroring the C++ capi
/// `write_string`). Negative codes pass through untranslated.
pub fn string_result(module_ret: c_int) -> c_int {
if module_ret > 0 {
module_ret - 1
} else {
module_ret
}
}