fix(plugin): full OFX plugin discovery — host conformance fixes
Real openfx-misc/CImg/Shadertoy bundles (148 plugins at /Library/OFX/Plugins) all failed to load before; every failure was silent. Root causes found one by one with a probe example + lldb: - property suite rejected propSet on undefined properties and propGetDimension on empty ones, and disallowed the index==size append — OFX semantics are create-on-set and appendable dimensions (this alone failed every plugin's describe) - host property set missed the mandatory OfxPropType/OfxPropAPIVersion and the capability props ofxs' fetchHostDescription reads with throwOnFailure=true (IsBackground, TemporalClipAccess, MaxPages, PageRowColumnCount, host SupportedContexts, ...) — one missing prop aborted the read chain and left a half-initialised host description, which made every temporal plugin refuse to load - MultiThreadSuiteV1 lacked the five mutex functions (the plugin reads past the short table — UB); implemented as a real counting-semaphore registry - the OfxHost struct was a stack local; ofxs keeps the POINTER past setHost, so describe/render-time fetchSuite calls dereferenced a dangling stack address (bus error once plugins actually loaded) — the struct is now a leaked process global - General is a standard OFX context and is no longer filtered out (Roto/AppendClip/STMap declare only it) - every scan/load/describe early-out now logs its reason; suite entry points report non-OK statuses with caller location under OAK_OFX_TRACE - examples/scan_probe.rs: scans the real plugin dirs and prints discovered/registered counts (also usable from CI) Result: 148/148 plugins discovered, 134 registered as node types (the remaining 14 need vendor suites — Vegas stereoscopic etc. — and are logged, not silent)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
//! Throwaway probe: scan the real system OFX directory and print what the
|
||||
//! host actually discovers (diagnosing why the effect library is empty).
|
||||
|
||||
fn main() {
|
||||
let host = oakplugin::host::Host::global();
|
||||
match host.cache.scan() {
|
||||
Ok(()) => println!("scan: ok"),
|
||||
Err(e) => println!("scan: FAILED: {e}"),
|
||||
}
|
||||
println!("plugins discovered: {}", host.cache.count());
|
||||
// Direct probe: read the host-level props through the C suite table.
|
||||
let suite = oakplugin::suites::property::suite_v1();
|
||||
unsafe {
|
||||
let handle = &host.props as *const oakplugin::property::PropertySet as *mut std::ffi::c_void;
|
||||
let name = std::ffi::CString::new("OfxPropName").unwrap();
|
||||
let mut out: *mut std::ffi::c_char = std::ptr::null_mut();
|
||||
let stat = (suite.get_string)(handle, name.as_ptr(), 0, &mut out);
|
||||
println!("direct propGetString(OfxPropName) -> {stat}");
|
||||
if stat == 0 && !out.is_null() {
|
||||
println!(" value: {:?}", std::ffi::CStr::from_ptr(out));
|
||||
}
|
||||
}
|
||||
let registered = oakplugin::node_factory::register_plugin_nodes();
|
||||
println!("factory node types registered: {}", registered.len());
|
||||
for id in ®istered {
|
||||
println!(" type_id={id}");
|
||||
}
|
||||
}
|
||||
+128
-11
@@ -149,11 +149,42 @@ unsafe extern "C" fn host_fetch_suite(
|
||||
crate::suites::fetch_suite(name, version)
|
||||
}));
|
||||
match result {
|
||||
Ok(Some(p)) => p,
|
||||
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 宿主没有(describe 常因此返回
|
||||
// kOfxStatErrMissingHostFeature)。
|
||||
if !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<usize> = 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。
|
||||
@@ -743,12 +774,15 @@ impl PluginCache {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载一个 bundle(幂等:按 bundle 路径去重)。
|
||||
/// 加载一个 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;
|
||||
};
|
||||
{
|
||||
@@ -760,6 +794,10 @@ impl PluginCache {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -825,8 +863,10 @@ impl PluginCache {
|
||||
) -> Option<Arc<Plugin>> {
|
||||
let ofx_ref = unsafe { &*ofx };
|
||||
// API 匹配(kOfxImageEffectPluginApi "OfxImageEffectPluginAPI" v1,
|
||||
// ofxImageEffect.h:28-32)。
|
||||
// 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) }
|
||||
@@ -834,6 +874,11 @@ impl PluginCache {
|
||||
.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) }
|
||||
@@ -842,14 +887,10 @@ impl PluginCache {
|
||||
.to_string();
|
||||
let entry = ofx_ref.main_entry?;
|
||||
|
||||
// setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。
|
||||
let host = Host::global();
|
||||
let mut ofx_host = OfxHost {
|
||||
host: &host.props as *const PropertySet as *mut c_void,
|
||||
fetch_suite: host_fetch_suite,
|
||||
};
|
||||
// setHost 是 mandatory 的第一个调用(ofxCore.h:124-132)。宿主
|
||||
// 结构体是进程级静态(ofxs 只存指针,见 global_ofx_host)。
|
||||
if let Some(f) = ofx_ref.set_host {
|
||||
unsafe { f(&mut ofx_host) };
|
||||
unsafe { f(global_ofx_host()) };
|
||||
}
|
||||
|
||||
let mut plugin = Plugin {
|
||||
@@ -871,6 +912,7 @@ impl PluginCache {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -881,12 +923,15 @@ impl PluginCache {
|
||||
);
|
||||
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<String> = contexts
|
||||
.into_iter()
|
||||
.filter(|c| {
|
||||
@@ -895,10 +940,15 @@ impl PluginCache {
|
||||
"OfxImageEffectContextFilter"
|
||||
| "OfxImageEffectContextGenerator"
|
||||
| "OfxImageEffectContextTransition"
|
||||
| "OfxImageEffectContextGeneral"
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if contexts.is_empty() {
|
||||
eprintln!(
|
||||
"[ofx] {}: no standard contexts after describe",
|
||||
plugin.identifier
|
||||
);
|
||||
return None;
|
||||
}
|
||||
plugin.contexts = contexts;
|
||||
@@ -1227,6 +1277,12 @@ impl Host {
|
||||
/// 宿主属性集(对照 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(
|
||||
@@ -1242,6 +1298,16 @@ fn init_host_props(props: &PropertySet) {
|
||||
"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![
|
||||
@@ -1250,6 +1316,57 @@ fn init_host_props(props: &PropertySet) {
|
||||
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")));
|
||||
|
||||
@@ -717,9 +717,15 @@ pub fn register_plugin_nodes() -> Vec<String> {
|
||||
}
|
||||
};
|
||||
|
||||
// 元数据实例(name/description;建完即弃)。
|
||||
let Ok(inst) = host.create_instance(&plugin.identifier, Some(&context)) else {
|
||||
continue;
|
||||
// 元数据实例(name/description;建完即弃)。describeInContext
|
||||
// 失败的插件无法实例化(常见于只支持 Vegas 立体声等厂商套件
|
||||
// 的插件)——记录原因,不静默跳过。
|
||||
let inst = match host.create_instance(&plugin.identifier, Some(&context)) {
|
||||
Ok(inst) => inst,
|
||||
Err(e) => {
|
||||
eprintln!("[ofx] {}: instance creation failed: {e}", plugin.identifier);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let name = plugin_display_name(&inst.value);
|
||||
let description = plugin_description(&inst.value);
|
||||
|
||||
@@ -90,11 +90,17 @@ static LIVE_IMAGES: std::sync::LazyLock<Mutex<HashMap<usize, std::sync::Arc<Imag
|
||||
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
#[track_caller]
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
catch_unwind(AssertUnwindSafe(f)).map_or_else(
|
||||
let caller = std::panic::Location::caller();
|
||||
let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else(
|
||||
|_| status::FAILED,
|
||||
|r| r.map_or_else(|c| c, |()| status::OK),
|
||||
)
|
||||
);
|
||||
if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
eprintln!("[ofx] image-effect suite error {code} at {caller}");
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
/// 属性名(空指针/非 UTF-8 → ErrValue)。
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
//!
|
||||
//! 参照:HS: ofxhImageEffect.cpp gMultiThreadSuite。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_int, c_uint, c_void};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::suites::status;
|
||||
|
||||
@@ -50,6 +53,16 @@ pub struct MultiThreadSuiteV1 {
|
||||
pub index: unsafe extern "C" fn(*mut c_int) -> c_int,
|
||||
/// multiThreadIsSpawnedThread:当前线程是否插件线程。
|
||||
pub is_spawned: unsafe extern "C" fn(*mut c_int) -> c_int,
|
||||
/// mutexCreate:`count` 是初始可用计数(>1 时是信号量语义)。
|
||||
pub mutex_create: unsafe extern "C" fn(*mut *mut c_void, c_int) -> c_int,
|
||||
/// mutexDestroy
|
||||
pub mutex_destroy: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// mutexLock
|
||||
pub mutex_lock: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// mutexUnLock
|
||||
pub mutex_unlock: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
/// mutexTryLock
|
||||
pub mutex_try_lock: unsafe extern "C" fn(*mut c_void) -> c_int,
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
@@ -118,6 +131,112 @@ unsafe extern "C" fn multi_thread_is_spawned(out: *mut c_int) -> c_int {
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 互斥锁(OfxMultiThreadSuiteV1 的 mutex* 函数组)-----------------------
|
||||
//
|
||||
// 句柄表:全局注册表按 usize 键发号;`count > 1` 是信号量语义(ofxMultiThread.h
|
||||
// "a mutex with a count greater than 1 is a counting semaphore")。
|
||||
// 插件在多线程渲染期用它们保护共享状态——ofxs 支持库的
|
||||
// ofxsThreadSuiteCheck 在 load 时逐一检查这些函数非空,缺一个整批插件
|
||||
// 直接拒载。
|
||||
|
||||
/// 计数信号量(count==1 时即普通互斥锁)。
|
||||
struct OfxMutex {
|
||||
permits: std::sync::Mutex<c_int>,
|
||||
released: std::sync::Condvar,
|
||||
}
|
||||
|
||||
/// 句柄注册表(句柄即下一个递增 id 转指针;0 保留给空)。
|
||||
static MUTEXES: std::sync::LazyLock<Mutex<HashMap<usize, Arc<OfxMutex>>>> =
|
||||
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// 下一个互斥锁 id。
|
||||
static NEXT_MUTEX: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
fn mutex_lookup(handle: *mut c_void) -> Option<Arc<OfxMutex>> {
|
||||
if handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
MUTEXES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&(handle as usize))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mutex_create(out: *mut *mut c_void, count: c_int) -> c_int {
|
||||
caught(|| {
|
||||
if out.is_null() {
|
||||
return status::ERR_VALUE;
|
||||
}
|
||||
let id = NEXT_MUTEX.fetch_add(1, Ordering::Relaxed);
|
||||
MUTEXES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(id, Arc::new(OfxMutex {
|
||||
permits: std::sync::Mutex::new(count.max(1)),
|
||||
released: std::sync::Condvar::new(),
|
||||
}));
|
||||
unsafe { *out = id as *mut c_void };
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mutex_destroy(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
if mutex_lookup(handle).is_none() {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
}
|
||||
MUTEXES
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.remove(&(handle as usize));
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mutex_lock(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let Some(m) = mutex_lookup(handle) else {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
};
|
||||
let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner());
|
||||
while *permits <= 0 {
|
||||
permits = m.released.wait(permits).unwrap_or_else(|e| e.into_inner());
|
||||
}
|
||||
*permits -= 1;
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mutex_unlock(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let Some(m) = mutex_lookup(handle) else {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
};
|
||||
{
|
||||
let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*permits += 1;
|
||||
}
|
||||
m.released.notify_one();
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn mutex_try_lock(handle: *mut c_void) -> c_int {
|
||||
caught(|| {
|
||||
let Some(m) = mutex_lookup(handle) else {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
};
|
||||
let mut permits = m.permits.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if *permits <= 0 {
|
||||
// 规范:占不到锁返回 kOfxStatFailed(不是错误)。
|
||||
return status::FAILED;
|
||||
}
|
||||
*permits -= 1;
|
||||
status::OK
|
||||
})
|
||||
}
|
||||
|
||||
/// 静态函数表实例。
|
||||
pub fn suite_v1() -> &'static MultiThreadSuiteV1 {
|
||||
static SUITE: std::sync::OnceLock<MultiThreadSuiteV1> = std::sync::OnceLock::new();
|
||||
@@ -126,6 +245,11 @@ pub fn suite_v1() -> &'static MultiThreadSuiteV1 {
|
||||
num_cpus: multi_thread_num_cpus,
|
||||
index: multi_thread_index,
|
||||
is_spawned: multi_thread_is_spawned,
|
||||
mutex_create: mutex_create,
|
||||
mutex_destroy: mutex_destroy,
|
||||
mutex_lock: mutex_lock,
|
||||
mutex_unlock: mutex_unlock,
|
||||
mutex_try_lock: mutex_try_lock,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -152,11 +152,17 @@ unsafe fn c_name<'a>(name: *const c_char) -> Result<&'a str, c_int> {
|
||||
}
|
||||
|
||||
/// 公共入口模板:panic 兜底。
|
||||
#[track_caller]
|
||||
fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else(
|
||||
let caller = std::panic::Location::caller();
|
||||
let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_or_else(
|
||||
|_| status::FAILED,
|
||||
|r| r.map_or_else(|c| c, |()| status::OK),
|
||||
)
|
||||
);
|
||||
if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
eprintln!("[ofx] param suite error {code} at {caller}");
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
// ---- 变长参数实现(C shim 转发)----------------------------------------
|
||||
|
||||
@@ -118,15 +118,21 @@ impl Kind {
|
||||
/// `# Safety`:`handle` 必须指向活的 `PropertySet` 或已注册对象
|
||||
/// (suite 生命周期契约:宿主对象先于 suite 调用创建,后于全部调用
|
||||
/// 销毁)。
|
||||
#[track_caller]
|
||||
unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(), c_int>) -> c_int {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let caller = std::panic::Location::caller();
|
||||
let code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if handle.is_null() {
|
||||
return status::ERR_BAD_HANDLE;
|
||||
}
|
||||
let set = unsafe { &*crate::suites::tag::strip(handle) };
|
||||
f(set).map_or_else(|code| code, |()| status::OK)
|
||||
}))
|
||||
.unwrap_or(status::FAILED)
|
||||
.unwrap_or(status::FAILED);
|
||||
if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
eprintln!("[ofx] property suite error {code} at {caller}");
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
/// 属性名:空指针 / 非 UTF-8 → kOfxStatErrValue(防御性;HostSupport
|
||||
@@ -154,36 +160,79 @@ fn get_value<'a>(
|
||||
name: &str,
|
||||
index: c_int,
|
||||
kind: Kind,
|
||||
) -> Result<&'a Value, c_int> {
|
||||
let result = get_value_inner(props, name, index, kind);
|
||||
if std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
match &result {
|
||||
Ok(_) => eprintln!("[ofx] propGet({name}[{index}]) -> ok"),
|
||||
Err(c) => eprintln!("[ofx] propGet({name}[{index}]) -> {c}"),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn get_value_inner<'a>(
|
||||
props: &'a [Property],
|
||||
name: &str,
|
||||
index: c_int,
|
||||
kind: Kind,
|
||||
) -> Result<&'a Value, c_int> {
|
||||
let p = props
|
||||
.iter()
|
||||
.find(|p| p.name == name)
|
||||
.ok_or(status::ERR_UNKNOWN)?;
|
||||
// 首元素代理整条属性的类型(宿主只定义同构数组;HS 是定义期
|
||||
// 固定类型,等价)。
|
||||
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
|
||||
if Kind::of(probe) != kind {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
// 固定类型,等价)。空数组没有类型探针,跳过类型检查,越界
|
||||
// 由下面的索引访问报 BadIndex(HS getValueRaw 语义)。
|
||||
if let Some(probe) = p.values.first() {
|
||||
if Kind::of(probe) != kind {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
}
|
||||
p.values.get(idx(index)?).ok_or(status::ERR_BAD_INDEX)
|
||||
}
|
||||
|
||||
// ---- propSet(单元素)-----------------------------------------------------
|
||||
|
||||
/// propSet 通用实现:类型不符/未定义 → Unknown;越界 → BadIndex。
|
||||
/// propSet 通用实现:属性未定义时按 OFX 语义**隐式创建**(ofxProperty.h:
|
||||
/// "If the property does not exist it is created",HS setValue 同)——
|
||||
/// 插件 describe 期写的大量描述符属性(grouping、各 capability 开关)
|
||||
/// 并未由宿主预定义,拒绝创建会让 describe 直接失败。已定义但类型
|
||||
/// 不符 → Unknown;越界 → BadIndex。
|
||||
fn set_value(set: &PropertySet, name: &str, index: c_int, value: Value) -> Result<(), c_int> {
|
||||
set.with_locked(|props| {
|
||||
let p = props
|
||||
.iter_mut()
|
||||
.find(|p| p.name == name)
|
||||
.ok_or(status::ERR_UNKNOWN)?;
|
||||
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
|
||||
let idx = idx(index)?;
|
||||
let Some(p) = props.iter_mut().find(|p| p.name == name) else {
|
||||
// 隐式创建:维度 index+1,前导槽位以同值填充(HS 的新属性
|
||||
// 默认值语义——新建属性先有一个默认元素再逐位写)。
|
||||
let mut values = vec![value.clone(); idx];
|
||||
values.push(value);
|
||||
props.push(crate::property::Property {
|
||||
name: name.to_string(),
|
||||
values,
|
||||
});
|
||||
return Ok(());
|
||||
};
|
||||
// 空属性(宿主预定义的空数组,如 OfxImageEffectPropSupportedContexts)
|
||||
// 没有类型探针:按 HS 的 index == size 追加语义直接 push。
|
||||
if p.values.is_empty() {
|
||||
if idx != 0 {
|
||||
return Err(status::ERR_BAD_INDEX);
|
||||
}
|
||||
p.values.push(value);
|
||||
return Ok(());
|
||||
}
|
||||
let probe = &p.values[0];
|
||||
if Kind::of(probe) != Kind::of(&value) {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
// HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284);
|
||||
// 本 crate 维度固定语义(扩容只能经 define)——越界一律 BadIndex。
|
||||
let slot = p.values.get_mut(idx(index)?).ok_or(status::ERR_BAD_INDEX)?;
|
||||
// HS `setValue` 允许 index == size 时追加(ofxhPropertySuite.cpp:284)
|
||||
// ——addSupportedContext 等协商属性正是这样逐位增长的。
|
||||
if idx == p.values.len() {
|
||||
p.values.push(value);
|
||||
return Ok(());
|
||||
}
|
||||
let slot = p.values.get_mut(idx).ok_or(status::ERR_BAD_INDEX)?;
|
||||
*slot = value;
|
||||
Ok(())
|
||||
})
|
||||
@@ -262,10 +311,14 @@ fn set_values(
|
||||
values: Vec<Value>,
|
||||
) -> Result<(), c_int> {
|
||||
set.with_locked(|props| {
|
||||
let p = props
|
||||
.iter_mut()
|
||||
.find(|p| p.name == name)
|
||||
.ok_or(status::ERR_UNKNOWN)?;
|
||||
let Some(p) = props.iter_mut().find(|p| p.name == name) else {
|
||||
// 与单元素 set_value 一致:未定义属性按 OFX 语义隐式创建。
|
||||
props.push(crate::property::Property {
|
||||
name: name.to_string(),
|
||||
values,
|
||||
});
|
||||
return Ok(());
|
||||
};
|
||||
// count == 0 时无类型可探(HS 仍会做 fetchTypedProperty)。
|
||||
if let Some(first) = p.values.first() {
|
||||
if let Some(v) = values.first() {
|
||||
@@ -426,7 +479,15 @@ unsafe extern "C" fn prop_get_string(
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
*out = get_string(set, name, index)?;
|
||||
let r = get_string(set, name, index);
|
||||
if std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
let status = match &r {
|
||||
Ok(_) => 0,
|
||||
Err(c) => *c,
|
||||
};
|
||||
eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {status}");
|
||||
}
|
||||
*out = r?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -476,6 +537,12 @@ unsafe extern "C" fn prop_get_int(
|
||||
}
|
||||
_ => Err(status::FAILED),
|
||||
}
|
||||
.map_err(|c| {
|
||||
if std::env::var_os("OAK_OFX_TRACE").is_some() {
|
||||
eprintln!("[ofx] propGetInt({name}) -> {c}");
|
||||
}
|
||||
c
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -614,12 +681,13 @@ unsafe extern "C" fn prop_get_dimension(
|
||||
if out.is_null() {
|
||||
return Err(status::ERR_VALUE);
|
||||
}
|
||||
// HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992)。
|
||||
let dim = set.dimension(name);
|
||||
if dim == 0 {
|
||||
// HS: fetchProperty 失败(未定义)→ Unknown(ofxhPropertySuite.cpp:992);
|
||||
// 已定义的空数组(如协商属性的初始状态)是合法维度 0,不是错误。
|
||||
let exists = set.with_locked(|props| props.iter().any(|p| p.name == name));
|
||||
if !exists {
|
||||
return Err(status::ERR_UNKNOWN);
|
||||
}
|
||||
*out = dim as c_int;
|
||||
*out = set.dimension(name) as c_int;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user