Skip to content

MS_UnmanagedCodeCrossPlatform

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

アンマネージドコードのクロスプラットフォーム化

概要

  • 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);

詳細

準備

補足(名前空間で分ける案は妥当): 「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 でロードの過程が見える ★

参考

.NET Core on Linuxのポイント > P/Invoke(DNET_DotNetCoreOnLinux.md

Microsoft Learn


Tags: 移行, Windows, プログラミング, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally