一、
P/Invoke
在
.NET Compact Framework
的支持下,可以方便高效地开发出适合于移动设备的应用程序,而不需要去考虑特定的硬件环境。
.NET Compact Framework
向开发者屏蔽了硬件底层的细节,使开发者可以集中精力于业务逻辑的解决方案。
作为
.NET Framework
的一个子集,
.NET Compact Framework
只提供了
.NET Framework
的一部分功能,因此有时在实现一些功能时不得不借助于
Windows CE API
。另外还存在一些第三方的组件
/
资源,或以动态链接库形式提供,或者已经是
COM
组件。相对于
.NET Compact Framework
,它们都属于非托管资源。我们需要一种功能,实现由托管环境访问这些非托管资源。和
.NET Framework
一样,平台调用
P/Invoke(Platform Invocation Services)
提供托管代码调用驻留于
DLL
中的非托管函数的功能。下面是一张P/Invoke原理图,来自
MSDN
。
归纳起来,使用
P/Invoke
的场合包括:
1
、
.NET Compact Framework
没有实现某功能,需要借助
Windows CE API
;
2
、已有
DLL
或
COM
组件等资源,希望能充分利用,减少开发成本和风险;
3
、鉴于
DLL
的执行性能和反编译能力都可能高于
.NET Compact Framework
,借助
DLL
提高程序性能和安全性。当然关于
DLL
的执行性能是否高于托管代码,不能一概而论。
二、
.NET Compact Framework
下的
P/Invoke
先看一个
P/Invoke
的例子。下面使用
DllImport
特征导入
Windows CE
的
API
函数
MessageBoxW
的定义。
public
class APIHelper
{
[DllImport("coredll.dll", SetLastError = true)]
public
static extern int MessageBoxW(IntPtr hWnd, String text, String caption, uint type);
}
然后可以对它进行调用。
private
void button1_Click(object sender, EventArgs e)
{
APIHelper
.MessageBoxW(IntPtr.Zero, "
测试
MessageBoxW
函数
"
,
"api
调用
"
, 0);
}
可以看到,使用 P/Invoke 包括声明和调用两个过程,另外还有一个错误处理的过程。通过声明来指定要调用的非托管函数, .NET Compact Framework 也是使用 DllImport 特性来进行声明,包括模块名、函数名及调用约定。与 .NET Framework 完整版的 DllImport 特性不同, .NET Compact Framework 的一共包括五个公共字段: CallingConvention , CharSet , EntryPoint , PreserveSig 和 SetLastError 。具体各字段的说明可以参考 MSDN 。
EntryPoint
可以指定为函数名或函数的序号值,如
EntryPoint = "MessageBoxW"
或
EntryPoint = "#858"
。值得注意的是
.NET Compact Framework
下
CallingConvention
只支持
CallingConvention.Winapi
,即默认的平台调用;编码方式只支持
Unicode
,因此
CharSet
实际只有
CharSet.Unicode
一个取值。因此在导入定义时省略
CallingConvention
和
CharSet
字段的效果没有分别。
另外,
DllImport
修饰的方法必须用
static
和
extern
关键字来指明方法是在外部实现的,对其可见性修饰符则没有限制。
调用
DllImport
导入的非托管函数时,
CLR
的
P/Invoke
服务从声明中提取出元数据,定位要调用的模块(
coredll.dll
),将其加载到内存,然后根据入口点信息检索非托管函数地址。如果不出现错误,则
P/Invoke
完成参数的封送并调用该函数,并把返回函数的返回值。
P/Invoke
会产生两种错误,一种是上面说到的
P/Invoke
在定位调用模块,检索函数地址时出错。如
P/Invoke
找不到入口点时会出错,并抛出
MissingMethodException
异常;函数的调用约定声明有误时会抛出
NotSupportedException
异常,这时应检查函数的参数及返回值定义是否与模块中函数吻合。
P/Invoke
的另一种错误是执行非托管函数过程中发生的错误。
另一个需要特别注意的是,
.NET Compact Framework
下
P/Invoke
不支持回调,即无法向非托管函数传递一个委托并在非托管函数中被调用。使用需要回调的非托管函数时会引发异常。
三、
P/Invoke
的参数封送
我们知道,托管代码与非托管代码存在很大的差异,
P/Invoke
在传递参数、返回值时需要先在托管类型和非托管类型之间进行转换,这整个过程有个专门术语,就叫做封送
(Marshal)
。
P/Invoke
可以为常规的数据类型进行正确地封送处理,如整型
int
,字节型
byte
。这些常规的数据类型被称为
blittable
类型,它们在托管代码和非托管代码中具有相同的表示,
P/Invoke
在对它们进行封送时不需要进行任何特殊处理。因此使用
blittable
类型的非托管函数以用相同表示的托管类型导入其定义。假如有一个
DLL
中的函数的
C++
签名为:
int
DllFunction_1(int index);
按如下方式导入定义:
[DllImport("dllfile.dll")]
public
static extern int DllFunction_1(int i);
在这种情况下,
P/Invoke
不需要进行任何特殊处理,可以正确地进行参数和返回值的封送。
具体
blittable
类型包括:
托管代码类型
|
C++
代码类型
|
System.Byte
|
unsigned char
|
System.SByte
|
byte
|
System.Int16
|
short
|
System.UInt16
|
unsigned short
|
System.Int32
|
int
|
System.Int64
|
long
|
System.UInt64
|
unsigned long
|
System.IntPtr
|
指针、句柄
|
System.Boolean
|
bool
|
System.Char
|
wchar_t
或
TCHAR(16
位
UNICODE
字符
)
|
System.String*
|
TCHAR *
或
LPWSTR(UNICODE
字符数组
)
|
不仅以
blittable
类型本身作为参数或返回值的非托管函数可以用相同表示的托管类型导入其定义,只包含
blittable
类型(除标记
*
的
System.String
外)字段成员的构成的类或结构同样遵守这条规则。需要特别说明的是
System.String
是一个特例,它作为字段成员构成类或结构时不遵守这条规则。
然而,其它的类型无法不能直接按这种对应方式导入调用约定。假如有一个
DLL
中的函数的
C++
签名为:
double
DllFunction_2(int index);
按如下方式导入定义:
[DllImport("dllfile.dll")]
public
static extern double DllFunction_2(int i);
在这种情况下,会引发一个
NotSupportedException
异常。这是因为托管世界的
double
类型和非托管世界的
double
类型(这里是
C++
)有着显著的不同,
P/Invoke
无法正确地将返回值从非托管的
double
类型转换到托管
double
类型,因此调用失败。应该说,
P/Invoke
同样无法正确的完成参数从托管
double
类型转换到非托管的
double
类型,但测试发现,实际上是可以完成的。如下
C++
签名为:
int
DllFunction_3(double index);
按如下方式导入定义:
[DllImport("dllfile.dll")]
public
static extern int DllFunction_3(double d);
作为参数值的托管
double
类型实际上可以被正确转换为非托管的
double
类型,
P/Invoke
正确地完成了封送。但
MSDN
文档明确说明
.NET Compact Framework
的
P/Invoke
是无法支持对浮点类型通过值进行封送处理的,因此我们不能依赖作为参数的
double
类型可以被封送这个特例。在需要在托管世界和非托管世界之间传递
double
类型时,应该借助引用来对其进行封送,该引用将由
P/Invoke
封送为非托管世界的指针。下面是一个处理的例子
//
原有函数,由于返回值是
double
类型,无法被封送,需要改写
double
DllFunction_2(int index);
//
对
DllFunction_2
函数加上一个包装,改为以指针作为参数值递返回值
void
DllFunction_Sub(int index, double * result)
{
*result = DllFunction_2(index);//
这里调用原有函数
}
按如下方式导入定义,注意导入的函数是
DllFunction_Sub
:
[DllImport("dllfile.dll")]
public
static extern void DllFunction_Sub(int index, out double result);
可以为托管代码的调用再增加一个包装,保持与实际调用函数具有相同的签名:
public
static double DllFunction_2(int index)
{
double
result;
DllFunction_Sub
(index, out result);
return
result;
}
通过增加一个包装这种间接的方式,我们利用
P/Invoke
可以对引用进行封送的特性,完成了对实际具有如下签名的非托管函数的调用:
double
DllFunction_2(int index);
四、使用
P/Invoke
调用
Windows CE
的
API
coredll.dll
是
Windows CE
的核心模块,大致相当于
Windows 2000/XP
的
kernel32.dll
。
coredll.dll
是
Windows CE
系统最重要的文件,基本上每个
CE
系统都会在
ROM
中包括该文件。在
Pocket PC 2003 Second Edition(Window CE 4.2
,以下简称
PPC)
系统中,其全路径为
/windows/coredll.dll
。由于该文件在
ROM
中,因此使用
PPC
自带的资源管理器无法看到该文件。可以通过
TotalCommander
查看该文件,但仍然无法复制该文件。在安装了
platform builder
后可以在安装目录下找到
coredll.dll
文件。
绝大多数的
Windows CE API
都是通过
coredll.dll
向外暴露。因此在使用
P/Invoke
调用
coredll.dll
中的
api
时,值得关心的是该文件中所包含的
api
函数。可以通过
dumpbin.exe
来查看其导出符号。
在无法直接得到
coredll.dll
时,可以通过该文件对应的
LIB
导入文件进行分析而获知
coredll.dll
包含的
api
函数。安装
vs2005
后,可以在
[
安装目录
]/SmartDevices/SDK/PocketPC2003/Lib/armv4
下找到
coredll.lib
文件,它就是
coredll.dll
对应的
LIB
导入文件。运行
vs2005
命令行工具,执行
dumpbin /exports [
安装目录
]/SmartDevices/SDK/PocketPC2003/Lib/armv4/coredll.lib
即可得到
coredll.dll
包含函数的列表。如果进一步需要得到某个
api
函数的参数及返回值类型则需要进行深入逆向工程分析了,就比较复杂。好在这些可以从微软的
MSDN
文档中得到某
api
的具体说明。
当需要确定其它
dll
中有什么函数时,同样可以通过使用
dumpbin
导出查看其中包含的函数。
文中代码全部在
vs2005(C#
智能设备
Pocket PC 2003
应用程序和
MFC
智能设备
DLL)
和
PPC
模拟器下运行通过。