概要
P/Invoke的机制让我们能在托管环境下使用原先已实现的Native Code。本文主要讨论的是P/Invoke中的参数传递和.NET CF的一些不同于完整版本的 .NET Fx之处,最后介绍了如何提高P/invoke的效率
.NET Compact Framework, Windows Mobile, P/Invoke ,data marshaling
好吧,先看个例子。为了获得用户按键的状态,下面的代码段演示了将GetAsyncKeyState函数从Coredll.dll中导出,并在托管代码中重命名为 GetMyKeyState供调用:
2
3 [DLLImport( " coredll.dll " , EntryPoint = " GetAsyncKeyState " )]
4 public static extern short GetMyKeyState ( int nKey)
5
6
7 // 调用时候如下:
8 int i = 2 ;
9 short nkeyState = GetMyKeyState(i);
10
在写下上面这几行代码之前,应该确定的是
· 所调用的dll确实存在,且路径正确
· 该dll所依赖的其他dll存在
· 入口点的函数名正确
· 传递给函数的参数在类型上是正确的
上述任何一点出错,都会导致MissingMethodException异常。前三条都比较好理解,往往让人困惑的是在参数的传递上。本地(native) 的libraries只能访问本地数据, 而默认状态下托管代码也只能访问托管的data. 为了使人们在托管的代码环境中仍然能访问到本地的类库,通常需要进行两种环境下的数据类型映射,这就是所谓的marshaling.
Marshaling一个对象的过程就是一个序列化(deflating)的过程, 相应的unmarshaling 就可以看作是反序列化(inflating)的过程.
对于一些简单的数据类型和对象, marshaling通常已经由.NET Framework自动完成了. 对于一些复杂的数据类型,可以使用Marshal类将托管的数据复制到非托管的内存空间上或者将非托管的数据复制到托管的内存空间上。
2 double b = 4.0 ;
3 myRef.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
这里的参数mean只是作为函数的 一个操作产物获取出来
下面来看看在本地代码中应当如何操作以供调用
简单的int型值传递:
2 {
3 return (x+y);
4}
5 // 注意double型的参数为指针形式:
6 extern " C " __declspec(dllexport)
7 void DoubleMean2( double * x, double * y, double * mean)
8 {
9 *mean = (*x + *y) / 2.0;
10}
11
对于constant的常量值类型,在C/C++中我们通常这样定义它们:
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
而在托管代码中我们直接使用const关键字修饰这种常量
2 const int MONDAY = 1 ;
3 const int TUESDAY = 2 ;
4 const int WEDNESDAY = 3 ;
5 const int THURSDAY = 4 ;
6 const int FRIDAY = 5 ;
7 const int SATURDAY = 6 ;
在C#中也可以简单的定义在一个枚举类型里面:
2 {
3 SUNDAY = 0,
4 MONDAY = 1,
5 TUESDAY = 2,
6 WEDNESDAY = 3,
7 THURSDAY = 4,
8 FRIDAY = 5,
9 SATURDAY = 6,
10}
11
前面都是讲的值类型的传递的例子,下面来看看引用类型是如何Marshal的
1. 数组
方案:
C/C++参数:头指针,长度
C#参数:数组实例,长度
以下函数用来计算数组的平均值:
2 int 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}
C#中调用如下:
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));
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给可能改变它的非托管代码。
3. 传结构和类
方案:使用 MarshalAsAtrribute
对于那些只含有简单又通用(通用指的是有相同内存存储结构)的数据类型,直接写就是了。但是很多情况下,我们遇到的都是一些包含复杂元素的数据类型。这时就需要用到MarshalAsAttribute和 UnmanagedType 枚举了.下面一行C#代码的作用是传递一个两个字节空截止(Null-terminated)的Unicode 字符序列到非托管代码中:
void PInvokeAnCFunction([MarshalAs(UnmanagedType.LPWStr)] string s);
再看一个稍微复杂点的
在C++里面我们这样描述:
{
int intAry[10];
char charAry[80];
WCHAR *pStr;
} ;
看看我们在 C#中是如何定义这个结构的:
{
[MarshalAs(UnmanagedType.ByValArray, sizeConst=10)] int[] intAry;
[MarshalAs(UnmanagedType.ByValTStr, sizeConst=80)] string str1;
[marshalAs(UnmanagedType.LPWStr)] string str2;
} ;
.NET CF不能直接Marshal为浮点数,但可以通过指针传递,用IntPtr承接指针,再从IntPtr去Marshal成String。
为了将这种损失降到最小,应该怎么去做呢?显然,在数据混合(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去解决关键的一小部分托管代码不能完成的部分就可以了。
P/Invoke in .NET Compact Framework
黄季冬<fox23> @ HUST All Rights Reserved
2008-1-11