平台调用服务

平台调用服务

平台叫用服务(英语:Platform Invocation Services),或称P/Invoke,是微软的公共语言基础设施实现的一个特性,类似于微软公共语言运行时提供的跨平台调用方式,允许受控代码调用原生代码

托管代码(例如C#VB.NETC++/CLI)提供对.NET Framework的组成库中定义的类、方法和类型的本机访问。虽然.NET Framework提供了一组广泛的功能,但它可能无法访问通常以非托管代码编写的许多较低级别的操作系统库或也以非托管代码编写的第三方库。P/Invoke是程序员可以用来访问这些库中的函数的技术。通过在托管代码中声明非托管函数的签名来调用这些库中的函数,该签名充当可以像任何其他托管方法一样调用的实际函数。该声明引用库的文件路径,并定义托管类型中的函数参数和返回值,这些托管类型最有可能由公共语言运行时(CLR)与非托管类型隐式封送。当非托管数据类型对于从托管类型到托管类型的简单隐式转换来说变得过于复杂时,框架允许用户在函数、返回和/或参数上定义属性,以显式细化数据的编组方式,以免试图隐式地这样做会导致异常。

与使用非托管语言进行编程相比,托管代码程序员可以使用许多低级编程概念的抽象。因此,仅具有托管代码经验的程序员需要温习编程概念,例如指针、结构和引用传递,以克服使用P/Invoke时的一些障碍。

平台叫用服务这一特性与微软的公共语言运行时提供的较为类似,因此一般提到P/Invoke多数指微软的.NET实现方案。这一方案能够实现通过托管代码访问原生代码。使用P/Invoke可以通过CLR来控制DLL的加载,以及将非托管代码的数据类型转换为托管数据类型

Windows

编辑

在Microsoft Windows作业系统中,Native API有时也是以COM介面方式来推出,像是ADSI,FSRM(File Server Resource Manager)等,通常是新的服务或是介面才会广泛使用COM原生介面方式。因为.NET Framework的推行,Windows的应用程式介面被分为两种,一种是遵循原本Windows API方式的,称为Native API,另一种则是以.NET Framework为基础开发的,称为Managed API,例如Managed DirectX或是IIS Admin APIs等。

在Microsoft Windows作业系统中,若是透过VB或是.NET Framework存取直接开放C函式的Native API时,则必须要利用平台叫用服务方式存取;若是存取以COM方式开放的Native API时,若该API支援COM Automation规格时,即可利用COM Interop Services来存取。

体系架构

编辑

概述

编辑

有两类P/Invoke:

显式

编辑
  • 原生代码通过动态链接库 (DLL)导入
  • 嵌入到调用者的程序集(assembly)的元数据英语Metadata (CLI),用来定义如何调用原生代码、如何访问数据(通常需要属性源说明符来帮助编译器生成整集(marshal)代码)
    • 这种定义就是“显式”部分。

隐式

编辑
  • 使用C++/CLI编程语言的程序可以同时使用受管堆(以追踪指针)和任何原生内存区域,不必显式声明。(即“隐式”)
  • 隐式方法最主要好处是如果原生数据结构改变了,只要保持命名兼任,就能保持向后兼容
    • 即,只要结构成员名称没有改变,就可以透明地支持在原生代码头文件中添加/删除/重新排序结构。

细节

编辑

使用P/Invoke 时,CLR处理DLL加载以及将非托管以前的类型转换为CTS] 类型(也称为“参数编组”)。[1]要执行此操作,CLR

  • 定位包含该函数的DLL
  • DLL加载到内存中。
  • 定位内存中函数的地址,并将其参数压入堆栈,根据需要封送(marshaling)数据。

P/Invoke对于使用标准(非托管)CC++ DLL非常有用。当程序员需要访问广泛的Windows API时,可以使用它,因为Windows系统函式库提供的许多功能缺乏可用的包装器。当Win32 API未由.NET Framework公开时,必须手动编写此API的包装器。

缺点

编辑

编写P/Invoke包装器可能很困难并且容易出错。使用本机DLL意味着程序员无法再像.NET环境中通常提供的那样受益于类型安全垃圾回收。 当它们使用不当时,可能会导致诸如记忆体区段错误内存泄漏等问题。获取在.NET环境中使用的原生函数的准确签名可能很困难。

