次の方法で共有


チュートリアル: ソース生成 P/Invoke でカスタム マーシャラーを使用する

このチュートリアルでは、マーシャラーを実装し、ソース生成 P/Invokesカスタム マーシャリングに使用する方法について説明します。

組み込み型のマーシャラーを実装し、特定のパラメーターとユーザー定義型のマーシャリングをカスタマイズし、ユーザー定義型の既定のマーシャリングを指定します。

このチュートリアルで使用されるすべてのソース コードは、 dotnet/samples リポジトリで入手できます。

LibraryImport ソース ジェネレーターの概要

System.Runtime.InteropServices.LibraryImportAttribute型は、.NET 7 で導入されたソース ジェネレーターのユーザー エントリ ポイントです。 このソース ジェネレーターは、実行時ではなくコンパイル時にすべてのマーシャリング コードを生成するように設計されています。 エントリ ポイントはこれまで DllImportを使用して指定されてきましたが、そのアプローチには、常に許容できない可能性があるコストが付属しています。詳細については、「 P/Invoke source generation」を参照してください。 LibraryImport ソース ジェネレーターは、すべてのマーシャリング コードを生成し、DllImportに組み込まれている実行時生成要件を削除できます。

ランタイムとユーザーが独自の型に合わせてカスタマイズするために、生成されたマーシャリング コードに必要な詳細を表現するには、いくつかの型が必要です。 このチュートリアルでは、次の種類を使用します。

  • MarshalUsingAttribute – 使用サイトでソース ジェネレーターによって検索され、属性付き変数をマーシャリングするためのマーシャラーの種類を決定するために使用される属性。

  • CustomMarshallerAttribute – 型のマーシャラーを示すために使用される属性と、マーシャリング操作を実行するモード (たとえば、マネージドからアンマネージドへの by-ref)。

  • NativeMarshallingAttribute 属性付き型に使用するマーシャラーを示すために使用される属性。 これは、型を提供するライブラリ作成者や、それらの型に付随するマーシャラーに役立ちます。

ただし、これらの属性は、カスタム マーシャラー作成者が使用できる唯一のメカニズムではありません。 ソース ジェネレーターはマーシャラー自体を調べて、マーシャリングの発生方法を通知する他のさまざまな兆候がないか調べます。

設計の詳細については、 dotnet/runtime リポジトリを参照してください。

ソース ジェネレーター アナライザーと Fixer

ソース ジェネレーター自体と共に、アナライザーと Fixer の両方が提供されます。 アナライザーと Fixer は有効になっており、.NET 7 RC1 以降では既定で使用できます。 アナライザーは、開発者がソース ジェネレーターを適切に使用できるように設計されています。 fixer は、多くの DllImport パターンから適切な LibraryImport シグネチャへの自動変換を提供します。

ネイティブ ライブラリの概要

LibraryImport ソース ジェネレーターを使用すると、ネイティブまたはアンマネージド ライブラリを使用することになります。 ネイティブ ライブラリは、.NET 経由で公開されていないオペレーティング システム API を直接呼び出す共有ライブラリ (つまり、 .dll.so、または dylib) である可能性があります。 ライブラリは、.NET 開発者が使用するアンマネージ言語で大幅に最適化されたライブラリである場合もあります。 このチュートリアルでは、C スタイルの API サーフェスを公開する独自の共有ライブラリを構築します。 次のコードは、C# から使用するユーザー定義型と 2 つの API を表します。 これら 2 つの API は "in" モードを表しますが、サンプルで探索する追加のモードがあります。

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);

上記のコードには、 char32_t*error_dataの 2 種類の対象が含まれています。 char32_t* は UTF-32 でエンコードされた文字列を表します。これは、.NET が歴史的にマーシャリングする文字列エンコードではありません。 error_data は、32 ビット整数フィールド、C++ ブール型フィールド、および UTF-32 エンコード文字列フィールドを含むユーザー定義型です。 どちらの型でも、ソース ジェネレーターがマーシャリング コードを生成する方法を提供する必要があります。

組み込み型のマーシャリングをカスタマイズする

この型のマーシャリングはユーザー定義型で必要であるため、最初に char32_t* 型を検討してください。 char32_t* はネイティブ側を表しますが、マネージド コードでの表現も必要です。 .NET では、"string" 型は 1 つだけ string。 そのため、マネージド コードの string 型との間でネイティブ UTF-32 エンコード文字列をマーシャリングします。 UTF-8、UTF-16、ANSI、さらには Windows BSTR 型としてマーシャリングするstring型には、既にいくつかの組み込みマーシャラーがあります。 ただし、UTF-32 としてのマーシャリング用のファイルはありません。 定義する必要があります。

