// 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"; /// kOfxActionUnload。 pub(crate) const ACTION_UNLOAD: &str = "OfxActionUnload"; /// 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"; /// 工作空间(任务要求:全链路 ACEScg)。 pub(crate) const WORKING_COLOURSPACE: &str = "ACEScg"; /// 宿主支持的 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); } }