// 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 . //! oaknode C ABI 导入(include/node/node.h 中 param 桥用到的子集)。 //! //! ## Value 布局冻结(M11 第 1 期) //! //! [`Value`] 即 include/node/node.h:93 的 `oaknode_value` POD,字段 //! 逐字一致(type/num/den/f[4];`type` 取值见 [`node_value_type`])。 //! 字符串族输入(k_file/k_text/k_font/k_str_combo,node.h:48-52)没有 //! POD 表示——走 `*_input_string_*` 专用函数(本桥的 //! [`set_input_string_undoable`]);`OAKNODE_VALUE_STRING` 的 POD 里 //! 不携带字符串数据。`crate::ffi::OakNodeValue` 与此同布局(出口层 //! 的镜像,两处独立声明避免模块环)。 //! //! ## 双态实现(同 [`crate::bridge::render`]) //! //! - 默认:直接 Rust 调用 oaknode 的 `ffi`(单库化,见 //! `docs/zh/plans/riir/single-lib.md`); //! - `--features test-stubs`:库内状态桩([`stub`],纯 Rust、无 //! `#[no_mangle]`,与真实 oaknode 的导出不冲突)——节点值、undo //! 命令全链路可在 cargo test 跑通。两种形态的调用面完全一致。 use std::ffi::c_char; use crate::bridge::undo::CommandHandle; use crate::handle::CHandle; /// oaknode 节点句柄(值型)。 pub type NodeHandle = crate::handle::CHandle; /// oaknode_value_type 的取值(node.h:74;与 /// `crate::ffi::node_value_type` 逐值一致)。 pub mod node_value_type { /// OAKNODE_VALUE_NONE。 pub const NONE: i32 = 0; /// OAKNODE_VALUE_INT。 pub const INT: i32 = 1; /// OAKNODE_VALUE_FLOAT。 pub const FLOAT: i32 = 2; /// OAKNODE_VALUE_BOOL。 pub const BOOL: i32 = 3; /// OAKNODE_VALUE_RATIONAL。 pub const RATIONAL: i32 = 4; /// OAKNODE_VALUE_COLOR。 pub const COLOR: i32 = 5; /// OAKNODE_VALUE_VEC2。 pub const VEC2: i32 = 6; /// OAKNODE_VALUE_VEC3。 pub const VEC3: i32 = 7; /// OAKNODE_VALUE_VEC4。 pub const VEC4: i32 = 8; /// OAKNODE_VALUE_COMBO。 pub const COMBO: i32 = 9; /// OAKNODE_VALUE_STRING。 pub const STRING: i32 = 10; } /// oaknode_value(include/node/node.h:93,字段逐字一致)。 #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Value { /// 类型([`node_value_type`])。 pub r#type: i32, /// INT/COMBO 值、BOOL 0/1、RATIONAL 分子。 pub num: i64, /// RATIONAL 分母。 pub den: i64, /// FLOAT f[0];VEC2/3/4 f[0..n-1];COLOR r,g,b,a。 pub f: [f64; 4], } impl Value { /// 类型化构造:整数 / choice 索引。 pub const fn int(v: i64) -> Self { Self { r#type: node_value_type::INT, num: v, den: 0, f: [0.0; 4], } } /// 类型化构造:浮点。 pub const fn float(v: f64) -> Self { Self { r#type: node_value_type::FLOAT, num: 0, den: 0, f: [v, 0.0, 0.0, 0.0], } } /// 类型化构造:布尔。 pub const fn bool_(v: bool) -> Self { Self { r#type: node_value_type::BOOL, num: v as i64, den: 0, f: [0.0; 4], } } /// 类型化构造:choice(COMBO)。 pub const fn combo(v: i64) -> Self { Self { r#type: node_value_type::COMBO, num: v, den: 0, f: [0.0; 4], } } /// 类型化构造:颜色。 pub const fn color(r: f64, g: f64, b: f64, a: f64) -> Self { Self { r#type: node_value_type::COLOR, num: 0, den: 0, f: [r, g, b, a], } } /// 类型化构造:vec2/3/4(长度按 f 数组尾部 0 判定)。 pub const fn vec(v: &[f64]) -> Self { let t = match v.len() { 2 => node_value_type::VEC2, 3 => node_value_type::VEC3, _ => node_value_type::VEC4, }; let mut f = [0.0; 4]; let mut i = 0; while i < v.len() && i < 4 { f[i] = v[i]; i += 1; } Self { r#type: t, num: 0, den: 0, f, } } /// 类型化构造:字符串族(POD 不携带数据;值经 /// [`set_input_string_undoable`] 传递)。 pub const fn string() -> Self { Self { r#type: node_value_type::STRING, num: 0, den: 0, f: [0.0; 4], } } } // ---- 桥调用面 ------------------------------------------------------------ /// 按身份取节点句柄(M9 身份注册表;`oaknode_node_from_identity`)。 /// 身份未登记 / 符号缺失 → 空句柄。 pub(crate) unsafe fn node_from_identity(id: usize) -> NodeHandle { #[cfg(feature = "test-stubs")] { unsafe { stub::node_from_identity_impl(id) } } #[cfg(not(feature = "test-stubs"))] { unsafe { oaknode::ffi::node::oaknode_node_from_identity(id) } } } /// 写输入值的标准值并产出一条 undo 命令(`*out` 收到拥有型命令句柄; /// `oaknode_node_set_input_undoable`,node.h:327)。字符串族输入走 /// [`set_input_string_undoable`]。失败(含符号缺失)返回负错误码。 /// /// # Safety /// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。 pub(crate) unsafe fn set_input_undoable( node: NodeHandle, input: *const c_char, value: *const Value, out: *mut CommandHandle, ) -> i32 { #[cfg(feature = "test-stubs")] { unsafe { stub::set_input_undoable_impl(node, input, value, out) } } #[cfg(not(feature = "test-stubs"))] { unsafe { oaknode::ffi::node::oaknode_node_set_input_undoable( node, input, value as *const oaknode::value::OakNodeValue, out, ) } } } /// 写字符串族输入的标准值并产出一条 undo 命令 /// (`oaknode_node_set_input_string_undoable`,node.h:346)。 /// /// # Safety /// `input`/`value`/`out` 必须指向有效内存;`node` 是有效句柄。 pub(crate) unsafe fn set_input_string_undoable( node: NodeHandle, input: *const c_char, value: *const c_char, out: *mut CommandHandle, ) -> i32 { #[cfg(feature = "test-stubs")] { unsafe { stub::set_input_string_undoable_impl(node, input, value, out) } } #[cfg(not(feature = "test-stubs"))] { unsafe { oaknode::ffi::node::oaknode_node_set_input_string_undoable(node, input, value, out) } } } // ---- 测试桩(--features test-stubs)-------------------------------------- /// oaknode 测试桩:库内符号 + 节点值状态。 /// /// 节点句柄的 ctx 约定(桩内约定):`ctx = 身份 id`。undo 命令的 /// 登记与 undo/redo 语义在 [`crate::bridge::undo::stub`](两边共享 /// 同一张命令表)。 #[cfg(feature = "test-stubs")] pub mod stub { use super::*; use std::collections::HashMap; use std::ffi::CStr; use std::sync::LazyLock; use std::sync::Mutex; /// 一个输入的标准值(数值走 [`Value`] POD;字符串族走 `string`)。 #[derive(Clone, Debug, PartialEq)] pub struct StubInput { /// 数值值(字符串族为 STRING 类型空 POD)。 pub value: Value, /// 字符串值(仅字符串族输入)。 pub string: Option, } /// 全部桩节点:身份 → 输入名 → 值。 static NODES: LazyLock>>> = LazyLock::new(|| Mutex::new(HashMap::new())); fn lock() -> std::sync::MutexGuard<'static, HashMap>> { NODES.lock().unwrap_or_else(|e| e.into_inner()) } /// 重置全部桩节点(测试隔离)。 pub fn reset() { lock().clear(); } /// 登记一个可被 [`super::node_from_identity`] 找到的节点。 pub fn register_node(id: usize) { lock().entry(id).or_default(); } /// 直接设置输入值(测试前置)。 pub fn set_input(id: usize, input: &str, value: Value) { lock().entry(id).or_default().insert( input.to_string(), StubInput { value, string: None, }, ); } /// 直接设置字符串输入值(测试前置)。 pub fn set_input_string(id: usize, input: &str, value: &str) { lock().entry(id).or_default().insert( input.to_string(), StubInput { value: Value::string(), string: Some(value.to_string()), }, ); } /// 读输入当前值(断言用)。 pub fn input(id: usize, input: &str) -> Option { lock().get(&id)?.get(input).cloned() } /// 命令回写用的应用入口(redo/undo 落值;由 /// [`crate::bridge::undo::stub`] 调用)。 pub(crate) fn apply(node: usize, input: &str, value: Value, string: Option) { let mut m = lock(); if let Some(n) = m.get_mut(&node) { n.insert(input.to_string(), StubInput { value, string }); } } /// 读输入当前值(命令创建时取 prev)。 pub(crate) fn current(node: usize, input: &str) -> StubInput { lock() .get(&node) .and_then(|n| n.get(input)) .cloned() .unwrap_or(StubInput { value: Value::default(), string: None, }) } pub(super) unsafe fn node_from_identity_impl(id: usize) -> NodeHandle { if lock().contains_key(&id) { CHandle { ctx: id as *mut std::ffi::c_void, addref: None, release: None, abi_version: crate::handle::OAKPLUGIN_ABI_VERSION, } } else { CHandle::null() } } pub(super) unsafe fn set_input_undoable_impl( node: NodeHandle, input: *const c_char, value: *const Value, out: *mut CommandHandle, ) -> i32 { if node.is_null() || input.is_null() || value.is_null() || out.is_null() { return -30001; } let id = node.ctx as usize; let name = unsafe { CStr::from_ptr(input) } .to_str() .map_err(|_| ()) .unwrap_or_default(); // 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。 if !lock().get(&id).is_some_and(|n| n.contains_key(name)) { return -30004; } let next = unsafe { *value }; let prev = current(id, &name).value; let h = crate::bridge::undo::stub::create_numeric(id, name.to_string(), prev, next); unsafe { *out = h }; 0 } pub(super) unsafe fn set_input_string_undoable_impl( node: NodeHandle, input: *const c_char, value: *const c_char, out: *mut CommandHandle, ) -> i32 { if node.is_null() || input.is_null() || value.is_null() || out.is_null() { return -30001; } let id = node.ctx as usize; let name = unsafe { CStr::from_ptr(input) } .to_str() .map_err(|_| ()) .unwrap_or_default(); // 输入名必须已存在(node.h: 未知输入 id → OAKNODE_E_NOT_FOUND)。 if !lock().get(&id).is_some_and(|n| n.contains_key(name)) { return -30004; } let next = unsafe { CStr::from_ptr(value) } .to_string_lossy() .into_owned(); let prev = current(id, &name).string.unwrap_or_default(); let h = crate::bridge::undo::stub::create_string( id, name.to_string(), prev.clone(), next.clone(), ); unsafe { *out = h }; 0 } } // ---- 默认模式单测(无桩;空/NULL 句柄经真实 crate 的可解释失败路径)---------------------- #[cfg(all(test, not(feature = "test-stubs")))] mod tests { use super::*; /// 空/NULL 句柄经真实 oaknode 容错:未登记身份 → 空句柄;空句柄 /// 与空指针参数被拒(OAKNODE_E_INVALID)。 #[test] fn empty_handle_error_paths() { let mut cmd = CommandHandle::null(); unsafe { assert!(node_from_identity(0xDEAD).is_null(), "未登记身份 → 空句柄"); assert_eq!( set_input_undoable( NodeHandle::null(), std::ptr::null(), std::ptr::null(), &mut cmd ), -30001 ); assert_eq!( set_input_string_undoable( NodeHandle::null(), std::ptr::null(), std::ptr::null(), &mut cmd ), -30001 ); } } }