C# 接口使用说明#
本页集中说明 VisionFlow C++ 原生接口与 C# 绑定不一致 的地方,供 C# 开发者查阅。 环境配置见 在开发环境中引入VisionFlow;本页只讲差异,不是完整 API 参考。
Note
C# 绑定由 SWIG 从原生 visionflow:: 接口生成,覆盖多数接口。具体成员是否存在、
是字段还是属性、是否可 Dispose,一律以你所用发布包中实际生成的 visionflow.cs
为准。
运行时文件与生成文件#
visionflow.cs 只是 C# 侧的托管包装代码,不能单独运行。编译 C# 项目时,还需要
同时部署与其匹配的 native 包装库 wrapper_csharp_visionflow.dll 以及 VisionFlow
本体和算法模块所需的其他 native DLL。进程位数也必须匹配(x64)。
C# 程序集
↓ DllImport("wrapper_csharp_visionflow")
wrapper_csharp_visionflow.dll
↓
VisionFlow native DLL 及算法模块
常见加载问题包括:
DllNotFoundException:wrapper_csharp_visionflow.dll或其依赖库不在 native DLL 搜索路径中;BadImageFormatException:通常是 x86/x64 位数不匹配,也可能是 DLL 格式无效 或文件损坏;EntryPointNotFoundException:native DLL 已找到,但不包含当前 C# 包装代码 所需的导出入口;常见原因是 C# 包装文件与 native DLL 的生成配置或导出入口不匹配。
visionflow.cs 是自动生成文件,不应直接修改。
命名空间与符号映射#
命名空间:C++
visionflow::→ C#visionflow.;子命名空间同名对应,如visionflow::props→visionflow.props、visionflow::img→visionflow.img。STL 容器:
std::落到 C# 的std命名空间。自由函数(非成员函数):不再有裸函数,按所属模块挂到某个
visionflow_*_global静态类下—— 子命名空间多与其同名(visionflow::img→visionflow_img_global),顶层visionflow::的函数则落在visionflow_global或visionflow_core_global``(如 ``adapt)。
// 初始化等全局自由函数 → visionflow_global
visionflow_global.initialize(opts);
// 各命名空间的自由函数 → 对应的 visionflow_*_global 静态类
visionflow_img_global.draw(image, rect, color, -1); // visionflow::img
visionflow_helper_global.add_image_to_sample(sample, image, input_id); // visionflow::helper
// 顶层 visionflow::adapt 落在 core 模块类 visionflow_core_global:
var adapter = visionflow_core_global.adapt(project, configurator);
// STL 容器 → std 命名空间
var ids = new std.VectorInt { 100, 10000 };
var modules = new std.VectorString { "Segmentation", "Input" };
var kv = new std.MapStringString();
下表为常用映射:
C++ |
C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
命名风格#
Note
C# 绑定并未完全遵循 .NET 命名规范。以下描述的是当前生成代码的 实际形态, 调用时请以生成的 API 为准,不要按 C# 习惯自行改写名称。
实例方法:多为 C++ 原样
snake_case,如model.tool_list()、region.name()、param.to_json()。静态工厂:多为
PascalCase,如visionflow.img.Image.FromFile(...)、visionflow.img.Image.Zeros(...)、visionflow.Project.Create(...)/Open/Exists、visionflow.ProductInfo.Get()。生命周期方法:原生包装对象通常提供
Dispose(),用于显式释放 native 资源。属性:命名不完全统一。VisionFlow 相关属性多保留 C++ 的
snake_case,如sample_set_name、input_tool_id、device_name;.NET 集合和异常等框架侧 属性多为PascalCase,如ex.Type/ex.Code/ex.What/ex.Details、 容器的.Count/.Keys/.IsEmpty。
Warning
JSON 序列化为字符串是 to_json().to_string(),不是 .NET 的 ToString()。
节点名常量为 静态字段,标识符中的 . 被替换为 _,字符串值保留 .:
string s1 = visionflow.Segmentation.pred; // "pred"
string s2 = visionflow.Input.input_image_param; // "input_image.param"
string s3 = visionflow.OCR.pred_characters; // "pred.characters"
枚举值带 k 前缀:
var tag = visionflow.SplitTag.kTrain;
var depth = visionflow.img.Image.Depth.kDepthU16;
var level = visionflow.compatible.Level.kFullyCompatible;
重载与默认参数#
C++ 的默认参数和重载通常会生成多个 C# 重载,而不是 C# 的可选参数。调用时应根据 参数个数选择对应的重载。
// C++ 原型(尾部两个参数带默认值):
// void visionflow::helper::add_image_to_sample(
// ISample& sample, const Image& image, const std::string& input_tool_id,
// bool with_fingerprint = false, size_t thumbnail_long_side = 0);
// 这两个默认参数在 C# 里展开成三个重载,按参数个数选用:
visionflow_helper_global.add_image_to_sample(sample, image, input_id); // with_fingerprint=false, thumbnail_long_side=0
visionflow_helper_global.add_image_to_sample(sample, image, input_id, true); // with_fingerprint=true(导入时建议 true)
visionflow_helper_global.add_image_to_sample(sample, image, input_id, true, 512); // 再加 thumbnail_long_side=512(缩略图长边)
类型转换:区分 CLR 继承转换与 dump/load 复制#
Note
CLR 是 Common Language Runtime (公共语言运行时)的缩写,即 .NET 执行 C# 程序的
运行环境。它负责托管对象的类型信息、继承关系、引用和垃圾回收。C# 的 as 是
CLR 提供的类型兼容转换运算符:当对象的实际托管类型是目标类型,或实现了目标接口时, as
返回一个指向同一对象的引用;类型不兼容时返回 null,不会抛出转换异常。
C++ 常用 property->as<visionflow::props::PolygonRegionList>() 这类成员模板做转换;
C# 当前绑定 没有 等价的通用 as<T>()。C# 的 as 需要同时满足两个条件:
生成的 C# 类型之间存在 CLR 继承或接口关系;
返回值在托管侧实际是派生包装类,而不是仅仅持有派生 C++ 对象地址的基类包装类。
因此,不能仅凭两个类型存在 C++ 继承关系,或仅凭 native 指针实际指向派生对象,
就断定 C# as 一定可以成功。按以下两种情况处理:
情况 1:存在继承关系时,用 CLR 的 as(不经 dump/load)
// 假设 regions 是实际的 PolygonRegionList 包装对象,且 key 已存在。
// 当前生成代码中的 PolygonRegionList.at() 会构造 PolygonRegion 包装对象。
var regions = new visionflow.props.PolygonRegionList();
// ... 向 regions 添加 key 对应的区域(示例省略)...
visionflow.IRegion region = regions.at(key);
var polygon = region as visionflow.PolygonRegion;
if (polygon != null)
{
// polygon 与 region 引用同一个托管包装对象
polygon.set_ext_info("key", "value");
Console.WriteLine(polygon.get_ext_info("key"));
}
// 显式强制转换在类型不兼容时会抛 InvalidCastException;不确定类型时优先使用 as:
// var polygon2 = (visionflow.PolygonRegion)region;
regions.at(key) as visionflow.PolygonRegion 能成功的具体原因是 SWIG 为
PolygonRegionList 生成了一个覆盖方法:
// 逻辑上等价于生成代码中的实现
public override visionflow.IRegion at(string key)
{
return new visionflow.PolygonRegion(/* native 返回指针 */);
}
虽然方法声明的返回类型是 IRegion,但实际创建的托管对象是
visionflow.PolygonRegion,所以 CLR 的 as 可以成功。
当前 release 中,返回基类的 API 是否能使用 as,取决于生成代码实际创建的包装类。
可以按下面的规则判断,而不需要靠试错:
API |
生成代码实际创建的 C# 对象 |
能否直接向下 |
|---|---|---|
|
|
可以转换为 |
以基类 |
|
不能直接转换为具体区域类 |
|
|
不能直接转换为具体属性类 |
例如,sample.get 的返回类型是 IProperty,当前生成代码会构造
new IProperty(cPtr, true):
// sample.get 的返回类型是 IProperty;当前生成代码会构造 new IProperty(cPtr, true)
visionflow.props.IProperty property = sample.get(pred_node_id);
var polygon = property as visionflow.props.PolygonRegionList;
// polygon 可能为 null,即使 native 对象实际是 PolygonRegionList
// 如果 regions 的实际托管对象是 PolygonRegionList,调用的是它的 override,
// 因而会构造 PolygonRegion;如果实际托管对象只是 IRegionList,
// 则会构造 IRegion,不能向下转换。
这里失败的原因不是 C++ 没有继承关系。以当前生成代码为例,继承链实际是:
PolygonRegionList → IRegionList → IProperty
但如果 SWIG 返回时已经把指针包装成了 C# 的 IProperty 或 IRegion 实例,
CLR 的 as 不会读取 native RTTI,也不会根据指针重新创建派生包装类。
此时应使用 dump/load 创建目标类型的包装对象。
情况 2:无法直接 CLR 转换、但类型提供 dump()/load() 时,用序列化复制
// 推理属性结果:sample.get(...) 返回 IProperty,用具体类型解码
var pred = new visionflow.props.PolygonRegionList();
using (var raw_property = sample.get(pred_node_id))
{
if (raw_property == null)
throw new InvalidOperationException("Property does not exist.");
pred.load(raw_property.dump());
}
// 参数读写同理
var filter_args = new visionflow.param.PolygonsFilterParameters();
using (var raw_param = model.get_param(node))
{
if (raw_param == null)
throw new InvalidOperationException("Parameter does not exist.");
filter_args.load(raw_param.dump());
}
这里的 load 不是把原对象“改成”另一个 C# 类型,而是把 dump 产生的序列化数据
读取到一个已经创建好的目标对象中。因此得到的是一个新的对象状态:
方式 |
前提 |
是否序列化 |
对象身份和修改结果 |
|---|---|---|---|
CLR |
存在 CLR 继承/接口关系,且返回值实际是派生包装类 |
否 |
仍引用原对象;对对象的修改直接作用于原对象 |
|
源类型和目标类型都支持兼容的 |
是 |
创建序列化副本;修改副本不会自动回写原对象 |
Warning
dump/load 是 序列化后的复制,不是 C++ as<T>() 那种引用转换:可能产生额外
拷贝,不能假定保留对象身份、共享状态或相同生命周期;load() 失败会抛出包装层异常,
处理方式见下文『异常模型』一节。没有 dump/load 的类型不能套用此法。
资源管理:RAII → IDisposable#
需显式释放的原生包装类型通常实现
IDisposable;是否需要Dispose以生成的 C# 类型为准,不要假设所有返回对象释放语义相同。GC 不保证及时、也不保证按依赖顺序 释放原生资源。
为兼容 .NET Framework 4.8 示例工程,正文用传统
using (...) {}或finally中 显式Dispose();调用方明确使用 C# 8+ 时才可改用using var。
依赖关系包括 Project → SampleSet → PropertySet → PropertySetIterator;
SampleSetIterator 和输入辅助对象依赖 SampleSet。
释放时应按逆依赖顺序进行:先释放辅助对象与迭代器,再释放 PropertySet、SampleSet,最后释放 Project。
尤其应确保 SampleSet / PropertySet 在其所属 Project 之前 Dispose(),否则可能触发 native 侧的无效访问或其他不可恢复错误。
var prop_set = sample_set.property_set(node_id);
// ... 使用 prop_set ...
prop_set.Dispose(); // 先释放下游
sample_set.Dispose(); // 再释放 SampleSet
project.Dispose(); // 最后释放 Project
推荐让 using 变量按依赖关系从上游到下游依次声明,利用作用域结束时的逆序释放:
using (var project = visionflow.Project.Open(project_path))
using (var sample_set = project.main_sample_set())
using (var property_set = sample_set.property_set(node_id))
{
// 使用 property_set
}
不要把子对象保存到比其 Project 更长的生命周期中;具体类型是否实现
IDisposable,仍以当前生成的 visionflow.cs 为准。
回调与 C# 继承#
部分原生虚函数通过 SWIG Director 映射为可被 C# 继承的包装类;当前常见类型有
visionflow.ILogSink、visionflow.util.IProgressCallback、
visionflow.IScriptStdSink。它们在 C++ 中是接口类,但在生成的 C# API 中表现为可继承的
包装类。C# 子类用 override 重写虚方法即可被 C++ 回调;下面以前两个为例:
// 继承 ILogSink 并 override log,即可接收 C++ 侧的日志回调
public class MyLogSink : visionflow.ILogSink
{
public override void log(int level, string message)
{
// 打上前缀,运行时即可看出日志确实回调进了这个自定义 sink
Console.WriteLine($"[MyLogSink] level={level}: {message}");
}
}
// 通过 InitOptions 注册;初始化后 C++ 产生的日志会回调进 C# 的 log()
var opts = new visionflow.InitOptions();
var log_sink = new MyLogSink();
opts.logger.custom_sink = log_sink;
visionflow_global.initialize(opts);
// custom_sink 是 native 侧保存的非拥有指针;只要日志系统仍会回调,
// log_sink 就必须由 C# 侧持有引用。确认日志系统不再使用后再 Dispose。
进度回调 IProgressCallback 同理——重写 on_progress,再注册到需要进度的对象上(如 ConfigureRuntimeProjectAdapter):
// 继承 IProgressCallback 并 override on_progress,接收进度回调
public class MyProgress : visionflow.util.IProgressCallback
{
public override visionflow.TaskCtrlSignal on_progress(
int total, int progress, string progress_info)
{
// 打上前缀,运行时即可看出进度确实回调进了这个自定义回调
Console.WriteLine($"[MyProgress] {progress}/{total}: {progress_info}");
return visionflow.TaskCtrlSignal.kContinue; // 返回 kAbort 可请求中止任务
}
}
// 注册到需要进度回调的对象上
var callback = new MyProgress();
adapter.set_progress_callback(callback);
// callback 必须保持有效到 adapter 生命周期结束;adapter 不会替调用方释放它。
// 释放顺序:先释放 adapter,再释放 callback。
// adapter.Dispose();
// callback.Dispose();
回调对象的有效期取决于 native 侧保存方式:IProgressCallback 至少要保持到持有它的
adapter 或 task 销毁;异步任务中应由外层对象保存引用,不能只依赖局部变量和 GC。
回调不再使用时,按生成类型的要求调用 Dispose(),并确保 native 侧已经不再回调。
回调方法中的异常也不应直接跨越 native 边界传播,应在回调内部捕获并转换为可由调用方
处理的状态。
值语义回写陷阱#
许多参数组对象的 get_xxx() 返回的是 副本:修改副本后必须用 set_xxx()
写回,否则改动不会生效。并非所有 getter 都有相同的返回语义,具体以生成的 C# API
和对应的 C++ 返回类型为准。
var infer_args = new visionflow.param.SegmentationInferenceParameters();
infer_args.load(model.get_param(node).dump());
var infer_shape = infer_args.get_infer_shape(); // 取回的是副本
infer_shape.set_enable(false);
infer_args.set_infer_shape(infer_shape); // 必须写回,否则丢失
model.set_param(node, infer_args);
Note
具体的 get_/set_ 成员名以你所用发布包生成的 C# API 为准,可对照
visionflow/param/decl_params/segmentation_inference_parameters.py 核对。
异常模型#
派生自原生 visionflow::excepts::DefaultException 的异常,在 C# 中统一映射为托管类型
visionflow.excepts.DefaultException。用其字段区分,不能 catch VisionFlow 原生的具体子类 (如 FileNotFound)。
try
{
var image = visionflow.img.Image.FromFile("not_exist_file.image");
}
catch (visionflow.excepts.DefaultException ex)
{
// ex.Type == "class visionflow::excepts::FileNotFound"(原生类名字符串)
// ex.Code == -10002
Console.Error.WriteLine($"{ex.Type} ({ex.Code}): {ex.What}");
Console.Error.WriteLine(ex.Details);
}
Warning
原生 std::exception 走另一条映射路径,转换为 System.SystemException,
因此不能把”所有原生异常”一概视为 DefaultException。应按异常的实际内容
(Type / Code)判断,而非依赖异常子类。
两条路径可以分别 catch。如果还要捕获 System.Exception,应将它放在最后;
DefaultException 和 System.SystemException 彼此没有继承关系,二者的先后顺序
本身不影响匹配:
try
{
// ... 调用 VisionFlow 接口 ...
}
catch (visionflow.excepts.DefaultException ex)
{
// 源自 visionflow::excepts::DefaultException:有 Type / Code / What / Details
Console.Error.WriteLine($"VisionFlow: {ex.Type} ({ex.Code}) {ex.What}");
}
catch (System.SystemException ex)
{
// 源自原生 std::exception:只有 Message,没有 Type / Code
Console.Error.WriteLine($"native std::exception: {ex.Message}");
}
容器语义#
std.VectorXxx、SWIG map、model.tool_list() 等返回的是 原生容器的 C# 包装,
不要将这些类型与 .NET 原生集合 List<T> / Dictionary<TKey, TValue> 混用。
它们实现了 .NET 集合接口,同时保留原生方法:
// 序列容器:.NET 风格成员 + 索引器 + 集合初始化器
var modules = new std.VectorString { "Segmentation", "Input" };
int n = modules.Count;
string first = modules[0];
// map:.Add / .ContainsKey / .TryGetValue / 索引器 / foreach(KeyValuePair)
var kv = new std.MapStringString();
kv.Add("k", "v");
if (kv.ContainsKey("k")) { }
string val;
kv.TryGetValue("k", out val);
foreach (var pair in kv) { var key = pair.Key; }
// 生成容器也可能有 .IsEmpty / .Contains 等
var tools = model.tool_list();
bool empty = tools.IsEmpty;
bool has = tools.Contains("Input");
Note
不同容器暴露的成员不完全一致(
.Countvs 原生.size()、.Keys、索引器、 集合初始化器),以生成类型为准,不要假定都支持。是否需要
Dispose以生成类型为准;若实现IDisposable,须在其依赖对象仍有效时释放。LINQ 仅对真正实现
IEnumerable<T>的包装可用;能foreach不代表能直接Select()/ToList()。需要在包装器释放后仍保留数据时,应先在其有效期内枚举 复制到托管集合。