-
Notifications
You must be signed in to change notification settings - Fork 0
MS_UnmanagedCodeCrossPlatform
-
Windows で使用できていた P/Invoke は、
.NET Core、Linux でも利用可能である模様。 -
それでは、アンマネージドコードを P/Invoke して、
どのように .NET Core をクロスプラットフォーム対応させるのか?
補足(P/Invoke は Linux でも動く ── 仕組み): 原文の「利用可能である模様」
は正しい。なぜ動くのかを補っておく。【P/Invoke の実体】 ① 指定された名前のネイティブ ライブラリを探してロードする ② その中の関数のアドレスを取得する ③ マネージド/アンマネージドの境界で引数を変換して呼ぶ 【OS ごとの差は ①② だけ】★ Windows: LoadLibrary / GetProcAddress → xxx.dll Linux : dlopen / dlsym → libxxx.so macOS : dlopen / dlsym → libxxx.dylib → CLR が【各 OS の API を呼び分ける】ので、 C# 側のコードは同じでよいライブラリ名の解決規則が重要である。
[DllImport("mylib")] // ← 拡張子・接頭辞を書かない ★ static extern int Foo();【.NET が試す名前】 Windows: mylib.dll Linux : libmylib.so, mylib.so, libmylib, mylib macOS : libmylib.dylib, mylib.dylib, … → 【拡張子を書くと移植性が落ちる】 → "mylib.dll" と書くと、Linux でも "mylib.dll" を探しに行く (見つからず DllNotFoundException)より柔軟に解決したい場合(.NET Core 3.0 以降):
// 実行時に解決方法を差し込める NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, asm, path) => name == "mylib" && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? NativeLibrary.Load("/opt/myapp/libmylib.so.1") : IntPtr.Zero);
-
先ず、DLL や SO などのアンマネージ・ライブラリを作成する。
-
次いで、上記のアンマネージ・ライブラリ、若しくは、
Win32 API や syscall, libc, etc. を呼出す、P/Invoke ライブラリを作成する。 -
ヤルとすれば、こんな感じか。
補足(名前空間で分ける案は妥当): 「
Win32/POSIXという名前空間に
分割する」という原文の案は、設計として正しい。【なぜ分けるのか】 ・P/Invoke の宣言は【OS 依存そのもの】である → シグネチャ、構造体のレイアウト、エラーコードが違う ・混在させると、どちらの OS 用か分からなくなる ・【片方の OS でしかテストできないコード】が 共通の名前空間に紛れる 【分けると】 ・「この名前空間は Windows 専用」と明示できる ・条件付きコンパイルやプラットフォーム属性を付けやすい現在は属性で明示できる(.NET 5 以降)。
[SupportedOSPlatform("windows")] // ← アナライザーが警告してくれる ★ internal static class Win32 { [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool Beep(uint dwFreq, uint dwDuration); } [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] internal static class Posix { [DllImport("libc", SetLastError = true)] internal static extern int chmod(string path, int mode); }【SupportedOSPlatform の価値】★ ・対応外の OS 向けビルドで使うと【コンパイル時に警告】が出る ・OperatingSystem.IsWindows() で囲むと警告が消える → 分岐漏れを静的に検出できる
libcを直接呼ぶ際の注意:・libc の関数は【glibc / musl で挙動やシンボルが違う】ことがある → Alpine Linux(musl)で動かない、という事故が起きる ★ ・構造体のレイアウトは【32bit / 64bit、ABI で変わる】 ・errno の取得は SetLastError = true + Marshal.GetLastWin32Error() (名前は Win32 だが、Linux でも errno が取れる) ・可能なら【.NET の標準 API で済ませる】 → File.SetUnixFileMode(.NET 7 以降)等、 P/Invoke なしで扱える範囲が広がっている ★
- ラッパー・ライブラリから、P/Invoke ライブラリを呼び出す。
- ラッパー・ライブラリはマネージドコード上でインターフェイスを定義。
Environment.OSVersion を使用して分岐させる。
移行メモ(
Environment.OSVersionでの分岐は現在は非推奨): この方法は
.NET Framework 期の書き方であり、
現在はより明確な API がある。// ✗ 旧来の方法(PlatformID は Unix と MacOSX の区別が曖昧等の問題がある) if (Environment.OSVersion.Platform == PlatformID.Win32NT) { } // ○ .NET Core 以降(推奨) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { } // ◎ .NET 5 以降(最も簡潔。アナライザーとも連動する)★ if (OperatingSystem.IsWindows()) { } else if (OperatingSystem.IsLinux()) { } else if (OperatingSystem.IsMacOS()) { } // 版まで判定する if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) { }【OperatingSystem.IsXxx() が優れている点】★ ・[SupportedOSPlatform] 属性のアナライザーが 【この分岐を理解する】 → 分岐の中でだけ Windows 専用 API を呼べば警告が消える ・トリミング時に【不要な分岐を削れる】 → Native AOT で有利【Environment.OSVersion の落とし穴】 ・Windows では【互換性マニフェストがないと古い版を返す】ことがある ・Platform が Unix になる OS が複数ある(Linux / macOS / FreeBSD) → 【新規コードでは使わない】
-
ラッパー・ライブラリ内部で、
Environment.OSVersion を使用して分岐させる。 -
ラッパー・ライブラリを Windows、Linux の2系統を作成して、
呼び出し元で Environment.OSVersion を使用して分岐させる。
※ 暗号化ライブラリでは、内部で
CAPI(CSP)、CNG、OpenSSL などのプロバイダが切り替えられるので、基本、
前者のように、ラッパー・ライブラリ内部で分岐させるのが良いと考える。
補足(この判断は正しい ── .NET 自身が同じ設計をしている): 原文の結論
「ラッパー内部で分岐させる」は妥当で、
.NET のランタイム自体がまさにこの設計である。【.NET の暗号化 API の構造】 System.Security.Cryptography.Aes.Create() ← 利用側は OS を意識しない │ ▼ 内部で分岐 Windows → CNG / CAPI(Bcrypt) Linux → OpenSSL macOS → CommonCrypto / SecurityFramework → [.NETの署名・暗号化アルゴリズム](MS_DotNetCryptoAlgorithms) / [OpenSSL](MS_OpenSSL) 参照なぜ内部で分岐させる方が良いのか:
内部で分岐(推奨) 外部で分岐 利用側のコード OS を意識しない(1 本) ★ OS ごとに書く 追加 OS への対応 ラッパーだけ直す 全呼び出し元を直す ★ テスト ラッパーのテストに集約 呼び出し元ごとに要る 依存の向き 利用側 → 抽象 利用側 → 具体(OS) 【これは [依存性反転原則](MS_DependencyInversionPrinciple) そのもの】★ ・利用側は【抽象(インターフェイス)】に依存する ・OS 固有の実装は【抽象の裏側】に隠す → 原文の「ラッパー・ライブラリはマネージドコード上で インターフェイスを定義」という記述が、まさにこれ実装の形:
// 抽象 public interface IFilePermission { void SetReadOnly(string path); } // 実装(内部で分岐、または DI で差し替え) public static class FilePermission { public static void SetReadOnly(string path) { if (OperatingSystem.IsWindows()) Win32.SetFileAttributes(path, /* ... */); else Posix.chmod(path, 0x124 /* 0444 */); } }【もう一段良い設計】 OS 判定すら不要にできないか、先に検討する ★ // .NET 7 以降:標準 API で済む File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.GroupRead); new FileInfo(path).IsReadOnly = true; → 【P/Invoke を書かないのが最善】 → 標準 API で足りるかを必ず確認する
補足(ネイティブ ライブラリの配布): 原文が扱っていない
実務上の難所を補っておく。**「どうやって .so を配るか」**である。【問題】 ・Windows 向けには mylib.dll、Linux 向けには libmylib.so が要る ・さらに【CPU アーキテクチャ】ごとに別(x64 / arm64) ・利用者に「事前に入れておいて」とは言いにくいNuGet の
runtimesフォルダ規約で解決できる。MyPackage.nupkg ├ lib/net8.0/MyWrapper.dll ← マネージドのラッパー └ runtimes/ ├ win-x64/native/mylib.dll ├ win-arm64/native/mylib.dll ├ linux-x64/native/libmylib.so ★ ├ linux-arm64/native/libmylib.so └ osx-arm64/native/libmylib.dylib → dotnet publish -r linux-x64 とすると 【該当する native だけが出力される】 → SkiaSharp、Microsoft.Data.SqlClient 等がこの方式【コンテナでの注意】 ・Alpine(musl)向けは linux-musl-x64 と別 RID になる ★ ・依存する共有ライブラリ(libssl 等)が イメージに入っているかを確認する → 「開発機では動くのにコンテナで DllNotFoundException」 の典型的な原因 ・ldd libmylib.so で依存を確認できる
DllNotFoundExceptionの切り分け:① ファイルは配置されているか(publish 出力を確認) ② 名前は正しいか(lib 接頭辞、拡張子) ③ 依存する共有ライブラリは揃っているか(ldd) ④ アーキテクチャは一致しているか(file コマンド) ⑤ 実行権限はあるか(Linux) 【デバッグ用の環境変数】 DOTNET_EnableWriteXorExecute=0 LD_DEBUG=libs ./myapp ← Linux でロードの過程が見える ★
- ネイティブ ライブラリの読み込み(NativeLibrary)
https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/native-library-loading - プラットフォーム互換性アナライザー
https://learn.microsoft.com/ja-jp/dotnet/standard/analyzers/platform-compat-analyzer - ランタイム識別子(RID)のカタログ
https://learn.microsoft.com/ja-jp/dotnet/core/rid-catalog - ソース生成される P/Invoke(LibraryImport)
https://learn.microsoft.com/ja-jp/dotnet/standard/native-interop/pinvoke-source-generation
Tags: 移行, Windows, プログラミング, .NET開発
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。