plugin: fix OFX host property-suite gaps that purple-framed real plugins

Real plugins (CImg ChromaKeyerOFX, AddOFX) failed the render action with
kOfxStatFailed / MissingHostFeature and painted the magenta failure frame:

- images lacked the mandatory ImageBase properties (OfxPropType,
  PixelAspectRatio, PreMultiplication, Field, RenderScale); the ofxs
  ImageBase constructor throws on the missing/invalid strong reads
- PreMultiplication used the made-up string "OfxImagePreMultiplied";
  kOfxImagePreMultiplied is actually "OfxImageAlphaPremultiplied", the
  only value mapStrToPreMultiplicationEnum accepts (lldb __cxa_throw
  backtrace pinpointed this)
- RenderWindow is Int x4 per ofxsPropertyValidation, not Double x4
- field strings use the real constant "OfxFieldNone"
- clips define OfxImageClipPropConnected (isConnected is a strong read;
  optional mask clips blew up without it)
- choice params predefine empty ChoiceEnum / ChoiceLabelOption arrays
- isIdentity failure is no longer fatal (the C++ plugin renderer never
  calls it; plugins that error on it simply render normally)
- property suite coerces Int <-> Double on reads (the CImg framework
  reads the render window with propGetIntN against a Double store)
- in-args carry NatronOfxPropNativeOverlays=0 for the Natron framework
- plugin jobs pass a GL-kind marker so GL-only plugins take the real
  gl_bridge offscreen path instead of the CPU MissingHostFeature path
- trace-gated [ofx] diagnostics for property misses and suite calls

