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:
2026-08-20 22:28:23 +08:00
parent bf0416c50b
commit 5498504398
7 changed files with 397 additions and 42 deletions
+28
View File
@@ -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 &registered {
println!(" type_id={id}");
}
}
+128 -11
View File
@@ -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 {
// loadHS: ofxhImageEffectAPI.cpp:158-165OK/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::OliveHostolivehost.cpp:193-207
/// Name/Label/Version;能力宣告为 phase 1 最小集,协商按需扩展)。
fn init_host_props(props: &PropertySet) {
// ofxCore.h 宿主属性集的必备项:OfxType=OfxTypeHost 与
// OfxPropAPIVersionint[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 时读成
// ImageEffectHostDescriptiontemporalClipAccess 等为 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));
// 像素深度只支持 Floatphase 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 §4ofxGPURender.h "OpenGL House Keeping"
// 宿主在描述符置 "true")。
props.set_one(PROP_GL_RENDER_SUPPORTED, Value::String(cs("true")));
+9 -3
View File
@@ -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);
+8 -2
View File
@@ -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)。
+124
View File
@@ -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,
})
}
+8 -2
View File
@@ -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 转发)----------------------------------------
+92 -24
View File
@@ -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);
// 固定类型,等价)。空数组没有类型探针,跳过类型检查,越界
// 由下面的索引访问报 BadIndexHS 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 失败(未定义)→ UnknownofxhPropertySuite.cpp:992
let dim = set.dimension(name);
if dim == 0 {
// HS: fetchProperty 失败(未定义)→ UnknownofxhPropertySuite.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(())
})
}