共用方式為


教學課程:在來源產生的 P/Invokes 中使用自定義封送器

在本教學課程中,您將學習如何實作封送器,並用於來源生成的 P/Invokes 中的自定義封送處理。

您將為內建類型實作封送處理器,並為特定參數和使用者定義型別自定義封送處理,還有設定使用者定義型別的預設封送處理方式。

本教學課程中使用的所有原始程式碼都可在 dotnet/samples 存放庫中取得。

LibraryImport 來源產生器概覽

System.Runtime.InteropServices.LibraryImportAttribute 類型是 .NET 7 中引進之來源產生器的用戶進入點。 此原始碼產生器設計為在編譯時產生所有封組程式碼,而非執行時。 過去已使用 DllImport指定進入點,但該方法隨附的成本可能不一定可以接受,如需詳細資訊,請參閱 P/Invoke 來源產生LibraryImport原始碼產生器可以產生所有的編組程式碼,並移除與DllImport相關的執行階段生成需求。

若要表達在執行階段生成封送代碼以及讓使用者自定義其自身類型時所需的詳細資料,則需要數種類型。 本教學課程會使用下列類型:

  • MarshalUsingAttribute – 來源程式碼生成器在使用端搜尋的屬性,並用來決定屬性變數所用之封送器類型。

  • CustomMarshallerAttribute – 屬性,用來指出要執行封送處理作業的類型和模式的封送處理器(例如,從 Managed 到 Unmanaged 的 by-ref)。

  • NativeMarshallingAttribute – 用來指出要用於屬性型別之封送器的屬性。 這對於提供型別及相應的封送處理器之程式庫作者而言很有用。

不過,這些屬性並不是自定義封送器作者唯一可用的機制。 來源產生器會檢查封送器本身是否有各種其他指示,以告知封送處理應該如何發生。

您可以在 dotnet/runtime 存放庫中找到設計的完整詳細數據。

來源產生器分析器和修正程式

除了來源產生器本身,也會提供分析器和修正程式。 自 .NET 7 RC1 起,分析器和修正程式預設會啟用且可供使用。 分析器的設計目的是協助引導開發人員正確使用來源產生器。 修正程式提供從許多 DllImport 模式到適當 LibraryImport 簽章的自動化轉換。

原生庫簡介

使用LibraryImport 原始碼產生器表示取用原生或未受控的程式庫。 原生程式庫可能是直接呼叫未透過 .NET 公開的作業系統 API 的共享程式庫(也就是 .dll.sodylib)。 這個函式庫可能是用非受控語言大幅度優化過的,而 .NET 開發人員想要使用它。 在本教學課程中,您將建置自己的共享連結庫,以公開 C 樣式 API 介面。 下列程式代碼代表使用者定義型別和兩個要從 C# 取用的 API。 這兩個 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_datachar32_t* 代表一個以 UTF-32 編碼的字串,而這種編碼歷來並不是 .NET 通常處理的字串編碼。 error_data 是包含 32 位整數位段、C++布爾值欄位和 UTF-32 編碼字串字段的使用者定義類型。 這兩種類型都需要您提供一種方式,讓來源產生器產生封送處理程式碼。

自定義內建類型的封送處理

請先考慮 char32_t* 類型,因為使用者定義型別需要封送處理此類型。 char32_t* 代表原生端,但您也需要在受管理的程式碼中表示。 在 .NET 中,只有一個 “string” 類型: string。 因此,您會將原生 UTF-32 編碼字串在受控代碼中與 string 類型進行雙向封送處理。 已經有數個用於 string 類型的內建封送處理器,可將其封送為 UTF-8、UTF-16、ANSI,甚至是 Windows BSTR 類型。 不過,沒有用於 UTF-32 編碼的序列化方法。 這就是您需要定義的內容。

Utf32StringMarshaller 類型會以 CustomMarshaller 屬性標示,其會描述對來源產生器執行的動作。 屬性的第一個類型參數是 string 型別,也就是要封送處理的受控型別,第二個參數是模式,用以指出何時使用封送處理器,第三個類型參數是 Utf32StringMarshaller,用於封送處理的型別。 您可以多次套用 CustomMarshaller 以更清楚地指定模式及用於該模式的封送處理器類型。

目前的範例會展示「無狀態」封送處理器,該封送器會接受某些輸入,並以封送形式傳回數據。 方法 Free 會與 Unmanaged 封送處理對稱,而垃圾收集行程是 Managed 封送器的「免費」作業。 實作者可以自由執行任何想要將輸入封送處理至輸出的作業,但請記住來源產生器不會明確保留任何狀態。

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();
    }
}

您可以在範例中找到這個特定封送器如何執行從 stringchar32_t* 轉換的詳細數據。 請注意,任何 .NET API 都可以使用 (例如 , Encoding.UTF32

請考慮狀態被視為理想的情況。 觀察額外的 CustomMarshaller,並記下更具體的模式 MarshalMode.ManagedToUnmanagedIn。 此特製化封送處理器會實作為「有狀態」,並可跨互操作呼叫儲存狀態。 更特製化和狀態允許優化和針對模式量身打造的封送處理。 例如,可以指示來源產生器提供堆疊分配的緩衝區,以避免在封送處理期間明確分配資源。 為了表示支援堆疊配置的緩衝區,封送處理器會實作一個 BufferSize 屬性和一個接受 FromManaged 類型的 Span 參數的 unmanaged 方法。 屬性BufferSize會指出堆疊空間的數量—傳遞至SpanFromManaged長度—封送器希望在封送呼叫期間取得。

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 字串封送處理器來呼叫這兩個原生函式中的第一個。 下列宣告使用 LibraryImport 屬性,就像使用 DllImport 一樣,但依賴 MarshalUsing 屬性告訴源代碼生成器在呼叫本機函數時使用哪種封送器。 不需要釐清是否應該使用無狀態或有狀態的封送處理器。 這是由實作者透過在封送器的屬性上定義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 指定封送處理器,不如只在某些模式下定義封送處理器。 在此情況下,如果封送器用於未提供的模式,來源產生器將會失敗。 從定義「輸入」方向的封送器開始。 這是「無狀態」封送處理器,因為封送器本身只包含 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 模擬 Unmanaged 類型的圖形。 使用 ErrorData,從 ErrorDataUnmanaged 轉換成 Utf32StringMarshaller 現在非常簡單。

封送處理 int 是不必要的,因為在非受控和受控程式代碼中其表示方式完全相同。 bool在 .NET 中未定義二進位表示,因此請使用其目前的值,在非受控類型中定義為零值及非零值。 然後,重複使用您的 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」,因此您必須清除封送處理期間執行的任何配置。 intbool 欄位並未配置任何記憶體,但Message欄位確實如此。 重複使用 Utf32StringMarshaller 以清除封送處理字串。

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

讓我們簡短地考慮「外部」情境。 請考慮傳回一或多個 實例 error_data 的情況。

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」模式。

對於受控的「輸出」封送器,您要做一些特別的事情。 您需要封送處理的數據類型名稱稱為 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” 參數會從 Unmanaged 內容轉換成受控內容,因此您可以實作 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;
}

也許您不僅會使用原生程式庫,還想與社群分享您的工作,並提供一個互操作程式庫。 當您在 P/Invoke 使用隱含封送器時,可以透過將 ErrorData 新增至 [NativeMarshalling(typeof(ErrorDataMarshaller))] 定義來提供 ErrorData。 現在,任何使用您定義的LibraryImport類型來呼叫的人都會受益於您的封送器。 他們始終可以在使用位置使用 MarshalUsing 來覆蓋您的封送器。

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

另請參閱