Utf32StringMarshaller型は、ソース ジェネレーターに対する処理を記述するCustomMarshaller属性でマークされます。 属性の最初の型引数は string 型、マーシャリングするマネージド型、2 つ目はマーシャラーを使用するタイミングを示すモード、3 番目の型はマーシャリングに使用する型 Utf32StringMarshallerです。 CustomMarshallerを複数回適用して、モードと、そのモードに使用するマーシャラーの種類をさらに指定できます。

現在の例では、何らかの入力を受け取り、マーシャリングされた形式でデータを返す "ステートレス" マーシャラーを示しています。 Freeメソッドはアンマネージド マーシャリングの対称性のために存在し、ガベージ コレクターはマネージド マーシャラーの "空き" 操作です。 実装者は、入力を出力にマーシャリングするために必要な操作を自由に実行できますが、ソース ジェネレーターによって状態が明示的に保持されないことに注意してください。

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    internal static unsafe class Utf32StringMarshaller
    {
        public static uint* ConvertToUnmanaged(string? managed)
            => throw new NotImplementedException();

        public static string? ConvertToManaged(uint* unmanaged)
            => throw new NotImplementedException();

        public static void Free(uint* unmanaged)
            => throw new NotImplementedException();
    }
}

この特定のマーシャラーが string から char32_t* への変換を実行する方法の詳細については、サンプルを参照してください。 任意の .NET API を使用できることに注意してください (たとえば、 Encoding.UTF32)。

状態が望ましいケースを考えてみましょう。 追加の CustomMarshaller を確認し、より具体的なモード ( MarshalMode.ManagedToUnmanagedIn) に注意してください。 この特殊なマーシャラーは "ステートフル" として実装され、相互運用呼び出し全体で状態を格納できます。 より特殊化と状態により、モードの最適化と調整されたマーシャリングが可能になります。 たとえば、マーシャリング中に明示的な割り当てを回避できるスタック割り当てバッファーを提供するようにソース ジェネレーターに指示できます。 スタック割り当てバッファーのサポートを示すために、マーシャラーは、BufferSize プロパティと、unmanaged型のSpanを受け取るFromManaged メソッドを実装します。 BufferSize プロパティは、マーシャラーがマーシャリング呼び出し中に取得するスタック領域 (FromManagedに渡されるSpanの長さ) の量を示します。

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    [CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
    internal static unsafe class Utf32StringMarshaller
    {
        //
        // Stateless functions removed
        //

        public ref struct ManagedToUnmanagedIn
        {
            public static int BufferSize => 0x100;

            private uint* _unmanagedValue;
            private bool _allocated; // Used stack alloc or allocated other memory

            public void FromManaged(string? managed, Span<byte> buffer)
                => throw new NotImplementedException();

            public uint* ToUnmanaged()
                => throw new NotImplementedException();

            public void Free()
                => throw new NotImplementedException();
        }
    }
}

UTF-32 文字列マーシャラーを使用して、2 つのネイティブ関数の最初の関数を呼び出すようになりました。 次の宣言では、DllImportと同様にLibraryImport属性を使用しますが、ネイティブ関数を呼び出すときに使用するマーシャラーをソース ジェネレーターに指示するには、MarshalUsing属性に依存します。 ステートレス マーシャラーまたはステートフル マーシャラーを使用する必要があるかどうかを明確にする必要はありません。 これは、マーシャラーのCustomMarshaller属性にMarshalModeを定義する実装者によって処理されます。 ソース ジェネレーターは、 MarshalUsing が適用されるコンテキストに基づいて最も適切なマーシャラーを選択し、フォールバック MarshalMode.Default します。

// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

ユーザー定義型のマーシャリングをカスタマイズする

ユーザー定義型をマーシャリングするには、マーシャリング ロジックだけでなく、マーシャリングの対象となる C# の型も定義する必要があります。 マーシャリングしようとしているネイティブ型を思い出してください。

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

次に、C# での理想的な外観を定義します。 intは、最新の C++ と .NET の両方で同じサイズです。 boolは、.NET のブール値の標準的な例です。 Utf32StringMarshaller上に構築すると、char32_t*を .NET stringとしてマーシャリングできます。 .NET スタイルを説明すると、C# の結果は次の定義になります。

struct ErrorData
{
    public int Code;
    public bool IsFatalError;
    public string? Message;
}

