教學課程:在來源產生的 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
、.so
或 dylib
),直接呼叫未透過 .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_data
。 char32_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
類型 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 字串封送處理器,來呼叫這兩個原生函式的第一個。 下列宣告使用 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」,因此您必須清除封送處理期間執行的任何配置。 int
和 bool
欄位並未配置任何記憶體,但 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 { ... }