Files
oak-editor/crates/oakengine/README.md
T
Mike-Solar c5f1d0d76c refactor(engine): pure cdylib + undo-stack test race fix (M14 R4)
- oakengine is now cdylib-only (no rlib/staticlib consumers anywhere;
  cargo tree verified) — the plugin/external C ABI layer; README and
  docs updated
- cd.yml drops the dylib embedding/re-sign steps (the app no longer
  links it)
- test race root-caused and fixed for good: the global undo stack lock
  is now a re-entrant mutex (parking_lot) shared by every test that
  drives the stack, including the previously unlocked node/render
  families; the render-manager serial-ordering bug (an earlier repro
  test initialized the global manager before the not-initialized test)
  is fixed with a shared SERIAL guard and a manager shutdown
- 5 consecutive parallel runs clean; serial 209/209
2026-08-17 00:51:36 +08:00

141 lines
8.3 KiB
Markdown

# oakengine — the `liboakengine` cdylib (plugin / external C ABI)
The frozen `oakengine_*` C ABI (`engine/include/oakengine/*.h`) as a
**pure cdylib**. This is the plugin / external-consumer layer: OFX plugins
and third-party embedders link `liboakengine` and call the C ABI. The app,
oak-cli and oak-worker do not use it anymore (M14 R4) — they link the
module crates directly as Rust rlibs. The C ABI itself is frozen: only
additive changes, plus major version bumps.
Downward, every export is a direct Rust call into the module crates
(oakundo/oaknode/oaktimeline/oakcodec/oakaudio/oakrender/oaktask/
oakcommon/oakplugin/oakstorage/oakcore-rs) through `src/stubs.rs` (the
rewired replacement for the deleted `bridge/`). The facade itself owns
only the engine's box/unbox, buf/size and error-code conventions;
cross-cutting state that used to live here (the process-wide undo stack,
the open undo group) has sunk into the modules (M14 R1:
`oakundo::global`).
## Architecture
```
src/
lib.rs crate docs, module list, test-only test_link
error.rs OAKENGINE error codes (module 00 → -1..-6)
handle.rs CHandle mirror, OakEngine* opaque wrappers, box/unbox,
catch_unwind guards, buf/size string helpers
stubs.rs direct-Rust shims replacing the deleted bridge/ (the
engine's only downward path to the modules)
linkage.rs #[used] anchors pulling every module rlib into the cdylib
undo.rs engine/include/oakengine/undo.h (thin forward to
oakundo::global)
common.rs config.h + videoparams.h (facade-static tables + POD↔handle)
audio.rs audio.h (manager + sync + processor)
codec.rs encoding.h + exporter.h (metadata, params POD, exporter)
render.rs renderer.h + color.h + lut.h (renderer/frame/color processor)
plugin.rs plugin.h
node.rs node.h + project.h + footage.h (node graph / project / footage)
timeline.rs timeline.h (sequences, clips, tracks, markers, workarea)
task.rs task.h (background tasks over oaktask)
storage.rs write-through session (project ↔ oakstorage backend)
deferred.rs documented deferrals (stub detail lives here and in the
family modules' stub bodies)
test_support/ the former tests/*.rs, now in-crate unit tests
```
### Dependencies
The facade's regular dependency is `thiserror` (the `Display` +
`std::error::Error` impl for the facade error enum, `src/error.rs`). Every
module call is a compile-time Rust call into the module crate's direct API
(`src/stubs.rs`); the module crates are real dependencies, and
[`linkage`](src/linkage.rs) anchors them so their rlibs are embedded in
the `liboakengine` cdylib next to the facade's `oakengine_*` exports. The
`oakcore_audioparams_*` accessors the audio paths read through are
implemented in the dylib too (src/stubs.rs, module `audio`, M12 P5) —
they used to be C++ host symbols left as runtime lookups (macOS
`-undefined dynamic_lookup`); the cdylib now carries no undefined imports.
### Handle mapping
The engine headers' opaque pointers (`OakEngineNode*`, `OakEngineTrack*`,
...) are thin newtype wrappers around the module layer's `CHandle`
(`{ctx, addref, release, abi_version}`) values. A box is created by
`handle::box_handle` and freed by `handle::free_box` (release + dealloc);
consuming exports (`oakengine_*_free`, `oakengine_undo_push`, ...) free
their box, borrowed results never are. The process-wide undo stack and
open undo group live in the oakundo module (`oakundo::global`); the
undo.h exports are thin forwards over it (module 00 analogues of
`EngineCore::undo_stack()` and the C++ capi's `g_undo_group`).
### Error codes
Facade-local codes are -1..-6 (`OAKENGINE_E_*`); module codes pass
through **untranslated** (the -MMCCCC prefix preserves provenance, e.g.
-20004 is oakundo's NOT_FOUND). String getters follow the engine buf/size
convention: the return value is the length excluding the NUL
(`handle::string_result` converts the modules' size-including-NUL).
## Scope
| Family | Header | Wrapped | Notes |
|---|---|---|---|
| undo | undo.h | 37 | stack/group/command lifecycle over oakundo (`oakundo::global` + undocommand) + Qt leftovers (update_actions/actions → no-op/NULL) |
| common | config.h, videoparams.h | 34 | config over oakcommon; videoparams static tables ported from `engine/render/videoparams.cpp` |
| audio | audio.h | 26 | manager + sync; processor convert/output_params stubbed (interface mismatch) |
| plugin | plugin.h | 4 | callbacks are facade state; push_button stubbed (no module API) |
| codec | encoding.h, exporter.h | 81/85 | metadata family over oakcodec (`include/codec/format.h`); params handle is a facade box over the `oakcodec_encoding_params` POD; the exporter family drives the export task synchronously (integration-tested, real mp4 via FFmpeg); presets/load-save deferred |
| render | renderer.h, color.h, lut.h | 60/60 | renderer over oakrender tickets; frame accessors over `OakCodecFrame`; color processor over `oakrender_color_processor_*`; color-manager list queries + LUT library stubs |
| node | node.h, project.h, footage.h | 226/327 | the node graph, project and footage families over the oaknode crate; documented stubs where the module lacks the surface (gizmos, plugin messages, input properties, brush, thumbnail/waveform caches, shape/subtitle, keyframe enumeration, ...) — see the stub bodies |
| timeline | timeline.h | 126/139 | sequences/clips/tracks/markers/workarea over oaknode + oaktimeline; documented stubs (ripple-tracks command, default transitions, move-track/clip, marker-create, auto-cache, cache invalidation, multicam find/switch — module-surface gaps, see the stub bodies) |
| task | task.h | 27 | the background-task system over oaktask (manager + load/save/import/export creators + result accessors); `create_proxy` stubbed (the module has no proxy-task C creator); start-time/is-cancelled are facade-approximated |
Deferred/stub detail lives in [`deferred`](src/deferred.rs) and in the
stub bodies' doc comments. The worker/IPC families (`worker.h`/`ipc.h`)
are **not** part of the facade anymore: the frozen C++ ABI does not
include them, so the render worker's runtime and the shared-memory
frame-slot transport moved into the `oak-worker` crate
(`crates/oak-worker/src/{worker,ipc}.rs`, self-contained, direct Rust
calls into oakrender); the ipc.h control-plane message serializers remain
unwrapped.
## Testing
The crate is cdylib-only (no rlib artifact), so integration tests cannot
link it as a crate; the former `tests/*.rs` moved into `src/test_support/`
and run as in-crate unit tests (pulled in from `src/lib.rs` under
`#[cfg(test)]`), addressing the modules through `crate::*`. The module
crates are real dependencies, so the test binary statically links the same
rlibs the cdylib embeds:
- `src/linkage.rs` (always-on) anchors the module rlibs into the cdylib
for `cargo build`; the test-only `test_link` module in `src/lib.rs`
forces the oakrender/oaknode/oaktimeline/oaktask rlibs into the lib
unit-test binary, and `src/test_support/common/mod.rs::force_link`
covers the test-support module (same symbol list, so the anchor paths
stay proven against the current module layouts).
- Tests that touch process-wide state (the audio manager, the undo stack,
the task manager) take a shared serialization lock instead of relying
on the process isolation the old integration tests had
(`test_support/common::with_manager`, the per-family `SERIAL` mutexes).
The smoke tests exercise the module crates' real implementations. Where a
wrapped family needs module behavior the crates do not implement yet, the
engine function is a documented stub with its reason (see `deferred.rs`).
```
cargo test # in-crate unit tests (undo, common, audio, codec,
# exporter, plugin, render, node, timeline, task,
# library/storage families)
cargo build # liboakengine cdylib embeds the module rlibs + the
# folded-in oakcore_audioparams_* accessors (no
# undefined imports)
```
## FFI discipline
Every export goes through a `catch_unwind` guard
(`handle::guard*`); `*_free` is a NULL no-op; strings use the two-stage
buf/size convention; module error codes pass through untranslated;
handles are refcounted module values wrapped in opaque boxes.