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:
@@ -66,7 +66,7 @@ use oaknode::sequence::SequenceBehavior;
|
||||
use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType};
|
||||
|
||||
use crate::backend::LoadResult;
|
||||
use crate::bridge::node;
|
||||
use crate::nodeutil as node;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::uri::StorageUri;
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ impl crate::backend::StorageBackend for OveXmlBackend {
|
||||
Probe::Current | Probe::Old => {}
|
||||
}
|
||||
|
||||
let project = crate::bridge::node::serializer_load(&xml)?;
|
||||
let handle = crate::bridge::node::make_project_owned(project);
|
||||
let project = crate::nodeutil::serializer_load(&xml)?;
|
||||
let handle = crate::nodeutil::make_project_owned(project);
|
||||
let version_info = if matches!(probe, Probe::Old) {
|
||||
OAKSTORAGE_TOO_OLD
|
||||
} else {
|
||||
@@ -141,12 +141,12 @@ impl crate::backend::StorageBackend for OveXmlBackend {
|
||||
.local_path()
|
||||
.ok_or(Error::Invalid)?
|
||||
.to_string();
|
||||
let arc = unsafe { crate::bridge::node::project_arc(&project)? };
|
||||
let arc = unsafe { crate::nodeutil::project_arc(&project)? };
|
||||
let xml = {
|
||||
let guard = arc
|
||||
.lock()
|
||||
.map_err(|_| Error::State)?;
|
||||
crate::bridge::node::serializer_save(&guard)?
|
||||
crate::nodeutil::serializer_save(&guard)?
|
||||
};
|
||||
std::fs::write(&path, xml).map_err(|e| Error::Io(e.to_string()))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,19 +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 imports from other oak modules.
|
||||
|
||||
pub mod node;
|
||||
@@ -1,430 +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 M10 §2.2/§2.3 verbatim
|
||||
//! (`oakstorage_probe/open/save/project_free/project_take_project/
|
||||
//! project_project/project_uri/last_error/debug_alive_count` and the
|
||||
//! backend vtable registration pair).
|
||||
//!
|
||||
//! `oakstorage_open` returns a session handle (alive-counted). The
|
||||
//! project inside is an `OakNodeProject*`-compatible handle boxed via
|
||||
//! the shared `oakcore_rs::handle::CHandle`, so `take_project` hands
|
||||
//! back something `oaknode_*` functions accept, and `save` accepts
|
||||
//! handles produced by `oaknode_project_init`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_int, c_uint, CStr, CString};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::backend::StorageBackend;
|
||||
use crate::bridge::node;
|
||||
use crate::error::{Error, OAKSTORAGE_E_FAILED, OAKSTORAGE_OK, Result};
|
||||
use crate::handle::CHandle;
|
||||
use crate::session::Session;
|
||||
use crate::uri::StorageUri;
|
||||
|
||||
/// Save options bitmask: compress the ove-xml container. Accepted but
|
||||
/// not implemented by the built-in backends (the oaknode serializer
|
||||
/// emits plain XML only); unknown bits are ignored (M10 §2.2).
|
||||
pub const OAKSTORAGE_SAVE_COMPRESS: c_uint = 0x1;
|
||||
|
||||
/// Live session count (`oakstorage_debug_alive_count`).
|
||||
static ALIVE: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
// Per-thread last-error detail (`oakstorage_last_error`).
|
||||
thread_local! {
|
||||
static LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// Record the thread's last error detail.
|
||||
fn set_last_error(msg: &str) {
|
||||
LAST_ERROR.with(|e| *e.borrow_mut() = msg.to_string());
|
||||
}
|
||||
|
||||
/// Read the thread's last error detail.
|
||||
fn take_last_error() -> String {
|
||||
LAST_ERROR.with(|e| e.borrow().clone())
|
||||
}
|
||||
|
||||
/// Release for session handles (alive-counted; the session's Drop
|
||||
/// releases the held project handle).
|
||||
unsafe extern "C" fn session_release(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *mut crate::handle::RefBox<Session>;
|
||||
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(Box::from_raw(rb));
|
||||
ALIVE.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakstorage_probe`: which backend claims this URI (two-stage
|
||||
/// string; negative = E_NO_BACKEND).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_probe(
|
||||
uri: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
let uri_str = unsafe { cstr(uri) }.map(str::to_string);
|
||||
match catch_unwind(AssertUnwindSafe(|| {
|
||||
let s = uri_str.as_deref().ok_or(Error::Invalid)?;
|
||||
let parsed = StorageUri::parse(s)?;
|
||||
let backend = crate::registry::Registry::global().resolve(&parsed)?;
|
||||
Ok::<String, Error>(backend.name().to_string())
|
||||
})) {
|
||||
Ok(Ok(name)) => unsafe { node::copy_string_out(&name, buf, buf_size) },
|
||||
Ok(Err(e)) => {
|
||||
set_last_error(&e.to_string());
|
||||
e.code()
|
||||
}
|
||||
Err(_) => {
|
||||
set_last_error("storage: panic in oakstorage_probe");
|
||||
OAKSTORAGE_E_FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakstorage_open`: open a project session by URI (owned handle;
|
||||
/// `result_code` receives an OAKSTORAGE_* code incl. the positive
|
||||
/// version info codes).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_open(uri: *const c_char, result_code: *mut c_int) -> CHandle {
|
||||
let uri_str = unsafe { cstr(uri) }.map(str::to_string);
|
||||
match catch_unwind(AssertUnwindSafe(|| open_inner(uri_str.as_deref()))) {
|
||||
Ok(Ok((handle, info))) => {
|
||||
if !result_code.is_null() {
|
||||
unsafe { *result_code = info };
|
||||
}
|
||||
handle
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
set_last_error(&e.to_string());
|
||||
if !result_code.is_null() {
|
||||
unsafe { *result_code = e.code() };
|
||||
}
|
||||
CHandle::null()
|
||||
}
|
||||
Err(_) => {
|
||||
set_last_error("storage: panic in oakstorage_open");
|
||||
if !result_code.is_null() {
|
||||
unsafe { *result_code = OAKSTORAGE_E_FAILED };
|
||||
}
|
||||
CHandle::null()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a session: parse + resolve + backend load, then wrap the
|
||||
/// project in an alive-counted session handle. The returned info code
|
||||
/// is the backend's version verdict (OK or a positive info code).
|
||||
fn open_inner(uri: Option<&str>) -> Result<(CHandle, i32)> {
|
||||
let s = uri.ok_or(Error::Invalid)?;
|
||||
let parsed = StorageUri::parse(s)?;
|
||||
let backend = crate::registry::Registry::global().resolve(&parsed)?;
|
||||
let loaded = backend.load(&parsed)?;
|
||||
let info = loaded.version_info;
|
||||
if loaded.project.is_null() {
|
||||
// Version probing declined to load (TOO_NEW / UNKNOWN_VERSION).
|
||||
return Ok((CHandle::null(), info));
|
||||
}
|
||||
ALIVE.fetch_add(1, Ordering::Relaxed);
|
||||
let session = Session::new(parsed, loaded.project);
|
||||
Ok((crate::handle::make_owned_with(session, session_release), info))
|
||||
}
|
||||
|
||||
/// `oakstorage_save`: save a project to a URI (options bitmask;
|
||||
/// unknown bits ignored by backends).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_save(
|
||||
project: CHandle,
|
||||
uri: *const c_char,
|
||||
options: c_uint,
|
||||
) -> c_int {
|
||||
let uri_str = unsafe { cstr(uri) }.map(str::to_string);
|
||||
match catch_unwind(AssertUnwindSafe(|| {
|
||||
let s = uri_str.as_deref().ok_or(Error::Invalid)?;
|
||||
let parsed = StorageUri::parse(s)?;
|
||||
let backend = crate::registry::Registry::global().resolve(&parsed)?;
|
||||
backend.save(project, &parsed, options)
|
||||
})) {
|
||||
Ok(Ok(())) => OAKSTORAGE_OK,
|
||||
Ok(Err(e)) => {
|
||||
set_last_error(&e.to_string());
|
||||
e.code()
|
||||
}
|
||||
Err(_) => {
|
||||
set_last_error("storage: panic in oakstorage_save");
|
||||
OAKSTORAGE_E_FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakstorage_project_free` (NULL/empty no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_project_free(session: *mut CHandle) {
|
||||
crate::handle::guard_void(|| unsafe {
|
||||
if session.is_null() || (*session).ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
let h = (*session).clone();
|
||||
if let Some(f) = h.release {
|
||||
f(h.ctx);
|
||||
}
|
||||
(*session).ctx = std::ptr::null_mut();
|
||||
});
|
||||
}
|
||||
|
||||
/// `oakstorage_project_take_project`: ownership transfer out of the
|
||||
/// session (the session becomes an empty shell; still free it).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_project_take_project(session: CHandle) -> CHandle {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe { session_take(session) })) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutate the boxed session to take its project out (empty after an
|
||||
/// earlier take — then an empty handle, not an error).
|
||||
unsafe fn session_take(session: CHandle) -> Result<CHandle> {
|
||||
let rb = session.ctx as *mut crate::handle::RefBox<Session>;
|
||||
if rb.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
Ok(unsafe { (*rb).value.take() }.unwrap_or_else(CHandle::null))
|
||||
}
|
||||
|
||||
/// `oakstorage_project_project`: borrowed project handle (empty after
|
||||
/// take). A fresh box over a clone of the project Arc, so releasing it
|
||||
/// (or not) cannot disturb the session's own handle.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_project_project(session: CHandle) -> CHandle {
|
||||
match catch_unwind(AssertUnwindSafe(|| -> crate::error::Result<CHandle> {
|
||||
unsafe {
|
||||
let sess = crate::handle::get::<Session>(&session).ok_or(Error::Invalid)?;
|
||||
let ph = sess.project().ok_or(Error::State)?;
|
||||
let arc = oaknode::handle::get::<node::ProjectArc>(ph)
|
||||
.ok_or(Error::Invalid)?
|
||||
.clone();
|
||||
Ok(oaknode::handle::make_owned(arc))
|
||||
}
|
||||
})) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakstorage_project_uri` (two-stage string).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_project_uri(
|
||||
session: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
let uri_str = match unsafe { crate::handle::get::<Session>(&session) } {
|
||||
Some(s) => s.uri().to_uri_string(),
|
||||
None => return crate::error::OAKSTORAGE_E_INVALID,
|
||||
};
|
||||
unsafe { node::copy_string_out(&uri_str, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// `oakstorage_last_error` (two-stage string; per-thread last error
|
||||
/// detail).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let msg = take_last_error();
|
||||
unsafe { node::copy_string_out(&msg, buf, buf_size) }
|
||||
}
|
||||
|
||||
/// `oakstorage_debug_alive_count` (leak assertions in tests).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_debug_alive_count() -> c_int {
|
||||
ALIVE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Foreign (C-side) backend vtable registration — M10 §2.3
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// C-side backend vtable (M10 §2.3; the layout is the C header's
|
||||
/// struct — implementer copies it field-for-field).
|
||||
#[repr(C)]
|
||||
pub struct OakStorageBackendVtable {
|
||||
/// Backend name (static string).
|
||||
pub name: *const c_char,
|
||||
/// URI scheme served (static string).
|
||||
pub uri_scheme: *const c_char,
|
||||
/// can_handle.
|
||||
pub can_handle: Option<unsafe extern "C" fn(uri: *const c_char) -> c_int>,
|
||||
/// load.
|
||||
pub load: Option<
|
||||
unsafe extern "C" fn(
|
||||
uri: *const c_char,
|
||||
result_code: *mut c_int,
|
||||
err_buf: *mut c_char,
|
||||
err_buf_size: c_int,
|
||||
) -> CHandle,
|
||||
>,
|
||||
/// save.
|
||||
pub save: Option<
|
||||
unsafe extern "C" fn(
|
||||
project: CHandle,
|
||||
uri: *const c_char,
|
||||
options: c_uint,
|
||||
err_buf: *mut c_char,
|
||||
err_buf_size: c_int,
|
||||
) -> c_int,
|
||||
>,
|
||||
}
|
||||
|
||||
/// A backend registered from C: every call funnels through the vtable
|
||||
/// function pointers (the vtable itself is not copied — the caller
|
||||
/// guarantees it outlives unregistration).
|
||||
struct ForeignBackend {
|
||||
name: String,
|
||||
uri_scheme: String,
|
||||
can_handle: unsafe extern "C" fn(uri: *const c_char) -> c_int,
|
||||
load: unsafe extern "C" fn(
|
||||
uri: *const c_char,
|
||||
result_code: *mut c_int,
|
||||
err_buf: *mut c_char,
|
||||
err_buf_size: c_int,
|
||||
) -> CHandle,
|
||||
save: unsafe extern "C" fn(
|
||||
project: CHandle,
|
||||
uri: *const c_char,
|
||||
options: c_uint,
|
||||
err_buf: *mut c_char,
|
||||
err_buf_size: c_int,
|
||||
) -> c_int,
|
||||
}
|
||||
|
||||
impl StorageBackend for ForeignBackend {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn uri_scheme(&self) -> &str {
|
||||
&self.uri_scheme
|
||||
}
|
||||
|
||||
fn can_handle(&self, uri: &StorageUri) -> bool {
|
||||
let c = match CString::new(uri.to_uri_string()) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let claimed = unsafe { (self.can_handle)(c.as_ptr()) };
|
||||
claimed != 0
|
||||
}
|
||||
|
||||
fn load(&self, uri: &StorageUri) -> Result<crate::backend::LoadResult> {
|
||||
let c = CString::new(uri.to_uri_string()).map_err(|_| Error::Invalid)?;
|
||||
let mut result_code: c_int = OAKSTORAGE_OK;
|
||||
let handle = unsafe {
|
||||
(self.load)(c.as_ptr(), &mut result_code, std::ptr::null_mut(), 0)
|
||||
};
|
||||
if !handle.is_null() {
|
||||
Ok(crate::backend::LoadResult::with_info(handle, result_code))
|
||||
} else if result_code > 0 {
|
||||
// Version info code without a project.
|
||||
Ok(crate::backend::LoadResult::info_only(result_code))
|
||||
} else {
|
||||
Err(code_to_error(result_code))
|
||||
}
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
project: CHandle,
|
||||
uri: &StorageUri,
|
||||
options: u32,
|
||||
) -> Result<()> {
|
||||
let c = CString::new(uri.to_uri_string()).map_err(|_| Error::Invalid)?;
|
||||
let rc = unsafe { (self.save)(project, c.as_ptr(), options, std::ptr::null_mut(), 0) };
|
||||
if rc == OAKSTORAGE_OK {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(code_to_error(rc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a foreign backend's return code to the crate error.
|
||||
fn code_to_error(code: c_int) -> Error {
|
||||
match code {
|
||||
crate::error::OAKSTORAGE_E_INVALID => Error::Invalid,
|
||||
crate::error::OAKSTORAGE_E_STATE => Error::State,
|
||||
crate::error::OAKSTORAGE_E_NOT_FOUND => Error::NotFound,
|
||||
crate::error::OAKSTORAGE_E_FAILED => Error::Failed("foreign backend failed".to_string()),
|
||||
crate::error::OAKSTORAGE_E_NO_BACKEND => Error::NoBackend,
|
||||
crate::error::OAKSTORAGE_E_FORMAT => Error::Format("foreign backend format error".to_string()),
|
||||
crate::error::OAKSTORAGE_E_IO => Error::Io("foreign backend I/O error".to_string()),
|
||||
crate::error::OAKSTORAGE_E_NOMEM => Error::NoMem,
|
||||
_ => Error::Failed(format!("foreign backend error code {code}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakstorage_backend_register`: register a foreign backend (the
|
||||
/// vtable is not copied — the caller guarantees it outlives
|
||||
/// unregistration).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_backend_register(
|
||||
backend: *const OakStorageBackendVtable,
|
||||
) -> c_int {
|
||||
crate::handle::guard(|| unsafe {
|
||||
if backend.is_null() {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let v = &*backend;
|
||||
let name = cstr(v.name).ok_or(Error::Invalid)?.to_string();
|
||||
let uri_scheme = cstr(v.uri_scheme).ok_or(Error::Invalid)?.to_string();
|
||||
let can_handle = v.can_handle.ok_or(Error::Invalid)?;
|
||||
let load = v.load.ok_or(Error::Invalid)?;
|
||||
let save = v.save.ok_or(Error::Invalid)?;
|
||||
let foreign = ForeignBackend {
|
||||
name,
|
||||
uri_scheme,
|
||||
can_handle,
|
||||
load,
|
||||
save,
|
||||
};
|
||||
crate::registry::Registry::global().register(Arc::new(foreign))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakstorage_backend_unregister`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakstorage_backend_unregister(name: *const c_char) -> c_int {
|
||||
crate::handle::guard(|| unsafe {
|
||||
let name = cstr(name).ok_or(Error::Invalid)?;
|
||||
crate::registry::Registry::global().unregister(name)
|
||||
})
|
||||
}
|
||||
|
||||
/// Safe read of a NUL-terminated C string argument.
|
||||
///
|
||||
/// # Safety
|
||||
/// `p` must be a valid NUL-terminated C string for the returned
|
||||
/// reference's lifetime.
|
||||
unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> {
|
||||
if p.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { CStr::from_ptr(p) }.to_str().ok()
|
||||
}
|
||||
@@ -25,10 +25,9 @@
|
||||
|
||||
pub mod backend;
|
||||
pub mod backends;
|
||||
pub mod bridge;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod handle;
|
||||
pub mod nodeutil;
|
||||
pub mod registry;
|
||||
pub mod session;
|
||||
pub mod uri;
|
||||
|
||||
@@ -14,23 +14,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! oaknode calls — direct Rust calls into the oaknode crate (single-lib
|
||||
//! unification, see `docs/zh/plans/riir/single-lib.md`).
|
||||
//! oaknode project/serializer helpers (direct Rust calls into the
|
||||
//! oaknode crate, single-lib unification; the former `bridge/node.rs`).
|
||||
//!
|
||||
//! The ove-xml backend needs oaknode's project + serializer families
|
||||
//! (`oaknode_project_init` / `oaknode_serializer_save_to_file` /
|
||||
//! `load_from_file` in the C++ world). With direct calls, graph
|
||||
//! (de)serialization stays oaknode's own serializer (`load`/`save`),
|
||||
//! and this bridge only boxes the resulting `Arc<Mutex<Project>>` into
|
||||
//! the canonical `CHandle` form the public C API moves around.
|
||||
//!
|
||||
//! A project handle boxed here is layout-identical to one produced by
|
||||
//! `oaknode_project_init`: both box `Arc<Mutex<Project>>` behind the
|
||||
//! shared `oakcore_rs::handle::CHandle`, so `oakstorage_save` can
|
||||
//! consume an oaknode-created handle and `oakstorage_project_take_*`
|
||||
//! returns one the caller can hand back to oaknode functions.
|
||||
//! The ove-xml/otio backends need oaknode's project + serializer
|
||||
//! families. Graph (de)serialization stays oaknode's own serializer
|
||||
//! (`load`/`save`); these helpers box the resulting `Arc<Mutex<Project>>`
|
||||
//! into the canonical handle form the storage session API moves around.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::error::Result;
|
||||
@@ -39,24 +30,6 @@ use crate::handle::CHandle;
|
||||
/// The project payload boxed by project handles.
|
||||
pub type ProjectArc = Arc<Mutex<oaknode::project::Project>>;
|
||||
|
||||
/// New empty project, boxed as an owned handle (refcount 1). The
|
||||
/// caller owns it (free with [`project_free`] / `oaknode_project_free`).
|
||||
pub fn project_init() -> CHandle {
|
||||
oaknode::handle::make_owned(oaknode::project::Project::new())
|
||||
}
|
||||
|
||||
/// Free a project handle (`NULL`/empty no-op).
|
||||
pub fn project_free(project: *mut CHandle) {
|
||||
if project.is_null() || unsafe { (*project).ctx.is_null() } {
|
||||
return;
|
||||
}
|
||||
let h = unsafe { (*project).clone() };
|
||||
if let Some(f) = h.release {
|
||||
unsafe { f(h.ctx) };
|
||||
}
|
||||
unsafe { (*project).ctx = std::ptr::null_mut() };
|
||||
}
|
||||
|
||||
/// Box an existing project as an owned handle (refcount 1).
|
||||
pub fn make_project_owned(project: ProjectArc) -> CHandle {
|
||||
oaknode::handle::make_owned(project)
|
||||
@@ -65,8 +38,8 @@ pub fn make_project_owned(project: ProjectArc) -> CHandle {
|
||||
/// Read the boxed project of a project handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// The handle must box `Arc<Mutex<Project>>` (created by this bridge,
|
||||
/// `oaknode_project_init`, or `oakstorage_project_take_project`).
|
||||
/// The handle must box `Arc<Mutex<Project>>` (created by this module or
|
||||
/// oaknode's project helpers).
|
||||
pub unsafe fn project_arc(h: &CHandle) -> Result<ProjectArc> {
|
||||
unsafe { oaknode::handle::get::<ProjectArc>(h) }
|
||||
.cloned()
|
||||
@@ -87,9 +60,9 @@ pub fn serializer_save(project: &oaknode::project::Project) -> Result<String> {
|
||||
/// subtitle) in `graph`. Returns `(sequence_id, [video, audio,
|
||||
/// subtitle] list ids)`.
|
||||
///
|
||||
/// Mirrors the private `append_default_track_lists` wiring of
|
||||
/// `oaknode_sequence_create` (ffi.rs) so the otio backend can import
|
||||
/// timelines into real projects; shared with tests through this bridge.
|
||||
/// Mirrors the private `append_default_track_lists` wiring of the former
|
||||
/// `oaknode_sequence_create` export so the otio backend can import
|
||||
/// timelines into real projects.
|
||||
pub fn create_sequence(
|
||||
graph: &mut oaknode::graph::Graph,
|
||||
) -> (oaknode::id::NodeId, Vec<oaknode::id::NodeId>) {
|
||||
@@ -148,23 +121,3 @@ pub fn create_sequence(
|
||||
}
|
||||
(seq_id, lists)
|
||||
}
|
||||
|
||||
/// Two-stage C string copy helper: returns the required size including
|
||||
/// the NUL; writes up to `buf_size - 1` bytes plus NUL when
|
||||
/// `buf_size > 0` (`// CPP-PARITY: src/node/c_api/valueconvert.h`
|
||||
/// `copy_string`).
|
||||
///
|
||||
/// # Safety
|
||||
/// `buf`/`buf_size` must describe a valid writable buffer (or `buf` may
|
||||
/// be null).
|
||||
pub unsafe fn copy_string_out(value: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let required = value.len() + 1;
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let copy_len = value.len().min(buf_size as usize - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(value.as_ptr() as *const c_char, buf, copy_len);
|
||||
*buf.add(copy_len) = 0;
|
||||
}
|
||||
}
|
||||
required as c_int
|
||||
}
|
||||
@@ -30,11 +30,13 @@ pub struct Session {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Wrap a freshly loaded project.
|
||||
/// Wrap a freshly loaded project. A null handle (the version-info
|
||||
/// path: TOO_OLD/TOO_NEW/UNKNOWN_VERSION carries no project) maps to
|
||||
/// `None`, not `Some(null)`.
|
||||
pub fn new(uri: StorageUri, project: CHandle) -> Self {
|
||||
Session {
|
||||
uri,
|
||||
project: Some(project),
|
||||
project: (!project.is_null()).then_some(project),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user