Verified with new smoke tests that render the real AddOFX and
ChromaKeyerOFX plugins through the executor and assert the output is
not the purple failure frame.
This commit is contained in:
2026-08-25 19:04:18 +08:00
parent f2aab8ce15
commit 2828984187
11 changed files with 284 additions and 25 deletions
+17
View File
@@ -112,6 +112,15 @@ impl ClipInstance {
),
);
}
// OfxImageClipPropConnectedofxsImageEffect.cpp:1106 的
// `Clip::isConnected()` 是无默认值强读——缺这个属性时,CImg 这类
// 带可选 mask clip 的插件直接抛
// PropertyUnknownToHost → MissingHostFeature 紫帧)。默认 0
// (未连接),挂接输入纹理时由 set_input_texture 置 1。
props.set_one(
crate::host::PROP_CLIP_CONNECTED,
crate::property::Value::Int(0),
);
Self {
props,
name,
@@ -166,6 +175,14 @@ impl ClipInstance {
/// 挂接输入纹理(oaknode 侧 clip 输入值变化时由 param/render 桥
/// 调用)。`time` 用于多帧纹理选择。None 断开。
pub fn set_input_texture(&self, texture: Option<crate::render::Texture>, _time: f64) {
// The connection state follows the texture hand-off (the render
// driver only feeds clips that have input; optional mask clips
// stay 0, so `Clip::isConnected()` answers false for them).
let connected = i32::from(texture.is_some());
self.props.set_one(
crate::host::PROP_CLIP_CONNECTED,
crate::property::Value::Int(connected),
);
*self.input_texture.lock().unwrap_or_else(|e| e.into_inner()) = texture;
}
+32
View File
@@ -207,6 +207,38 @@ impl Image {
K_IMAGE_PROP_UNIQUE_ID,
vec![Value::String(unique_identifier())],
);
// OfxPropType="OfxTypeImage":图像实例的类型标识(支持库
// validateImageBaseProperties 的必备项,带可校验默认值)。
img.props.define(
"OfxPropType",
vec![Value::String(std::ffi::CString::new("OfxTypeImage").unwrap())],
);
// OFX 必备图像属性(支持库 ImageBase/Image 构造的无默认值强读;
// 缺失即抛 PropertyUnknownToHost → MissingHostFeature 紫帧):
// 方形像素 1.0;预乘声明(本管线按预乘 alpha 处理);无场。
img.props.define(
"OfxImagePropPixelAspectRatio",
vec![Value::Double(1.0)],
);
img.props.define(
"OfxImageEffectPropPreMultiplication",
// kOfxImagePreMultiplied 的真实字符串值是
// "OfxImageAlphaPremultiplied"ofxImageEffect.h),
// ofxs mapStrToPreMultiplicationEnum 只认这三个精确值。
vec![Value::String(
std::ffi::CString::new("OfxImageAlphaPremultiplied").unwrap(),
)],
);
img.props.define(
"OfxImagePropField",
vec![Value::String(std::ffi::CString::new("OfxFieldNone").unwrap())],
);
// OfxImageEffectPropRenderScaleopenfx-misc 的
// checkBadRenderScaleOrField 用它比对渲染参数(1:1)。
img.props.define(
"OfxImageEffectPropRenderScale",
vec![Value::Double(1.0), Value::Double(1.0)],
);
img
}
+54 -13
View File
@@ -264,6 +264,10 @@ impl Instance {
crate::suites::tag::INSTANCE,
);
let empty = PropertySet::new();
Self::add_action_arg_compat_props(&empty);
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[probe-args] getClipPreferences in_args {:p} out {:p}", &empty, &out);
}
let stat = unsafe {
self.plugin
.call_action(ACTION_GET_CLIP_PREFERENCES, inst_handle, &empty, &out)
@@ -360,6 +364,10 @@ impl Instance {
PROP_RENDER_SCALE,
vec![Value::Double(scale.x), Value::Double(scale.y)],
);
Self::add_action_arg_compat_props(&in_args);
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[probe-args] getRod in_args {:p}", &in_args);
}
// out args 预定义(HS 行为:宿主先建属性表,插件只管写——
// 缺失属性上插件的 propSet 会失败被忽略)。
let out = PropertySet::new();
@@ -407,6 +415,10 @@ impl Instance {
PROP_RENDER_SCALE,
vec![Value::Double(scale.x), Value::Double(scale.y)],
);
Self::add_action_arg_compat_props(&in_args);
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[probe-args] getRoi in_args {:p}", &in_args);
}
in_args.define(
PROP_ROI,
vec![
@@ -454,6 +466,19 @@ impl Instance {
}
/// isIdentity action:返回 Some((time, input_clip_name)) 表示本帧
/// OFX 1.4+/Natron 框架对 action in-args 的强读属性:缺了会被抛成
/// PropertyUnknownToHost → MissingHostFeature(紫帧根因)。每个
/// action in-args 都带上。
fn add_action_arg_compat_props(in_args: &PropertySet) {
use crate::property::Value;
// NatronOfxPropNativeOverlaysNatron 支持库强读(0 = 宿主不
// 支持原生 overlay,插件走自己的回退)。OfxImageEffectPropRenderPlanes
// 不放 in-argsNatron 框架按**字符串**读它,存在但类型是 Int
// 反而会抛 PropertyUnknownToHost(缺失它才容忍)——它只属于
// 图像/clip 属性(OFX 1.4 语义在 clipGetImagePlane 一侧)。
in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0));
}
/// 直接透传该输入 clipNone 表示需要真正 render。
///
/// 参照 HS: ofxhImageEffect.cpp:1378-1450in args = time + scale +
@@ -475,16 +500,17 @@ impl Instance {
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Double(0.0),
Value::Int(0),
Value::Int(0),
Value::Int(0),
Value::Int(0),
],
);
in_args.set_one(
PROP_FIELD_TO_RENDER,
Value::String(CString::new("OfxImageFieldBoth").unwrap()),
Value::String(CString::new("OfxFieldNone").unwrap()),
);
in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0));
// out args 预定义:IsIdentityString+ TimeDouble)。
let out = PropertySet::new();
out.set_one(PROP_IS_IDENTITY, Value::String(CString::new("").unwrap()));
@@ -494,14 +520,23 @@ impl Instance {
&self.props as *const PropertySet,
crate::suites::tag::INSTANCE,
);
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] isIdentity in_args {:p} out_args {:p}", &in_args, &out);
}
let stat = unsafe {
self.plugin
.call_action(ACTION_IS_IDENTITY, inst_handle, &in_args, &out)
};
if stat != crate::suites::status::OK && stat != crate::suites::status::REPLY_DEFAULT {
return Err(crate::error::Error::Failed(format!(
"isIdentity 失败:{stat}"
)));
// A failed isIdentity is NOT fatal: the C++ plugin renderer
// never calls this action at all (plugins that error on it —
// the CImg suite answers MissingHostFeature — simply render
// normally). Treat any non-OK as "not identity" and let the
// real render decide.
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] isIdentity stat {stat}: proceeding to render");
}
return Ok(None);
}
let identity = out.get(PROP_IS_IDENTITY, 0);
match identity {
@@ -541,23 +576,29 @@ impl Instance {
PROP_RENDER_SCALE,
vec![Value::Double(scale.x), Value::Double(scale.y)],
);
// RenderWindow 必须是 Int x4(支持库 ofxsPropertyValidation 对
// render in-args 的强制类型;像素坐标本就是整型——Double 会校验
// 失败成 kOfxStatFailed)。
in_args.define(
PROP_RENDER_WINDOW,
vec![
Value::Double(window.x1),
Value::Double(window.y1),
Value::Double(window.x2),
Value::Double(window.y2),
Value::Int(window.x1.round() as i32),
Value::Int(window.y1.round() as i32),
Value::Int(window.x2.round() as i32),
Value::Int(window.y2.round() as i32),
],
);
in_args.set_one(
PROP_FIELD_TO_RENDER,
Value::String(CString::new("OfxImageFieldBoth").unwrap()),
Value::String(CString::new("OfxFieldNone").unwrap()),
);
in_args.set_one(PROP_SEQUENTIAL_RENDER, Value::Int(0));
in_args.set_one(PROP_INTERACTIVE_RENDER, Value::Int(0));
in_args.set_one(PROP_RENDER_QUALITY_DRAFT, Value::Int(0));
in_args.set_one(PROP_NO_SPATIAL_AWARENESS, Value::Int(0));
// Natron 支持库对 NatronOfxPropNativeOverlays 的强读(0 = 宿主
// 不支持原生 overlay——插件据此走自己的回退路径)。
in_args.set_one("NatronOfxPropNativeOverlays", Value::Int(0));
if gl_enabled {
in_args.set_one(crate::host::PROP_GL_ENABLED, Value::Int(1));
}
+75 -1
View File
@@ -899,7 +899,11 @@ fn execute_plugin_job(
effect_input_id: effect_input_id.clone(),
inputs: inputs.clone(),
values: pod_values,
renderer: None,
// GL 渲染路径:OpenGL-only 插件(CImg 套件)在 CPU 路径的
// isIdentity 直接回 kOfxStatErrMissingHostFeature(紫帧根因)。
// 标记上下文只服务于 use_opengl 的 kind 判定;真正的 GL 渲染走
// gl_bridge 的离屏上下文(无头的 oak-worker 进程同样可用)。
renderer: Some(std::sync::Arc::new(crate::render::GlKindMarker)),
clear_destination: false,
interactive: false,
};
@@ -1129,6 +1133,76 @@ mod tests {
assert!(shared_plugin_instance("com.example.definitely-missing").is_none());
}
/// 最简单的真实插件(AddOFX:每个像素加一个常量)经 executor
/// 路径渲染成功——区分"系统性宿主问题"与"单插件问题"(机器上
/// 没装该插件则跳过)。
#[test]
fn add_plugin_renders_through_the_executor() {
render_real_plugin_smoke("net.sf.openfx.AddPlugin");
}
/// 真实 GL-only 插件(CImg 的 ChromaKeyerOFX)经 executor 路径渲染
/// 成功——GL-kind 标记必须把它引上可用的 GL 路径,而不是
/// MissingHostFeature 紫帧(机器上没装该插件则跳过)。
#[test]
fn cimg_chromakeyer_renders_through_the_executor() {
render_real_plugin_smoke("net.sf.openfx.ChromaKeyerPlugin");
}
fn render_real_plugin_smoke(identifier: &str) {
let host = crate::host::Host::global();
let _ = host.cache.scan();
if host.cache.find(identifier).is_none() {
eprintln!("{identifier} not installed; skipping");
return;
}
install_render_executor();
let instance = shared_plugin_instance(identifier)
.expect("the shared instance factory resolves the plugin");
if std::env::var_os("OAK_OFX_TRACE").is_some() {
let inst = instance_from_id(instance).expect("registered");
let props = &inst.value.plugin.descriptor.props;
props.with_locked(|all| {
for p in all.iter().filter(|p| p.name.contains("GL") || p.name.contains("OpenGL")) {
eprintln!("[probe] desc prop {} = {:?}", p.name, p.values);
}
});
}
// 8x8 不透明绿帧(典型 keyer 输入)。
let mut frame = oak_render::eval::generate_frame(
oak_core::Rational::new(0, 1),
(8, 8),
oak_core::PixelFormat::F32,
)
.unwrap();
for px in frame.data.chunks_exact_mut(16) {
for (c, v) in px.chunks_exact_mut(4).zip([0.1f32, 0.9, 0.1, 1.0]) {
c.copy_from_slice(&v.to_le_bytes());
}
}
let spec = oak_render::eval::JobSpec::Plugin {
instance,
time: 0.0,
effect_input_id: Some("Source".to_string()),
inputs: Vec::new(),
values: Vec::new(),
};
let out = execute_plugin_job(&oak_render::eval::PluginJobRequest {
spec: &spec,
src: oak_render::texture::Texture::wrap_frame(frame),
})
.expect("the GL-capable render succeeds (no MissingHostFeature purple)");
let oak_render::texture::Texture::Cpu(out_frame) = &out else {
panic!("a CPU frame comes back");
};
let mut px = [0f32; 4];
for i in 0..4 {
px[i] = f32::from_le_bytes(out_frame.data[i * 4..i * 4 + 4].try_into().unwrap());
}
assert_ne!(px, [1.0, 0.0, 1.0, 1.0], "must not be the purple failure frame");
}
/// 构造一个只含 push-button 参数的最小实例(直接登记进注册表)。
fn instance_with_push_button() -> u64 {
use std::ffi::{c_char, c_void};
+6 -1
View File
@@ -442,8 +442,13 @@ impl ParamDef {
props.set_one(P_STRING_FILE_EXISTS, Value::Int(1));
}
TYPE_CHOICE => {
// 维度 0:选项数由插件 SetN 决定。
// 维度 0:选项数由插件 SetN 决定。ChoiceEnum(枚举值映射)
// 与 ChoiceLabelOptionOFX 1.5 按值标签)同样预定义空
// 数组——新支持库读它们时不是"未知属性"(其强读会抛成
// PropertyUnknownToHost → MissingHostFeature)。
props.define(P_CHOICE_OPTION, vec![]);
props.define("OfxParamPropChoiceEnum", vec![]);
props.define("OfxParamPropChoiceLabelOption", vec![]);
}
TYPE_CUSTOM => {
props.set_one(P_CUSTOM_INTERP, Value::Pointer(std::ptr::null_mut()));
+31
View File
@@ -144,6 +144,37 @@ pub fn renderer_is_open_gl(renderer: &Renderer) -> bool {
renderer.kind() == oak_render::backend::BackendKind::Gl
}
/// A GL-kind marker context for [`RenderJob::renderer`]. The field's only
/// consumer is the use_opengl decision's kind check
/// ([`renderer_is_open_gl`]); the actual GL work (offscreen context, FBO,
/// readback) goes through [`crate::gl_bridge`], which needs no renderer
/// object. Passing this marker flips OpenGL-requiring plugins (the CImg
/// suite, which answers `kOfxStatErrMissingHostFeature` on the CPU path)
/// onto the real GL path in any process — including the headless
/// oak-worker, where gl_bridge creates its own offscreen context.
pub struct GlKindMarker;
impl oak_render::backend::GpuContextLike for GlKindMarker {
fn kind(&self) -> oak_render::backend::BackendKind {
oak_render::backend::BackendKind::Gl
}
fn destroy_texture(&self, _token: u64) {}
fn upload(&self, _token: u64, _frame: &oak_render::texture::Frame) -> oak_render::error::Result<()> {
Ok(())
}
fn download(&self, _token: u64) -> oak_render::error::Result<oak_render::texture::Frame> {
Ok(oak_render::texture::Frame::new())
}
fn blit(
&self,
_src: u64,
_dst: u64,
_processor: Option<&oak_render::color::ColorProcessor>,
) -> oak_render::error::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
+13
View File
@@ -171,6 +171,16 @@ pub fn render_frame(
crate::suites::gl_render::pick_gl_pixel_depth(&inst.plugin.descriptor.props)
.is_some();
let gl_name_ok = crate::gl_bridge::gl_available();
if std::env::var_os("OAK_OFX_TRACE").is_some() {
let raw = inst
.plugin
.descriptor
.props
.get(crate::host::PROP_GL_RENDER_SUPPORTED, 0);
eprintln!(
"[ofx] use_opengl decision: plugin_gl={plugin_gl} depth_ok={depth_ok} gl_available={gl_name_ok} (raw GL prop: {raw:?})"
);
}
plugin_gl && depth_ok && gl_name_ok
}
_ => false,
@@ -203,6 +213,9 @@ pub fn render_frame(
// 4. getClipPreferencespluginrenderer.cpp:1554-1594)。
let prefs = inst.get_clip_preferences()?;
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] clip prefs: components={} (inst {})", prefs.output_components, inst.plugin.identifier);
}
let components = match prefs.output_components.as_str() {
"OfxImageComponentRGBA" => crate::image::Components::Rgba,
"OfxImageComponentRGB" => crate::image::Components::Rgb,
+1 -1
View File
@@ -318,7 +318,7 @@ fn make_texture_props(
rect_props(&props, crate::image::K_IMAGE_PROP_BOUNDS, bounds);
rect_props(&props, crate::image::K_IMAGE_PROP_ROD, bounds);
props.set_one(crate::image::K_IMAGE_PROP_ROW_BYTES, Value::Int(row_bytes));
props.set_one("OfxImagePropField", Value::String(cs("OfxImageFieldNone")));
props.set_one("OfxImagePropField", Value::String(cs("OfxFieldNone")));
props.set_one(
crate::image::K_IMAGE_PROP_UNIQUE_ID,
Value::String(crate::image::unique_identifier()),
@@ -100,6 +100,9 @@ fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
if code != status::OK && std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] image-effect suite error {code} at {caller}");
}
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] image-effect suite call at {caller} -> {code}");
}
code
}
+4 -2
View File
@@ -160,8 +160,10 @@ fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int {
|_| 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}");
if std::env::var_os("OAK_OFX_TRACE").is_some() {
if code != status::OK {
eprintln!("[ofx] param suite error {code} at {caller}");
}
}
code
}
+48 -7
View File
@@ -130,7 +130,8 @@ unsafe fn caught(handle: *mut c_void, f: impl FnOnce(&PropertySet) -> Result<(),
}))
.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}");
let tag = crate::suites::tag::kind(handle);
eprintln!("[ofx] property suite error {code} at {caller} (handle {handle:p} tag {tag})");
}
code
}
@@ -185,7 +186,13 @@ fn get_value_inner<'a>(
// 固定类型,等价)。空数组没有类型探针,跳过类型检查,越界
// 由下面的索引访问报 BadIndexHS getValueRaw 语义)。
if let Some(probe) = p.values.first() {
if Kind::of(probe) != kind {
// Int <-> Double 数值协随(与 get_n 同款宽容宿主语义)。
let numeric = matches!(Kind::of(probe), Kind::Int | Kind::Double)
&& matches!(kind, Kind::Int | Kind::Double);
if Kind::of(probe) != kind && !numeric {
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] property kind mismatch: {name}");
}
return Err(status::ERR_UNKNOWN);
}
}
@@ -481,11 +488,11 @@ unsafe extern "C" fn prop_get_string(
}
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,
let rendered = match &r {
Ok(p) => format!("0 <- {:?}", unsafe { std::ffi::CStr::from_ptr(*p) }),
Err(c) => format!("{c}"),
};
eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {status}");
eprintln!("[ofx] propGetString(h={handle:p}, {name}[{index}]) -> {rendered}");
}
*out = r?;
Ok(())
@@ -511,6 +518,11 @@ unsafe extern "C" fn prop_get_double(
*out = d;
Ok(())
}
// Int <-> Double 数值协随(宽容宿主语义,同 get_n)。
Value::Int(i) => {
*out = i as f64;
Ok(())
}
_ => Err(status::FAILED),
}
})
@@ -535,6 +547,11 @@ unsafe extern "C" fn prop_get_int(
*out = i;
Ok(())
}
// Int <-> Double 数值协随(宽容宿主语义,同 get_n)。
Value::Double(d) => {
*out = d as i32;
Ok(())
}
_ => Err(status::FAILED),
}
.map_err(|c| {
@@ -564,15 +581,33 @@ fn get_n(
.find(|p| p.name == name)
.ok_or(status::ERR_UNKNOWN)?;
let probe = p.values.first().ok_or(status::ERR_UNKNOWN)?;
if Kind::of(probe) != kind {
// Int <-> Double coercion (forgiving-host semantics): the CImg
// framework reads the DOUBLE render window with propGetIntN and
// must still get the values; strict kind checks turn that into a
// bogus MissingHostFeature at render time.
let numeric = matches!(Kind::of(probe), Kind::Int | Kind::Double)
&& matches!(kind, Kind::Int | Kind::Double);
if Kind::of(probe) != kind && !numeric {
return Err(status::ERR_UNKNOWN);
}
let n = idx(count)?.min(p.values.len());
for (i, v) in p.values.iter().take(n).enumerate() {
write(v, i);
}
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] propGetN({name} x{n}) -> ok");
}
Ok(())
})
.map_err(|code| {
// The property the plugin asked for and we did not have — the
// single most useful line when a plugin reports
// MissingHostFeature (trace-gated, like fetchSuite misses).
if std::env::var_os("OAK_OFX_TRACE").is_some() {
eprintln!("[ofx] property miss: {name} (code {code})");
}
code
})
}
unsafe extern "C" fn prop_get_pointer_n(
@@ -629,6 +664,7 @@ unsafe extern "C" fn prop_get_double_n(
}
get_n(set, name, count, Kind::Double, |v, i| match v {
Value::Double(d) => *out.add(i) = *d,
Value::Int(d) => *out.add(i) = *d as f64,
_ => {}
})
})
@@ -649,6 +685,7 @@ unsafe extern "C" fn prop_get_int_n(
}
get_n(set, name, count, Kind::Int, |v, i| match v {
Value::Int(d) => *out.add(i) = *d,
Value::Double(d) => *out.add(i) = *d as i32,
_ => {}
})
})
@@ -685,6 +722,10 @@ unsafe extern "C" fn prop_get_dimension(
// 已定义的空数组(如协商属性的初始状态)是合法维度 0,不是错误。
let exists = set.with_locked(|props| props.iter().any(|p| p.name == name));
if !exists {
if std::env::var_os("OAK_OFX_TRACE").is_some() {
let tag = crate::suites::tag::kind(handle);
eprintln!("[ofx] property miss (dimension): {name} (handle {handle:p} tag {tag})");
}
return Err(status::ERR_UNKNOWN);
}
*out = set.dimension(name) as c_int;