平台调用的源生成
.NET 7 为 P/Invoke 引入了一个源生成器,用于识别 C# 代码中的 LibraryImportAttribute。
当 P/Invoke 不使用源生成时,.NET 运行时中的内置互操作系统将生成一个 IL 存根,即在运行时以 JIT 为特征的 IL 指令流,以便于从托管到非托管的转换。 以下代码演示如何定义并随后调用使用此机制的 P/Invoke:
[DllImport(
"nativelib",
EntryPoint = "to_lower",
CharSet = CharSet.Unicode)]
internal static extern string ToLower(string str);
// string lower = ToLower("StringToConvert");
IL 存根处理参数和返回值的封送并调用非托管代码,同时遵循 DllImportAttribute 上影响非托管代码调用方式的设置(例如,SetLastError)。 由于此 IL 存根是在运行时生成的,因此它不适用于预先 (AOT) 编译器或 IL 剪裁方案。 IL 生成是封送要考虑的重要成本。 可以根据应用程序性能以及对可能不允许动态代码生成的潜在目标平台的支持来衡量此成本。 本机 AOT 应用程序模型通过将所有代码提前直接预编译为本机代码来解决动态代码生成问题。 对于需要完整本机 AOT 方案的平台,不能选择使用 DllImport
选项,因此,使用其他方法(例如源生成)更合适。 在 DllImport
方案中,调试封送逻辑也是一项重要的练习。
P/Invoke 源生成器(随 .NET 7 SDK 一起提供并默认启用)在 static
和 partial
方法上查找 LibraryImportAttribute 以触发封送代码的编译时源生成,从而无需在运行时生成 IL 存根,并允许内联 P/Invoke。 此外,还包括分析器和代码修复程序,以帮助从内置系统迁移到源生成器以及一般使用情况。
基本用法
LibraryImportAttribute 在用法上与 DllImportAttribute 类似。 我们可以通过使用 LibraryImportAttribute 并将方法标记为 partial
而不是 extern
将前述示例转换为使用 P/Invoke 源生成:
[LibraryImport(
"nativelib",
EntryPoint = "to_lower",
StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);
在编译期间,源生成器将触发以生成 ToLower
方法的实现,该方法处理 string
参数的封送并返回值 UTF-16。 由于封送现已生成源代码,因此实际上可以在调试器中查看并逐步执行逻辑。
MarshalAs
源生成器也遵循 MarshalAsAttribute。 前面的代码还可以编写为:
[LibraryImport(
"nativelib",
EntryPoint = "to_lower")]
[return: MarshalAs(UnmanagedType.LPWStr)]
internal static partial string ToLower(
[MarshalAs(UnmanagedType.LPWStr)] string str);
不支持 MarshalAsAttribute 的某些设置。 如果尝试使用不受支持的设置,源生成器将发出错误。 有关详细信息,请参阅与 DllImport 的差异。
调用约定
若要指定调用约定,请使用 UnmanagedCallConvAttribute,例如:
[LibraryImport(
"nativelib",
EntryPoint = "to_lower",
StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(
CallConvs = new[] { typeof(CallConvStdcall) })]
internal static partial string ToLower(string str);
与 DllImport
的差异
在大多数情况下,LibraryImportAttribute 旨在从 DllImportAttribute 直接转换,但会特意进行一些更改:
- CallingConvention 在 LibraryImportAttribute 上无等效项。 应改用 UnmanagedCallConvAttribute。
- CharSet(用于 CharSet)已替换为 StringMarshalling(用于 StringMarshalling)。 ANSI 已删除,UTF-8 现已作为一级选项提供。
- BestFitMapping 和 ThrowOnUnmappableChar 无等效项。 这些字段仅在 Windows 上封送 ANSI 字符串时相关。 用于封送 ANSI 字符串的生成代码具有与
BestFitMapping=false
和ThrowOnUnmappableChar=false
等效的行为。 - ExactSpelling 无等效项。 此字段是以 Windows 为中心的设置,对非 Windows 操作系统没有影响。 方法名称或 EntryPoint 应是入口点名称的准确拼写。 此字段的历史用途与 Win32 编程中使用的
A
和W
后缀相关。 - PreserveSig 无等效项。 此字段是以 Windows 为中心的设置。 生成的代码始终直接转换签名。
- 必须使用 AllowUnsafeBlocks 将项目标记为不安全。
对 MarshalAsAttribute 上的某些设置、某些类型的默认封送以及其他与互操作相关的属性的支持也存在差异。 有关更多信息,请参阅有关兼容性差异的文档。