P/invoke in .NET Compact Framework
概要
P/Invoke的机制让我们能在托管环境下使用原先已实现的Native Code。本文主要讨论的是P/Invoke中的参数传递和.NET CF的一些不同于完整版本的 .NET Fx之处,最后介绍了如何提高P/invoke的效率
Keywords
.NET Compact Framework, Windows Mobile, P/Invoke ,data marshaling
正文
好吧,先看个例子。为了获得用户按键的状态,下面的代码段演示了将GetAsyncKeyState函数从Coredll.dll中导出,并在托管代码中重命名为 GetMyKeyState供调用:
2
3[DLLImport("coredll.dll", EntryPoint=" GetAsyncKeyState")]
4public static extern short GetMyKeyState (int nKey)
5
6
7//调用时候如下:
8int i = 2;
9short nkeyState = GetMyKeyState(i);
10
在写下上面这几行代码之前,应该确定的是
· 所调用的dll确实存在,且路径正确
· 该dll所依赖的其他dll存在
· 入口点的函数名正确
· 传递给函数的参数在类型上是正确的
上述任何一点出错,都会导致MissingMethodException异常。前三条都比较好理解,往往让人困惑的是在参数的传递上。本地(native) 的libraries只能访问本地数据, 而默认状态下托管代码也只能访问托管的data. 为了使人们在托管的代码环境中仍然能访问到本地的类库,通常需要进行两种环境下的数据类型映射,这就是所谓的marshaling.
Marshaling一个对象的过程就是一个序列化(deflating)的过程, 相应的unmarshaling 就可以看作是反序列化(inflating)的过程.
对于一些简单的数据类型和对象, marshaling通常已经由.NET Framework自动完成了. 对于一些复杂的数据类型,可以使用Marshal类将托管的数据复制到非托管的内存空间上或者将非托管的数据复制到托管的内存空间上。
需要注意的是: 在完整版的.NET Framework中,长整形(64-bit integer) 和浮点型 (float and double)可以直接进行值传递.而在 .NET Compact Framework中, 他们则需要通过引用传递的方式实现”值传递”。对于这种引用传递,CSharp提供了两种方式. 关键字 out 仅用于从函数中将值回传出来. 关键字 ref 则既可以用来传入参数,也可以用来从函数中获取值,但是在之前,你必须先给ref修饰的参数指定一个值,例如: mean只是作为函数的 一个操作产物获取出来
下面来看看在本地代码中应当如何操作以供调用
简单的int型值传递: 在函数声明的前面,添加了一段extern "C"__declspec(dllexport),作用是以C风格的形式将函数暴露以供外部调用
对于constant的常量值类型,在C/C++中我们通常这样定义它们:而在托管代码中我们直接使用const关键字修饰这种常量在C#中也可以简单的定义在一个枚举类型里面:前面都是讲的值类型的传递的例子,下面来看看引用类型是如何Marshal的
1. 数组
方案:
C/C++参数:头指针,长度
C#参数:数组实例,长度
以下函数用来计算数组的平均值:C#中调用如下:2. 字符串
在Windows Mobile的系统下只支持 Unicode 的字符编码。
方案:
C/C++参数:WCHAR */CString
C#参数:如果在本地代码中可能要变化字符串则用StringBuilder,静止的(unmutable)字符串值就直接用String传。
可以使用 System.String 和System.Text.StringBuilder 类来传递 Unicode 字符序列给本地代码。.NET Compact Framework的运行时会自动附加上一个表示终结的字符’/0’到该字符序列的后面,这样就是我们熟知的C 风格的string了,值得一提的是在 .NET Compact Framework中, System.String类型的对象从设计上采取了不可变模式, 也就是说它们在运行时值不可被改变. 如果程序运行的时候它们的数据需要被改变, 比如附加一个串到它结尾或者要移除某些字符,那么 .NET CLR 会为这样的操作产生一个新的对象拷贝. 所以你不能直接传入一个string给可能改变它的非托管代码。
2double b = 4.0;
3myRef.DoubleMean2(ref a,ref b,out mean);
4 public class myRef
5 {
6 [DllImport("nativetest1.dll")]
7 public static extern void DoubleMean2(ref double a,ref double b,out double mean);
8 }
9
这里的参数
2{
3 return (x+y);
4}
5//注意double型的参数为指针形式:
6extern "C"__declspec(dllexport)
7void DoubleMean2(double *x, double *y, double *mean)
8{
9 *mean = (*x + *y) / 2.0;
10}
11
2#define MONDAY 1
3#define TUESDAY 2
4#define WEDNESDAY 3
5#define THURSDAY 4
6#define FRIDAY 5
7#define SATURDAY 6
8
2const int MONDAY = 1;
3const int TUESDAY = 2;
4const int WEDNESDAY = 3;
5const int THURSDAY = 4;
6const int FRIDAY = 5;
7const int SATURDAY = 6;
2{
3 SUNDAY = 0,
4 MONDAY = 1,
5 TUESDAY = 2,
6 WEDNESDAY = 3,
7 THURSDAY = 4,
8 FRIDAY = 5,
9 SATURDAY = 6,
10}
11
2int MeanArray(int *pItem, int len)
3{
4 if (len < 1)
5 return -1; // Empty
6 int Sum = 0;
7 for ( int i= 0; i < len; i++)
8 Sum += pItem[i];
9 return Sum / len;
10}
extern static int MeanArray(int[] pItem, int len);
int[] stuScores = new int[] { 78, 85, 51, 92, 81, 96, 65};
int mean = MeanArray(stuScores, stuScores.Length);
MessageBox.Show(String.Format("The class average of final exam is {0}", mean));
3. 传结构和类
方案:使用 MarshalAsAtrribute
对于那些只含有简单又通用(通用指的是有相同内存存储结构)的数据类型,直接写就是了。但是很多情况下,我们遇到的都是一些包含复杂元素的数据类型。这时就需要用到MarshalAsAttribute和 UnmanagedType 枚举了.下面一行C#代码的作用是传递一个两个字节空截止(Null-terminated)的Unicode 字符序列到非托管代码中:
void PInvokeAnCFunction([MarshalAs(UnmanagedType.LPWStr)] string s);
再看一个稍微复杂点的
在C++里面我们这样描述: 看看我们在 C#中是如何定义这个结构的: 这里要注意当使用 ByValArray 或者ByValTStr时, 你必须指定 sizeConst 因为它们都是定长的数据类型。
{
int intAry[10];
char charAry[80];
WCHAR *pStr;
};
{
[MarshalAs(UnmanagedType.ByValArray, sizeConst=10)] int[] intAry;
[MarshalAs(UnmanagedType.ByValTStr, sizeConst=80)] string str1;
[marshalAs(UnmanagedType.LPWStr)] string str2;
};
4. 最后来一个特殊的,浮点数(float):
.NET CF不能直接Marshal为浮点数,但可以通过指针传递,用IntPtr承接指针,再从IntPtr去Marshal成String。
好了,似乎有关.NET Compact Framework的p/invoke的方法和数据混合(Marshal)已经介绍得差不多了。不过在实际的工程中还有一个重要的问题需要考虑,那就是P/invoke的效率。一般来说,追求便捷的高效开发总是伴随着程序效率的降低,MS的一些研究表明,从CLR调用和执行非托管的DLL比执行托管的的内容要慢5倍左右,虽然在PC上这根本不算什么,但是在移动设备上这种差别有时简直就是我们能用“肉眼”识别的。
究竟是什么造成了P/inveke中性能的损失呢?主要有两个方面的原因: 一方面,损耗来自于混合(marshaling) 托管数据成非托管的数据的时候。显然,参数越多,计算所用的时间和内存就会越耗,尤其是那些非通用数据类型的参数。另一方面, 由于托管的数据是由.NET CLR 控制的,像垃圾回收(GC)这样的操作一直在密切‘监视’着这些数据当我们在托管的代码中调用本地(Native)APIs的时候, .NET Compact Framework 首先会将调用的那些本地数据从GC中注销掉,也就是说调用的本地数据并不被托管的GC回收和释放,所以一些Free的工作需要在本地代码内部完成,当对本地数据的处理和解析(如marshal)结束之后, .NET runtime又会将这些数据重新声明为托管的数据类型,重新交给CLR处理,这一系列额外的操作代价是不小的。
为了将这种损失降到最小,应该怎么去做呢?显然,在数据混合(Marshal)的时候,我们应当尽量使用那些通用的数据类型,或者说是由CLR帮我们做了Marshal的数据类型。 其次,针对上述的第二个原因,少调用多封装也是优化的方案之一,因为每一次调用的厄外操作成本差不多。这就像来到大学的第一年把后面四年的注册手续都办好了,后面就不必那么麻烦了(事实上,我有同学真的是这么干的…Orz)。
最后再来看几条来自官方的建议,他们来自 Microsoft .NET Compact Framework team:
· 当参数是基本的通用类型,或者简单类型的时候,P/Invoke的调用会更快。这里所说的类型包括:
o 所有通用的64位值类型,除了long型 ,通过传引用(reference)的方式要比直接传值效率要高
o 简单数据类型,如 String 和Array, .NET CF 会很快的实现数据混合(Marshal)
o 仅包含上述两种类型的结构和类
· 给参数使用使用in 和 out 特性可以加速marshaling 的过程.
· Marshal.Prelink 和Marshal.PrelinkAll 在 .NET Compact Framework 2.0 中可以用来在程序启动的时候就做一些对本地函数的初始化的预链接,这样用户只需在程序一开始稍作等待,后面的调用是很迅速的。
总结
.NET Compact Framework 通过P/invoke的方式使得我们得以调用本地代码编写的类库,只需要导入à声明à调用的一个简单的过程即可实现。值得注意的是参数的传递,具有有相同内存存储结构的通用类型,可以直接传递,如int。 较复杂的数据类型如数组和字符串可以由引用(指针)的方式传递,对于结构和类,通过LayoutKind和 MarshalAs 两个特性标签可以让我们指定数据来内存中的映射关系。最后,P/invoke存在效率的问题,不要滥用,一般来说,用P/invoke去解决关键的一小部分托管代码不能完成的部分就可以了。