Platform Invoke in CLR (1)--基础及基本数据类型的封送
最近需要在C#中调用C++代码,正好学习一下.NET中的平台调用服务(Platform Invoke Service)。在此记录下学习的心得。
我会从以下几方面去学习:
1. Platform Invoke的基础(对应.NET类库的基本用法,基本数据结构的封送规则)
2. Platform Invoke in CLR (2) --字符的封送(Marshal)
3. Platform Invoke in CLR (3) --结构体的封送(Marshal)
4. Platform Invoke in CLR (4) --不透明指针(IntPtr)和Marshal工具类
5. Platform Invoke in CLR (5) --通过封送函数来实现Call back.
本篇文章是属于第一节。
我理解的平台调用是指在.NET的托管环境中利用CLR所提供的Platform Invoke服务去对非托管环境中的资源的调用。
- 基本调用过程
从MSDN中可以得知,其基本过程如下:
当“平台调用”调用非托管函数时,它将依次执行以下操作:
1. 查找包含该函数的 DLL。
2. 将该 DLL 加载到内存中。
3. 查找函数在内存中的地址并将其参数推到堆栈上,以封送所需的数据。 注意
4. 将控制权转移给非托管函数。(平台调用会向托管调用方引发由非托管函数生成的异常。)
(以上摘自MSDN http://msdn.microsoft.com/zh-cn/library/0h9e9t7d(v=vs.100).aspx)
在CLR的环境中,对一个方法的调用是依赖于对其所在类进行描述的元数据和对其方法体进行描述的IL代码的。编绎器通过元数据能找到该函数据在的类以及
该函数的IL代码,然后将其IL代码编绎成本机代码进行调用。
那在Platform Invoke中依然是依赖于元数据与IL,但是非托管的代码是没有CLR中的元数据与IL代码的啊,那CLR的调用过程如何完成呢?
所以在这里仍然需要为非托管的函数提供一下包装类,让其能够为非托管的函数提供元数据与IL代码。
比如我们需要调用如下一个C++函数(位于native.dll中):
int Add(int a,int b);
那么我们需要为其实现如下一个封装类:
public class NativeWrapper
{
[DllImport("native.dll", EntryPoint = "Add")]
public static extern int Add(int a, int b);
}
这样一个封装类在C#编绎器编译后就为Add方法保存了元数据与IL(并不是Add方法体的IL)。
当我们在托管代码中想要调用该非托管方法时,只需做如下调用:
int result = NativeWrapper.Add(1, 2);
至于native.dll如何被load,非托管方法如何被调用则前面对MSDN中的引用已描述得很清楚。
- 剖析非托管方法的封装类NativeWrapper
之所以说NativeWrapper保存了非托管方法Add的元数据,主要得力于在NativeWrapper类中声明的方法
[DllImport("native.dll", EntryPoint = "Add")]
public static extern int Add(int a, int b);
首先该方法对非托管方法Add的签名进行描述,这可以算得上是一种元数据。然后该静态法与一般普通的public static方法的区别在于
1. 方法声明中有extern关键字
声明该关键字可以告诉编绎器该方法为外部实现方法,不需要方法体。
2. 在方法上标注了DllImport标签
我个人认为这是对非托管方法Add的元数据描述最多的地方。从MSDN得知,该标签提供对从非托管 DLL 导出的函数进行调用所必需的信息。
DllImport Attribute有如下常用属性可以设置,用以描述非托管方法的元数据。
- dll name(构造函数参数) 用来描述非托管方法所在的dll的名称
- EntryPoint 用来描述非托管函数在dll中的入口点,也就是dll所导出的非托管函数的方法名。
- CharSet 用来描述在调用非托管函数时传递的字符参数的字符编码规则。(在学习字符封送时会详细学习)
- CallingConvention 用来描述入口点的调用约定
调用约定主要是用来描述非托管代码中对于调用过程中参数的入栈顺序及清理责任的规定。C++中常用的有两种调用约定:_stdcall,cdecl
具体介绍参考:http://blog.csdn.net/lindenq/article/details/4775840
到这里,对于非托管函数的元数据描述已经足够CLR在运行时去调用到它了。接下来还需要处理的事情是在调用的过程中的参数的传递了。因
为在.NET 中的类型和非托管代码中的类型不能直接匹配,因此在调用非托管方法时需要将.NET中的类型进行封送处理(Marshal).
- 常用基本数据类型
对于一些常用的基本数据类型(比如说数值类型,可以找到非托管环境中相匹配的类型),可以直接进行类型匹配。参考下表:
- 指针
非托管环境中最常用的就是指针了,可以通过指针完全自主的控制内存。但是在托管环境中内存是由CLR管理的,那么如何将内存地址作为指针
传递给非托管环境呢?
在托管环境中如果传递的参数是引用类型的,其实直接传递的便可以认为是指针(内存地址),只不过我们不能去完全自主的控制
这块内存而已。而如果传递的是值类型,传递的过程中其实是将实参所在栈上的内存块拷贝到了形参所在栈上的内存块(在方法体内操作的实际是形参所在内存)。
那如何在托管环境中直接在调用方法中操作实参所在栈上的内存块呢?答案是使用out或ref关键字来传递值类型的引用(其实out和ref在IL级别是没有区别的,只是编绎器级别
进行区分)。那么在调用非托管方法的时候,道理是一样的,依然采用out和ref来传递值类型的引用(即指针)
For example:
C++ Code
_declspec(dllexport) void _stdcall AddWithPointer(int a,int b,int* c)
{
if(c)
*c = a + b;
}
非托管方法包装类 Code:
[DllImport("native.dll", EntryPoint = "AddWithPointer")]
public static extern void AddWithPointer(int a, int b, out int c);
托管环境调用Code:
int result;
NativeWrapper.AddWithPointer(2, 2, out result);
补遗:之前由于学艺不精,此处其实并不是直接将CLR中栈的地址传送给C++。而是Marshal Service为拷贝一份内存到非托管环境中并会同步这两块内存。