其他陷阱包括:

  • 托管语言中用户定义的类型数据对齐不正确:根据C语言中的编译器或编译器指令,数据对齐的方式有不同,必须小心地显式告诉CLR如何对齐非blittable类型的数据。 一个常见的例子是尝试在.NET中定义数据类型以表示C语言中的union,即多个不同的变量在内存中重叠。而在.NET中的类型中定义这两个变量会导致它们位于内存中的不同位置,因此必须使用特殊属性来纠正此问题。
  • 托管语言的垃圾收集器对数据位置的干扰:如果引用是.NET中方法的本地引用并传递给原生函数,则当托管方法返回时,垃圾收集器可能会回收该引用。 需要注意的是,对象引用是pinned,防止它被垃圾收集器收集或移动,从而导致本机模块的无效访问。

当使用C++/CLI时,发出的CIL可以自由地与托管堆上的对象交互,同时与任何可寻址的本机内存位置交互。可以使用简单的“object->field”表示法来分配值或指定方法调用,从而调用、修改或构造托管堆驻留对象。消除了任何不必要的上下文切换,降低了内存需求(更短的堆栈),从而显著提高了性能。

C++/CLI带来了新的挑战:

  • 代码容易出现双重雷电(Double Thunking)[2]如果没有特别说明
  • “加载程序锁定问题”(Loader Lock issue)[3]

如果遇到这些问题,这些参考资料会为每个问题指定解决方案。一个主要的好处是消除了结构声明,字段声明的顺序和对齐问题在C++互操作的上下文中不存在。

例子

编辑

基本例子

编辑

第一个简单示例显示了如何获取特定DLL的版本:

Windows API中的“DllGetVersion”函数签名:

HRESULT DllGetVersion
(
    DLLVERSIONINFO* pdvi
)

P/Invoke C#代码以调用“DellGetVersion”函数:

[StructLayout(LayoutKind.Sequential)]
private struct DLLVERSIONINFO {
    public int cbSize;
    public int dwMajorVersion;
    public int dwMinorVersion;
    public int dwBuildNumber;
    public int dwPlatformID;
}
[DllImport("shell32.dll")]
static extern int DllGetVersion(ref DLLVERSIONINFO pdvi);

第二个示例显示了如何提取文件中的图标:

Windows API中的“ExtractIcon”函数签名:

HICON ExtractIcon
(      
    HINSTANCE hInst,
    LPCTSTR lpszExeFileName,
    UINT nIconIndex
);

P/Invoke C#代码以调用“ExtractIcon”函数:

[DllImport("shell32.dll")]
static extern IntPtr ExtractIcon(
    IntPtr hInst, 
    [MarshalAs(UnmanagedType.LPStr)] string lpszExeFileName, 
    uint nIconIndex);

下一个复杂的示例显示了如何在Windows平台中的两个进程之间共享事件

“CreateEvent”函数签名:

 HANDLE CreateEvent(
     LPSECURITY_ATTRIBUTES lpEventAttributes,
     BOOL bManualReset,
     BOOL bInitialState,
     LPCTSTR lpName
 );

P/Invoke C#代码以调用“CreateEvent”函数:

[DllImport("kernel32.dll", SetLastError=true)]
static extern IntPtr CreateEvent(
    IntPtr lpEventAttributes, 
    bool bManualReset,
    bool bInitialState, 
    [MarshalAs(UnmanagedType.LPStr)] string lpName);

一个更复杂的例子

编辑
// native declaration
typedef struct _PAIR 
{ 
	DWORD Val1; 
	DWORD Val2; 
} PAIR, *PPAIR;
// Compiled with /clr; use of #pragma managed/unmanaged can lead to double thunking;
// avoid by using a stand-alone .cpp with .h includes.
// This would be located in a .h file.

template<>
inline CLR_PAIR^ marshal_as<CLR_PAIR^, PAIR> (const PAIR&Src) {    // Note use of de/referencing. It must match your use.
	CLR_PAIR^ Dest = gcnew CLR_PAIR;
	Dest->Val1 = Src.Val1;
	Dest->Val2 = Src.Val2;
	return Dest;
};
CLR_PAIR^ mgd_pair1;
CLR_PAIR^ mgd_pair2;
PAIR native0,*native1=&native0;

native0 = NativeCallGetRefToMemory();

// Using marshal_as. It makes sense for large or frequently used types.
mgd_pair1 = marshal_as<CLR_PAIR^>(*native1);

// Direct field use
mgd_pair2->Val1 = native0.Val1;
mgd_pair2->val2 = native0.val2;

return(mgd_pair1); // Return to C#

工具

编辑

有许多工具旨在帮助生成P/Invoke签名。

