NETクロスプラットフォームネイティブライブラリの実装

NETクロスプラットフォームネイティブライブラリの実装

NETプロジェクトがサードパーティ製のネイティブライブラリをエレガントに導入し、Windows、Linuxマルチプラットフォームをサポートする方法、ピット回避ガイド

最后更新 2026/04/20 23:18
沙漠尽头的狼
预计阅读 16 分钟
分类
.NET
标签
.NET C# クロスプラットフォーム Native Library

NET開発では、ハードウェアSDK、暗号化ライブラリ、基盤となる通信コンポーネントなど、サードパーティが提供するネイティブ·ライブラリを呼び出す必要があることがあります。この記事では、クロスプラットフォームのネイティブライブラリを導入する際の2つの主要なシナリオとピット回避の経験を実際のデモプロジェクトで共有します。

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. 2つのプログラム概要

ローカルライブラリは主に2つのプログラムに分かれています **:

  1. 动态加载:使用 NativeLibrary API 运行时手动加载
  2. 静态加载:使用 DllImport 特性声明(做了3种情况测试)

VC-LTLおよびYYY-Thunksすべてのサンプルに共通

4つのサンプルプログラムすべてに以下の2つのNuGetパッケージが導入され、現在テストされているサンプルはすべてWindows 7以降と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の互換性レイヤーを提供し、最新のコードをWin 7/XPでも動作させる(XP未テスト記事の例)

3. シナリオ1:動的負荷(成功)

动态加载是最灵活的方式,使用 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. シナリオ2:静的ロード

静态加载使用 DllImport 特性声明,这是.NET中调用本地库的标准方式。我们做了3种情况测试:主要是测试三方库封装代码是直接放在主工程,还是提取出来通过NuGet分发时,使用条件编译宏是否可行、路径灵活度如何。

条件付きコンパイルマクロの2つの定義

使用条件编译宏前,需要先定义 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パッケージとしてパッケージ化し、他のプロジェクトに配布すると、次の問題が発生します。

  1. Directory.Build.props 不会被打包:NuGet 包中不会包含仓库根目录的 Directory.Build.props,上游用户无法继承宏定义
  2. ** マクロはコンパイル時に固定**:パッケージ化時に、コードはその時点のマクロ条件に従って分岐トリミングを完了し、結果のdllは上流マクロの影響を受けなくなりました。
  3. 上游无法改变行为:安装 NuGet 包的项目,即使定义了自己的 PLATFORM_XXX 宏,也无法改变已打包库的代码分支

** ソリューション **:NuGet経由でクロスプラットフォームライブラリを配布するには、シナリオ4(複数のプロジェクト+ライブラリ名のみ)またはNuGetビルドプロップメカニズムを使用することをお勧めします(下記の補足説明を参照)。


ケース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>

動作の原理

  • WindowsDllImport("Lib/TimeMeaning") 自动查找 Lib/TimeMeaning.dll
  • LinuxDllImport("Lib/TimeMeaning") 会查找 Lib/TimeMeaningLib/TimeMeaning.so不会查找 Lib/libTimeMeaning.so
  • 所以Linux下需要通过 <Link>Lib\TimeMeaning.so</Link>libTimeMeaning.so 复制为 TimeMeaning.so

結果は

成功:これは最も推奨されるソリューションで、シンプルで信頼性が高く、条件付きコンパイルマクロを定義する手間がかかり、異なるプラットフォーム間のライブラリファイル名の統一性を確保するだけです(Linuxではlibプレフィックスを削除します)。


プログラムの概要比較

カテゴリー:Category プログラムプログラムは 実践の実践 結果は 適用可能なシーン
ダイナミックロード NativeLibrary動的ロード コード内でオペレーティングシステムを手動で判断し、ライブラリをロード プラットフォーム全体が利用可能 ロードパスの柔軟な制御が必要
スタティック·ロード シングルプロジェクト+ 条件付きコンパイル #if PLATFORM_WIN_X64 条件编译 その成功は メインプロジェクトのみ使用し、クラスライブラリをカプセル化せず、異なるパスライブラリをサポート
スタティック·ロード マルチプロジェクト+ 条件付きコンパイル パブリッシュ前にスクリプトを使用したマクロのグローバル設定 * 成功(リリーススクリプトで) プラットフォームによって異なるライブラリパスが必要です。
スタティック·ロード マルチエンジニアリング+ライブラリ名のみ拡張子なし クラスライブラリはライブラリ名+ csproj 条件付きコピーのみを書き込みます(Linuxはlibプレフィックスを削除) * ** 完全なクロスプラットフォーム ** ** 最も推奨 **、シンプルで信頼性が高く、ライブラリファイル名を統一したい

6. コア体験

  1. ** DllImport定数ライブラリ名(拡張子なし)を推奨します ** これは、シンプルさに焦点を当てた最も安定した信頼性の高いスキームです。動的負荷も可能ですが、使用には少し面倒です。
  2. ** 静的ロード条件付きコンパイルマクロを使用すると、異なるライブラリ名を処理できます **、単一プロジェクトとマルチプロジェクト(マルチプロジェクトにはパブリッシュスクリプトのグローバル設定マクロが必要です)
  3. 多工程场景下宏不继承的问题可以通过发布脚本解决:使用 publish.bat + SetPlatformMacro.ps1 在发布前修改全局宏
  4. ** クラスライブラリのコンパイル時にRuntimeIdentifierに依存しないでください ** クラスライブラリはRuntimeIdentifierコンテキストなしでコンパイルされ、条件付きコンパイルマクロが動作しない可能性があります。
  5. Linux 下注意去掉 lib 前缀,通过 csproj 的 <Link> 机制重命名
  6. ** Windows 7のサポートが必要な場合 ** VC-LTLおよびYY-Thanks NuGetパッケージをインストールします。
  7. ** ライブラリファイルはLibサブディレクトリに置くことができます ** ルートディレクトリではありません。
  8. ** 重要:Directory. Build.propsグローバルマクロはNuGetディストリビューションをサポートしていません **:条件付きコンパイルマクロを使用するクラスライブラリをNuGetとしてパッケージ化すると、上流プロジェクトはマクロを完全に継承せず、NuGetパッケージの内部コードもパッケージ化時にコンパイルブランチを固定します。NuGet経由で配布する場合は、シナリオ4(ライブラリ名のみ、条件付きマクロコンパイルなし)を推奨します。

** 補足事項 **:初心者の場合は、ケース3(マルチプロジェクト+ライブラリ名のみ)をマスターするのが最善であり、最も安全で理解しやすい方法です。パスの違いを柔軟に処理する必要がある場合は、動的ロードまたはケース2を検討してください。

7. 補足:NuGetパッケージと条件付きコンパイルマクロ

7.1 Direct.build.propsとNuGetの制限

结论Directory.Build.props 是源码仓库级别的构建配置,不属于项目本身,更不属于 NuGet 包。

発効の範囲

シーンはこちら 有効かどうか 説明書の作成
マルチエンジニアリング開発のローカル ↓は、 MSBuildはプロジェクトディレクトリから自動的にロードする
パッケージ化時のコンパイル ↓は、 パッケージ化時のコンパイル段階でマクロが適用され、分岐がトリミングされる
NuGetパッケージ内実行時 いいえ コンパイル時にマクロが消え、dllブランチが固定されました
アップストリームプロジェクトインストール後 いいえ NuGet 不包含 Directory.Build.props,无法继承

主な理由

  1. **NuGetは本質的にコンパイル済み **:dll +メタデータを含み、ビルド設定は含まれません。
  2. 宏是编译时概念#if 在编译时已完成分支裁剪,运行时不再存在
  3. **Directory. Build.propsはパッケージ化されません **:ローカルリポジトリ構造でのみ有効

7.2 NuGetディストリビューションの代替方法

シナリオA:NuGetビルドプロップを使用する(マクロを下流に渡す)

如果确实需要通过 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配布シナリオは、ライブラリコードで条件付きコンパイルマクロを使用せず、代わりにシナリオ4(ライブラリ名のみ)またはランタイム判定を使用することです。

// 推荐:库内使用运行时判断或仅库名方式
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-x64
  • YourLibrary.linux-x64
  • 待って待って

しかし、これはメンテナンスコストを増加させ、通常は推奨されません。

7.3最終的な勧告

  • 同一仓库内多工程:可以使用 Directory.Build.props + 发布脚本(情况2)
  • ** NuGetディストリビューションが必要 **:シナリオ4を強く推奨(ライブラリ名のみ、条件付きマクロコンパイルなし)
  • ** 条件付きコンパイル+ NuGetでなければならない **:NuGetビルドプロップ+ランタイム判断の組み合わせを検討する

8. よくあるご質問Q&A

Q 1:なぜLinuxでlibプレフィックスを削除するのですか?

**A ** 2つのに分ける。

  • 库在根目录DllImport("TimeMeaning") 在 Linux 下会查找 TimeMeaningTimeMeaning.solibTimeMeaning.so,无需去掉前缀
  • 库在子目录DllImport("Lib/TimeMeaning") 在 Linux 下仅会查找 Lib/TimeMeaningLib/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-x64osx-arm64 的配置即可。

Q5デバッグ時にライブラリファイルが正しくロードされているかどうかを確認するにはどうすればよいですか

**A ** は次の方法で使用できます。

  1. 检查输出目录的 Lib/ 子目录是否有正确的库文件
  2. Process Monitor Windowsまたはlsof Linuxを使用したライブラリファイルのロードの監視
  3. 在代码中调用 NativeLibrary.TryLoad 测试加载是否成功

上記のコンテンツは、C++ライブラリコードと4つのソリューションの完全なコード例を含む実際のデモプロジェクトに基づいています。エラーやより良いソリューションがある場合は、コメントエリアにコメントを残してください!

オープンソースプロジェクトのアドレスhttps//github.com/dotnet9/DotnetCrossPlatformNativeLibrary

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读
同分类 / 同标签 2026/02/07

AOTの使用経験

プロジェクトの最初から、新しい機能が追加されたり、新しい構文が使用されたりするたびに、AOTリリーステストを行うという良い習慣を身につける必要があります。

继续阅读
同分类 / 同标签 2026/01/11

Avalonia ClipboardとDataGridの問題点

Avaloniaデスクトップソフトウェアの最近の開発で解決された2つの問題を文書化します:クリップボードのコピーのクラッシュ、タブの切り替えDataGridのキートン、原因の分析と解決策

继续阅读