共用方式為


教學課程:在來源產生的 P/Invoke 中使用自訂封送處理器

在本教學課程中,您將了解如何實作封送處理器,並將其用於來源產生的 P/Invoke 中的自訂封送處理

您將實作內建類型的封送處理器、針對特定參數和使用者定義型別自訂封送處理,並指定使用者定義型別的預設封送處理。

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

LibraryImport 來源產生器的概觀

System.Runtime.InteropServices.LibraryImportAttribute 型別是 .NET 7 中所引進來源產生器的使用者進入點。 此來源產生器的設計目的是在編譯時間 (而不是在執行階段) 產生所有封送處理程式碼。 過去已使用 DllImport 指定進入點,但該方法隨附的成本可能不一定皆可接受;如需詳細資訊,請參閱 P/Invoke 來源產生LibraryImport 來源產生器可以產生所有封送處理程式碼,並移除 DllImport 內建的執行階段產生需求。

若要表達執行階段產生封送處理程式碼所需的詳細資料,以及讓使用者針對自己的類型進行自訂所需的詳細資料,則需要數種類型。 本教學課程中會使用下列類型:

  • MarshalUsingAttribute – 這個屬性是使用網站的來源產生器所搜尋的,並用來判斷封送處理屬性變數的封送處理器類型。

  • CustomMarshallerAttribute – 這個屬性是用來指出類型的封送處理器,以及要執行封送處理作業的模式 (例如,從受控到非受控傳址)。

  • NativeMarshallingAttribute – 這個屬性是用來指出要用於屬性類型的封送處理器。 這適用於為這些類型提供類型和隨附封送處理器的程式庫作者。

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

您可以在 dotnet/runtime 存放庫中找到關於設計的完整詳細資料。

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

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

原生程式庫簡介

使用 LibraryImport 來源產生器表示取用原生或非受控程式庫。 原生程式庫可能是共用程式庫 (也就是 .dll.sodylib),直接呼叫未透過 .NET 公開的作業系統 API。 程式庫也可能是 .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。 因此,您將封送處理原生 UTF-32 編碼字串往返受控程式碼中的 string 類型。 已經有數個 string 類型的內建封送處理器,可封送處理為 UTF-8、UTF-16、ANSI,甚至是 Windows BSTR 類型。 不過,並沒有封送處理為 UTF-32 的類型。 這就是您需要定義的項目。

Utf32StringMarshaller 類型會以 CustomMarshaller 屬性標示,會描述其對來源產生器所做的動作。 屬性的第一個類型引數是 string 類型、要封送處理的受控類型、第二個是模式,表示何時要使用封送處理器,而第三個類型是 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)。 這個特製化封送處理器會實作為「具狀態」,並可跨 Interop 呼叫儲存狀態。 更多特製化和狀態允許最佳化,以及針對模式量身打造的封送處理。 例如,可以指示來源產生器提供堆疊配置的緩衝區,以避免在封送處理期間明確配置。 為了指出堆疊配置緩衝區的支援,封送處理器會實作 BufferSize 屬性和採用 unmanaged 類型 SpanFromManaged 方法。 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 字串封送處理器,來呼叫這兩個原生函式的第一個。 下列宣告使用 LibraryImport 屬性,就像 DllImport 一樣,但仰賴 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 會模擬非受控類型的圖形。 現在使用 Utf32StringMarshaller 即可輕鬆從 ErrorData 轉換成 ErrorDataUnmanaged

封送處理 int 是不必要的,因為其標記法在非受控和受控程式碼中完全相同。 在 .NET 中未定義 bool 值的二進位標記法,因此請使用其目前的值,在非受控類型中定義零和非零值。 然後,重複使用您的 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);

讓我們簡短地考慮「out」案例。 請考慮傳回一或多個 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」模式。

針對受控到非受控「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;
}

或許您不僅要取用原生程式庫,也想要與社群共用您的工作,並提供 Interop 程式庫。 只要在 P/Invoke 中使用隱含封送處理器,您就可以將 [NativeMarshalling(typeof(ErrorDataMarshaller))] 新增至 ErrorData 定義,以向 ErrorData 提供隱含封送處理器。 現在,在 LibraryImport 通話中使用您此類型定義的任何人員,都會獲得封送處理器的好處。 他們一律可以在使用網站上使用 MarshalUsing,以覆寫您的封送處理器。

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

另請參閱