教程:在源生成的 P/Invoke 中使用自定义封送程序
在本教程中,你将学习如何实现封送程序,并将它用于源生成的 P/Invoke 中的自定义封送。
你将为内置类型实现封送程序,自定义特定参数和用户定义类型的封送,并为用户定义类型指定默认封送。
本教程中使用的所有源代码都可以在 dotnet/samples 存储库中找到。
LibraryImport
源生成器概述
System.Runtime.InteropServices.LibraryImportAttribute
类型是 .NET 7 中引入的源生成器的用户入口点。 根据设计,此源生成器可在编译时(而不是在运行时)生成所有封送代码。 过去,入口点使用 DllImport
来指定,但这种方法产生的成本可能并不总是在可接受的范围内。有关详细信息,请参阅 P/Invoke 源生成。 LibraryImport
源生成器可生成所有封送代码,并移除 DllImport
固有的运行时生成要求。
若要表达为运行时和用户生成用来为其自己的类型进行自定义的封送代码所需的详细信息,需要使用几种类型。 在本教程中,将使用以下类型:
MarshalUsingAttribute
- 源生成器在使用站点中查找的特性,用来确定用于封送特性化变量的封送程序类型。CustomMarshallerAttribute
- 该特性用于指示类型的封送程序和执行封送操作的模式(例如,通过 by-ref 从“托管”封送到“非托管”)。NativeMarshallingAttribute
- 该特性用于指示要对特性化类型使用哪个封送程序。 对于提供类型和为这些类型提供随附封送程序的作者,这非常有用。
不过,这些特性并不是自定义封送程序作者可使用的唯一机制。 源生成器会检查封送程序本身,了解其他各种说明应如何进行封送的指示。
有关设计的完整详细信息,可查看 dotnet/runtime 存储库。
源生成器分析器和修复程序
除了源生成器本身,还提供分析器和修复程序。 从 .NET 7 RC1 开始,分析器和修复程序默认启用且可用。 根据设计,分析器有助于指导开发人员正确使用源生成器。 修复程序提供从许多 DllImport
模式到相应 LibraryImport
签名的自动转换。
本机库简介
使用 LibraryImport
源生成器意味着使用本机库(也称为非托管库)。 本机库可能是共享库(即 .dll
、.so
或 dylib
),它直接调用未通过 .NET 公开的操作系统 API。 该库也可能是在 .NET 开发人员想要使用的非托管语言中进行了大量优化的库。 在本教程中,你将生成自己的共享库,用于公开 C 样式的 API 图面。 以下代码表示一个用户定义类型和两个 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
类型或从该类型封送。 对于封送为 UTF-8、UTF-16、ANSI 甚至 Windows BSTR
类型的 string
类型,已经有多个内置封送程序。 但是,它们都不用于封送为 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
。 这个专用的封送程序实现为“有状态”,可跨互操作调用存储状态。 更多专用化和状态允许针对模式进行优化和定制封送。 例如,可指示源生成器提供堆栈分配的缓冲区,以避免在封送期间进行显式分配。 为了指示对堆栈分配的缓冲区的支持,封送程序实现了 BufferSize
属性和 FromManaged
方法,该方法采用 unmanaged
类型的 Span
。 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
,因此是“无状态的”,所有“元素”模式都要求是无状态的。 你还会注意到有一个 ConvertToUnmanaged
方法,就像在“in”模式下一样。 所有“Element”模式都需要处理“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;
}
也许你不仅要使用本机库,还希望与社区共享你的工作并提供互操作库。 每当在 P/Invoke 中使用时,你都可以通过向 ErrorData
定义添加 [NativeMarshalling(typeof(ErrorDataMarshaller))]
来向 ErrorData
提供隐式封送程序。 现在,在 LibraryImport
调用中使用此类型的定义的用户都将从你的封送程序中获益。 他们始终可通过在使用站点中使用 MarshalUsing
来重写你的封送程序。
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }