.NET開発時、サードパーティが提供するネイティブライブラリ(ハードウェアSDK、暗号ライブラリ、低レベル通信コンポーネントなど)を呼び出す必要が生じることがあります。この記事では、実際のデモプロジェクトを通じて、クロスプラットフォームネイティブライブラリを導入する際の2つの主要なアプローチと注意点を共有します。
1. プロジェクト準備
デモプロジェクトでは、シンプルな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つのアプローチがあります:
- 動的ロード:
NativeLibraryAPI を使用して実行時に手動でロードする - 静的ロード:
DllImport属性を使用して宣言する(3つのケースをテスト)
VC-LTL と YY-Thunks(全サンプル共通)
4つのサンプルプログラムすべてに以下の2つのNuGetパッケージが含まれています。現在テストしたサンプルはすべてWindows 7以降のWindowsバージョンとLinuxプラットフォームをサポートしています:
Windows 7での動作原理:Microsoft公式の.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. アプローチ1:動的ロード(✅ 成功)
動的ロードは最も柔軟な方法で、NativeLibrary APIを使用して実行時に手動でネイティブライブラリをロードします。この方法の利点は、ライブラリのロードロジックを完全にカスタマイズでき、同じライブラリが異なるディレクトリに保存され、異なるファイル名で使用されるような複雑なシナリオを処理できることです。実行時に条件に応じて異なるバージョンをロードしたり、ライブラリのパスを動的に計算する必要がある場合など、特別な要件がある場合に最適です。
コード実装
using System.Runtime.InteropServices;
namespace csharp.test.dynamic;
internal static class TimeMeaningNative
{
// 現在のOSに応じてライブラリファイル名を判定: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属性は出力ディレクトリ内のライブラリのパスを指定し、コード内のロードパスと一致させる
重要なポイント
動的ロードの流れ:
- 実行時にOSに応じてライブラリファイル名を判断
- 完全なパスを組み立ててライブラリをロード
- エクスポート関数のアドレスを取得
- デリゲートに変換して呼び出し
4. アプローチ2:静的ロード
静的ロードでは DllImport 属性を使用して宣言します。これは.NETでネイティブライブラリを呼び出す標準的な方法です。3つのケースをテストしました。主に、サードパーティライブラリのラッパーコードをメインプロジェクトに直接置くか、NuGetで配布するために別のライブラリに抽出する場合に、条件付きコンパイルマクロが使用可能かどうか、パスの柔軟性がどの程度かを検証しています。
条件付きコンパイルマクロの2つの定義方法
条件付きコンパイルマクロを使用するには、まず PLATFORM_XXX マクロを定義する必要があります。2つの方法があります:
方法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 など)を柔軟に処理できます。これはアプローチ1の動的ロードと似ており、複雑なパスの違いを処理できます。
コード実装
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経由でクロスプラットフォームライブラリを配布する必要がある場合は、アプローチ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>
動作原理
- 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 動的ロード | コード内でOSを手動判定してライブラリをロード | ✅ 全プラットフォーム使用可能 | ロードパスを柔軟に制御する必要がある場合 |
| 静的ロード | 単一プロジェクト + 条件付きコンパイル | #if PLATFORM_WIN_X64 条件付きコンパイル |
✅ 成功 | メインプロジェクトのみで使用し、クラスライブラリにラップしない。異なるパスのライブラリをサポート |
| 静的ロード | マルチプロジェクト + 条件付きコンパイル | 公開前にスクリプトでグローバルにマクロを設定 | ✅ 成功(公開スクリプトと併用) | 推奨。プラットフォームごとにライブラリパスが異なる場合に適する |
| 静的ロード | マルチプロジェクト + ライブラリ名のみ(拡張子なし) | クラスライブラリはライブラリ名のみ記述 + csprojで条件付きコピー(Linuxではlibプレフィックスを除去) | ✅ クロスプラットフォームで完全成功 | 最も推奨。シンプルで信頼性が高く、統一されたライブラリファイル名を希望する場合 |
6. 核となる経験
- DllImport に定数ライブラリ名(拡張子なし)を使用することを推奨。これが最も安定した信頼性の高いアプローチであり、何よりもシンプルで理解しやすい。アプローチ1の動的ロードも可能だが、使用がやや煩雑。
- 静的ロードで条件付きコンパイルマクロを使用すると、ライブラリ名が異なる場合を処理できる。単一プロジェクトおよびマルチプロジェクト(マルチプロジェクトの場合は公開スクリプトでグローバルにマクロを設定する必要あり)に適用可能。
- マルチプロジェクトシナリオでのマクロ継承問題は公開スクリプトで解決可能:
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経由で配布する場合は、アプローチ4(ライブラリ名のみ、条件付きコンパイルマクロに依存しない)を推奨。
補足説明:初心者には、まずケース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 ビルドプロパティを使用(下流にマクロを渡す)
どうしても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-x64YourLibrary.linux-x64- など
ただし、これにより保守コストが増加するため、通常は推奨しません。
7.3 最終推奨
- 同一リポジトリ内のマルチプロジェクト:
Directory.Build.props+ 公開スクリプト(ケース2)を使用可能 - NuGet 配布が必要な場合:アプローチ4(ライブラリ名のみ、条件付きコンパイルマクロに依存しない)を強く推奨
- どうしても条件付きコンパイル + NuGet が必要な場合:NuGetビルドプロパティ + 実行時判断の組み合わせを検討
8. よくある質問 Q&A
Q1: Linux ではなぜ lib プレフィックスを削除する必要があるのですか?
A: 2つのケースがあります:
- ライブラリがルートディレクトリにある場合:
DllImport("TimeMeaning")は Linux でTimeMeaning、TimeMeaning.so、libTimeMeaning.soを検索します。プレフィックスの削除は不要です。 - ライブラリがサブディレクトリにある場合:
DllImport("Lib/TimeMeaning")は Linux でLib/TimeMeaning、Lib/TimeMeaning.soのみを検索し、Lib/libTimeMeaning.soは検索しません。そのため、csproj の<Link>メカニズムを使用して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を呼び出してロードが成功するかテストする
上記の内容は実際のデモプロジェクトに基づいてまとめられています。C++ライブラリコードと4つのアプローチの完全なコードサンプルが含まれています。誤りやより良いアプローチがあれば、コメント欄でご指摘ください!
オープンソースプロジェクトのアドレス:https://github.com/dotnet9/DotnetCrossPlatformNativeLibrary