在上一篇关于
P/INVOKE的文章中,我们学习了如何从
Unity内部调用非托管方法,以及如何跨互操作屏障传递参数和返回值。
现在让我们开始Dllimport
在我们的代码库中到处撒播以获得乐趣和利润!对?好…
这种
做法将违反
单一
责任原则
多平台支持会有很多代码重复
这将使任何使用Dllimport
更难或不可能进行单元测试的类
我们还没有真正的策略来安全地管理非托管对象的生命周期——基本上避免内存泄漏。
那么让我们来看看我们是如何在 Baracoda 的项目中应对这些挑战的!
支持多种目标平台
假设您有一个针对静态库编译的 iOS 代码的现有版本,libios_plugin.a
. 因此,让我们添加插件的 Android 版本libandroid_plugin.aar
,其中包含内部libandroid_plugin.so
.
然后在尝试执行本机代码时观察它失败:
E/Unity: DllNotFoundException: __Internal
Dllimport 和程序集命名
我们的第一个问题是,在 iOS 上,我们使用的是静态链接库,它要求传递给的名称Dllimport
是__Internal.
但是,我们有一个名为 的 Android 版本的动态链接库libandroid_plugin.so
,并且该名称需要在Dllimport
.
我们可以使用///指令和Unity的平台脚本符号来使用条件编译#if
,如#elif
或选择#else
将基于当前平台编译的属性的版本。#endif
UNITY_ANDROID
UNITY_IOS
万岁,它有效!但是,呃……如果我们需要更多方法,那就太冗长了,而且我们现在只支持 2 个平台。那么我们能做些什么来避免重复这个巨大的块呢?
好吧,库名称必须是一个常量字符串,所以const string
也可以。让我们重构:
现在声明一个新方法只是坚持LIBNAME
下去的问题,而支持另一个平台只是#if
所有方法中的一个例子!
SRP和DRY,让代码更好
我们编写的内容适用于单个类,但随着 API 表面变大,我们可能希望将这些本地方法分组为对象——根据
单一责任
原则
——每个对象代表我们想要的不同服务访问。
但是因为这个LIBNAME
变量现在是私有的,所以我们必须在每个类中复制/粘贴指令,这与
D on't
R epeat
Y ourself原则相矛盾。所以让我们创建另一个类来为我们保存它!
导入现在看起来像这样:
不太冗长,易于阅读,易于扩展。现在我们肯定完成了,对吧?
非托管代码和单元测试
那么,你能为最终调用那些非托管方法的特性编写单元测试吗?
单元测试是对抗回归的好工具,可以确保你的代码符合你的预期,如果写得好,甚至可以作为可运行的文档。
因此,它们对于确保和维护我们的软件和游戏的健康至关重要。
因此,也许您可以针对实际实现编写测试,因为本机库只是提供一些业务逻辑,但也许您首先拥有它的原因是因为它使您可以访问外部资源?也许没有可用的桌面版本的库,测试甚至无法在编辑器中运行?
无论如何,这个非托管代码应该被考虑在被测单元之外,但是您仍然需要访问它在真实代码中提供的服务。
在这种情况下,最好的解决方案通常是将服务的实现与其接口分离。
现在我们可以为测试实例化一个假的,但仍然将真实的实现用于生产!
非托管对象和生命周期管理
这仍然是一个玩具示例,但是因为我们没有创建非托管对象的实例,而只是在讨论似乎是自由函数或静态方法的东西。
有时只有一个服务实例可以与之对话是有意义的,有时则不然,您需要能够动态地创建新实例。那么,我们如何从 C# 中与它们交互呢?
内部指针
在 C 或 C++ 中动态创建对象时,程序将分配一些内存,在其中构造对象,并返回指向它的指针。
等等,别跑!没关系!
在编组指向 C# 的非托管指针时,运行时可以将其转换为IntPtr
. 您可以将其视为非托管对象的不透明句柄,除了将其交还给非托管端外,您不能直接使用它做很多事情。
所以现在创建 C# 类的新实例也会创建非托管对象的新实例,然后我们可以对其进行方法调用。甜的!
我们只是忘记了一个细节:我们创建了一个非托管对象,这意味着 GC 不知道如何回收它,甚至默认 都不尝试! 因此,当 C# 类被垃圾回收时,让我们停止 泄漏该非托管对象。 所有权 在 C# 中,类发出需要清理步骤的信号的首选方式是实现 决定何时以及从何处调用IDisposable
接口。Dispose
超出了本文的范围,但请注意,出色的Extenject 框架能够自动处理它创建的对象。
假设有一个函数用于销毁我们的非托管对象,它的 API 如下所示:
现在实现Dispose
非常简单:
随着这一变化,我们现在可以很好地管理我们的资源并执行必要的清理工作。
随着
static extern
方法数量的增加,类趋于混乱。将它们提取到侧面的静态类中以保持业务逻辑和非托管方法声明分开通常是一个好主意。请参阅下面的示例。
然而,既然我们已经引入了手动资源管理,我们就会冒着尝试引用已被释放的非托管对象的风险,所以让我们让它更安全!
自定义手柄
幸运的是,C# 标准库正是我们所需要的:SafeHandle
!它本来是用来保持 Win32 句柄的,但它的 API 和终结保证使它非常适合我们的目的。
它还具有抽象的额外好处,因此您必须自己继承它,从而启用类型检查,而
IntPtr
对于编译器来说,任何一个看起来都像其他任何一个。
从 继承时SafeHandle
,需要做 3 件事。
告诉内部句柄的无效值是什么,以及通过其构造函数SafeHandle
是否是该句柄的唯一所有者
实现抽象属性IsInvalid
。对于构造函数中给出的无效值,它应该返回 true。为什么没有默认实现令人惊讶……
ReleaseHandle
实现当句柄被处理或垃圾收集时调用的抽象方法。显然,它应该释放这个句柄持有的资源。
因此,这就是SafeHandle
我们示例中自定义的样子:
现在我们只需要在任何地方都替换IntPtr
为 with CameraServiceHandle
,除了在销毁方法中仍然需要一个IntPtr
.
对于非托管方法声明,将它们提取到自己的静态类中后,它将如下所示:
我们的 C# 端服务现在在内部使用句柄:
我们已经做到了!
我们现在已经从到处添加临时[Dllimport] static extern
方法转变为专门设计的方法。
我们有一些小包装;它们封装良好,不会阻止对依赖它们的代码进行测试,易于添加跨平台支持,并且我们现在有系统的方法来保证与非托管对象交互时的资源和类型安全!
这就是我们在 Baracoda 如何使用 P/Invoke 的导览!
利用 P/Invoke 使我们能够编写跨平台库并与之交互,从而将我们研发团队在机器学习和计算机视觉方面的内部知识带到我们的 Unity 游戏中!
我们计划发布更多 Unity 开发者内容,敬请期待!