编写一个实用程序来导入C++头文件和本机DLL文件并自动生成接口程序集是相当困难的。为P/Invoke签名生成这样一个导入器/导出器的主要问题是某些C++函数调用参数类型的模糊性。

Brad Abrams在这个问题上这样说:[4]


问题在于C++函数,如下所示:

__declspec(dllexport) void MyFunction(char *params);


P/Invoke签名中的参数params应该使用什么类型?这可以是以C++null结尾的字符串,也可以是char数组或输出char参数。那么我们应该使用string,StringBuilder, char []还是ref char呢?

不管这个问题如何,有一些工具可以使P/Invoke签名的生成更加简单。

下面列出的工具之一xInterop C++.NET Bridge通过在.NET世界中实现同一C++方法的多个重写解决了这个问题,开发人员可以选择正确的方法进行调用。

PInvoke.net

编辑

PInvoke.net是一个包含大量标准Windows API的P/Invoke签名的wiki。签名由wiki用户手动生成。在Microsoft Visual Studio的免费插件可直接搜索。

PInvoker

编辑

PInvoker是一个导入本机DLL和C++头文件并导出完全格式和编译的P/Invoke互操作DLL的应用程序。它通过将本机指针函数参数封装在PInvoker特定的.NET接口类中,克服了歧义问题。它没有在P/Invoke方法定义中使用标准的.NET参数类型 (char[], string, 等),而是在P/Invokefunction调用中使用这些接口类。

例如,如果我们考虑上面的示例代码,PInvoker将生成一个.NET P/Invoke函数,该函数接受一个包装本机char *指针的.NET接口类。此类的构造可以来自stringchar []数组。两者的实际本机内存结构是相同的,但每种类型的相应接口类构造函数将以不同的方式填充内存。因此,决定需要传递到函数中的.NET类型的责任将传递给开发人员。

Microsoft Interop Assistant

编辑

Microsoft Interop Assistant是一个免费工具,提供二进制文件和源代码,可在CodePlex上下载。它是根据微软有限公司公共许可证(Ms LPL)许可证。

它包括两部分:

  • 一个转换器,它接受包含struct和方法定义的本地C++头文件代码部分。然后,它生成C# P/Invoke代码,供复制并粘贴到应用程序中。
  • 转换了Windows API常量、方法和结构定义的可搜索数据库。

因为该工具生成C#源代码而不是编译的dll,所以用户可以在使用前对代码进行任何必要的更改。因此,通过应用程序选择一个特定的.NET类型来在P/Invoke方法签名中使用来解决模糊性问题。如果需要,用户可以将其更改为所需的类型。

P/Invoke向导

编辑

P/Invoke向导使用与Microsoft Interop Assistant类似的方法,因为它接受本机C++头文件代码,并生成C#(或VB.NET)代码供粘贴到.NET应用程序代码中。

它还提供了要针对的框架的选项:用于桌面的.NET framework或用于Windows Mobile智能设备(和Windows CE)的.NET Compact framework。

xInterop C++ .NET Bridge

编辑

xInterop C++ .NET Bridge是一个windows应用程序,用于为本机C++ DLL创建C#包装器,并使用C++桥访问.NET程序集,它附带了一个C#/.NET库,用于包装标准C++类,如字符串、iostream等。可以从.NET访问C++类和对象。

该工具从现有的本机C++ DLL和相关的头文件生成C#包装器DLL,这些头文件是该工具构建C#包装器DLL所需的。P/Invoke签名和数据封送处理由应用程序生成。生成的C#包装器具有与C++对应程序类似的接口,参数类型转换为.NET代码。

此工具识别未从C++DLL导出的模板类,并实例化模板类并将其导出到补充DLL中,相应的C++接口可以在.NET中使用。

参见

编辑

参考文献

编辑
  1. ^ “参数编组”(Parameter marshaling)不应与通用术语“编组”(marshalling)混淆,意思是序列化。 封送参数在转换为 CTS类型后将复制到CLR堆栈中,但不会序列化。
  2. ^ {引用web|url=https://docs.microsoft.com/en-us/cpp/dotnet/double-thunking-cpp%7Ctitle=Double[失效链接] Thunking(C++)}}
  3. ^ {{引用web|url=https://docs.microsoft.com/en-us/cpp/dotnet/initialization-of-mixed-assemblies%7Ctitle=初始化混合程序集}}[失效链接]
  4. ^ The PInvoke problem. learn.microsoft.com. February 6, 2004 [2023-06-28]. (原始内容存档于2022-10-09). 

外部链接

编辑