// 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 . //! Host 单例与插件缓存。 //! //! 对应 C++ 的 `OliveHost` + HostSupport 的 `PluginCache`。职责: //! bundle 扫描、`dlopen` 插件、调 `setHost`/`load`/`describe` //! 建立 [`Plugin`] 记录、按标识创建实例。参照: //! HS: ofxhPluginCache.cpp(扫描去重与索引语义)、 //! HS: ofxhImageEffectAPI.cpp:150-238(load/describe/ //! describeInContext 的 action 序列)。 //! //! ## 扫描语义(对照 olivehost.cpp:118-191 + OFX 官方规范) //! //! 默认路径集按平台组织: //! 用户级(`$HOME` 非空时)——`$HOME/.OFX/Plugins`、 //! `$HOME/.local/share/OFX/Plugins`、`$HOME/.local/share/olive/ofx/Plugins` //! (macOS 另含 `$HOME/Library/OFX/Plugins`); //! 系统级——macOS `/Library/OFX/Plugins`,Linux `/usr/OFX/Plugins`、 //! `/usr/local/OFX/Plugins`,Windows `%ProgramFiles%\Common Files\OFX\Plugins` //! (`%ProgramFiles%` 未设置时回退 `C:\Program Files\...`); //! Olive app-relative——`../OFX/Plugins`、`../share/olive/ofx/Plugins`、 //! `../lib/olive/ofx/Plugins`(olivehost.cpp:104-107); //! 以及 `OFX_PLUGIN_PATH`(OFX 官方)与 `OLIVE_OFX_PLUGIN_PATH`/ //! `OLIVE_PLUGIN_PATH`(Olive 扩展)三个环境变量(平台路径分隔符: //! Unix ':' / Windows ';')。不存在的目录直接跳过;重复扫描按规范化 //! 路径去重。 //! `// [P2]`:Info.plist 解析(CFBundleExecutable)不做——bundle 内 //! 二进制以探测方式定位(`Contents/MacOS/*`、`Contents/Linux-*/*`、 //! 根下 `*.so`/`*.dylib`)。 //! //! ## dlopen(零依赖) //! //! 直接声明 `dlopen`/`dlsym`/`dlclose`(macOS libSystem / Linux libc //! 内建;RTLD 常量按平台取值)。加载顺序按 ofxCore.h:574: //! `OfxGetNumberOfPlugins` → `OfxGetPlugin`,随后 //! `OfxPlugin::setHost`(mandatory,ofxCore.h:124-132)→ //! `load` → `describe`(HS: ofxhImageEffectAPI.cpp:158-190)。 use std::ffi::{c_char, c_int, c_uint, c_void, CStr, CString}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use crate::descriptor::EffectDescriptor; use crate::handle::RefBox; use crate::instance::Instance; use crate::property::{PropertySet, Value}; use crate::suites::status; // ---- dlopen FFI(零依赖;RTLD 常量按平台)---- #[cfg(target_os = "macos")] const RTLD_NOW: c_int = 0x2; #[cfg(target_os = "linux")] const RTLD_NOW: c_int = 0x2; #[cfg(target_os = "macos")] const RTLD_LOCAL: c_int = 0x4; #[cfg(target_os = "linux")] const RTLD_LOCAL: c_int = 0x0; // Windows has no dlopen/dlsym/dlclose: the POSIX FFI below is compiled // out and replaced by LoadLibraryExW/GetProcAddress/FreeLibrary (see the // `win32` module further down), keeping the same function signatures. #[cfg(not(target_os = "windows"))] extern "C" { fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; fn dlclose(handle: *mut c_void) -> c_int; } #[cfg(not(target_os = "windows"))] /// 动态加载共享库;失败返回 None。 fn dl_open(path: &Path) -> Option<*mut c_void> { let c = CString::new(path.to_str()?).ok()?; let h = unsafe { dlopen(c.as_ptr(), RTLD_NOW | RTLD_LOCAL) }; if h.is_null() { None } else { Some(h) } } #[cfg(not(target_os = "windows"))] /// 查符号;失败返回 None。 fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> { let c = CString::new(name).ok()?; let p = unsafe { dlsym(handle, c.as_ptr()) }; if p.is_null() { None } else { Some(p) } } #[cfg(not(target_os = "windows"))] /// 从 `dlsym` 结果取函数指针(libloading 同款转换;调用方保证符号 /// 类型正确)。 /// /// # Safety /// 符号必须确实是该函数类型。 unsafe fn dlsym_fn(handle: *mut c_void, name: &str) -> Option { let p = dl_sym(handle, name)?; Some(unsafe { std::mem::transmute_copy(&p) }) } // ---- Windows dynamic loading (kernel32) ---- // // LoadLibraryExW / GetProcAddress / FreeLibrary in place of the POSIX // dlopen family. The Windows handle is an HMODULE, which is pointer-sized // and stored in the same `*mut c_void` slot, so the public signatures // (`dl_open` / `dl_sym` / `dlsym_fn` / `dlclose`) are unchanged. /// Windows dynamic loading (`LoadLibraryExW` / `GetProcAddress` / /// `FreeLibrary`, kernel32). The handle is an `HMODULE` kept as /// `*mut c_void` for signature parity with the POSIX path. #[cfg(target_os = "windows")] mod win32 { use super::*; use std::os::windows::ffi::OsStrExt; /// `LOAD_WITH_ALTERED_SEARCH_PATH` (winbase.h): resolve the loaded /// module's dependent DLLs from the module's own directory first, so a /// plugin bundle's sibling DLLs are found next to the binary. const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x0000_0008; #[link(name = "kernel32")] unsafe extern "C" { fn LoadLibraryExW( lp_file_name: *const u16, h_file: *mut c_void, dw_flags: u32, ) -> *mut c_void; fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void; fn FreeLibrary(h_module: *mut c_void) -> c_int; } /// Dynamically load a shared library from an arbitrary (possibly /// non-UTF-8) path; `None` on failure. The path is converted to a /// NUL-terminated UTF-16 string for `LoadLibraryExW`. pub(super) fn dl_open(path: &Path) -> Option<*mut c_void> { let wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); let h = unsafe { LoadLibraryExW(wide.as_ptr(), std::ptr::null_mut(), LOAD_WITH_ALTERED_SEARCH_PATH) }; if h.is_null() { None } else { Some(h) } } /// Look up an exported symbol by name; `None` on failure. The symbol /// name is a narrow (ANSI) string — `OfxGetNumberOfPlugins` / /// `OfxGetPlugin`, both plain ASCII. pub(super) fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> { let c = CString::new(name).ok()?; let p = unsafe { GetProcAddress(handle, c.as_ptr()) }; if p.is_null() { None } else { Some(p) } } /// Function pointer from a `GetProcAddress` result (same transmute as /// the POSIX `dlsym_fn`; the caller guarantees the symbol type). /// /// # Safety /// The symbol must actually be that function type. pub(super) unsafe fn dlsym_fn(handle: *mut c_void, name: &str) -> Option { let p = dl_sym(handle, name)?; Some(unsafe { std::mem::transmute_copy(&p) }) } /// Unload a library (`FreeLibrary`; reference-counted like POSIX /// `dlclose`). pub(super) unsafe fn dlclose(handle: *mut c_void) -> c_int { unsafe { FreeLibrary(handle) } } } #[cfg(target_os = "windows")] use win32::{dl_open, dl_sym, dlsym_fn, dlclose}; // ---- OFX 宿主侧结构(ofxCore.h,字段序与 SDK 逐字一致)---- /// `OfxHost`(ofxCore.h:44)。 #[repr(C)] struct OfxHost { /// 宿主属性集句柄(`Host::props` 的裸指针,标签 0)。 host: *mut c_void, /// fetchSuite。 fetch_suite: unsafe extern "C" fn(*mut c_void, *const c_char, c_int) -> *const c_void, } /// `OfxPlugin`(ofxCore.h:96)。 #[repr(C)] struct OfxPlugin { plugin_api: *const c_char, api_version: c_int, plugin_identifier: *const c_char, plugin_version_major: c_uint, plugin_version_minor: c_uint, set_host: Option, main_entry: Option< unsafe extern "C" fn(*const c_char, *const c_void, *mut c_void, *mut c_void) -> c_int, >, } /// fetchSuite 的宿主出口(插件经 `OfxHost::fetchSuite` 拿到; /// 经 [`crate::suites::fetch_suite`] 分发表)。 unsafe extern "C" fn host_fetch_suite( host: *mut c_void, name: *const c_char, version: c_int, ) -> *const c_void { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if host.is_null() || name.is_null() { return None; } let name = unsafe { CStr::from_ptr(name) }.to_str().ok()?; crate::suites::fetch_suite(name, version) })); match result { Ok(Some(p)) => { if std::env::var_os("OAK_OFX_TRACE").is_some() && !name.is_null() { if let Ok(n) = unsafe { CStr::from_ptr(name) }.to_str() { eprintln!("[ofx] fetchSuite hit: {n} v{version}"); } } p } Ok(None) => { // 诊断:插件请求的 suite 宿主没有。厂商套件(Nuke/Vegas/ // Foundry)的探测是插件的正常行为,不打正式日志——需要 // 排查时用 OAK_OFX_TRACE 打开。 if std::env::var_os("OAK_OFX_TRACE").is_some() && !name.is_null() { if let Ok(n) = unsafe { CStr::from_ptr(name) }.to_str() { eprintln!("[ofx] fetchSuite miss: {n} v{version}"); } } std::ptr::null() } _ => std::ptr::null(), } } /// 进程级 `OfxHost`(ofxs 支持库的 setHost 只保存**指针**而不拷贝 /// 结构体——栈上临时变量会在 setHost 返回后悬垂,describe/渲染期 /// 再经 fetchSuite 回调就是野指针)。堆泄漏一次,永久有效。 fn global_ofx_host() -> *mut OfxHost { static PTR: OnceLock = OnceLock::new(); *PTR.get_or_init(|| { let host = Host::global(); Box::into_raw(Box::new(OfxHost { host: &host.props as *const PropertySet as *mut c_void, fetch_suite: host_fetch_suite, })) as usize }) as *mut OfxHost } // ---- 动作与属性常量(ofxCore.h / ofxImageEffect.h)---- /// kOfxActionLoad。 pub(crate) const ACTION_LOAD: &str = "OfxActionLoad"; /// kOfxActionDescribe。 pub(crate) const ACTION_DESCRIBE: &str = "OfxActionDescribe"; /// kOfxActionCreateInstance。 pub(crate) const ACTION_CREATE_INSTANCE: &str = "OfxActionCreateInstance"; /// kOfxActionDestroyInstance。 pub(crate) const ACTION_DESTROY_INSTANCE: &str = "OfxActionDestroyInstance"; /// kOfxImageEffectActionDescribeInContext。 pub(crate) const ACTION_DESCRIBE_IN_CONTEXT: &str = "OfxImageEffectActionDescribeInContext"; /// kOfxImageEffectActionGetClipPreferences。 pub(crate) const ACTION_GET_CLIP_PREFERENCES: &str = "OfxImageEffectActionGetClipPreferences"; /// kOfxImageEffectActionGetRegionOfDefinition。 pub(crate) const ACTION_GET_ROD: &str = "OfxImageEffectActionGetRegionOfDefinition"; /// kOfxImageEffectActionGetRegionsOfInterest。 pub(crate) const ACTION_GET_ROI: &str = "OfxImageEffectActionGetRegionsOfInterest"; /// kOfxImageEffectActionIsIdentity。 pub(crate) const ACTION_IS_IDENTITY: &str = "OfxImageEffectActionIsIdentity"; /// kOfxImageEffectActionRender。 pub(crate) const ACTION_RENDER: &str = "OfxImageEffectActionRender"; /// kOfxImageEffectActionBeginSequenceRender。 pub(crate) const ACTION_BEGIN_SEQUENCE: &str = "OfxImageEffectActionBeginSequenceRender"; /// kOfxImageEffectActionEndSequenceRender。 pub(crate) const ACTION_END_SEQUENCE: &str = "OfxImageEffectActionEndSequenceRender"; /// kOfxActionOpenGLContextAttached(ofxGPURender.h:345)。 pub(crate) const ACTION_GL_CONTEXT_ATTACHED: &str = "OfxActionOpenGLContextAttached"; /// kOfxActionOpenGLContextDetached(ofxGPURender.h:371;宏值自带 /// "kOfx" 前缀,是规范原文)。 pub(crate) const ACTION_GL_CONTEXT_DETACHED: &str = "kOfxActionOpenGLContextDetached"; /// kOfxImageEffectActionGetOutputColourspace(ofxColour.h:283)。 pub(crate) const ACTION_GET_OUTPUT_COLOURSPACE: &str = "OfxImageEffectActionGetOutputColourspace"; /// kOfxActionInstanceChanged(ofxCore.h:449):宿主侧参数/时间变更 /// 通知。按下 push button 后宿主须以 kOfxChangeUserEdited 的原因调用 /// 它(ofxCore.h:405-435 的 inArgs 契约)。 pub(crate) const ACTION_INSTANCE_CHANGED: &str = "OfxActionInstanceChanged"; // ---- Interact(OFX 自定义交互;ofxInteract.h / ofxDrawSuite.h / ofxKeySyms.h)---- /// kOfxActionNewInteract:宿主创建 interact 时发给 interact 入口的 /// 首个 action。vendored ofxInteract.h 未定义该宏(官方头文件无此 /// action;任务契约按 "向插件 main entry 发 kOfxActionNewInteract" /// 命名)——取值 "OfxActionNewInteract",与 OFX action 命名惯例一致。 /// 插件不实现该 action 时返回 kOfxStatReplyDefault(宿主视为"无 /// interact"或"不参与 NewInteract 协议",见 [`crate::instance::Instance::new_interact`])。 pub(crate) const ACTION_NEW_INTERACT: &str = "OfxActionNewInteract"; /// kOfxInteractActionIdle:宿主空闲泵(任务契约;vendored ofxInteract.h /// 未收录——OFX 官方规范无 Idle action,属本宿主扩展,取值 /// "OfxInteractActionIdle")。插件实现与否自愿;未处理返回 /// kOfxStatReplyDefault。 pub(crate) const ACTION_INTERACT_IDLE: &str = "OfxInteractActionIdle"; /// kOfxInteractActionDraw(ofxInteract.h:265)。 pub(crate) const ACTION_INTERACT_DRAW: &str = "OfxInteractActionDraw"; /// kOfxInteractActionPenMotion(ofxInteract.h:302)。 pub(crate) const ACTION_INTERACT_PEN_MOTION: &str = "OfxInteractActionPenMotion"; /// kOfxInteractActionPenDown(ofxInteract.h:340)。 pub(crate) const ACTION_INTERACT_PEN_DOWN: &str = "OfxInteractActionPenDown"; /// kOfxInteractActionPenUp(ofxInteract.h:376)。 pub(crate) const ACTION_INTERACT_PEN_UP: &str = "OfxInteractActionPenUp"; /// kOfxInteractActionKeyDown(ofxInteract.h:410)。 pub(crate) const ACTION_INTERACT_KEY_DOWN: &str = "OfxInteractActionKeyDown"; /// kOfxInteractActionKeyUp(ofxInteract.h:443)。 pub(crate) const ACTION_INTERACT_KEY_UP: &str = "OfxInteractActionKeyUp"; /// kOfxInteractActionGainFocus(ofxInteract.h:501)。 pub(crate) const ACTION_INTERACT_GAIN_FOCUS: &str = "OfxInteractActionGainFocus"; /// kOfxInteractActionLoseFocus(ofxInteract.h:526)。 pub(crate) const ACTION_INTERACT_LOSE_FOCUS: &str = "OfxInteractActionLoseFocus"; /// kOfxInteractPropPixelScale(ofxInteract.h:58):canonical→屏幕像素 /// 换算比例(Double×2)。 pub(crate) const PROP_INTERACT_PIXEL_SCALE: &str = "OfxInteractPropPixelScale"; /// kOfxInteractPropViewportSize(OFX 1.3 命名 "OfxInteractPropViewport"; /// vendored 1.5 头文件已删,任务契约要求 draw inArgs 携带视口尺寸)。 pub(crate) const PROP_INTERACT_VIEWPORT_SIZE: &str = "OfxInteractPropViewport"; /// kOfxInteractPropBackgroundImage:任务契约的 draw inArgs 背景图像 /// 句柄(Pointer;无背景时为空——本宿主 Phase 1 无合成背景,恒空)。 /// 非官方 OFX 属性(官方只有 BackgroundColour),属本宿主扩展。 pub(crate) const PROP_INTERACT_BACKGROUND_IMAGE: &str = "OfxInteractPropBackgroundImage"; /// kOfxInteractPropBackgroundColour(ofxInteract.h:71):宿主视口背景色 /// (Double×3)。 pub(crate) const PROP_INTERACT_BACKGROUND_COLOUR: &str = "OfxInteractPropBackgroundColour"; /// kOfxInteractPropSuggestedColour(ofxInteract.h:86):宿主建议的 overlay /// 颜色(Double×3;宿主不支持颜色选择时返回 ReplyDefault)。 pub(crate) const PROP_INTERACT_SUGGESTED_COLOUR: &str = "OfxInteractPropSuggestedColour"; /// kOfxInteractPropSlaveToParam(ofxInteract.h:50):值变化触发 interact /// 重绘的参数名(String×N)。 pub(crate) const PROP_INTERACT_SLAVE_TO_PARAM: &str = "OfxInteractPropSlaveToParam"; /// kOfxInteractPropPenPosition(ofxInteract.h:95):笔的 canonical 位置 /// (Double×2,只读 inArgs)。 pub(crate) const PROP_INTERACT_PEN_POSITION: &str = "OfxInteractPropPenPosition"; /// kOfxInteractPropPenViewportPosition(ofxInteract.h:104):笔的视口像素 /// 位置(Int×2,只读 inArgs)。 pub(crate) const PROP_INTERACT_PEN_VIEWPORT_POSITION: &str = "OfxInteractPropPenViewportPosition"; /// kOfxInteractPropPenPressure(ofxInteract.h:114):笔压(Double×1, /// 0..1;两态笔映射 0/1)。 pub(crate) const PROP_INTERACT_PEN_PRESSURE: &str = "OfxInteractPropPenPressure"; /// kOfxInteractPropBitDepth(ofxInteract.h:122):interact 帧缓冲位深 /// (Int×1,只读)。 pub(crate) const PROP_INTERACT_BIT_DEPTH: &str = "OfxInteractPropBitDepth"; /// kOfxInteractPropHasAlpha(ofxInteract.h:132):interact 帧缓冲是否含 /// alpha(Int×1,只读)。 pub(crate) const PROP_INTERACT_HAS_ALPHA: &str = "OfxInteractPropHasAlpha"; /// kOfxInteractPropDrawContext(ofxDrawSuite.h:34):Draw suite 上下文句柄 /// (Pointer;draw inArgs 携带,插件取来传给 Draw suite 函数)。 pub(crate) const PROP_INTERACT_DRAW_CONTEXT: &str = "OfxInteractPropDrawContext"; /// kOfxPropKeySym(ofxKeySyms.h:30):键盘事件的关键码(Int×1)。 pub(crate) const PROP_KEY_SYM: &str = "kOfxPropKeySym"; /// kOfxPropKeyString(ofxKeySyms.h:49):键盘事件的 UTF-8 字符(String×1)。 pub(crate) const PROP_KEY_STRING: &str = "kOfxPropKeyString"; /// kOfxImageEffectPluginPropOverlayInteractV2(ofxImageEffect.h:825): /// 插件声明的 overlay interact 入口(Pointer→OfxPluginEntryPoint;V2 /// 要求 Draw suite 绘制)。 pub(crate) const PROP_OVERLAY_INTERACT_V2: &str = "OfxImageEffectPluginPropOverlayInteractV2"; /// kOfxImageEffectPluginPropOverlayInteractV1(ofxImageEffect.h:812)。 pub(crate) const PROP_OVERLAY_INTERACT_V1: &str = "OfxImageEffectPluginPropOverlayInteractV1"; /// kOfxImageEffectPropSupportsOverlays(ofxImageEffect.h:801):宿主是否 /// 允许插件在输出图像上绘制 overlay(能力宣告)。 pub(crate) const PROP_SUPPORTS_OVERLAYS: &str = "OfxImageEffectPropSupportsOverlays"; // ---- OFX 关键码(ofxKeySyms.h;X11 keysym 值,测试/宿主常用子集)---- // // 公共:app 侧(WG3b)经 [`crate::suites::interact::Interact::key_down`]/ // `key_up` 传关键码。 /// kOfxKey_Unknown(ofxKeySyms.h:121)。 pub const KEY_UNKNOWN: i32 = 0x0; /// kOfxKey_BackSpace(ofxKeySyms.h:128)。 pub const KEY_BACKSPACE: i32 = 0xFF08; /// kOfxKey_Tab(ofxKeySyms.h:129)。 pub const KEY_TAB: i32 = 0xFF09; /// kOfxKey_Return(ofxKeySyms.h:132)。 pub const KEY_RETURN: i32 = 0xFF0D; /// kOfxKey_Escape(ofxKeySyms.h:136)。 pub const KEY_ESCAPE: i32 = 0xFF1B; /// kOfxKey_Delete(ofxKeySyms.h:137)。 pub const KEY_DELETE: i32 = 0xFFFF; /// kOfxKey_Home(ofxKeySyms.h:172)。 pub const KEY_HOME: i32 = 0xFF50; /// kOfxKey_Left(ofxKeySyms.h:173)。 pub const KEY_LEFT: i32 = 0xFF51; /// kOfxKey_Up(ofxKeySyms.h:174)。 pub const KEY_UP: i32 = 0xFF52; /// kOfxKey_Right(ofxKeySyms.h:175)。 pub const KEY_RIGHT: i32 = 0xFF53; /// kOfxKey_Down(ofxKeySyms.h:176)。 pub const KEY_DOWN: i32 = 0xFF54; /// kOfxKey_Page_Up(ofxKeySyms.h:178)。 pub const KEY_PAGE_UP: i32 = 0xFF55; /// kOfxKey_Page_Down(ofxKeySyms.h:180)。 pub const KEY_PAGE_DOWN: i32 = 0xFF56; /// kOfxKey_End(ofxKeySyms.h:181)。 pub const KEY_END: i32 = 0xFF57; /// kOfxKey_F1(ofxKeySyms.h:252)。 pub const KEY_F1: i32 = 0xFFBE; /// kOfxKey_Shift_L(ofxKeySyms.h:315)。 pub const KEY_SHIFT_L: i32 = 0xFFE1; /// kOfxKey_Control_L(ofxKeySyms.h:317)。 pub const KEY_CONTROL_L: i32 = 0xFFE3; /// kOfxKey_Alt_L(ofxKeySyms.h:323)。 pub const KEY_ALT_L: i32 = 0xFFE9; /// kOfxKey_space(ofxKeySyms.h:331)。 pub const KEY_SPACE: i32 = 0x020; /// kOfxKey_a(ofxKeySyms.h:398)。 pub const KEY_A: i32 = 0x061; /// kOfxKey_z(ofxKeySyms.h:423)。 pub const KEY_Z: i32 = 0x07a; /// kOfxPropChangeReason(ofxCore.h:763):instanceChanged 的 inArgs 里 /// 说明变更来源(UserEdited / PluginEdited / Time)。 pub(crate) const PROP_CHANGE_REASON: &str = "OfxPropChangeReason"; /// kOfxChangeUserEdited(ofxCore.h:792)。 pub(crate) const CHANGE_USER_EDITED: &str = "OfxChangeUserEdited"; /// kOfxChangePluginEdited(ofxCore.h:795)。 pub(crate) const CHANGE_PLUGIN_EDITED: &str = "OfxChangePluginEdited"; /// kOfxChangeTime(ofxCore.h:798)。 pub(crate) const CHANGE_TIME: &str = "OfxChangeTime"; /// kOfxPropTime(ofxCore.h:613)。 pub(crate) const PROP_TIME: &str = "OfxPropTime"; /// kOfxPropEffectInstance(ofxCore.h:776):interact/渲染 inArgs 里指向 /// 效果实例句柄的指针属性。 pub(crate) const PROP_EFFECT_INSTANCE: &str = "OfxPropEffectInstance"; /// kOfxImageEffectPropRenderScale。 pub(crate) const PROP_RENDER_SCALE: &str = "OfxImageEffectPropRenderScale"; /// kOfxImageEffectPropRenderWindow。 pub(crate) const PROP_RENDER_WINDOW: &str = "OfxImageEffectPropRenderWindow"; /// kOfxImageEffectPropRegionOfInterest。 pub(crate) const PROP_ROI: &str = "OfxImageEffectPropRegionOfInterest"; /// kOfxImageEffectPropRegionOfDefinition。 pub(crate) const PROP_ROD: &str = "OfxImageEffectPropRegionOfDefinition"; /// kOfxImageEffectPropIsIdentity。 pub(crate) const PROP_IS_IDENTITY: &str = "OfxImageEffectPropIsIdentity"; /// kOfxImageEffectPropFieldToRender。 pub(crate) const PROP_FIELD_TO_RENDER: &str = "OfxImageEffectPropFieldToRender"; /// kOfxImageEffectPropSequentialRenderStatus。 pub(crate) const PROP_SEQUENTIAL_RENDER: &str = "OfxImageEffectPropSequentialRenderStatus"; /// kOfxImageEffectPropInteractiveRenderStatus。 pub(crate) const PROP_INTERACTIVE_RENDER: &str = "OfxImageEffectPropInteractiveRenderStatus"; /// kOfxImageEffectPropRenderQualityDraft。 pub(crate) const PROP_RENDER_QUALITY_DRAFT: &str = "OfxImageEffectPropRenderQualityDraft"; /// kOfxImageEffectPropNoSpatialAwareness。 pub(crate) const PROP_NO_SPATIAL_AWARENESS: &str = "OfxImageEffectPropNoSpatialAwareness"; /// kOfxImageEffectPropFrameRange。 pub(crate) const PROP_FRAME_RANGE: &str = "OfxImageEffectPropFrameRange"; /// kOfxImageEffectPropFrameStep。 pub(crate) const PROP_FRAME_STEP: &str = "OfxImageEffectPropFrameStep"; /// kOfxImageEffectPropFrameRate。 pub(crate) const PROP_FRAME_RATE: &str = "OfxImageEffectPropFrameRate"; /// kOfxImageEffectPropPreMultiplication。 pub(crate) const PROP_PREMULT: &str = "OfxImageEffectPropPreMultiplication"; /// kOfxImageClipPropFieldOrder。 pub(crate) const PROP_FIELD_ORDER: &str = "OfxImageClipPropFieldOrder"; /// kOfxImageClipPropContinuousSamples。 pub(crate) const PROP_CONTINUOUS_SAMPLES: &str = "OfxImageClipPropContinuousSamples"; /// kOfxImageEffectFrameVarying。 pub(crate) const PROP_FRAME_VARYING: &str = "OfxImageEffectFrameVarying"; /// kOfxImageClipPropConnected。 pub(crate) const PROP_CLIP_CONNECTED: &str = "OfxImageClipPropConnected"; /// kOfxImageEffectPropContext。 pub(crate) const PROP_CONTEXT: &str = "OfxImageEffectPropContext"; /// kOfxImageEffectPropSupportedContexts。 pub(crate) const PROP_SUPPORTED_CONTEXTS: &str = "OfxImageEffectPropSupportedContexts"; /// kOfxImageEffectPropProjectSize。 pub(crate) const PROP_PROJECT_SIZE: &str = "OfxImageEffectPropProjectSize"; /// kOfxImageEffectPropProjectOffset。 pub(crate) const PROP_PROJECT_OFFSET: &str = "OfxImageEffectPropProjectOffset"; /// kOfxImageEffectPropProjectExtent。 pub(crate) const PROP_PROJECT_EXTENT: &str = "OfxImageEffectPropProjectExtent"; /// kOfxImageEffectPropProjectPixelAspectRatio(宏值即 PixelAspectRatio)。 pub(crate) const PROP_PROJECT_PAR: &str = "OfxImageEffectPropPixelAspectRatio"; /// kOfxImageEffectInstancePropEffectDuration。 pub(crate) const PROP_EFFECT_DURATION: &str = "OfxImageEffectInstancePropEffectDuration"; /// kOfxImageEffectInstancePropSequentialRender。 pub(crate) const PROP_SEQUENTIAL: &str = "OfxImageEffectInstancePropSequentialRender"; /// kOfxImageEffectPropPluginHandle。 pub(crate) const PROP_PLUGIN_HANDLE: &str = "OfxImageEffectPropPluginHandle"; /// kOfxPluginPropFilePath。 pub(crate) const PROP_PLUGIN_FILE_PATH: &str = "OfxPluginPropFilePath"; /// kOfxImageEffectPropSupportsTiles。 pub(crate) const PROP_SUPPORTS_TILES: &str = "OfxImageEffectPropSupportsTiles"; /// kOfxPropType。 pub(crate) const PROP_TYPE: &str = "OfxPropType"; /// kOfxPropLabel。 pub(crate) const PROP_LABEL: &str = "OfxPropLabel"; // ---- GL 能力与协商属性(ofxGPURender.h,M11 第 2 期)---- /// kOfxImageEffectPropOpenGLRenderSupported(ofxGPURender.h:62): /// 宿主描述符("true")与插件描述符("false"/"true"/"needed")。 pub(crate) const PROP_GL_RENDER_SUPPORTED: &str = "OfxImageEffectPropOpenGLRenderSupported"; /// kOfxImageEffectPropOpenGLEnabled(ofxGPURender.h:117):Render/ /// Begin/EndSequenceRender 的 in args。 pub(crate) const PROP_GL_ENABLED: &str = "OfxImageEffectPropOpenGLEnabled"; /// kOfxOpenGLPropPixelDepth(ofxGPURender.h:89):插件描述符的 GL /// 渲染支持位深(可选;格式协商的宿主侧输入)。 pub(crate) const PROP_GL_PIXEL_DEPTH: &str = "OfxOpenGLPropPixelDepth"; // ---- ofxColour 属性(ofxColour.h,M11 第 2 期)---- /// kOfxImageEffectPropColourManagementStyle(ofxColour.h:60)。 pub(crate) const PROP_COLOUR_STYLE: &str = "OfxImageEffectPropColourManagementStyle"; /// kOfxImageEffectColourManagementOCIO(ofxColour.h:73):宿主声明的 /// 色彩管理模式(任务要求)。 pub(crate) const COLOUR_STYLE_OCIO: &str = "OfxImageEffectColourManagementOCIO"; /// kOfxImageEffectPropColourManagementAvailableConfigs(ofxColour.h:85)。 pub(crate) const PROP_COLOUR_AVAILABLE_CONFIGS: &str = "OfxImageEffectPropColourManagementAvailableConfigs"; /// kOfxImageEffectPropColourManagementConfig(ofxColour.h:98):实例期 /// 协商出的 native 配置。 pub(crate) const PROP_COLOUR_CONFIG: &str = "OfxImageEffectPropColourManagementConfig"; /// kOfxImageEffectPropOCIOConfig(ofxColour.h:110):实例期 OCIO 配置 /// 路径/URI。 pub(crate) const PROP_OCIO_CONFIG: &str = "OfxImageEffectPropOCIOConfig"; /// kOfxImageClipPropColourspace(ofxColour.h:142):clip 实际色彩空间。 pub(crate) const PROP_CLIP_COLOURSPACE: &str = "OfxImageClipPropColourspace"; /// kOfxImageClipPropPreferredColourspaces(ofxColour.h:201):clip 偏好 /// 色彩空间(GetClipPreferences 的 out args 可写)。 pub(crate) const PROP_CLIP_PREFERRED_COLOURSPACES: &str = "OfxImageClipPropPreferredColourspaces"; /// 宿主支持的 native 色彩配置标识(ofxColour.h:78 的唯一现行值)。 pub(crate) const NATIVE_CONFIG_ID: &str = "ofx-native-v1.5_aces-v1.3_ocio-v2.3"; /// 宿主 OCIO 配置 URI(ofxColour.h:110 允许 ocio:// 内建配置;本宿主 /// 无自有 config 文件,用 OCIO 内建默认配置并以 clip 色彩空间属性 /// 传达 ACEScg 工作空间)。 pub(crate) const OCIO_CONFIG_URI: &str = "ocio://default"; /// 协商期 per-clip 约定名(HS: "OfxImageClipPropComponents_" /// 等,ofxhImageEffect.cpp:1725-1727)。 pub(crate) fn clip_pref_prop(prefix: &str, clip: &str) -> String { format!("{prefix}_{clip}") } /// 组件名前缀。 pub(crate) const CLIP_PREF_COMPONENTS: &str = "OfxImageClipPropComponents"; /// 位深名前缀。 pub(crate) const CLIP_PREF_DEPTH: &str = "OfxImageClipPropDepth"; /// 像素比名前缀。 pub(crate) const CLIP_PREF_PAR: &str = "OfxImageClipPropPAR"; fn cs(s: &str) -> CString { CString::new(s).unwrap() } // ---- Plugin -------------------------------------------------------------- /// 一个已加载插件(bundle 内的一个 effect 入口)。 pub struct Plugin { /// 插件标识(OfxPlugin::pluginIdentifier,如 "net.sf.cimg.CImgInvert")。 pub identifier: String, /// 版本号(major/minor)。 pub version: (u32, u32), /// bundle 路径。 pub bundle_path: PathBuf, /// describe 得到的支持上下文(filter/generator/transition)。 pub contexts: Vec, /// describe 产物(根描述符)。 pub descriptor: EffectDescriptor, /// dlopen 句柄(公开字段供测试构造假插件)。 pub lib: *mut c_void, /// 插件入口函数 `OfxPluginEntryPoint`(公开字段供测试构造; /// 签名与 SDK ofxCore.h:84 逐字一致——handle 是 `const void*`)。 pub entry: unsafe extern "C" fn( action: *const c_char, handle: *const c_void, in_args: *mut c_void, out_args: *mut c_void, ) -> i32, /// OfxPlugin 结构指针(kOfxImageEffectPropPluginHandle 属性用; /// 公开:测试构造假插件需要)。 pub ofx_plugin: *mut c_void, /// 二进制已卸载(host shutdown / unload_all)。置位后任何 /// call_action/call_entry 直接失败而不触碰入口——插件入口指向 /// 已 dlclose 的代码,调用即 SIGSEGV(测试串行 shutdown/重扫描 /// 会产生跨代实例,它们持有的正是旧代插件记录)。 pub unloaded: std::sync::atomic::AtomicBool, } // dlopen 句柄与 OfxPlugin 指针是**不透明令牌**(只经 dlsym/插件 // 结构字段访问,不直接解引用);跨线程搬运句柄是宿主分发语义。 // 与 property.rs 的 Value::Pointer 同理。 unsafe impl Send for Plugin {} unsafe impl Sync for Plugin {} /// 插件入口函数类型(`OfxPluginEntryPoint`,ofxCore.h:84)。 pub(crate) type EntryPoint = unsafe extern "C" fn( action: *const c_char, handle: *const c_void, in_args: *mut c_void, out_args: *mut c_void, ) -> i32; /// 插件声明的 overlay interact 入口(ofxImageEffect.h:825/812): /// V2 优先、V1 次之;两者都未声明返回 None。属性值是 /// `Pointer→OfxPluginEntryPoint`(宿主预定义,插件 describe 期写入)。 pub(crate) fn overlay_interact_entry(props: &PropertySet) -> Option { for name in [PROP_OVERLAY_INTERACT_V2, PROP_OVERLAY_INTERACT_V1] { if let Some(Value::Pointer(p)) = props.get(name, 0) { if !p.is_null() { // 属性值是函数指针(void* 存放);转换与 dlsym_fn 同款 // (调用方保证类型正确)。 return Some(unsafe { std::mem::transmute_copy(&p) }); } } } None } impl Plugin { /// 调插件的 action。`handle` 视 action 而定(describe 时为 /// descriptor,render 时为 instance)。`in_args`/`out_args` 按 /// OFX 规范组装属性集。 /// /// 返回 OFX 状态码(kOfxStatOK == 0)。任何插件侧异常/崩溃信号 /// 无法捕获——FFI 纪律只兜 Rust 侧 panic。 /// /// # Safety /// `handle`/参数集必须与 action 匹配(规范约定)。 pub(crate) unsafe fn call_action( &self, action: &str, handle: *mut c_void, in_args: &PropertySet, out_args: &PropertySet, ) -> i32 { if self.unloaded.load(std::sync::atomic::Ordering::Acquire) { return status::ERR_FATAL; } // 属性集经裸指针(标签 0)传给插件(property suite 接受)。 let in_ptr = in_args as *const PropertySet as *mut c_void; let out_ptr = out_args as *const PropertySet as *mut c_void; let action = cs(action); unsafe { (self.entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) } } /// 按指定入口函数调用(interact 的 overlay 入口与 main entry 可 /// 不同——插件经 kOfxImageEffectPluginPropOverlayInteractV2 声明)。 /// 语义同 [`Plugin::call_action`]。 /// /// # Safety /// `entry` 必须是插件导出的合法 OfxPluginEntryPoint;`handle`/参数 /// 集与 action 匹配。 pub(crate) unsafe fn call_entry( &self, entry: EntryPoint, action: &str, handle: *mut c_void, in_args: &PropertySet, out_args: &PropertySet, ) -> i32 { if self.unloaded.load(std::sync::atomic::Ordering::Acquire) { return status::ERR_FATAL; } let in_ptr = in_args as *const PropertySet as *mut c_void; let out_ptr = out_args as *const PropertySet as *mut c_void; let action = cs(action); unsafe { (entry)(action.as_ptr(), handle as *const c_void, in_ptr, out_ptr) } } } // ---- PluginCache --------------------------------------------------------- /// 一个已加载二进制(dlopen 句柄;多个 Plugin 可共享一个二进制)。 struct LoadedBinary { handle: *mut c_void, bundle_path: PathBuf, } impl Drop for LoadedBinary { fn drop(&mut self) { unsafe { dlclose(self.handle) }; } } // 同 Plugin:dlopen 句柄为不透明令牌。 unsafe impl Send for LoadedBinary {} unsafe impl Sync for LoadedBinary {} /// 插件缓存(进程单例,见 [`Host::global`])。 pub struct PluginCache { /// 已加载插件(identifier 唯一)。 plugins: Mutex>>, /// 已扫描的搜索路径(防重复扫描)。 scanned_paths: Mutex>, /// 已加载二进制(dlclose 责任;shutdown 时释放)。 binaries: Mutex>, } /// bundle 目录判定:目录名以 `.bundle` 或 `.plugin` 结尾。 fn is_bundle_dir(path: &Path) -> bool { let Some(name) = path.file_name().and_then(|n| n.to_str()) else { return false; }; name.ends_with(".bundle") || name.ends_with(".plugin") } /// 在 bundle 目录内定位二进制(探测式;`// [P2]` 起读 Info.plist 的 /// CFBundleExecutable)。 fn find_binary_in_bundle(bundle: &Path) -> Option { let contents = bundle.join("Contents"); for platform_dir in [ contents.join("MacOS"), contents.join("Linux-x86-64"), contents.join("Linux-aarch64"), // OFX 规范的 64 位 Windows 平台目录。 contents.join("Win64"), ] { if let Ok(entries) = std::fs::read_dir(&platform_dir) { for e in entries.flatten() { let p = e.path(); if p.is_file() && !p.extension().is_some_and(|x| x == "plist") { return Some(p); } } } } // 退而求其次:bundle 根下的 .so/.dylib/.ofx。 if let Ok(entries) = std::fs::read_dir(bundle) { for e in entries.flatten() { let p = e.path(); let ext = p.extension().and_then(|x| x.to_str()).unwrap_or(""); if p.is_file() && (ext == "so" || ext == "dylib" || ext == "ofx") { return Some(p); } } } None } /// 默认插件搜索路径(扫描顺序):OFX 规范的用户/系统级位置(按平台 /// cfg 组织)、Olive app-relative 位置(C++ 对齐,olivehost.cpp:104-107)。 /// /// OFX 官方标准位置(见 OFX "plug-in discovery" 规范): /// /// * macOS:用户 `~/Library/OFX/Plugins`、系统 `/Library/OFX/Plugins` /// * Windows:`%ProgramFiles%\Common Files\OFX\Plugins`(`%ProgramFiles%` /// 未设置时回退字面 `C:\Program Files\...`) /// * Linux:`/usr/OFX/Plugins`、`/usr/local/OFX/Plugins` /// /// 外加 Olive 的历史位置:`$HOME/.OFX/Plugins`、 /// `$HOME/.local/share/OFX/Plugins`、`$HOME/.local/share/olive/ofx/Plugins`、 /// `../OFX/Plugins`、`../share/olive/ofx/Plugins`、`../lib/olive/ofx/Plugins`。 /// 环境变量(`OFX_PLUGIN_PATH`/`OLIVE_OFX_PLUGIN_PATH`/`OLIVE_PLUGIN_PATH`) /// 由 [`PluginCache::scan`] 追加。`home` 为 `$HOME`;`None` 时跳过所有 /// home 基路径。 fn default_plugin_paths(home: Option<&Path>) -> Vec { let mut paths = Vec::new(); if let Some(home) = home { paths.push(home.join(".OFX/Plugins")); paths.push(home.join(".local/share/OFX/Plugins")); paths.push(home.join(".local/share/olive/ofx/Plugins")); // OFX 规范:macOS 用户级位置。 #[cfg(target_os = "macos")] paths.push(home.join("Library/OFX/Plugins")); } // OFX 规范:macOS 系统级位置。 #[cfg(target_os = "macos")] paths.push(PathBuf::from("/Library/OFX/Plugins")); // OFX 规范:Windows 系统级位置(优先真实安装根 `%ProgramFiles%`)。 #[cfg(target_os = "windows")] { match std::env::var("ProgramFiles") .or_else(|_| std::env::var("ProgramW6432")) .map(PathBuf::from) { Ok(root) => paths.push(root.join("Common Files").join("OFX").join("Plugins")), Err(_) => paths.push(PathBuf::from(r"C:\Program Files\Common Files\OFX\Plugins")), } } // OFX 规范:Linux 系统级位置。 #[cfg(target_os = "linux")] { paths.push(PathBuf::from("/usr/OFX/Plugins")); paths.push(PathBuf::from("/usr/local/OFX/Plugins")); } // Olive app-relative 位置(olivehost.cpp:104-107)。 paths.push(PathBuf::from("../OFX/Plugins")); paths.push(PathBuf::from("../share/olive/ofx/Plugins")); paths.push(PathBuf::from("../lib/olive/ofx/Plugins")); paths } /// 把 `canonical` 记入已扫描路径表,返回 `false` 时表示已存在(调用方 /// 跳过再次扫描)。保持首见顺序;去重键是规范化后的路径(HS: /// addFileToPath 的 weakly_canonical 语义)。 fn record_scanned_path(seen: &mut Vec, canonical: PathBuf) -> bool { if seen.iter().any(|p| p == &canonical) { return false; } seen.push(canonical); true } impl PluginCache { /// 扫描标准目录(OFX 规范 + C++ 对齐):用户级 `$HOME` 路径、平台 /// 系统级路径(macOS/Linux/Windows)、Olive app-relative 路径,以及 /// `OFX_PLUGIN_PATH`/`OLIVE_OFX_PLUGIN_PATH`/`OLIVE_PLUGIN_PATH` 三个 /// 环境变量。重复调用是 no-op(按规范化路径去重);不存在的目录 /// 直接跳过(见 [`Self::scan_path`])。 pub fn scan(&self) -> crate::error::Result<()> { let home = std::env::var("HOME").ok().filter(|h| !h.is_empty()); let mut paths = default_plugin_paths(home.as_deref().map(Path::new)); for var in [ "OLIVE_OFX_PLUGIN_PATH", "OLIVE_PLUGIN_PATH", "OFX_PLUGIN_PATH", ] { if let Ok(raw) = std::env::var(var) { // 平台路径分隔符(Unix ':' / Windows ';'),与 C++ 的 // `QDir::listSeparator` 一致。 paths.extend(std::env::split_paths(&raw)); } } for p in paths { self.scan_path(&p)?; } Ok(()) } /// 追加一个自定义搜索目录并扫描之(递归至深度 3;按路径去重)。 pub fn scan_path(&self, path: &std::path::Path) -> crate::error::Result<()> { // 规范化去重(HS: addFileToPath 的 weakly_canonical)。 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()); { let mut seen = self.scanned_paths.lock().unwrap_or_else(|e| e.into_inner()); if !record_scanned_path(&mut seen, canonical.clone()) { return Ok(()); } } if !canonical.is_dir() { return Ok(()); } let mut bundles = Vec::new(); walk_dirs(&canonical, 0, 3, &mut bundles); for bundle in bundles { self.load_bundle(&bundle); } Ok(()) } /// 加载一个 bundle(幂等:按 bundle 路径去重)。每个早退分支都打 /// 诊断日志——静默失败会让效果库毫无线索地缺插件。 fn load_bundle(&self, bundle: &Path) { let Some(binary) = find_binary_in_bundle(bundle) else { eprintln!("[ofx] {}: no plugin binary in bundle", bundle.display()); return; }; let Some(handle) = dl_open(&binary) else { eprintln!("[ofx] {}: dlopen failed", binary.display()); return; }; { let bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner()); if bins.iter().any(|b| b.bundle_path == bundle) { unsafe { dlclose(handle) }; return; } } let plugins = unsafe { self.collect_plugins(handle, bundle) }; if plugins.is_empty() { eprintln!( "[ofx] {}: no usable plugins (setHost/load/describe failed)", binary.display() ); unsafe { dlclose(handle) }; return; } let mut plugs = self.plugins.lock().unwrap_or_else(|e| e.into_inner()); let mut bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner()); for p in plugins { // identifier 去重(后加载者忽略,HS 语义)。 if !plugs.iter().any(|e| e.identifier == p.identifier) { plugs.push(p); } } bins.push(LoadedBinary { handle, bundle_path: bundle.to_path_buf(), }); } /// 从已加载二进制收集插件(setHost → load → describe)。 /// /// # Safety /// `handle` 必须是有效 dlopen 句柄。 unsafe fn collect_plugins(&self, handle: *mut c_void, bundle: &Path) -> Vec> { let get_no: unsafe extern "C" fn() -> c_int = match unsafe { dlsym_fn(handle, "OfxGetNumberOfPlugins") } { Some(f) => f, None => { return Vec::new(); } }; let get_plug: unsafe extern "C" fn(c_int) -> *mut OfxPlugin = match unsafe { dlsym_fn(handle, "OfxGetPlugin") } { Some(f) => f, None => { return Vec::new(); } }; let count = unsafe { get_no() }; if count <= 0 { return Vec::new(); } let mut out = Vec::new(); for i in 0..count { let ofx = unsafe { get_plug(i) }; if ofx.is_null() { continue; } if let Some(p) = unsafe { self.build_plugin(ofx, handle, bundle) } { out.push(p); } } out } /// 读 OfxPlugin 头部并跑 setHost/load/describe。 /// /// # Safety /// `ofx` 指向插件导出的有效 OfxPlugin 结构。 unsafe fn build_plugin( &self, ofx: *mut OfxPlugin, lib: *mut c_void, bundle: &Path, ) -> Option> { let ofx_ref = unsafe { &*ofx }; // API 匹配(kOfxImageEffectPluginApi "OfxImageEffectPluginAPI" v1, // ofxImageEffect.h:28-32)。每个拒绝分支都打诊断日志(静默拒绝 // 会让效果库毫无线索地缺插件)。 let api = if ofx_ref.plugin_api.is_null() { eprintln!("[ofx] {}: null plugin_api", bundle.display()); return None; } else { unsafe { CStr::from_ptr(ofx_ref.plugin_api) } .to_str() .ok()? }; if api != "OfxImageEffectPluginAPI" || ofx_ref.api_version != 1 { eprintln!( "[ofx] {}: unsupported api {api} v{}", bundle.display(), ofx_ref.api_version ); return None; } let identifier = unsafe { CStr::from_ptr(ofx_ref.plugin_identifier) } .to_str() .ok()? .to_string(); let entry = ofx_ref.main_entry?; // setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。宿主 // 结构体是进程级静态(ofxs 只存指针,见 global_ofx_host)。 if let Some(f) = ofx_ref.set_host { unsafe { f(global_ofx_host()) }; } let mut plugin = Plugin { identifier, version: (ofx_ref.plugin_version_major, ofx_ref.plugin_version_minor), bundle_path: bundle.to_path_buf(), contexts: Vec::new(), descriptor: EffectDescriptor::new(), lib, entry, ofx_plugin: ofx as *mut c_void, unloaded: std::sync::atomic::AtomicBool::new(false), }; // 根描述符属性(HS: effectDescriptorStuff,ofxhImageEffect.cpp:133-155)。 init_descriptor_props(&plugin.descriptor.props, bundle); let empty = PropertySet::new(); // load(HS: ofxhImageEffectAPI.cpp:158-165;OK/ReplyDefault 接受)。 let stat = unsafe { plugin.call_action(ACTION_LOAD, std::ptr::null_mut(), &empty, &empty) }; if stat != status::OK && stat != status::REPLY_DEFAULT { eprintln!("[ofx] {}: load action returned {stat}", plugin.identifier); return None; } // describe(handle = 打标描述符;HS: ofxhImageEffectAPI.cpp:173-180)。 let desc_handle = crate::suites::tag::make( &plugin.descriptor.props as *const PropertySet, crate::suites::tag::DESCRIPTOR, ); let stat = unsafe { plugin.call_action(ACTION_DESCRIBE, desc_handle, &empty, &empty) }; if stat != status::OK && stat != status::REPLY_DEFAULT { eprintln!("[ofx] {}: describe action returned {stat}", plugin.identifier); return None; } // 支持上下文(describe 产物;HS 从 props 读)。 let contexts = read_contexts(&plugin.descriptor.props); // 只收标准上下文(Filter/Generator/Transition 之外还有 // General——kOfxImageEffectContextGeneral 同样是规范上下文, // Roto/AppendClip/STMap 等插件只声明它)。 let contexts: Vec = contexts .into_iter() .filter(|c| { matches!( c.as_str(), "OfxImageEffectContextFilter" | "OfxImageEffectContextGenerator" | "OfxImageEffectContextTransition" | "OfxImageEffectContextGeneral" ) }) .collect(); if contexts.is_empty() { eprintln!( "[ofx] {}: no standard contexts after describe", plugin.identifier ); return None; } plugin.contexts = contexts; Some(Arc::new(plugin)) } /// 已加载插件数量。 pub fn count(&self) -> usize { self.plugins.lock().unwrap_or_else(|e| e.into_inner()).len() } /// 第 `index` 个插件(扫描顺序稳定)。 pub fn at(&self, index: usize) -> Option> { self.plugins .lock() .unwrap_or_else(|e| e.into_inner()) .get(index) .cloned() } /// 按标识查找。 pub fn find(&self, identifier: &str) -> Option> { self.plugins .lock() .unwrap_or_else(|e| e.into_inner()) .iter() .find(|p| p.identifier == identifier) .cloned() } /// 卸载全部(shutdown 路径)。`scanned_paths` 一并清空—— /// 否则再 init 后的扫描会被去重短路(缓存已空但路径仍在)。 /// 卸载前先给每个插件置 `unloaded`:实例可能跨代存活(测试的 /// 串行 shutdown/重扫描),它们持有的旧代入口已随 dlclose 失效, /// 置位后 call_action 直接失败而非跳野指针。 pub(crate) fn unload_all(&self) { { let plugins = self.plugins.lock().unwrap_or_else(|e| e.into_inner()); for p in plugins.iter() { p.unloaded.store(true, std::sync::atomic::Ordering::Release); } } self.plugins .lock() .unwrap_or_else(|e| e.into_inner()) .clear(); let mut bins = self.binaries.lock().unwrap_or_else(|e| e.into_inner()); bins.clear(); // Drop → dlclose self.scanned_paths .lock() .unwrap_or_else(|e| e.into_inner()) .clear(); } } /// 递归枚举目录下的 bundle 目录(深度受限,防符号链接环)。 fn walk_dirs(dir: &Path, depth: usize, max_depth: usize, out: &mut Vec) { if depth > max_depth { return; } let Ok(entries) = std::fs::read_dir(dir) else { return; }; for e in entries.flatten() { let p = e.path(); if !p.is_dir() { continue; } if is_bundle_dir(&p) { out.push(p); } else { walk_dirs(&p, depth + 1, max_depth, out); } } } /// 根描述符属性表(HS: effectDescriptorStuff 的子集, /// ofxhImageEffect.cpp:133-155)。 fn init_descriptor_props(props: &PropertySet, bundle: &Path) { props.set_one(PROP_TYPE, Value::String(cs("OfxTypeImageEffect"))); props.set_one(PROP_LABEL, Value::String(cs(""))); props.set_one("OfxPropShortLabel", Value::String(cs(""))); props.set_one("OfxPropLongLabel", Value::String(cs(""))); props.define("OfxPropVersion", vec![Value::Int(0)]); props.set_one("OfxPropVersionLabel", Value::String(cs(""))); props.set_one("OfxPropPluginDescription", Value::String(cs(""))); props.define(PROP_SUPPORTED_CONTEXTS, vec![]); props.set_one("OfxImageEffectPluginPropGrouping", Value::String(cs(""))); props.set_one("OfxImageEffectPluginPropSingleInstance", Value::Int(0)); props.set_one( "OfxImageEffectPluginRenderThreadSafety", Value::String(cs("OfxImageEffectRenderInstanceSafe")), ); props.set_one("OfxImageEffectPluginPropHostFrameThreading", Value::Int(1)); props.set_one( "OfxImageEffectPluginPropOverlayInteractV1", Value::Pointer(std::ptr::null_mut()), ); // overlay interact V2(ofxImageEffect.h:825):插件 describe 期声明 // 自定义交互入口(Pointer→OfxPluginEntryPoint;V2 要求 Draw suite // 绘制);宿主预定义空指针默认(propSet 不创建属性,宿主预定义 // 属性宇宙,与 V1 同款)。 props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Pointer(std::ptr::null_mut())); props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1)); props.set_one(PROP_SUPPORTS_TILES, Value::Int(1)); props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(0)); props.define("OfxImageEffectPropSupportedPixelDepths", vec![]); props.set_one( "OfxImageEffectPluginPropFieldRenderTwiceAlways", Value::Int(1), ); props.set_one( "OfxImageEffectPropSupportsMultipleClipDepths", Value::Int(0), ); props.set_one("OfxImageEffectPropSupportsMultipleClipPARs", Value::Int(0)); props.define("OfxImageEffectPropClipPreferencesSlaveParam", vec![]); props.set_one(PROP_SEQUENTIAL, Value::Int(0)); props.set_one( PROP_PLUGIN_FILE_PATH, Value::String(cs(bundle.to_str().unwrap_or(""))), ); // GL 能力声明(M11 §4;ofxGPURender.h):描述符预定义 // OpenGLRenderSupported(默认 "false")与 kOfxOpenGLPropPixelDepth // (空数组)——插件在 describe 期经属性 suite 写入(HostSupport // 同款:propSet 不创建属性,宿主预定义属性宇宙)。 props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("false"))); props.define(PROP_GL_PIXEL_DEPTH, vec![]); // ofxColour 描述符预定义(M11 §4;ofxColour.h):style(默认 // None)与可用配置(空数组),插件 describe 期声明自身能力。 props.set_one( PROP_COLOUR_STYLE, Value::String(cs("OfxImageEffectColourManagementNone")), ); props.define(PROP_COLOUR_AVAILABLE_CONFIGS, vec![]); } /// describe 后读支持上下文。 fn read_contexts(props: &PropertySet) -> Vec { let mut out = Vec::new(); let dim = props.dimension(PROP_SUPPORTED_CONTEXTS); for i in 0..dim { if let Some(Value::String(s)) = props.get(PROP_SUPPORTED_CONTEXTS, i) { out.push(s.to_string_lossy().into_owned()); } } out } // ---- Host ----------------------------------------------------------------- /// 宿主。持有宿主级属性集(kOfxPropName/Version 与能力宣告属性) /// 与插件缓存;是 suite 的注册与分发点。 pub struct Host { /// 宿主属性集(kOfxProp* 宿主级属性;能力宣告如 /// kOfxImageEffectPropSupportedPixelDepths——协商的宿主侧输入)。 pub props: PropertySet, /// 插件缓存。 pub cache: PluginCache, /// 活跃实例注册表(泄漏断言与调试)。 pub(crate) instances: Mutex>>>, } impl Host { /// 进程单例。首次调用构建宿主属性集(能力宣告在此写入)。 pub fn global() -> &'static Host { static HOST: OnceLock = OnceLock::new(); HOST.get_or_init(|| { let props = PropertySet::new(); init_host_props(&props); Host { props, cache: PluginCache { plugins: Mutex::new(Vec::new()), scanned_paths: Mutex::new(Vec::new()), binaries: Mutex::new(Vec::new()), }, instances: Mutex::new(Vec::new()), } }) } /// 上下文选择 + 实例化(filter 优先;实例化失败时按 general → /// tracker → paint → 其余支持上下文的顺序回退——TrackerPM 这类 /// 插件 describe 支持 filter,但其 createInstance 在非 /// paint/tracker/general 上下文抓不到 Mask clip 会炸 /// BadHandle;Natron 上它天然以 tracker 上下文实例化)。 /// 返回实际使用的上下文与实例;所有上下文都失败时返回最后一个 /// 错误。 pub fn create_instance_preferred( &self, identifier: &str, ) -> crate::error::Result<(String, Arc>)> { let plugin = self.cache.find(identifier).ok_or(crate::error::Error::NotFound)?; let mut order: Vec<&str> = Vec::with_capacity(plugin.contexts.len()); for pref in [ "OfxImageEffectContextFilter", "OfxImageEffectContextGeneral", "OfxImageEffectContextTracker", "OfxImageEffectContextPaint", ] { if plugin.contexts.iter().any(|c| c == pref) { order.push(pref); } } for c in &plugin.contexts { if !order.contains(&c.as_str()) { order.push(c); } } let mut last_err = crate::error::Error::Failed("插件无支持上下文".into()); for ctx in order { match self.create_instance(identifier, Some(ctx)) { Ok(inst) => return Ok((ctx.to_string(), inst)), Err(e) => last_err = e, } } Err(last_err) } /// 按标识创建实例(describeInContext → createInstance)。 /// 参照 HS: ofxhImageEffectAPI.cpp:200-238(describeInContext 的 /// in-args 只有 kOfxImageEffectPropContext)与 /// ofxhImageEffect.cpp:389-460(populate:clip/param 实例化)。 /// `context` 为空用插件首个支持上下文。失败返回 /// [`crate::error::Error::Failed`]/[`crate::error::Error::NotFound`]。 pub fn create_instance( &self, identifier: &str, context: Option<&str>, ) -> crate::error::Result>> { let plugin = self .cache .find(identifier) .ok_or(crate::error::Error::NotFound)?; let context = match context { Some(c) => c.to_string(), None => plugin .contexts .first() .cloned() .ok_or(crate::error::Error::Failed("插件无支持上下文".into()))?, }; if !plugin.contexts.iter().any(|c| c == &context) { return Err(crate::error::Error::Failed(format!( "插件不支持上下文 {context}" ))); } // describeInContext:上下文描述符 = 根描述符属性拷贝 + // 插件在 action 内重定义参数/clip。 let mut ctx_desc = EffectDescriptor::new(); ctx_desc.props = plugin.descriptor.props.clone(); let in_args = PropertySet::new(); in_args.set_one(PROP_CONTEXT, Value::String(cs(&context))); let desc_handle = crate::suites::tag::make( &ctx_desc.props as *const PropertySet, crate::suites::tag::DESCRIPTOR, ); let stat = unsafe { plugin.call_action(ACTION_DESCRIBE_IN_CONTEXT, desc_handle, &in_args, &in_args) }; if stat != status::OK && stat != status::REPLY_DEFAULT { return Err(crate::error::Error::Failed(format!( "describeInContext 失败:{stat}" ))); } // populate(HS: Instance::populate:clip 实例 + param 实例, // ofxhImageEffect.cpp:389-460)。 let clips = ctx_desc .clips .iter() .map(|c| Box::new(crate::clip::ClipInstance::from_descriptor(c.as_ref()))) .collect(); let params = crate::param::ParamSetInstance { params: ctx_desc .params .iter() .map(|d| Box::new(crate::param::ParamInstance::from_def((**d).clone()))) .collect(), }; let instance = Instance { props: PropertySet::new(), plugin, context: context.clone(), params, clips, node_identity: std::sync::atomic::AtomicUsize::new(0), destroyed: std::sync::atomic::AtomicBool::new(false), sequence_range: std::sync::Mutex::new(None), progress_cb: std::sync::Mutex::new(None), cancel: std::sync::atomic::AtomicBool::new(false), edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()), render_lock: std::sync::Mutex::new(()), interact: std::sync::Mutex::new(None), }; init_instance_props(&instance.props, &instance); let arc = Arc::new(RefBox { refs: std::sync::atomic::AtomicU32::new(1), value: instance, }); // createInstance action(HS: ofxhImageEffect.cpp:675-688)。 let inst_handle = crate::suites::tag::make( &arc.value.props as *const PropertySet, crate::suites::tag::INSTANCE, ); let empty = PropertySet::new(); let stat = unsafe { arc.value .plugin .call_action(ACTION_CREATE_INSTANCE, inst_handle, &empty, &empty) }; if stat != status::OK && stat != status::REPLY_DEFAULT { // 插件拒绝了 createInstance——它从没认领这个实例;把 // destroyed 门置位,让随后的 drop 跳过 destroyInstance 通知 // (否则插件对一个它没创建的实例回 BadIndex 之类的错误)。 arc.value .destroyed .store(true, std::sync::atomic::Ordering::Relaxed); return Err(crate::error::Error::Failed(format!( "createInstance 失败:{stat}" ))); } // 登记活跃实例表(泄漏断言用)+ param→instance 回写表。 self.instances .lock() .unwrap_or_else(|e| e.into_inner()) .push(Arc::downgrade(&arc)); let inst_props = &arc.value.props as *const PropertySet as usize; for p in &arc.value.params.params { let p_addr = &p.props as *const PropertySet as usize; crate::suites::param::register_param_owner(p_addr, inst_props); } Ok(arc) } /// 关闭宿主(`oakplugin_host_shutdown` 的宿主侧): /// 1. 对全部活实例发 destroyInstance 通知(此刻插件仍加载); /// 2. 打取消标记——shutdown 后仍被 C 侧持有的句柄不得再调 /// 已卸载的插件入口(render 入口即短路); /// 3. 卸载插件与二进制。 /// /// 说明:C 侧句柄持有 Arc,宿主无法(也不应)强制析构——实例 /// 内存随句柄释放;destroyed 标记保证 Drop 不再碰插件入口。 pub fn shutdown(&self) { let list = self.instances.lock().unwrap_or_else(|e| e.into_inner()); for w in list.iter() { if let Some(arc) = w.upgrade() { // 置 destroyed 标记后再通知:实例随后的 Drop 不得再 // 调已卸载的插件入口(二次 destroyInstance → 悬垂)。 arc.value .destroyed .store(true, std::sync::atomic::Ordering::Relaxed); arc.value.notify_destroy(); arc.value .cancel .store(true, std::sync::atomic::Ordering::Relaxed); } } self.cache.unload_all(); } /// 当前活跃实例数(泄漏断言)。 pub fn alive_count(&self) -> usize { let mut list = self.instances.lock().unwrap_or_else(|e| e.into_inner()); let alive = list.iter().filter(|w| w.strong_count() > 0).count(); list.retain(|w| w.strong_count() > 0); alive } } /// 宿主属性集(对照 C++ OliveHost::OliveHost,olivehost.cpp:193-207: /// Name/Label/Version;能力宣告为 phase 1 最小集,协商按需扩展)。 fn init_host_props(props: &PropertySet) { // ofxCore.h 宿主属性集的必备项:OfxType=OfxTypeHost 与 // OfxPropAPIVersion(int[2],宿主实现的 API 版本)——ofxs 支持库的 // loadAction 以 throwOnFailure=true 读它们,缺失会被映射成 // kOfxStatErrMissingHostFeature 让插件加载直接失败。 props.set_one("OfxPropType", Value::String(cs("OfxTypeHost"))); props.define("OfxPropAPIVersion", vec![Value::Int(1), Value::Int(4)]); props.set_one("OfxPropName", Value::String(cs("Oak Video Editor"))); props.set_one("OfxPropLabel", Value::String(cs("Oak Video Editor"))); props.set_one( "OfxPropVersionLabel", Value::String(cs(env!("CARGO_PKG_VERSION"))), ); props.define( "OfxPropVersion", vec![Value::Int(0), Value::Int(0), Value::Int(0)], ); // 能力宣告:协商的宿主侧输入(phase 1 全链路 F32+RGBA)。 props.define( "OfxImageEffectPropSupportedPixelDepths", vec![Value::String(cs("OfxBitDepthFloat"))], ); // 宿主支持的上下文集(ofxs 以 throwOnFailure=true 读)。 props.define( "OfxImageEffectPropSupportedContexts", vec![ Value::String(cs("OfxImageEffectContextFilter")), Value::String(cs("OfxImageEffectContextGenerator")), Value::String(cs("OfxImageEffectContextTransition")), Value::String(cs("OfxImageEffectContextGeneral")), ], ); props.define( "OfxImageEffectPropSupportedComponents", vec![ Value::String(cs("OfxImageComponentRGBA")), Value::String(cs("OfxImageComponentRGB")), Value::String(cs("OfxImageComponentAlpha")), ], ); // ofxs 支持库 fetchHostDescription 以 throwOnFailure=true 读取的 // 全部宿主能力位(IsBackground 缺一个都会让读取链中断—— // gHostDescriptionHasInit 已置位,后续插件拿到半初始化描述, // temporalClipAccess 为 0 → 时序类插件集体拒载)。 props.set_one("OfxImageEffectHostPropIsBackground", Value::Int(0)); props.set_one("OfxParamHostPropSupportsStringAnimation", Value::Int(0)); props.set_one("OfxParamHostPropSupportsChoiceAnimation", Value::Int(0)); props.set_one("OfxParamHostPropSupportsBooleanAnimation", Value::Int(0)); props.set_one("OfxParamHostPropSupportsCustomAnimation", Value::Int(0)); // 自定义 interact(检视器叠加层)宿主支持。 props.set_one("OfxParamHostPropSupportsCustomInteract", Value::Int(1)); // 能力宣告(ofxImageEffect.h 宿主属性集;ofxs 支持库在 load 时读成 // ImageEffectHostDescription,temporalClipAccess 等为 0 时整类插件 // ——Retime/FrameHold/TimeOffset/SlitScan——直接在 load 里拒载)。 props.set_one("OfxImageEffectPropTemporalClipAccess", Value::Int(1)); props.set_one("OfxImageEffectPropSupportsMultiResolution", Value::Int(1)); props.set_one("OfxImageEffectPropSupportsTiles", Value::Int(1)); props.set_one("OfxImageEffectPropSupportsMultipleClipPARs", Value::Int(1)); // 像素深度只支持 Float(phase 1 全链路 F32),不做多深度协商。 props.set_one("OfxImageEffectPropSupportsMultipleClipDepths", Value::Int(0)); // 旧版 ofxs 的 fetchHostDescription 读的是不带 Supports 的同义名 // (ofxImageEffect.h 的 kOfxImageEffectPropMultipleClipDepths)。 props.set_one("OfxImageEffectPropMultipleClipDepths", Value::Int(0)); props.set_one("OfxImageEffectPropSetableFrameRate", Value::Int(0)); props.set_one("OfxImageEffectPropSetableFielding", Value::Int(0)); // fetchHostDescription 的其余读取项(部分 ofxs 版本以 // throwOnFailure=true 读,缺一个首插件的 load 就炸): // 顺序渲染状态 0=宿主不强制;Draft 渲染质量支持(播放降档); // 参数数无上限;不支持参数化曲线动画;macOS 无 OS 窗口句柄; // 原生坐标原点按 OFX 规范默认 BottomLeft。 props.set_one("OfxImageEffectInstancePropSequentialRender", Value::Int(0)); props.set_one("OfxImageEffectPropRenderQualityDraft", Value::Int(1)); props.set_one("OfxParamHostPropMaxParameters", Value::Int(-1)); // 参数页数无上限;页内行列 0,0 = 自动排布(这两个也是 // throwOnFailure=true 的读取项)。 props.set_one("OfxParamHostPropMaxPages", Value::Int(-1)); props.define( "OfxParamHostPropPageRowColumnCount", vec![Value::Int(0), Value::Int(0)], ); props.set_one("OfxParamHostPropSupportsParametricAnimation", Value::Int(0)); props.set_one("OfxPropHostOSHandle", Value::Pointer(std::ptr::null_mut())); props.set_one( "OfxImageEffectHostPropNativeOrigin", Value::String(cs("OfxHostNativeOriginBottomLeft")), ); // 序列渲染:插件可随意选(0 = 宿主不强制)。 props.set_one("OfxImageEffectPropSequentialRenderStatus", Value::Int(0)); // 逐帧线程化程度:全帧线程安全(kOfxImageEffectRenderUnsafe 之外的 // 最强档——渲染在独立 worker 进程内串行驱动,无共享状态)。 props.set_one("OfxImageEffectPropRenderThreadSafety", Value::String(cs("OfxImageEffectRenderFullySafe"))); // GL 能力宣告(M11 §4;ofxGPURender.h "OpenGL House Keeping": // 宿主在描述符置 "true")。 props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true"))); // overlay 能力宣告(ofxImageEffect.h:801):宿主允许插件在输出 // 图像上绘制 overlay(interact draw 的前置)。 props.set_one(PROP_SUPPORTS_OVERLAYS, Value::Int(1)); // ofxColour 能力宣告(M11 §4):OCIO 模式 + native 配置列表。 props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO))); props.define( PROP_COLOUR_AVAILABLE_CONFIGS, vec![Value::String(cs(NATIVE_CONFIG_ID))], ); } /// 实例属性表(HS: effectInstanceStuff 的 phase 1 子集, /// ofxhImageEffect.cpp:313-330)。 fn init_instance_props(props: &PropertySet, instance: &Instance) { props.set_one(PROP_TYPE, Value::String(cs("OfxTypeImageEffectInstance"))); props.set_one(PROP_CONTEXT, Value::String(cs(&instance.context))); props.set_one( PROP_PLUGIN_HANDLE, Value::Pointer(instance.plugin.ofx_plugin), ); props.define( PROP_PROJECT_SIZE, vec![Value::Double(1920.0), Value::Double(1080.0)], ); props.define( PROP_PROJECT_OFFSET, vec![Value::Double(0.0), Value::Double(0.0)], ); props.define( PROP_PROJECT_EXTENT, vec![Value::Double(1920.0), Value::Double(1080.0)], ); props.set_one(PROP_PROJECT_PAR, Value::Double(1.0)); props.set_one(PROP_EFFECT_DURATION, Value::Double(1.0)); props.set_one(PROP_SEQUENTIAL, Value::Int(0)); props.set_one(PROP_FRAME_RATE, Value::Double(24.0)); props.set_one("OfxPropIsInteractive", Value::Int(0)); props.set_one(PROP_SUPPORTS_TILES, Value::Int(1)); // ofxColour 实例期协商结果(M11 §4;ofxColour.h:宿主必须在实例 // 上设置 style/config/OCIOConfig)。 props.set_one(PROP_COLOUR_STYLE, Value::String(cs(COLOUR_STYLE_OCIO))); props.set_one(PROP_COLOUR_CONFIG, Value::String(cs(NATIVE_CONFIG_ID))); props.set_one(PROP_OCIO_CONFIG, Value::String(cs(OCIO_CONFIG_URI))); } #[cfg(test)] mod tests { use super::*; /// The default path table includes every home- and app-relative location /// plus the platform-gated OFX standard locations. #[test] fn default_paths_cover_home_and_standard_locations() { let home = Path::new("/home/octa"); let paths = default_plugin_paths(Some(home)); // Olive legacy + home locations. assert!(paths.contains(&home.join(".OFX/Plugins"))); assert!(paths.contains(&home.join(".local/share/OFX/Plugins"))); assert!(paths.contains(&home.join(".local/share/olive/ofx/Plugins"))); // App-relative (C++ parity). assert!(paths.contains(&PathBuf::from("../OFX/Plugins"))); assert!(paths.contains(&PathBuf::from("../share/olive/ofx/Plugins"))); assert!(paths.contains(&PathBuf::from("../lib/olive/ofx/Plugins"))); // OFX standard locations, gated by platform. #[cfg(target_os = "macos")] { assert!(paths.contains(&home.join("Library/OFX/Plugins"))); assert!(paths.contains(&PathBuf::from("/Library/OFX/Plugins"))); } #[cfg(target_os = "linux")] { assert!(paths.contains(&PathBuf::from("/usr/OFX/Plugins"))); assert!(paths.contains(&PathBuf::from("/usr/local/OFX/Plugins"))); } #[cfg(target_os = "windows")] { assert!(paths .iter() .any(|p| p.ends_with("Common Files/OFX/Plugins") || p.ends_with("Common Files\\OFX\\Plugins"))); } } /// With no `$HOME` the home-based entries are skipped; the rest remain. #[test] fn default_paths_skip_home_entries_when_home_unset() { let paths = default_plugin_paths(None); assert!(!paths.iter().any(|p| p.to_string_lossy().starts_with("/home/"))); assert!(paths.contains(&PathBuf::from("../OFX/Plugins"))); assert_eq!( paths .iter() .filter(|p| p.to_string_lossy().contains(".OFX")) .count(), 0 ); } /// The dedupe record rejects an already-seen path and keeps first-seen /// order. #[test] fn record_scanned_path_dedupes() { let mut seen: Vec = Vec::new(); assert!(record_scanned_path(&mut seen, PathBuf::from("/a/Plugins"))); assert!(record_scanned_path(&mut seen, PathBuf::from("/b/Plugins"))); // A duplicate is rejected without being recorded again. assert!(!record_scanned_path(&mut seen, PathBuf::from("/a/Plugins"))); assert_eq!(seen, vec![PathBuf::from("/a/Plugins"), PathBuf::from("/b/Plugins")]); } /// Scanning an existing directory twice only records it once, and a /// non-existent directory is skipped without error. #[test] fn scan_path_skips_missing_and_dedupes_existing() { let dir = std::env::temp_dir().join(format!("oakplugin-scan-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let cache = PluginCache { plugins: Mutex::new(Vec::new()), scanned_paths: Mutex::new(Vec::new()), binaries: Mutex::new(Vec::new()), }; cache.scan_path(&dir).expect("existing dir scans"); cache.scan_path(&dir).expect("duplicate scan is a no-op"); { let seen = cache.scanned_paths.lock().unwrap_or_else(|e| e.into_inner()); assert_eq!(seen.len(), 1, "the same directory is scanned only once"); } // drop the guard before scanning again // A missing directory is skipped (no error, no plugin scan). let missing = dir.join("does-not-exist"); cache.scan_path(&missing).expect("missing dir scan is a no-op"); let plugins = cache.plugins.lock().unwrap_or_else(|e| e.into_inner()); assert!(plugins.is_empty(), "no bundles were loaded"); let _ = std::fs::remove_dir_all(&dir); } // ---- branch coverage batch: loader/cache/host ------------------------ fn temp_dir(tag: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!( "oak-plugin-host-{}-{tag}", std::process::id() )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir } fn test_cache() -> PluginCache { PluginCache { plugins: Mutex::new(Vec::new()), scanned_paths: Mutex::new(Vec::new()), binaries: Mutex::new(Vec::new()), } } /// build.rs 编出的最小测试插件共享库。 fn fixture_lib() -> Option { let out = PathBuf::from(env!("OUT_DIR")); let lib = out.join(if cfg!(target_os = "macos") { "oak_test_plugin.dylib" } else { "oak_test_plugin.so" }); lib.is_file().then_some(lib) } /// 现场装配一个含测试插件二进制的 `.ofx.bundle`(平台目录探测)。 fn make_fixture_bundle(tag: &str) -> Option<(PathBuf, PathBuf)> { let lib = fixture_lib()?; let platform = if cfg!(target_os = "macos") { "MacOS" } else if cfg!(target_os = "windows") { "Win64" } else { "Linux-x86-64" }; let root = temp_dir(tag); let bundle = root.join("oak-test-plugin.ofx.bundle"); let bin_dir = bundle.join("Contents").join(platform); std::fs::create_dir_all(&bin_dir).unwrap(); let target = bin_dir.join(if cfg!(target_os = "windows") { "plugin.dll" } else { "plugin" }); std::fs::copy(&lib, &target).unwrap(); Some((root, bundle)) } fn action_is(action: *const c_char, name: &str) -> bool { !action.is_null() && unsafe { CStr::from_ptr(action) }.to_bytes() == name.as_bytes() } unsafe extern "C" fn entry_ok( _: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { status::OK } unsafe extern "C" fn entry_reply_default( _: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { status::REPLY_DEFAULT } unsafe extern "C" fn entry_describe_fails( action: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if action_is(action, ACTION_DESCRIBE_IN_CONTEXT) { status::ERR_FATAL } else { status::OK } } unsafe extern "C" fn entry_create_fails( action: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if action_is(action, ACTION_CREATE_INSTANCE) { status::ERR_FATAL } else { status::OK } } fn fake_plugin(identifier: &str, contexts: Vec, entry: EntryPoint) -> Arc { Arc::new(Plugin { identifier: identifier.into(), version: (1, 0), bundle_path: PathBuf::new(), contexts, descriptor: EffectDescriptor::new(), lib: std::ptr::null_mut(), entry, ofx_plugin: std::ptr::null_mut(), unloaded: std::sync::atomic::AtomicBool::new(false), }) } /// 隔离的 Host(本地 cache/instances,避免触碰进程单例)。 fn local_host(plugins: Vec>) -> Host { Host { props: PropertySet::new(), cache: PluginCache { plugins: Mutex::new(plugins), scanned_paths: Mutex::new(Vec::new()), binaries: Mutex::new(Vec::new()), }, instances: Mutex::new(Vec::new()), } } // ---- dlopen/dlsym error paths ---------------------------------------- #[cfg(not(target_os = "windows"))] #[test] fn dl_open_and_dl_sym_error_paths_return_none() { // 不存在的库 → None。 assert!(dl_open(Path::new("/definitely/not/a/library-oak.so")).is_none()); // 可加载的库 + 不存在的符号 → None。 let lib = fixture_lib().expect("build.rs 产出测试插件库"); let handle = dl_open(&lib).expect("测试插件库可加载"); assert!(dl_sym(handle, "oak_definitely_missing_symbol").is_none()); unsafe { dlclose(handle) }; } // ---- fetchSuite 入口 ------------------------------------------------- #[test] fn host_fetch_suite_null_args_and_trace_paths() { use std::ffi::c_void as cv; static TRACE_LOCK: Mutex<()> = Mutex::new(()); let _g = TRACE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let host_set = PropertySet::new(); let host = &host_set as *const PropertySet as *mut cv; let name = CString::new("OfxPropertySuite").unwrap(); let bogus = CString::new("OfxBogusSuite").unwrap(); // NULL host / NULL name → None(不触达 suite 分发表)。 assert!(unsafe { host_fetch_suite(std::ptr::null_mut(), name.as_ptr(), 1) }.is_null()); assert!(unsafe { host_fetch_suite(host, std::ptr::null(), 1) }.is_null()); // OAK_OFX_TRACE 打开:hit 与 miss 诊断分支。 let prev = std::env::var_os("OAK_OFX_TRACE"); std::env::set_var("OAK_OFX_TRACE", "1"); assert!(!unsafe { host_fetch_suite(host, name.as_ptr(), 1) }.is_null()); assert!(unsafe { host_fetch_suite(host, bogus.as_ptr(), 1) }.is_null()); // 版本不符也走 miss。 assert!(unsafe { host_fetch_suite(host, name.as_ptr(), 2) }.is_null()); match prev { Some(v) => std::env::set_var("OAK_OFX_TRACE", v), None => std::env::remove_var("OAK_OFX_TRACE"), } } // ---- overlay interact 入口声明 --------------------------------------- #[test] fn overlay_interact_entry_null_pointer_and_v1_fallback() { unsafe extern "C" fn overlay( _: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { status::OK } let props = PropertySet::new(); // 属性缺失 → None。 assert!(overlay_interact_entry(&props).is_none()); // 空指针 → 跳过(V2 空、V1 也缺)。 props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Pointer(std::ptr::null_mut())); assert!(overlay_interact_entry(&props).is_none()); // 类型不是 Pointer → 跳过。 props.set_one(PROP_OVERLAY_INTERACT_V2, Value::Int(1)); assert!(overlay_interact_entry(&props).is_none()); // V1 回退命中。 props.set_one( PROP_OVERLAY_INTERACT_V1, Value::Pointer(overlay as *const () as *mut std::ffi::c_void), ); assert!(overlay_interact_entry(&props).is_some()); // V2 优先。 props.set_one( PROP_OVERLAY_INTERACT_V2, Value::Pointer(overlay as *const () as *mut std::ffi::c_void), ); assert!(overlay_interact_entry(&props).is_some()); } // ---- 卸载后的入口保护 ------------------------------------------------- #[test] fn unloaded_plugin_actions_fail_without_touching_entry() { let plugin = fake_plugin("test.unloaded", vec![], entry_ok); plugin .unloaded .store(true, std::sync::atomic::Ordering::Release); let empty = PropertySet::new(); let st = unsafe { plugin.call_action(ACTION_LOAD, std::ptr::null_mut(), &empty, &empty) }; assert_eq!(st, status::ERR_FATAL); let st = unsafe { plugin.call_entry(entry_ok, ACTION_LOAD, std::ptr::null_mut(), &empty, &empty) }; assert_eq!(st, status::ERR_FATAL); } // ---- bundle/二进制探测 ----------------------------------------------- #[test] fn is_bundle_dir_matches_suffixes_only() { assert!(is_bundle_dir(Path::new("/tmp/Foo.bundle"))); assert!(is_bundle_dir(Path::new("Bar.plugin"))); assert!(!is_bundle_dir(Path::new("/tmp/NotIt"))); // 无 file_name(根路径)→ false。 assert!(!is_bundle_dir(Path::new("/"))); } #[test] fn find_binary_in_bundle_prefers_platform_dir_and_falls_back_to_root() { let dir = temp_dir("find-bin"); // 空 bundle → None。 assert!(find_binary_in_bundle(&dir).is_none()); // 根目录 .so 回退。 std::fs::write(dir.join("liboak.so"), b"not a real lib").unwrap(); assert_eq!(find_binary_in_bundle(&dir).unwrap(), dir.join("liboak.so")); // 平台目录优先,且跳过 .plist。 let platform = dir.join("Contents").join("Linux-x86-64"); std::fs::create_dir_all(&platform).unwrap(); std::fs::write(platform.join("Info.plist"), b"meta").unwrap(); std::fs::write(platform.join("plugin"), b"bin").unwrap(); assert_eq!(find_binary_in_bundle(&dir).unwrap(), platform.join("plugin")); // 平台目录里只有 .plist → 回退根目录。 std::fs::remove_file(platform.join("plugin")).unwrap(); assert_eq!(find_binary_in_bundle(&dir).unwrap(), dir.join("liboak.so")); let _ = std::fs::remove_dir_all(&dir); } #[test] fn walk_dirs_respects_depth_and_read_errors() { let root = temp_dir("walk"); std::fs::write(root.join("file.txt"), b"x").unwrap(); let a = root.join("a"); let bundle = a.join("One.bundle"); let b = a.join("b"); let c = b.join("c"); std::fs::create_dir_all(&bundle).unwrap(); std::fs::create_dir_all(&c).unwrap(); std::fs::create_dir_all(b.join("Two.bundle")).unwrap(); let mut out = Vec::new(); walk_dirs(&root, 0, 1, &mut out); assert_eq!(out, vec![bundle.clone()], "深度 1 内只发现 One.bundle(文件与更深目录跳过)"); // 深度上限内全收。 let mut deep = Vec::new(); walk_dirs(&root, 0, 4, &mut deep); assert_eq!(deep.len(), 2); // read_dir 失败(路径是文件)→ 空返回。 let mut errs = Vec::new(); walk_dirs(&root.join("file.txt"), 0, 4, &mut errs); assert!(errs.is_empty()); let _ = std::fs::remove_dir_all(&root); } // ---- load_bundle ------------------------------------------------------ #[test] fn load_bundle_reports_missing_and_invalid_binaries() { let cache = test_cache(); // 空目录(无二进制)→ 记录并返回。 let empty = temp_dir("load-empty"); cache.load_bundle(&empty); assert_eq!(cache.count(), 0); // 根目录垃圾 .so → dlopen 失败,记录并返回。 let bad = temp_dir("load-bad"); std::fs::write(bad.join("garbage.so"), b"definitely not an ELF").unwrap(); cache.load_bundle(&bad); assert_eq!(cache.count(), 0); assert!(cache.binaries.lock().unwrap_or_else(|e| e.into_inner()).is_empty()); let _ = std::fs::remove_dir_all(&empty); let _ = std::fs::remove_dir_all(&bad); } #[test] fn load_bundle_loads_fixture_plugins_and_dedupes() { let Some((root, bundle)) = make_fixture_bundle("load-ok") else { eprintln!("SKIP: 测试插件未构建"); return; }; let cache = test_cache(); cache.load_bundle(&bundle); assert_eq!(cache.count(), 5, "测试插件导出 5 个效果"); assert!(cache.find("org.oak.test-plugin").is_some()); assert_eq!( cache.binaries.lock().unwrap_or_else(|e| e.into_inner()).len(), 1 ); // 同一 bundle 二次加载 → 路径去重(直接 dlclose 返回)。 cache.load_bundle(&bundle); assert_eq!(cache.count(), 5); assert_eq!( cache.binaries.lock().unwrap_or_else(|e| e.into_inner()).len(), 1 ); // at()/find() 边界。 assert!(cache.at(0).is_some()); assert!(cache.at(usize::MAX).is_none()); assert!(cache.find("org.oak.test-plugin.nope").is_none()); let _ = std::fs::remove_dir_all(&root); } #[test] fn load_bundle_ignores_duplicate_plugin_identifiers() { let Some((root1, bundle1)) = make_fixture_bundle("dup-a") else { eprintln!("SKIP: 测试插件未构建"); return; }; let Some((root2, bundle2)) = make_fixture_bundle("dup-b") else { eprintln!("SKIP: 测试插件未构建"); return; }; let cache = test_cache(); cache.load_bundle(&bundle1); assert_eq!(cache.count(), 5); // 第二个 bundle 的 identifier 全部重复 → 忽略,但二进制登记两份。 cache.load_bundle(&bundle2); assert_eq!(cache.count(), 5); assert_eq!( cache.binaries.lock().unwrap_or_else(|e| e.into_inner()).len(), 2 ); let _ = std::fs::remove_dir_all(&root1); let _ = std::fs::remove_dir_all(&root2); } /// 可加载但无 OFX 导出符号的库 → collect_plugins 空(Linux libc)。 #[cfg(target_os = "linux")] #[test] fn collect_plugins_without_ofx_symbols_is_empty() { let Some(libc) = [ "/lib/x86_64-linux-gnu/libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6", "/lib64/libc.so.6", "/usr/lib64/libc.so.6", "/lib/libc.so.6", "/usr/lib/libc.so.6", ] .iter() .map(PathBuf::from) .find(|p| p.is_file()) else { eprintln!("SKIP: 未找到 libc"); return; }; let Some(handle) = dl_open(&libc) else { eprintln!("SKIP: libc 不可 dlopen"); return; }; let cache = test_cache(); let plugins = unsafe { cache.collect_plugins(handle, Path::new("no-ofx")) }; unsafe { dlclose(handle) }; assert!(plugins.is_empty()); } /// 可加载但无插件的 bundle → 记录 no usable plugins 并 dlclose。 #[cfg(target_os = "linux")] #[test] fn load_bundle_without_ofx_plugins_reports_and_unloads() { let Some(libc) = [ "/lib/x86_64-linux-gnu/libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6", "/lib64/libc.so.6", "/usr/lib64/libc.so.6", "/lib/libc.so.6", "/usr/lib/libc.so.6", ] .iter() .map(PathBuf::from) .find(|p| p.is_file()) else { eprintln!("SKIP: 未找到 libc"); return; }; let dir = temp_dir("load-no-plugin"); let platform = dir.join("Contents").join("Linux-x86-64"); std::fs::create_dir_all(&platform).unwrap(); std::os::unix::fs::symlink(&libc, platform.join("plugin")).unwrap(); let cache = test_cache(); cache.load_bundle(&dir); assert_eq!(cache.count(), 0); assert!(cache.binaries.lock().unwrap_or_else(|e| e.into_inner()).is_empty()); let _ = std::fs::remove_dir_all(&dir); } // ---- build_plugin 拒绝与上下文过滤 ----------------------------------- unsafe extern "C" fn noop_set_host(_: *mut OfxHost) {} fn raw_ofx_plugin( api: Option<&CString>, api_version: c_int, identifier: Option<&CString>, set_host: Option, main_entry: Option, ) -> OfxPlugin { OfxPlugin { plugin_api: api.map_or(std::ptr::null(), |c| c.as_ptr()), api_version, plugin_identifier: identifier.map_or(std::ptr::null(), |c| c.as_ptr()), plugin_version_major: 1, plugin_version_minor: 0, set_host, main_entry, } } unsafe extern "C" fn entry_multi_contexts( _: *const c_char, handle: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if !handle.is_null() { let props = unsafe { &*crate::suites::tag::strip(handle as *mut c_void) }; props.define( PROP_SUPPORTED_CONTEXTS, vec![ Value::String(CString::new("OfxImageEffectContextGeneral").unwrap()), Value::String(CString::new("OfxImageEffectContextTransition").unwrap()), Value::String(CString::new("OfxImageEffectContextGenerator").unwrap()), Value::String(CString::new("OfxImageEffectContextFilter").unwrap()), Value::String(CString::new("OfxBogusContext").unwrap()), ], ); } status::OK } unsafe extern "C" fn entry_bogus_contexts( _: *const c_char, handle: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if !handle.is_null() { let props = unsafe { &*crate::suites::tag::strip(handle as *mut c_void) }; props.define( PROP_SUPPORTED_CONTEXTS, vec![Value::String(CString::new("OfxImageEffectContextRetimer").unwrap())], ); } status::OK } unsafe extern "C" fn entry_load_fails( action: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if action_is(action, ACTION_LOAD) { status::ERR_FATAL } else { status::OK } } unsafe extern "C" fn entry_build_describe_fails( action: *const c_char, _: *const c_void, _: *mut c_void, _: *mut c_void, ) -> c_int { if action_is(action, ACTION_DESCRIBE) { status::ERR_FATAL } else { status::OK } } #[test] fn build_plugin_rejects_invalid_headers_and_filters_contexts() { let cache = test_cache(); let bundle = Path::new("test-build.ofx.bundle"); let api = CString::new("OfxImageEffectPluginAPI").unwrap(); let wrong_api = CString::new("OfxSomethingElsePluginAPI").unwrap(); let identifier = CString::new("test.build").unwrap(); // NULL plugin_api → None。 let mut ofx = raw_ofx_plugin(None, 1, Some(&identifier), None, Some(entry_ok)); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // API 名不符 → None。 let mut ofx = raw_ofx_plugin(Some(&wrong_api), 1, Some(&identifier), None, Some(entry_ok)); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // API 版本不符 → None。 let mut ofx = raw_ofx_plugin(Some(&api), 2, Some(&identifier), None, Some(entry_ok)); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // main_entry 缺 → None。 let mut ofx = raw_ofx_plugin(Some(&api), 1, Some(&identifier), None, None); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // load action 失败 → None(setHost 存在分支也被走到)。 let mut ofx = raw_ofx_plugin( Some(&api), 1, Some(&identifier), Some(noop_set_host), Some(entry_load_fails), ); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // describe action 失败 → None。 let mut ofx = raw_ofx_plugin( Some(&api), 1, Some(&identifier), None, Some(entry_build_describe_fails), ); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // describe 后无非标准上下文 → None。 let mut ofx = raw_ofx_plugin( Some(&api), 1, Some(&identifier), None, Some(entry_bogus_contexts), ); assert!(unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) }.is_none()); // 四个标准上下文全收;非标准值被过滤。 let mut ofx = raw_ofx_plugin( Some(&api), 1, Some(&identifier), Some(noop_set_host), Some(entry_multi_contexts), ); let plugin = unsafe { cache.build_plugin(&mut ofx, std::ptr::null_mut(), bundle) } .expect("标准上下文应接受"); assert_eq!(plugin.identifier, "test.build"); assert_eq!(plugin.contexts.len(), 4); assert!(!plugin.contexts.iter().any(|c| c == "OfxBogusContext")); assert_eq!(plugin.version, (1, 0)); } // ---- scan 环境变量 ---------------------------------------------------- #[test] fn scan_reads_plugin_path_env_vars() { static ENV_LOCK: Mutex<()> = Mutex::new(()); let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let Some((root, _bundle)) = make_fixture_bundle("scan-env") else { eprintln!("SKIP: 测试插件未构建"); return; }; let prev = std::env::var_os("OLIVE_OFX_PLUGIN_PATH"); std::env::set_var("OLIVE_OFX_PLUGIN_PATH", &root); let cache = test_cache(); let result = cache.scan(); match prev { Some(v) => std::env::set_var("OLIVE_OFX_PLUGIN_PATH", v), None => std::env::remove_var("OLIVE_OFX_PLUGIN_PATH"), } result.expect("scan"); assert!( cache.find("org.oak.test-plugin").is_some(), "环境变量目录里的 bundle 应被发现" ); let _ = std::fs::remove_dir_all(&root); } // ---- create_instance / create_instance_preferred --------------------- #[test] fn create_instance_preferred_uses_custom_context_fallback() { let host = local_host(vec![fake_plugin( "test.pref.custom", vec!["OfxImageEffectContextCustom".into()], entry_ok, )]); let (context, inst) = host .create_instance_preferred("test.pref.custom") .expect("自定义上下文可实例化"); assert_eq!(context, "OfxImageEffectContextCustom"); assert_eq!(inst.value.context, "OfxImageEffectContextCustom"); // 实例属性表(init_instance_props)已建好。 assert!(inst.value.props.get("OfxPropType", 0).is_some()); // 未登记标识 → NotFound。 assert!(host.create_instance_preferred("nope").is_err()); } #[test] fn create_instance_preferred_returns_last_error() { let host = local_host(vec![fake_plugin( "test.pref.fail", vec![ "OfxImageEffectContextFilter".into(), "OfxImageEffectContextGeneral".into(), ], entry_describe_fails, )]); let err = host .create_instance_preferred("test.pref.fail") .err() .expect("全部上下文失败应返回错误"); assert!(matches!( err, crate::error::Error::Failed(ref m) if m.contains("describeInContext") )); } #[test] fn create_instance_rejects_unsupported_context_and_missing_contexts() { let host = local_host(vec![fake_plugin( "test.ctx.one", vec!["OfxImageEffectContextFilter".into()], entry_ok, )]); // 插件不支持请求的上下文。 assert!(host .create_instance("test.ctx.one", Some("OfxImageEffectContextGenerator")) .is_err()); // None → 取首个支持上下文并成功。 let inst = host.create_instance("test.ctx.one", None).expect("首个上下文"); assert_eq!(inst.value.context, "OfxImageEffectContextFilter"); // 无支持上下文 → 失败。 let none = local_host(vec![fake_plugin("test.ctx.none", vec![], entry_ok)]); assert!(none.create_instance("test.ctx.none", None).is_err()); // 未登记标识 → 失败。 assert!(none.create_instance("missing", None).is_err()); // ReplyDefault 的 describeInContext/createInstance 也被接受。 let default = local_host(vec![fake_plugin( "test.ctx.default", vec!["OfxImageEffectContextFilter".into()], entry_reply_default, )]); assert!(default.create_instance("test.ctx.default", None).is_ok()); } #[test] fn create_instance_action_failure_is_not_registered_alive() { let host = local_host(vec![fake_plugin( "test.create.fail", vec!["OfxImageEffectContextFilter".into()], entry_create_fails, )]); let err = host .create_instance("test.create.fail", None) .err() .expect("createInstance 失败"); assert!(matches!( err, crate::error::Error::Failed(ref m) if m.contains("createInstance") )); assert_eq!(host.alive_count(), 0); } #[test] fn alive_count_prunes_released_instances() { let host = local_host(vec![fake_plugin( "test.alive", vec!["OfxImageEffectContextFilter".into()], entry_ok, )]); let a = host.create_instance("test.alive", None).expect("a"); let b = host.create_instance("test.alive", None).expect("b"); assert_eq!(host.alive_count(), 2); drop(a); assert_eq!(host.alive_count(), 1); drop(b); assert_eq!(host.alive_count(), 0); } /// `register_plugin_nodes` 对无法实例化的缓存项记录原因并跳过, /// 对可实例化的登记动态节点。 #[test] fn register_plugin_nodes_skips_uninstantiable_and_registers_ok_plugins() { static LOCK: Mutex<()> = Mutex::new(()); let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); let failing_id = "test.node-factory.fail"; let ok_id = "test.node-factory.ok"; { let mut plugins = Host::global() .cache .plugins .lock() .unwrap_or_else(|e| e.into_inner()); plugins.push(fake_plugin(failing_id, vec![], entry_ok)); plugins.push(fake_plugin( ok_id, vec!["OfxImageEffectContextFilter".into()], entry_ok, )); } let registered = crate::node_factory::register_plugin_nodes(); { let mut plugins = Host::global() .cache .plugins .lock() .unwrap_or_else(|e| e.into_inner()); plugins.retain(|p| p.identifier != failing_id && p.identifier != ok_id); } assert!( registered.iter().any(|id| id == ok_id), "可实例化的插件应登记为动态节点" ); assert!( !registered.iter().any(|id| id == failing_id), "无法实例化的插件应跳过" ); } }