接前上一篇:平臺調用 (P/Invoke):DllImport特性說明 首先,我們知道C#和C/C++都是跨平臺的,但是原理上他們是不一樣的: C#首先編譯成一種中間語言(IL)的程式集,然後將這種程式集放到不同平臺下的解釋器裡面去執行,這就是說一次編譯到處運行 C/C++是針對不同的平臺直接編譯, ...
接前上一篇:平臺調用 (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
一個專註於.NetCore的技術小白