平台调用 (P/Invoke):跨平台方案
接前上一篇:平台调用 (P/Invoke):DllImport特性说明
首先,我们知道C#和C/C++都是跨平台的,但是原理上他们是不一样的:
C#首先编译成一种中间语言(IL)的程序集,然后将这种程序集放到不同平台下的解释器里面去执行,这就是说一次编译到处运行
C/C++是针对不同的平台直接编译,编译之后就不具备跨平台能力了
所以,当我们开发的应用需要跨平台时,我们就需要将C/C++程序分别对不同平台编译了,那么剩下的就是我们怎么调用的问题了。
调用时判断
一个简单的思路就是,在需要调用的时候做判断,这个大家应该都会,比如我们有window和linux的两个动态库,那么我们在调用的时候可以通过环境来判断:
[DllImport("lib/Project-win.dll", EntryPoint = "Add")]
static extern int WinAdd(int n1, int n2);
[DllImport("lib/libProject-linux.so", EntryPoint = "Add")]
static extern int LinuxAdd(int n1, int n2);
static void Main(string[] args)
{
//判断系统环境
if (OperatingSystem.IsLinux())
{
var sum = LinuxAdd(1, 2);
Console.WriteLine(sum);
}
else
{
var sum = WinAdd(1, 2);
Console.WriteLine(sum);
}
}
显然,这个办法很笨拙,难道我们每个要调用的时候都加这个判断么?当然,有些想法的同学可能会考虑使用继承来做一层封装,这样就不用每个地方都加这种判断了,但是这也需要增加一些无用的代码量,想想就因为不同平台的编译,就要拐个弯来处理,想想就不划算。
库名称变体
很庆幸,.net为了解决跨平台导致的问题,允许我们将动态库按照一些规则来命名,这样就可以自动根据环境来选择对应的动态库了,比如,在对C/C++程序进行编译的时候,我们可以把它们编译成相同名称不同后缀的动态库,比如windows下就是project.dll,linux下就是project.so,然后就可以简单的实现:
[DllImport("lib/project", EntryPoint = "Add")]
static extern int Add(int n1, int n2);
static void Main(string[] args)
{
//自动根据当前系统环境判断
var sum = Add(1, 2);
Console.WriteLine(sum);
}
上述代码可以在linux和windows下运行,只要对应的动态库存在就可以了。
这种方式得益于.net的库名变体搜索规则:
, Windows下按以下顺序搜索dll:
[DllImport("lib/nativedep")]将按下面的顺序搜索动态库:
1、先搜索全名不带后缀的库,即nativedep
2、没有则继续搜索带.dll后缀的库,即nativedep.dll
macOS下按以下顺序搜索dylib:
[DllImport("lib/nativedep")]将按下面的顺序搜索动态库:
1、先搜索带.dylib后缀的库,即nativedep.dylib
2、没有则继续搜索以lib开头,带.dylib后缀的库,即libnativedep.dylib
3、没有则继续搜索全名不带后缀的库,即nativedep
4、最后搜索以lib开头的库,即libnativedep
Linux下要分情况而定
- 如果引入的库名以.so为后缀,或者以.so.*的格式结尾,则按以下顺序搜索so:
[DllImport("lib/nativedep.so")]和[DllImport("lib/nativedep.so.1")]将按下面的顺序搜索动态库: 1、先搜索全名称没有处理的库,即nativedep.so、nativedep.so.1 2、没有则继续搜索带lib前缀的库,即libnativedep.so、libnativedep.so.1 3、没有则继续搜索带.so后缀的库,即nativedep.so.so、nativedep.so.1.so 4、没有则继续搜索带lib前缀、带.so后缀的库,即libnativedep.so.so、libnativedep.so.1.so
- 否则按以下顺序搜索so:
[DllImport("lib/nativedep")]将按下面的顺序搜索动态库: 1、先搜索带.so后缀的库,即nativedep.so 2、没有则继续搜索以lib开头,带.so后缀的库,即libnativedep.so 3、没有则继续搜索全名不带后缀的库,即nativedep 4、最后搜索以lib开头的库,即libnativedep
注:如果DllImport的时候使用的是绝对路径,那么在使用绝对路径名称搜索,以上命名规则将不生效
自定义导入解析
有时候,我们开发都要求速度快,所以,也许在项目开始的时候我们没有考虑这么多,可能是直接按照上面第一种做的,导致我们有多个名称的库,而又不方便按照第二种方式来处理,这个时候我们可以考虑下自定义导入解析的方式。
假如现在我们已经有window下的动态库Project-win.dll,以及Linux下的动态库libProject-linux.so,这是两个文件,名称不一样,我们不能使用上面第二种方式(库名称变体)来处理,那么可以自定义导入解析,首先,假如我们导入的程序是:
[DllImport("lib/plus", EntryPoint = "Add")]
static extern int Plus(int n1, int n2);
注意,这里的动态库名称是plus,接着提供一个自定义解析的委托函数:
static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
//如果库名是plus,则根据系统环境来更换
if (libraryName == "lib/plus")
{
if (OperatingSystem.IsLinux())
{
libraryName = "lib/libProject-linux.so";
}
else
{
libraryName = "lib/Project-win.dll";
}
return NativeLibrary.Load(libraryName, assembly, searchPath);
}
return IntPtr.Zero;
}
接着注册一下,我们就可以用了:
static void Main(string[] args)
{
//将当前运行的程序集注册自定义的处理方式
NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver);
//直接使用
var sum = Plus(1, 2);
Console.WriteLine(sum);
}
参考文档:https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform