feat(engine): rename facade to oakengine, build liboakengine.dylib

- src/facade/rust -> src/engine/rust; package oakfacade -> oakengine
- crate-type += cdylib; module crates are real deps; linkage anchors
  force-link module C ABIs into the dylib
- build.rs: -undefined dynamic_lookup for host-provided oakcore_*/fb_*
- nm: 749 oakengine_* + 687 module oak*_* exports; undefined set is
  only the intended host-provided symbols
- worker/cli updated to the new path/name; undo test race fix
This commit is contained in:
2026-08-10 16:55:35 +08:00
parent e563b340ac
commit 0ca9cad448
66 changed files with 3398 additions and 204 deletions
+2 -1
View File
@@ -784,13 +784,14 @@ name = "oakcore-rs"
version = "0.1.0"
[[package]]
name = "oakfacade"
name = "oakengine"
version = "0.1.0"
dependencies = [
"libc",
"oakaudio",
"oakcodec",
"oakcommon",
"oakcore-rs",
"oaknode",
"oakplugin",
"oakrender",
+70
View File
@@ -0,0 +1,70 @@
[package]
name = "oakengine"
version = "0.1.0"
edition = "2021"
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
license = "GPL-3.0-or-later"
[lib]
crate-type = ["cdylib", "staticlib", "rlib"]
[profile.release]
# FFI discipline: panics must be catchable at every exported entry.
panic = "unwind"
[dependencies]
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
# the shm error formatting in src/ipc.rs.
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
# shared-memory frame-slot transport (src/ipc.rs).
libc = "0.2"
# Every module call crosses the module C ABI as an `extern "C"` import
# (src/bridge/). The module crates below are REAL dependencies so their
# `#[no_mangle]` exports are linked into the final `liboakengine` cdylib:
# the dylib then carries the module C ABIs itself (oakundo_*, oakcommon_*,
# oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*,
# oakplugin_*, oaknode_*) alongside the facade's oakengine_* exports.
#
# The crates are linked WITHOUT their `test-stubs` features so the real
# exports ship. Cross-module calls that the modules resolve with
# dlsym(RTLD_DEFAULT) (oaknode, oakplugin, oakrender) now resolve against
# the sibling modules inside the same dylib; the remaining undefined
# imports are the C++ host-provided symbols (`oakcore_audioparams_*`,
# `oakcore_rational_*` from liboakcore, `fb_*` from ffmpeg_bridge), which
# build.rs leaves as runtime lookups for the host app.
#
# Tests link the same crates; the dev-dependencies below re-declare
# oakcommon/oakplugin WITH `test-stubs` so their in-crate C ABI mocks
# (the ffmpeg_bridge replacement and the oakrender dlsym stubs) are
# compiled into the test binaries (features union with the normal
# dependencies for test builds).
#
# NOTE (oaktimeline/oaktask): their `test-stubs` features are never used
# here — the in-crate mocks define `oakundo_command_init` etc., which
# would collide with the real oakundo rlib in one binary. Without
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
# C ABI symbols as link-time externs, provided by the sibling crates in
# the same dylib (or by the dev-dependency rlibs in a test binary).
#
# rustc would normally prune rlibs that are only touched through
# `extern "C"` imports from the link; src/linkage.rs anchors every crate
# (and oakcore-rs) so the linker pulls their object files.
oakcore-rs = { path = "../../oakcore-rs" }
oakundo = { path = "../../undo/rust" }
oakcommon = { path = "../../common/rust" }
oaktimeline = { path = "../../timeline/rust" }
oakcodec = { path = "../../codec/rust" }
oakaudio = { path = "../../audio/rust" }
oakrender = { path = "../../render/rust" }
oaktask = { path = "../../task/rust" }
oakplugin = { path = "../../plugin/rust" }
oaknode = { path = "../../node/rust" }
[dev-dependencies]
# Test-only feature union (see the comment above): tests keep the
# in-crate mocks these features compile.
oakcommon = { path = "../../common/rust", features = ["test-stubs"] }
oakplugin = { path = "../../plugin/rust", features = ["test-stubs"] }
@@ -42,9 +42,16 @@ tests/
The facade's regular dependencies are `serde`/`serde_json` (the worker's
NDJSON control-plane protocol, `src/worker.rs`) and `libc` (POSIX
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`). Every module
call still crosses the module C ABI as an `extern "C"` import
(`src/bridge/`), resolved at the final app link against the module shared
libraries.
call crosses the module C ABI as an `extern "C"` import (`src/bridge/`),
and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs)
anchors them so their `#[no_mangle]` exports are linked into the
`liboakengine` cdylib — the dylib carries the module C ABIs (oakundo_*,
oakcommon_*, oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*,
oaktask_*, oakplugin_*, oaknode_*) next to the facade's oakengine_*
exports. The only remaining imports are the C++ host symbols
(`oakcore_*` from liboakcore, `fb_*` from ffmpeg_bridge), which
`build.rs` leaves as runtime lookups (macOS `-undefined dynamic_lookup`)
resolved from the host Oak process.
### Handle mapping
@@ -89,16 +96,16 @@ control-plane message serializers remain unwrapped.
## Testing
`cargo test` links the module crates' rlibs (dev-dependencies) so the
facade's bridge imports resolve:
The module crates are real dependencies, so `cargo test` links the same
rlibs the cdylib embeds; the dev-dependencies re-declare
`oakcommon`/`oakplugin` with their `test-stubs` features so the test
binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks):
- `oakcommon`/`oakplugin` use their `test-stubs` features (ffmpeg_bridge
stub / in-crate render mocks).
- `oaknode`/`oaktimeline`/`oaktask` are linked WITHOUT their `test-stubs`
features: their in-crate mocks would collide with the real oakundo rlib
in one test binary. Without test-stubs their real exports reference the
oaknode/oakundo/oakcommon C ABI symbols as link-time externs, which the
dev-dependency rlibs provide; oaknode itself resolves cross-module
sibling crate rlibs provide; oaknode itself resolves cross-module
symbols at runtime with `dlsym(RTLD_DEFAULT)`.
- `tests/common/mod.rs` defines the `oakcore_*` (liboakcore) and `fb_*`
(libffmpeg_bridge) symbols the oakcodec/oakaudio rlibs reference, and
@@ -106,7 +113,9 @@ facade's bridge imports resolve:
factory so the oaknode serializer's dlsym lookups resolve in every test
binary.
- `src/lib.rs`'s test-only `test_link` forces the oakrender/oaknode/
oaktimeline/oaktask rlibs into the lib unit-test binary.
oaktimeline/oaktask rlibs into the lib unit-test binary (the always-on
`src/linkage.rs` anchors are `#[cfg(not(test))]`; they are what embeds
the module C ABIs in the cdylib for `cargo build`).
Families whose wrapped behavior requires the real module dylibs carry
`#[ignore]` tests with a documented reason; the smoke tests here exercise
@@ -118,7 +127,8 @@ payloads in both directions, including wraparound and full/empty edges.
cargo test # 71 tests green + 1 ignored (lib 35: ipc 17 + worker 18;
# integration: undo 3, common 4, audio 4, plugin 3, codec 5,
# render 6, linkage 1, node 3, timeline 2 + 1 ignored, task 5)
cargo build # staticlib + rlib; module symbols resolve at the final app link
cargo build # cdylib embeds the module C ABIs; oakcore_*/fb_* stay
# runtime lookups (see build.rs)
```
## FFI discipline
+33
View File
@@ -0,0 +1,33 @@
// 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/>.
//! Build-time link configuration for the `liboakengine` cdylib.
//!
//! The dylib now carries the module C ABIs itself (oakundo_*, oakcommon_*,
//! ... — see Cargo.toml), so the only remaining undefined imports are the
//! C++ host-provided symbols the modules call directly: `oakcore_*`
//! (liboakcore's `oakcore_audioparams_*` / `oakcore_rational_*`, called by
//! oakcodec) and `fb_find_best_pix_fmt_of_list` (ffmpeg_bridge, called by
//! oakcommon's pixel-format helper). Those live in the host Oak process,
//! which loads this dylib, so macOS `ld` must accept them as runtime
//! lookups instead of link-time errors. Only the cdylib gets this flag —
//! the rlib/staticlib (and the worker/cli consumers) are unaffected.
fn main() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined,dynamic_lookup");
}
}
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! # oakfacade — the `liboakengine` facade (Rust)
//! # oakengine — the `liboakengine` facade (Rust)
//!
//! Re-exports the frozen `oakengine_*` C ABI
//! (`engine/include/oakengine/*.h`) verbatim on top of the module C ABIs
@@ -42,12 +42,17 @@
//!
//! ## Testing
//!
//! `cargo test` links the module crates' rlibs (dev-dependencies) so the
//! bridge imports resolve; `tests/linkage.rs` references every crate to
//! force rustc to pull the rlibs into the link. Where a wrapped family
//! needs module behavior the crates do not implement yet, the engine
//! function is a documented stub and its test carries `#[ignore]` with a
//! reason (see README.md).
//! The module crates are real dependencies (see Cargo.toml) and
//! [`linkage`] anchors them into every link of this crate, so the module
//! C ABIs are embedded in the `liboakengine` cdylib next to the facade's
//! own exports. `cargo test` links the same crates' rlibs (plus the
//! `test-stubs` feature union declared in the dev-dependencies, which
//! compiles the oakcommon/oakplugin in-crate mocks); `tests/linkage.rs`
//! additionally references every crate for the integration-test binaries
//! and `test_link` (below) covers the unit-test binary. Where a wrapped
//! family needs module behavior the crates do not implement yet, the
//! engine function is a documented stub and its test carries `#[ignore]`
//! with a reason (see README.md).
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
@@ -60,6 +65,8 @@ pub mod deferred;
pub mod error;
pub mod handle;
pub mod ipc;
#[cfg(not(test))]
pub mod linkage;
pub mod node;
pub mod plugin;
pub mod render;
+66
View File
@@ -0,0 +1,66 @@
// 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/>.
//! Linkage anchors — force the module crates' rlibs into every link.
//!
//! The facade talks to the modules exclusively through `extern "C"`
//! imports (src/bridge/), so rustc would otherwise consider the module
//! crates unused and prune their rlibs from the link. This module
//! references one `#[no_mangle]` export of every module crate (and
//! `oakcore-rs`) from a `#[used]` static, which (a) marks each crate as
//! used so its rlib reaches the linker and (b) keeps the anchor alive so
//! the referenced object files are pulled. For the `liboakengine` cdylib
//! this is what actually embeds the module C ABIs (oakundo_*,
//! oakcommon_*, ...) into the dylib next to the facade's own oakengine_*
//! exports.
//!
//! The per-crate symbol mirrors the test-force-link in
//! tests/common/mod.rs (same paths, same `as usize` cast idiom), so the
//! crate/module paths are proven against the current module layouts.
#![allow(dead_code)]
/// Pull every module crate into the link. Mirrors
/// `tests/common/mod.rs::force_link`; the oakcommon XML/undo anchors are
/// repeated because oaknode's serializer resolves those C ABI symbols at
/// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib
/// for that lookup to succeed.
fn force_link() -> usize {
let fns: [usize; 13] = [
// oakcore-rs (pure value types; referenced so its rlib is linked).
oakcore_rs::Rational::new(1, 2).numerator() as usize,
// One exported C ABI symbol per module crate.
oakundo::ffi::undostack::oakundo_undostack_init as usize,
oakcommon::ffi::config::oakcommon_config_get_int as usize,
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
oaktask::ffi::manager::oaktask_manager_init as usize,
oakplugin::ffi::oakplugin_host_plugin_count as usize,
oaknode::ffi::project::oaknode_project_init as usize,
// oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs).
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
oakundo::ffi::command::oakundo_command_init as usize,
];
fns.iter().sum()
}
/// Keeps [`force_link`] (and through it every referenced export) alive in
/// the cdylib/staticlib even though nothing calls it directly.
#[used]
static FORCE_LINK_ANCHOR: fn() -> usize = force_link;
@@ -22,7 +22,7 @@
#[path = "common/mod.rs"]
mod common;
use oakfacade::audio::{
use oakengine::audio::{
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
oakengine_audio_get_output_device, oakengine_audio_hard_reset,
@@ -22,7 +22,7 @@ mod common;
use std::ffi::{c_char, c_int};
use oakfacade::codec::{
use oakengine::codec::{
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
oakengine_encoding_filename_remove_digit_placeholder, oakengine_encoding_format_audio_codec_count,
@@ -45,7 +45,7 @@ use oakfacade::codec::{
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_params_set_video_pix_fmt,
oakengine_encoding_pix_fmt_index,
};
use oakfacade::common::OakVideoParamsPod;
use oakengine::common::OakVideoParamsPod;
/// Container format / codec metadata queries.
#[test]
@@ -142,7 +142,7 @@ fn params_handle_round_trip() {
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe {
oakfacade::common::oakengine_video_params_make(&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1)
oakengine::common::oakengine_video_params_make(&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1)
},
0
);
@@ -25,7 +25,7 @@ mod common;
use std::ffi::{c_char, c_int};
use oakfacade::common::{
use oakengine::common::{
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int,
oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
@@ -87,12 +87,12 @@ fn config_error_handler() {
assert_eq!(unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) }, 0);
// Report an error through the handler.
assert_eq!(unsafe {
oakfacade::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr())
oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr())
}, 0);
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
// NULL handler clears; reporting then does not invoke.
assert_eq!(unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }, 0);
unsafe { oakfacade::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
}
@@ -28,7 +28,7 @@ mod common;
use std::ffi::{c_char, c_int};
use oakfacade::node::{
use oakengine::node::{
oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe,
oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id,
oakengine_node_factory_id_count, oakengine_node_factory_name_from_id,
@@ -83,7 +83,7 @@ fn float_value(x: f64) -> OakNodeValue {
}
/// The index of the first project node whose type id matches `id`, or -1.
unsafe fn find_node(project: *mut oakfacade::handle::OakEngineProject, id: &str) -> c_int {
unsafe fn find_node(project: *mut oakengine::handle::OakEngineProject, id: &str) -> c_int {
let count = unsafe { oakengine_project_node_count(project) };
for i in 0..count {
let node = unsafe { oakengine_project_node_at(project, i) };
@@ -137,15 +137,15 @@ fn project_node_keyframe_lifecycle() {
0
);
assert_eq!(
unsafe { oakengine_project_set_filename(project, c"/tmp/oakfacade_node_test.ovexml".as_ptr()) },
unsafe { oakengine_project_set_filename(project, c"/tmp/oakengine_node_test.ovexml".as_ptr()) },
0
);
let len = unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert!(unsafe { read_buf(&mut buf) }.ends_with("oakfacade_node_test.ovexml"));
assert!(unsafe { read_buf(&mut buf) }.ends_with("oakengine_node_test.ovexml"));
let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "oakfacade_node_test");
assert_eq!(unsafe { read_buf(&mut buf) }, "oakengine_node_test");
// ---- factory + node creation ---------------------------------------
let factory_count = oakengine_node_factory_id_count();
@@ -269,9 +269,9 @@ fn project_node_keyframe_lifecycle() {
assert!((at.f[0] - 0.5).abs() < 1e-6);
// ---- project save → fresh load round-trip ---------------------------
let path = c"/tmp/oakfacade_node_test.ovexml";
let path = c"/tmp/oakengine_node_test.ovexml";
assert_eq!(unsafe { oakengine_project_save(project, path.as_ptr()) }, 0);
assert!(std::path::Path::new("/tmp/oakfacade_node_test.ovexml").exists());
assert!(std::path::Path::new("/tmp/oakengine_node_test.ovexml").exists());
unsafe { oakengine_project_free(project) };
@@ -19,7 +19,7 @@
#[path = "common/mod.rs"]
mod common;
use oakfacade::plugin::{
use oakengine::plugin::{
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
};
@@ -27,7 +27,7 @@ use oakfacade::plugin::{
/// Callback registration round-trips (NULL clears).
#[test]
fn provider_registration() {
unsafe extern "C" fn viewer(_userdata: *mut std::ffi::c_void) -> *mut oakfacade::handle::OakEngineNode {
unsafe extern "C" fn viewer(_userdata: *mut std::ffi::c_void) -> *mut oakengine::handle::OakEngineNode {
std::ptr::null_mut()
}
assert_eq!(unsafe {
@@ -26,7 +26,7 @@ mod common;
use std::ffi::{c_char, c_double};
use oakfacade::render::{
use oakengine::render::{
oakengine_color_last_error, oakengine_color_manager_get_config_filename,
oakengine_color_processor_convert_color, oakengine_color_processor_create,
oakengine_color_processor_free, oakengine_color_processor_is_valid,
@@ -34,11 +34,11 @@ mod common;
use std::ffi::{c_char, c_int, c_void};
use oakfacade::node::{
use oakengine::node::{
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
oakengine_project_root, oakengine_project_set_filename,
};
use oakfacade::task::{
use oakengine::task::{
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
oakengine_task_create_project_import, oakengine_task_create_project_load,
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
@@ -187,7 +187,7 @@ fn project_task_lifecycle() {
// ---- save task on a real project → sync run writes the file ----------
let save_path = std::env::temp_dir().join(format!(
"oakfacade_task_save_{}.ovexml",
"oakengine_task_save_{}.ovexml",
std::process::id()
));
let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap();
@@ -217,7 +217,7 @@ fn project_task_lifecycle() {
// project's own filename; NULL without one, a real task with one. ------
assert!(unsafe { oakengine_task_create_project_save_otio(project) }.is_null());
assert_eq!(
unsafe { oakengine_project_set_filename(project, c"/tmp/oakfacade_task_otio.otio".as_ptr()) },
unsafe { oakengine_project_set_filename(project, c"/tmp/oakengine_task_otio.otio".as_ptr()) },
0
);
let otio_task = unsafe { oakengine_task_create_project_save_otio(project) };
@@ -31,12 +31,12 @@ mod common;
use std::ffi::{c_char, c_int};
use oakfacade::handle::{box_handle, OakEngineNode};
use oakfacade::node::{
use oakengine::handle::{box_handle, OakEngineNode};
use oakengine::node::{
oakengine_footage_borrow, oakengine_project_create, oakengine_project_free,
oakengine_project_new,
};
use oakfacade::timeline::{
use oakengine::timeline::{
oakengine_block_get_range, oakengine_block_get_track, oakengine_block_is_enabled,
oakengine_block_is_gap, oakengine_block_link_count, oakengine_block_next,
oakengine_block_prev, oakengine_block_set_enabled, oakengine_block_set_length_and_media_out,
@@ -103,7 +103,7 @@ fn force_runtime_syms() -> usize {
/// Convert a facade `CHandle` to the layout-identical oaknode `CHandle`
/// (distinct Rust types over the same C ABI struct).
fn to_node_handle(h: oakfacade::handle::CHandle) -> oaknode::handle::CHandle {
fn to_node_handle(h: oakengine::handle::CHandle) -> oaknode::handle::CHandle {
oaknode::handle::CHandle {
ctx: h.ctx,
addref: h.addref,
@@ -113,8 +113,8 @@ fn to_node_handle(h: oakfacade::handle::CHandle) -> oaknode::handle::CHandle {
}
/// Convert an oaknode `CHandle` back to the facade `CHandle`.
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakfacade::handle::CHandle {
oakfacade::handle::CHandle {
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakengine::handle::CHandle {
oakengine::handle::CHandle {
ctx: h.ctx,
addref: h.addref,
release: h.release,
@@ -28,7 +28,7 @@ mod common;
use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::{AtomicI32, Ordering};
use oakfacade::undo::{
use oakengine::undo::{
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
oakengine_undo_command_create, oakengine_undo_command_create_multi,
oakengine_undo_command_free, oakengine_undo_command_multi_add_child,
@@ -53,16 +53,28 @@ static CMD_FREE_COUNT: AtomicI32 = AtomicI32::new(0);
static STK_REDO_COUNT: AtomicI32 = AtomicI32::new(0);
static STK_UNDO_COUNT: AtomicI32 = AtomicI32::new(0);
/// Stack-test callbacks: bump only the `STK_*` counters. They must not
/// touch the `CMD_*` counters — the command-lifecycle tests reset and
/// assert those in parallel threads, so a stray bump here would race.
unsafe extern "C" fn redo_cb(_userdata: *mut c_void) {
CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
STK_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn undo_cb(_userdata: *mut c_void) {
CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
STK_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
/// Command-lifecycle-only callbacks: bump only the `CMD_*` counters. The
/// serialized stack test runs in a parallel thread and must not flip
/// these.
unsafe extern "C" fn cmd_redo_cb(_userdata: *mut c_void) {
CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn cmd_undo_cb(_userdata: *mut c_void) {
CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn free_cb(_userdata: *mut c_void) {
CMD_FREE_COUNT.fetch_add(1, Ordering::SeqCst);
}
@@ -77,8 +89,8 @@ fn command_create_redo_undo_free() {
let cmd = unsafe {
oakengine_undo_command_create(
c"custom".as_ptr(),
Some(redo_cb),
Some(undo_cb),
Some(cmd_redo_cb),
Some(cmd_undo_cb),
Some(free_cb),
std::ptr::null_mut(),
)
@@ -108,8 +120,8 @@ fn multi_command_add_child_count_redo() {
let child = unsafe {
oakengine_undo_command_create(
c"child".as_ptr(),
Some(redo_cb),
Some(undo_cb),
Some(cmd_redo_cb),
Some(cmd_undo_cb),
None,
std::ptr::null_mut(),
)
-55
View File
@@ -1,55 +0,0 @@
[package]
name = "oakfacade"
version = "0.1.0"
edition = "2021"
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
license = "GPL-3.0-or-later"
[lib]
crate-type = ["staticlib", "rlib"]
[profile.release]
# FFI discipline: panics must be catchable at every exported entry.
panic = "unwind"
[dependencies]
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
# the shm error formatting in src/ipc.rs.
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
# shared-memory frame-slot transport (src/ipc.rs).
libc = "0.2"
# Every module call crosses the module C ABI as an `extern "C"` import
# (src/bridge/), resolved at the final link against the module shared
# libraries (see README.md).
#
# `cargo test` links the module crates' rlibs instead (dev-dependencies
# below): the crates' `#[no_mangle]` exports satisfy the facade's bridge
# imports, so the smoke tests exercise the real module code where the
# crates implement it. Tests reference every crate so rustc pulls the
# rlibs into the link (see tests/common/mod.rs).
#
# test-stubs on oakcommon/oakplugin compiles those crates' in-crate C ABI
# mocks: oakcommon's stub replaces the ffmpeg_bridge symbol, and
# oakplugin's stubs replace its runtime dlsym lookups.
#
# NOTE (oaktimeline/oaktask): linked WITHOUT their `test-stubs` features.
# Their in-crate mocks define `oakundo_command_init` etc., which would
# collide with the real oakundo rlib in one test binary; without
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
# C ABI symbols as link-time externs, which the dev-dependency rlibs
# (oaknode, oakundo, oakcommon[test-stubs]) provide. oaknode itself
# resolves cross-module symbols at runtime with dlsym(RTLD_DEFAULT), which
# finds the linked rlibs in the test binary (see src/bridge/node.rs).
[dev-dependencies]
oakundo = { path = "../../undo/rust" }
oakcodec = { path = "../../codec/rust" }
oakaudio = { path = "../../audio/rust" }
oakrender = { path = "../../render/rust" }
oakcommon = { path = "../../common/rust", features = ["test-stubs"] }
oakplugin = { path = "../../plugin/rust", features = ["test-stubs"] }
oaknode = { path = "../../node/rust" }
oaktimeline = { path = "../../timeline/rust" }
oaktask = { path = "../../task/rust" }