0Pricing
C# Academy · レッスン

LibraryImportとソース生成P/Invoke

[LibraryImport](C# 11以降)を使い、DllImportを上回る、AOT対応のソース生成マーシャリングを利用します。

「LibraryImportとソース生成P/Invoke」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。

LibraryImportを使う理由

[LibraryImport]は、[DllImport]に代わる、ソース生成に対応したAOT互換の仕組みとして.NET 7(C# 11)で導入されました。従来のDllImportはリフレクションを介した実行時マーシャリングに依存するため、Native AOTでは問題になることがあります。LibraryImportはマーシャリングに必要なすべてのコードをコンパイル時に生成します。

LibraryImportメソッドの宣言

static partialメソッドに[LibraryImport]を付けます。ソースジェネレーターが実装を補完します。さらに、メソッドを含むクラスにもpartialを付ける必要があります。

using System.Runtime.InteropServices;

internal static partial class NativeMethods
{
    // Source generator creates the P/Invoke body at compile time
    [LibraryImport("mylib", EntryPoint = "add_integers")]
    internal static partial int AddIntegers(int a, int b);

    // String marshaling must be explicit in LibraryImport
    [LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
    internal static partial int ProcessString(string text);
}

LibraryImportでの文字列マーシャリング

DllImportとは異なり、LibraryImportでは文字列のマーシャリング方法を明示的に指定する必要があります。StringMarshalling列挙型またはMarshalAs属性を使用できるため、マーシャリングのコストを把握して制御できます。

// Option 1: StringMarshalling enum (all strings in method)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16)]
static partial bool CreateDirectory(string lpPathName, nint lpSecurityAttributes);

// Option 2: Per-parameter MarshalAs
[LibraryImport("libc")]
static partial int Open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string path,
    int flags);

// Option 3: MarshalUsing for custom marshalers
[LibraryImport("mylib")]
static partial void Process(
    [MarshalUsing(typeof(Utf8StringMarshaller))] string name);

構造体のマーシャリング

構造体では、[NativeMarshalling]を追加して、マネージド型をネイティブ表現にどのように対応付けるかを定義します。ソースジェネレーターはマーシャラー型を使用して、安全でアロケーションを最小限に抑えたコードを生成します。

[NativeMarshalling(typeof(PointMarshaller))]
public struct Point
{
    public int X;
    public int Y;
}

[CustomMarshaller(typeof(Point), MarshalMode.Default, typeof(PointMarshaller))]
public static class PointMarshaller
{
    public static Point ConvertToManaged(NativePoint native)
        => new Point { X = native.X, Y = native.Y };
    public static NativePoint ConvertToUnmanaged(Point managed)
        => new NativePoint { X = managed.X, Y = managed.Y };

    [StructLayout(LayoutKind.Sequential)]
    public struct NativePoint { public int X, Y; }
}

DllImportとLibraryImportの比較

DllImportは実行時に解釈されるため、起動が遅く、リフレクションベースで、AOTのトリミングと互換性がありません。LibraryImportはビルド時に最適化されたC#コードを生成するため、実行時のリフレクションが不要で、トリミングに安全であり、ベンチマークでも測定可能な高速化が確認できます。

// Old — DllImport (still works, but avoid for new AOT code)
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool MoveFile(string src, string dst);

// New — LibraryImport (AOT-safe, source-generated)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool MoveFile(string src, string dst);
// The compiler generates the actual P/Invoke wrapper body

SetLastErrorとエラー処理

[LibraryImport]でSetLastError = trueを設定し、OSのエラーコードを取得できるようにします。呼び出し後にMarshal.GetLastPInvokeError()(推奨)またはMarshal.GetLastWin32Error()を使用してエラーコードを取得します。

[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool CreateDirectory(string path, nint secAttr);

bool ok = CreateDirectory(@"C:\Temp\NewDir", 0);
if (!ok)
{
    int err = Marshal.GetLastPInvokeError();
    // err is ERROR_ALREADY_EXISTS (183) if folder exists
    throw new Win32Exception(err);
}

Span<T>とメモリのマーシャリング

ソース生成されるP/Invokeの利点の1つは、Span<T>を第一級にサポートしていることです。ReadOnlySpan<byte>を渡すと、配列を使うDllImportで必要になるピン留めやアロケーションを回避できます。

[LibraryImport("mylib")]
static partial int ProcessBuffer(
    ReadOnlySpan<byte> data,
    int length);

// Usage — no fixed or GCHandle needed
byte[] buffer = Encoding.UTF8.GetBytes("hello");
int result = ProcessBuffer(buffer, buffer.Length);

// For output buffers use Span<byte>
[LibraryImport("mylib")]
static partial int FillBuffer(Span<byte> output, int maxLen);

ソース生成の有効化

.NET 7以降を対象とするプロジェクトでSystem.Runtime.InteropServices名前空間を参照すると、ソース生成は自動的に有効になります。古いターゲットでは、Microsoft.Interop.SourceGenerationアナライザーパッケージが必要です。

<!-- In your .csproj — no extra package needed on .NET 7+ -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <!-- Enable Roslyn analyzers and source generators -->
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <!-- AllowUnsafeBlocks may be needed for some marshalers -->
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

生成されたコードの確認

csprojに<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>を追加すると、生成されたファイルがobj/に書き込まれます。これにより、ソースジェネレーターが実際に何を生成するのかを詳しく確認できます。学習に最適な方法です。

// The generator emits something like this for a LibraryImport method:
// (simplified view of generated stub)

internal static partial int AddIntegers(int a, int b)
{
    // Direct call — no reflection, no boxing, fully inlineable
    return __PInvoke(a, b);

    [System.Runtime.InteropServices.DllImportAttribute(
        "mylib",
        EntryPoint = "add_integers",
        ExactSpelling = true)]
    static extern int __PInvoke(int a, int b);
}

実践:Cライブラリのラッパー作成

よく使われるパターンは、すべてのLibraryImport宣言を含む静的ラッパークラスを定義し、その上に高レベルで安全なAPIを公開する方法です。partial宣言はinternalまたはprivateにして、公開するのは安全なラッパーだけにします。

internal static partial class LibSodiumNative
{
    private const string LibName = "libsodium";

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_keybytes")]
    internal static partial nuint KeyBytes();

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_easy")]
    internal static partial int SecretBoxEasy(
        Span<byte> ciphertext,
        ReadOnlySpan<byte> message,
        ulong mlen,
        ReadOnlySpan<byte> nonce,
        ReadOnlySpan<byte> key);
}

// Public safe wrapper hides the native signature
public static byte[] Encrypt(byte[] message, byte[] key, byte[] nonce)
{
    var cipher = new byte[message.Length + 16];
    LibSodiumNative.SecretBoxEasy(cipher, message, (ulong)message.Length, nonce, key);
    return cipher;
}

理解度チェック

[LibraryImport]が[DllImport]より優れている主な点は何ですか?

振り返り:LibraryImportとソース生成P/Invoke

主なポイント:

  • [LibraryImport]は、AOTに安全なP/Invokeのために[DllImport]に代わるものです
  • マーシャリングはコンパイル時に生成されるため、実行時のリフレクションが不要です
  • static partialメソッドとpartialクラスが必要です
  • 文字列のマーシャリングはStringMarshallingまたはMarshalAsを使って明示的に指定する必要があります
  • ピン留めのオーバーヘッドなしでSpan<T>を第一級にサポートします
  • EmitCompilerGeneratedFilesを使って生成されたコードを確認できます

よくある質問

「LibraryImportとソース生成P/Invoke」レッスンは無料ですか?

はい。「LibraryImportとソース生成P/Invoke」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。

「LibraryImportとソース生成P/Invoke」で何を学びますか?

[LibraryImport](C# 11以降)を使い、DllImportを上回る、AOT対応のソース生成マーシャリングを利用します。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

C# Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「LibraryImportとソース生成P/Invoke」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このC# Academyレッスンでコードを書いて実行できますか?

はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. P/Invokeの基礎
  2. LibraryImportとソース生成P/Invoke
  3. 安全でないコード、ポインター、固定バッファー
  4. COM 相互運用とランタイム呼び出し可能ラッパー
← C# Academyに戻る