名前付けパターンに従って、マーシャラー ErrorDataMarshallerに名前を付けます。 MarshalMode.Defaultのマーシャラーを指定する代わりに、一部のモードのマーシャラーのみを定義します。 この場合、マーシャラーが指定されていないモードに使用されている場合、ソース ジェネレーターは失敗します。 最初に、"in" 方向のマーシャラーを定義します。 マーシャラー自体は static 関数のみで構成されているため、これは "ステートレス" マーシャラーです。

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    internal static unsafe class ErrorDataMarshaller
    {
        // Unmanaged representation of ErrorData.
        // Should mimic the unmanaged error_data type at a binary level.
        internal struct ErrorDataUnmanaged
        {
            public int Code;        // .NET doesn't support less than 32-bit, so int is 32-bit.
            public byte IsFatal;    // The C++ bool is defined as a single byte.
            public uint* Message;   // This could be as simple as a void*, but uint* is closer.
        }

        public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
            => throw new NotImplementedException();

        public static void Free(ErrorDataUnmanaged unmanaged)
            => throw new NotImplementedException();
    }
}

ErrorDataUnmanaged はアンマネージ型の形状を模倣します。 ErrorDataからErrorDataUnmanagedへの変換は、Utf32StringMarshallerで簡単になりました。

intのマーシャリングは、アンマネージ コードとマネージド コードで同じ表現であるため、不要です。 bool値のバイナリ表現は .NET では定義されていないため、現在の値を使用して、アンマネージド型で 0 以外の値を定義します。 次に、UTF-32 マーシャラーを再利用して、 string フィールドを uint*に変換します。

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
    return new ErrorDataUnmanaged
    {
        Code = managed.Code,
        IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
        Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
    };
}

このマーシャラーを "in" として定義しているので、マーシャリング中に実行された割り当てをクリーンアップする必要があることを思い出してください。 intフィールドとbool フィールドはメモリを割り当てませんでしたが、Message フィールドはメモリを割り当てませんでした。 再び Utf32StringMarshaller を再利用して、マーシャリングされた文字列をクリーンアップします。

public static void Free(ErrorDataUnmanaged unmanaged)
    => Utf32StringMarshaller.Free(unmanaged.Message);

"out" シナリオについて簡単に考えてみましょう。 error_dataの 1 つまたは複数のインスタンスが返される場合を考えてみましょう。

extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)

extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);

[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);

コレクション以外の単一のインスタンス型を返す P/Invoke は、 MarshalMode.ManagedToUnmanagedOutとして分類されます。 通常、コレクションを使用して複数の要素を返します。この場合、 Array が使用されます。 MarshalMode.ElementOut モードに対応するコレクション シナリオのマーシャラーは、複数の要素を返し、後で説明します。

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class Out
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

ErrorDataUnmanagedからErrorDataへの変換は、"in" モードで行ったことの逆です。 また、アンマネージド環境で実行する必要がある割り当てもクリーンアップする必要があります。 また、ここでの関数は static マークされているため、"ステートレス" になっていることに注意することも重要です。ステートレスであることは、すべての "要素" モードの要件です。 また、"in" モードのような ConvertToUnmanaged メソッドがあることにも気付くでしょう。 すべての "要素" モードでは、"in" モードと "out" モードの両方の処理が必要です。

マネージドからアンマネージドの "out" マーシャラーの場合は、特別な処理を行います。 マーシャリングするデータ型の名前は error_data と呼ばれ、.NET では通常、エラーが例外として表されます。 一部のエラーは他のエラーよりも影響が大きく、"致命的" と識別されるエラーは、通常、致命的または回復不能なエラーを示します。 error_dataには、エラーが致命的かどうかを確認するフィールドがあることに注意してください。 error_dataをマネージド コードにマーシャリングし、致命的な場合は、単にErrorDataに変換して返すのではなく、例外をスローします。

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class ThrowOnFatalErrorOut
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

"out" パラメーターは、アンマネージ コンテキストからマネージド コンテキストに変換されるため、 ConvertToManaged メソッドを実装します。 アンマネージ呼び出し先が ErrorDataUnmanaged オブジェクトを返して提供する場合は、 ElementOut モードのマーシャラーを使用してそれを検査し、致命的なエラーとしてマークされているかどうかを確認できます。 その場合は、単に ErrorDataを返すのではなく、スローすることを示します。

public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
    ErrorData data = Out.ConvertToManaged(unmanaged);
    if (data.IsFatalError)
        throw new ExternalException(data.Message, data.Code);

    return data;
}

おそらく、ネイティブ ライブラリを使用するだけでなく、コミュニティと作業を共有し、相互運用ライブラリを提供したいと考えています。 ErrorData定義に[NativeMarshalling(typeof(ErrorDataMarshaller))]を追加することで、P/Invoke で使用される場合は常に暗黙的なマーシャラーでErrorDataを提供できます。 これで、 LibraryImport 呼び出しでこの型の定義を使用するすべてのユーザーがマーシャラーの利点を得られます。 これらは常に、使用サイトで MarshalUsing を使用してマーシャラーをオーバーライドできます。

[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }

こちらも参照ください