在做 .NET 開發時,偶爾需要呼叫第三方提供的原生函式庫(Native Library),例如硬體 SDK、加密函式庫或底層通訊元件。這篇文章透過一個實際的 Demo 專案,分享我在引入跨平台原生函式庫時的兩大方案和避坑經驗。
1. 專案準備
Demo 專案使用了一個簡單的 C++ 動態函式庫 TimeMeaning,它提供了一個 API:
// 傳入秒級時間戳記,回傳一段人生/時間意境文案
const char* GetTimeMeaning(int timestampSecond);
時間戳記取模 10 後回傳對應文案,寓意每個時刻都有不同的人生感悟:
static const char* TIME_MEANINGS[] = {
"黎明破曉,萬物甦醒,新的一天帶來新的希望",
"晨光熹微,思緒清晰,適合規劃一天的行程",
"日出東方,陽光燦爛,充滿活力與朝氣",
"上午時光,精力充沛,專注做事效率高",
"正午時分,陽光明媚,適合休息片刻",
"午後暖陽,慵懶愜意,時光靜靜流淌",
"夕陽西下,餘暉滿天,美好的黃昏時分",
"夜幕降臨,星光點點,思緒開始沉澱",
"夜深人靜,皓月當空,適合反思與冥想",
"午夜時分,萬籟俱寂,夢想在黑暗中萌芽"
};
第三方函式庫通常會提供針對不同平台的版本,目錄結構如下:
Lib/
├── x64/
│ ├── TimeMeaning.dll # 64 位元 Windows
│ └── (lib)TimeMeaning.so # 64 位元 Linux
├── x86/
│ └── TimeMeaning.dll # 32 位元 Windows
└── arm64/
└── (lib)TimeMeaning.so # ARM64 Linux
我們希望:
- 程式碼保持統一:不需要針對每個平台寫不同的呼叫程式碼
- 自動適配:編譯或發佈時自動選擇對應平台的函式庫檔案
Directory.Build.props 全域巨集定義
首先,我們在方案根目錄建立 Directory.Build.props,預設定義 PLATFORM_WIN_X86 條件編譯巨集:
<Project>
<PropertyGroup>
<DefineConstants>$(DefineConstants);PLATFORM_WIN_X86</DefineConstants>
</PropertyGroup>
</Project>
然後透過 publish.bat 發佈指令碼呼叫 SetPlatformMacro.ps1,在發佈前根據目標平台修改全域巨集定義:
# SetPlatformMacro.ps1
$macro = ""
switch ($Platform) {
"linux-x64" { $macro = "PLATFORM_LINUX_X64" }
"linux-arm64" { $macro = "PLATFORM_LINUX_ARM64" }
"win-x64" { $macro = "PLATFORM_WIN_X64" }
"win-x86" { $macro = "PLATFORM_WIN_X86" }
}
$content = $content -replace '<DefineConstants>.*?</DefineConstants>', "<DefineConstants>`$(DefineConstants);$macro</DefineConstants>"
這樣在程式碼中就可以方便地區分當前編譯的平台版本:
#if PLATFORM_WIN_X64
var platform = "Windows X64";
#elif PLATFORM_WIN_X86
var platform = "Windows X86";
#elif PLATFORM_LINUX_X64
var platform = "Linux X64";
#elif PLATFORM_LINUX_ARM64
var platform = "Linux ARM64";
#else
var platform = "Unknown";
#endif
2. 兩大方案概述
引入原生函式庫主要分為兩大方案:
- 動態載入:使用
NativeLibraryAPI 執行階段手動載入 - 靜態載入:使用
DllImport特性宣告(做了 3 種情況測試)
VC-LTL 和 YY-Thunks(所有範例通用)
所有 4 個範例程式都引入了以下兩個 NuGet 套件,目前測試的範例都支援 Win7 及以上 Windows 版本,以及 Linux 平台:
Windows 7 執行原理:雖然微軟官方 .NET 10 已不再支援 Windows 7,但透過使用 net10.0-windows 目標架構配合 AOT 發佈,可讓程式在 Windows 7 上正常執行。AOT 將 .NET 程式碼靜態編譯為原生可執行檔,完全擺脫對 .NET 執行階段的依賴;配合 VC-LTL 和 YY-Thunks 分別提供輕量 C 執行階段支援和舊版 Windows API 相容層,實現跨版本相容。
<PackageReference Include="VC-LTL" Version="5.3.1" />
<PackageReference Include="YY-Thunks" Version="1.2.1-Beta.4" />
- VC-LTL:使用開源的 VC 執行階段程式庫,無需安裝系統修補程式,大幅減少程式體積,相容舊系統。配合 AOT(
PublishAot=true)發佈可擺脫 .NET 執行階段依賴,直接產生原生可執行檔 - YY-Thunks:為舊版 Windows 提供新 API 的相容層,讓現代程式碼也能在 Win7/XP 上正常執行(XP 未測試文章範例)
3. 方案一:動態載入(✅ 成功)
動態載入是最靈活的方式,使用 NativeLibrary API 在執行階段手動載入原生函式庫。這種方式的優勢是可以完全自訂函式庫的載入邏輯,能夠處理相同函式庫但儲存在不同目錄、使用不同檔案命名的複雜場景。對於某些特殊需求,比如需要在執行階段根據條件選擇載入不同的版本,或者函式庫的路徑需要動態計算,動態載入是最好的選擇。
程式碼實作
using System.Runtime.InteropServices;
namespace csharp.test.dynamic;
internal static class TimeMeaningNative
{
// 根據目前作業系統判斷函式庫檔名:Windows 用 dll,Linux 用 so
private static readonly string DllName = OperatingSystem.IsWindows()
? "TimeMeaning.dll"
: "libTimeMeaning.so";
// 定義與 C++ 函式相同呼叫慣例的委派,用於後續轉換
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private delegate IntPtr GetTimeMeaningDelegate(int timestampSecond);
// 用於儲存轉換後的委派(呼叫時會用到)
private static GetTimeMeaningDelegate? _getTimeMeaning;
// 用於儲存函式庫的控制代碼(用於後續釋放)
private static IntPtr _handle;
// 靜態建構函式,在第一次使用該類別時自動執行
static TimeMeaningNative()
{
// 拼接函式庫的完整路徑:應用程式根目錄 + Lib 子目錄 + 檔名
var dllPath = Path.Combine(AppContext.BaseDirectory, "Lib", DllName);
// NativeLibrary.Load:載入指定路徑的原生函式庫,回傳函式庫控制代碼
_handle = NativeLibrary.Load(dllPath);
// NativeLibrary.GetExport:從已載入的函式庫中取得指定名稱的函式位址
var funcPtr = NativeLibrary.GetExport(_handle, "GetTimeMeaning");
// Marshal.GetDelegateForFunctionPointer:將函式指標轉換為可呼叫的委派
_getTimeMeaning = Marshal.GetDelegateForFunctionPointer<GetTimeMeaningDelegate>(funcPtr);
}
// 提供手動釋放函式庫的方法,避免記憶體洩漏
public static void Free()
{
if (_handle == IntPtr.Zero) return;
NativeLibrary.Free(_handle);
_handle = IntPtr.Zero;
}
// 封裝對外的呼叫介面,回傳字串結果
public static string GetTimeMeaningString(int timestampSecond)
{
if (_getTimeMeaning == null)
{
throw new InvalidOperationException("動態函式庫未正確載入");
}
// 呼叫委派,得到 C++ 函式回傳的指標
var ptr = _getTimeMeaning(timestampSecond);
// 將 UTF8 編碼的字元指標轉換為 C# 字串
return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
}
}
程式碼說明:
NativeLibrary.Load- 核心 API,傳入完整路徑載入原生函式庫NativeLibrary.GetExport- 從已載入函式庫中取得匯出函式的指標Marshal.GetDelegateForFunctionPointer- 將非受控函式指標轉換為 .NET 委派,這樣就可以像呼叫一般方法一樣呼叫原生函式了Marshal.PtrToStringUTF8- 將 C++ 回傳的 UTF8 字串指標轉換為 C# 字串
專案設定(csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net10.0-windows</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WindowsSupportedOSPlatformVersion>6.1</WindowsSupportedOSPlatformVersion>
<TargetPlatformMinVersion>6.1</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CodeWF.Log.Core" Version="11.3.14" />
<PackageReference Include="VC-LTL" Version="5.3.1" />
<PackageReference Include="YY-Thunks" Version="1.2.1-Beta.4" />
</ItemGroup>
<ItemGroup>
<!-- 偵錯狀態預設複製 Win x64 的函式庫,便於本機偵錯 -->
<None Update="Lib\x64\TimeMeaning.dll" Condition="'$(Configuration)' == 'Debug'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Lib\TimeMeaning.dll</Link>
</None>
</ItemGroup>
<ItemGroup>
<None Update="Lib\x64\libTimeMeaning.so" Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Lib\libTimeMeaning.so</Link>
</None>
<None Update="Lib\x64\TimeMeaning.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Lib\TimeMeaning.dll</Link>
</None>
<None Update="Lib\x86\TimeMeaning.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Lib\TimeMeaning.dll</Link>
</None>
</ItemGroup>
</Project>
csproj 設定說明:
Condition="'$(Configuration)' == 'Debug'"- 在 Debug 模式下,預設複製 Windows x64 版本的函式庫,方便直接在 Visual Studio 中偵錯執行Condition="'$(RuntimeIdentifier)' == 'linux-x64'"- 當使用-r linux-x64發佈時,複製 Linux 版本的函式庫Link屬性指定了函式庫在輸出目錄中的路徑,確保與程式碼中載入的路徑一致
關鍵點說明
動態載入流程:
- 執行階段根據作業系統判斷函式庫檔名
- 拼接完整路徑載入函式庫
- 取得匯出函式位址
- 轉換為委派呼叫
4. 方案二:靜態載入
靜態載入使用 DllImport 特性宣告,這是 .NET 中呼叫原生函式庫的標準方式。我們做了 3 種情況測試:主要是測試三方函式庫封裝程式碼是直接放在主專案,還是提取出來透過 NuGet 散佈時,使用條件編譯巨集是否可行、路徑靈活度如何。
條件編譯巨集的兩種定義方式
使用條件編譯巨集前,需要先定義 PLATFORM_XXX 巨集,有兩種方式:
方式 1:在發佈設定檔(pubxml)中定義
在 Properties/PublishProfiles/*.pubxml 檔案的 <PropertyGroup> 中新增:
<PropertyGroup>
<DefineConstants>$(DefineConstants);PLATFORM_WIN_X64</DefineConstants>
</PropertyGroup>
方式 2:在專案檔(csproj)中定義
在專案檔的 <PropertyGroup> 中新增:
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
<DefineConstants>$(DefineConstants);PLATFORM_LINUX_X64</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">
<DefineConstants>$(DefineConstants);PLATFORM_LINUX_ARM64</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<DefineConstants>$(DefineConstants);PLATFORM_WIN_X64</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x86'">
<DefineConstants>$(DefineConstants);PLATFORM_WIN_X86</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == ''">
<DefineConstants>$(DefineConstants);PLATFORM_EMPTY</DefineConstants>
</PropertyGroup>
重要說明:方式 2 在專案檔中新增的條件編譯巨集定義,僅對主專案有效。因為子專案編譯時 $(RuntimeIdentifier) 是空的(不會繼承主專案的 RID),所以子專案會符合 $(RuntimeIdentifier) == '' 條件,定義 PLATFORM_EMPTY 巨集。這表示在子專案中無法透過這種方式正確區分平台。如果需要在子專案中使用平台巨集,需要配合發佈指令碼動態修改 Directory.Build.props 全域巨集定義(情況 2)。
情況 1:單專案 + 條件編譯(✅ 成功)
直接在主專案中使用 DllImport,透過條件編譯巨集設定函式庫路徑,適合不封裝為類別庫的場景,例如小工具或不需要重複使用率低的專案。
靜態載入使用條件編譯巨集的優勢:可以靈活處理不同平台函式庫名稱完全不同的情況,當然也包括不同目錄(實際場景有可能),例如 Windows 用 Lib/Windows x64/TimeMeaning.dll,Linux 用 Lib/Linux x64/libTimeMeaning.so,這與方案一的動態載入有點像,都能處理複雜的路徑差異。
程式碼實作
using System.Runtime.InteropServices;
namespace csharp.test.static_;
internal static class TimeMeaningNative
{
#if PLATFORM_WIN_X64 || PLATFORM_WIN_X86
const string DLL = "Lib/TimeMeaning.dll";
#elif PLATFORM_LINUX_X64 || PLATFORM_LINUX_ARM64
const string DLL = "Lib/libTimeMeaning.so";
#else
const string DLL = "Lib/TimeMeaning.dll";
#endif
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern IntPtr GetTimeMeaning(int timestampSecond);
public static string GetTimeMeaningString(int timestampSecond)
{
var ptr = GetTimeMeaning(timestampSecond);
return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
}
}
結果
✅ 成功:在單專案場景下,條件編譯巨集正常運作,Windows 和 Linux 都能正確載入對應函式庫檔案。
情況 2:多專案 + 條件編譯(✅ 成功,推薦)
將函式庫呼叫封裝到獨立的類別庫專案,再由主專案參考。透過 publish.bat 發佈指令碼在發佈前呼叫 SetPlatformMacro.ps1 修改全域巨集定義,解決了類別庫不繼承條件編譯巨集的問題。
類別庫程式碼(TimeMeaningNative.csproj)
using System.Runtime.InteropServices;
namespace TimeMeaningNative;
public static class TimeMeaningApi
{
#if PLATFORM_WIN_X64 || PLATFORM_WIN_X86
const string DLL = "Lib/TimeMeaning.dll";
#elif PLATFORM_LINUX_X64 || PLATFORM_LINUX_ARM64
const string DLL = "Lib/libTimeMeaning.so";
#else
const string DLL = "Lib/TimeMeaning.dll";
#endif
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern IntPtr GetTimeMeaning(int timestampSecond);
public static string GetTimeMeaningString(int timestampSecond)
{
var ptr = GetTimeMeaning(timestampSecond);
return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
}
}
發佈指令碼(publish.bat)
for %%p in (%platforms%) do (
set "tfm="
set "pubxml="
if "%%p"=="linux-x64" set "tfm=net10.0" & set "pubxml=FolderProfile_linux-x64.pubxml"
...
powershell -ExecutionPolicy Bypass -File "SetPlatformMacro.ps1" -Platform "%%p"
for %%d in (%project_paths%) do (
dotnet publish "%%d" -f !tfm! /p:PublishProfile="%%d\Properties\PublishProfiles\!pubxml!"
)
)
成功原因
✅ 成功:publish.bat 發佈前呼叫 SetPlatformMacro.ps1 指令碼修改 Directory.Build.props 中的全域巨集定義,使子專案也能正確取得平台巨集定義(如 PLATFORM_LINUX_X64),而不是依賴編譯時的 RuntimeIdentifier。
備註:具體指令碼可在測試存放庫檢視,存放庫位址在文末。
⚠️ 重要:NuGet 封裝限制
注意:此方案僅適用於同一原始碼存放庫內的多專案參考。如果將此類別庫專案封裝為 NuGet 套件散佈給其他專案使用,會存在以下問題:
- Directory.Build.props 不會被封裝:NuGet 套件中不會包含存放庫根目錄的
Directory.Build.props,上游使用者無法繼承巨集定義 - 巨集已在編譯時固定:封裝時,程式碼已按當時的巨集條件完成分支裁剪,產生的 dll 不再受上游巨集影響
- 上游無法改變行為:安裝 NuGet 套件的專案,即使定義了自己的
PLATFORM_XXX巨集,也無法改變已封裝函式庫的程式碼分支
解決方案:如需透過 NuGet 散佈跨平台函式庫,推薦使用方案四(多專案+僅函式庫名稱),或者使用 NuGet build props 機制(詳見下方補充說明)。
情況 3:多專案 + 僅函式庫名稱(✅ 推薦)
這是最推薦的方案,相較動態載入方式,減小了使用難度。類別庫中不使用條件編譯巨集,只指定函式庫名稱(不加副檔名),解決跨平台函式庫參考問題。
類別庫程式碼(TimeMeaningNative.csproj)
using System.Runtime.InteropServices;
namespace TimeMeaningNative;
public static class TimeMeaningApi
{
const string DLL = "Lib/TimeMeaning";
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern IntPtr GetTimeMeaning(int timestampSecond);
public static string GetTimeMeaningString(int timestampSecond)
{
var ptr = GetTimeMeaning(timestampSecond);
return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
}
}
主專案設定(csproj)
這裡有一個關鍵技巧:如果 Linux 函式庫有 lib 等前置詞,需要去掉,和 Windows dll 改為相同檔案名稱,Linux 下複製時去掉 lib 前置詞!
<ItemGroup>
<!-- Linux 下關鍵:複製時去掉 lib 前置詞! -->
<None Update="Lib\x64\libTimeMeaning.so" Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>Lib\TimeMeaning.so</Link>
</None>
...
</ItemGroup>
工作原理
- Windows:
DllImport("Lib/TimeMeaning")自動尋找Lib/TimeMeaning.dll - Linux:
DllImport("Lib/TimeMeaning")會尋找Lib/TimeMeaning或Lib/TimeMeaning.so,不會尋找Lib/libTimeMeaning.so - 所以 Linux 下需要透過
<Link>Lib\TimeMeaning.so</Link>將libTimeMeaning.so複製為TimeMeaning.so
結果
✅ 成功:這是最推薦的方案,簡單可靠,懶得定義條件編譯巨集,只需要確保不同平台的函式庫檔案名稱統一(Linux 下去掉 lib 前置詞)。
方案對比總結
| 類別 | 方案 | 做法 | 結果 | 適用場景 |
|---|---|---|---|---|
| 動態載入 | NativeLibrary 動態載入 | 程式碼中手動判斷作業系統並載入函式庫 | ✅ 全平台可用 | 需要靈活控制載入路徑 |
| 靜態載入 | 單專案 + 條件編譯 | #if PLATFORM_WIN_X64 條件編譯 |
✅ 成功 | 僅主專案使用,不封裝類別庫,支援不同路徑函式庫 |
| 靜態載入 | 多專案 + 條件編譯 | 發佈前透過指令碼全域設定巨集 | ✅ 成功(配合發佈指令碼) | 推薦,需要不同平台函式庫路徑不同 |
| 靜態載入 | 多專案 + 僅函式庫名稱(無副檔名) | 類別庫只寫函式庫名稱 + csproj 條件複製(Linux 去 lib 前置詞) | ✅ 跨平台完美成功 | 最推薦,簡單可靠,希望統一函式庫檔案名稱 |
6. 核心經驗
- 推薦使用 DllImport 常數函式庫名稱(不加副檔名),這是最穩定可靠的方案,重點是簡單好理解。方案一動態載入也可行,只是使用上稍微麻煩一點
- 靜態載入使用條件編譯巨集能處理函式庫名稱不同的情況,適用於單專案和多專案(多專案需要配合發佈指令碼全域設定巨集)
- 多專案場景下巨集不繼承的問題可以透過發佈指令碼解決:使用
publish.bat+SetPlatformMacro.ps1在發佈前修改全域巨集 - 不要依賴類別庫編譯時的 RuntimeIdentifier,因為類別庫編譯時可能沒有 RuntimeIdentifier 上下文,導致條件編譯巨集不生效
- Linux 下注意去掉 lib 前置詞,透過 csproj 的
<Link>機制重新命名 - 需要支援 Windows 7 時,安裝 VC-LTL 和 YY-Thunks NuGet 套件
- 可以將函式庫檔案放在 Lib 子目錄,不一定非要在根目錄
- ⚠️ 重要:Directory.Build.props 全域巨集不支援 NuGet 散佈:如果將使用條件編譯巨集的類別庫封裝為 NuGet,上游專案完全繼承不到該巨集,且 NuGet 套件內部程式碼也會在封裝時就固定編譯分支。如需透過 NuGet 散佈,推薦使用方案四(僅函式庫名稱,不依賴條件編譯巨集)
補充說明:對於初學者來說,先掌握情況 3(多專案+僅函式庫名稱)是最好的,這是最穩妥且容易理解的方式。如果確實需要靈活處理路徑差異,再考慮動態載入或情況 2。
7. 補充:NuGet 封裝與條件編譯巨集
7.1 Directory.Build.props 與 NuGet 的限制
結論:Directory.Build.props 是原始碼存放庫層級的建置設定,不屬於專案本身,更不屬於 NuGet 套件。
生效範圍
| 場景 | 是否生效 | 說明 |
|---|---|---|
| 本機多專案開發 | ✅ 是 | MSBuild 自動從專案目錄向上遍歷載入 |
| 封裝時編譯 | ✅ 是 | 封裝時編譯階段會應用巨集,裁剪分支 |
| NuGet 套件內部(執行階段) | ❌ 否 | 巨集已在編譯時消失,dll 分支已固定 |
| 上游專案安裝後 | ❌ 否 | NuGet 不包含 Directory.Build.props,無法繼承 |
核心原因
- NuGet 本質是編譯產物:包含 dll + 中繼資料,不包含建置設定
- 巨集是編譯時概念:
#if在編譯時已完成分支裁剪,執行階段不再存在 - Directory.Build.props 不封裝:只在本機存放庫結構中生效
7.2 NuGet 散佈的替代方案
方案 A:使用 NuGet build props(向下游傳遞巨集)
如果確實需要透過 NuGet 向下游專案傳遞巨集定義,可以在函式庫專案中新增 build/ 資料夾:
<!-- build/YourPackageName.props -->
<Project>
<PropertyGroup>
<!-- 基於 RuntimeIdentifier 自動定義巨集 -->
<DefineConstants Condition="'$(RuntimeIdentifier)'=='win-x86'">
$(DefineConstants);PLATFORM_WIN_X86
</DefineConstants>
<DefineConstants Condition="'$(RuntimeIdentifier)'=='win-x64'">
$(DefineConstants);PLATFORM_WIN_X64
</DefineConstants>
<DefineConstants Condition="'$(RuntimeIdentifier)'=='linux-x64'">
$(DefineConstants);PLATFORM_LINUX_X64
</DefineConstants>
<DefineConstants Condition="'$(RuntimeIdentifier)'=='linux-arm64'">
$(DefineConstants);PLATFORM_LINUX_ARM64
</DefineConstants>
</PropertyGroup>
</Project>
然後在函式庫專案的 csproj 中設定封裝:
<ItemGroup>
<None Include="build\**" Pack="true" PackagePath="build\" />
</ItemGroup>
注意:這只能讓下游專案獲得巨集定義,但無法改變已封裝函式庫內部的程式碼分支(因為函式庫 dll 已在編譯時固定)。
方案 B:函式庫內部不使用條件編譯巨集(推薦)
最穩妥的 NuGet 散佈方案是避免在函式庫程式碼中使用條件編譯巨集,改用方案四(僅函式庫名稱)或執行階段判斷:
// 推薦:函式庫內使用執行階段判斷或僅函式庫名稱方式
public static class TimeMeaningApi
{
const string DLL = "Lib/TimeMeaning"; // 不加副檔名
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr GetTimeMeaning(int timestampSecond);
// ...
}
方案 C:按 RID 分別封裝 NuGet
為每個平台單獨封裝 NuGet,使用不同的套件 ID 或版本:
YourLibrary.win-x64YourLibrary.linux-x64- 等等
不過這會增加維護成本,通常不推薦。
7.3 最終建議
- 同一存放庫內多專案:可以使用
Directory.Build.props+ 發佈指令碼(情況 2) - 需要 NuGet 散佈:強烈推薦方案四(僅函式庫名稱,不依賴條件編譯巨集)
- 必須用條件編譯 + NuGet:考慮 NuGet build props + 執行階段判斷結合
8. 常見問題 Q&A
Q1: Linux 下為什麼要去掉 lib 前置詞?
A: 分兩種情況:
- 函式庫在根目錄:
DllImport("TimeMeaning")在 Linux 下會尋找TimeMeaning、TimeMeaning.so、libTimeMeaning.so,無需去掉前置詞 - 函式庫在子目錄:
DllImport("Lib/TimeMeaning")在 Linux 下僅會尋找Lib/TimeMeaning、Lib/TimeMeaning.so,不會尋找Lib/libTimeMeaning.so**,因此需要透過 csproj 的機制將libTimeMeaning.so複製為TimeMeaning.so`
Q2: 函式庫檔案必須和可執行檔在同一目錄嗎?
A: 不需要!可以放在子目錄(如 Lib/),只需要在 DllImport 中指定子目錄路徑,如 DllImport("Lib/TimeMeaning")。注意使用子目錄時 Linux 不會尋找帶 lib 前置詞的函式庫。
Q3: CallingConvention 是什麼意思?
A: 定義了函式呼叫時參數傳遞和堆疊清理的方式。常見的有:
Cdecl:C 語言預設慣例,呼叫者清理堆疊(Linux 常用)StdCall:Windows API 常用,被呼叫者清理堆疊Winapi:平台預設(Windows 下是 StdCall,Linux 下是 Cdecl)
Q4: macOS 支援嗎?
A: 支援!macOS 使用 .dylib 後綴,同樣可以用 DllImport("Lib/TimeMeaning"),系統會自動尋找 Lib/TimeMeaning.dylib。csproj 設定中增加 osx-x64 和 osx-arm64 的設定即可。
Q5: 偵錯時如何知道函式庫檔案是否正確載入?
A: 可以透過以下方式:
- 檢查輸出目錄的
Lib/子目錄是否有正確的函式庫檔案 - 使用 Process Monitor(Windows)或 lsof(Linux)監視函式庫檔案載入
- 在程式碼中呼叫
NativeLibrary.TryLoad測試載入是否成功
以上內容基於實際 Demo 專案整理,包含 C++ 函式庫程式碼及四大方案的完整程式碼範例,如有錯誤或更好的方案,歡迎在留言區留言指正!
開源專案網址:https://github.com/dotnet9/DotnetCrossPlatformNativeLibrary