平台调用 (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

 

posted @ 2023-02-20 18:05  没有星星的夏季  阅读(605)  评论(4编辑  收藏  举报