如何在Windows Mobile下使用Native C++动态加载DLL

背景

Windows Mobile Sensors API库的一个重要的功能是自动发现(detect)和判断(determine)设备类型,调用相应设备(HTC或者Samsung)上的Sensor API,并返回统一的结果。因此该库不能依赖于任何设备的上的Sensor库。因此产生了动态加载各自设备上Sensor库的需求。关于Windows Mobile Sensors API库的开发可以参考下面的文章:

Mobile Sensors API - Native unified APIs for Windows Mobile Sensors

Windows Mobile下的重力感应器(Gravitational Sensor)开发

Windows Mobile下使用CppUnitLite输出测试结果

Windows Mobile Sensors API库的设计

 

简介

本文以Windows Mobile Sensors API库为例子讲述在Windows Mobile下使用Native C++动态加载DLL的方法。

 

静态加载DLL的方法

使用Native C++的开发,一般使用静态加载的方法加载DLL,所谓静态加载就是在程序编译时(Compile Time)直接调用DLL的头文件定义的函数,链接时(Link Time)链接*.lib文件指向DLL的接口,在程序开始运行时(Run Time Start up)加载DLL。

下面讲述使用静态加载DLL的方法,在程序中需要指定加载的*.lib文件,用于链接(Link)。

#pragma comment(lib, "SamsungMobileSDK_1.lib")

 

使用静态加载的DLL可以直接调用头文件定义的函数,例如:

SmiAccelerometerCapabilities cap;
if( SmiAccelerometerGetCapabilities(&cap) != SMI_SUCCESS)
{
throw;
}
SmiAccelerometerHandler h = &GetVectorHandler;

if(SmiAccelerometerRegisterHandler(1000, h) != SMI_SUCCESS)
{
throw;
}

SmiAccelerometerGetCapabilities()和SmiAccelerometerRegisterHandler()是定义在头文件smiAccelerometer.h中的,可以直接调用,定义如下:

/**
* Start receiving accelerometer data periodically.
*
* The period interval must be a multiple of the callbackPeriod specified
* in SmiAccelerometerCapabilities. If it is less than the callbackPeriod, it will be
* set to the callbackPeriod. If it is not a multiple of the callbackPeriod, it will be
* truncated to fit the value. ( (period / callbackPeriod) * callbackPeriod )
*
* Only one handler per process is allowed. Successive calls per process will replace the previous handler
* function and period.
*
* @param period [in] callback interval.
* @param handler [in] callback function for every period interval.
*
* @return
* SMI_SUCCESS on success
* \n SMI_ERROR_INVALID_PARAMETER if the handler input parameter is NULL
* \n SMI_ERROR_DEVICE_NOT_FOUND if the device is not present or supported
* \n SMI_ERROR_CANNOT_ACTIVATE_SERVER if the sensor server cannot be started
*/
SMI_API SMI_RESULT SmiAccelerometerRegisterHandler(UINT period, SmiAccelerometerHandler handler);

使用静态加载的方法使用方面还是很方便的,可是在动态加载的时候就不能直接调用头文件的函数了,增加了复杂度,下面会讲到。

 

动态加载DLL的方法

静态加载DLL是比较简单的开发方法,可是有个缺点是程序开始运行的时候就需要加载DLL,如果该DLL不存在,程序就不能启动了。由于Windows Mobile Sensors API库需要自适应具体的设备,也就是说Windows Mobile Sensors API库不能依赖于具体设备的Sensor库,所以不能使用静态加载的方法来引用DLL。下面讲述动态加载DLL的方法。

定义指向函数的指针

动态加载DLL,需要根据头文件来定义指向函数的指针,如下:

typedef UINT (WINAPI * PFN_SmiAccelerometerGetVector)(SmiAccelerometerVector*);
typedef UINT (WINAPI * PFN_SmiAccelerometerGetCapabilities)(SmiAccelerometerCapabilities*);
typedef UINT (WINAPI * PFN_SmiAccelerometerRegisterHandler)(UINT, SmiAccelerometerHandler);
typedef UINT (WINAPI * PFN_SmiAccelerometerUnregisterHandler)();

PFN_SmiAccelerometerGetVector pfnSmiAccelerometerGetVector;
PFN_SmiAccelerometerGetCapabilities pfnSmiAccelerometerGetCapabilities;
PFN_SmiAccelerometerRegisterHandler pfnSmiAccelerometerRegisterHandler;
PFN_SmiAccelerometerUnregisterHandler pfnSmiAccelerometerUnregisterHandler;

这些指向函数的指针可以对应下面的在smiAccelerometer.h头文件的函数进行定义:

SMI_API SMI_RESULT SmiAccelerometerGetVector(SmiAccelerometerVector *accel);
SMI_API SMI_RESULT SmiAccelerometerGetCapabilities(SmiAccelerometerCapabilities *capabilities);
SMI_API SMI_RESULT SmiAccelerometerRegisterHandler(UINT period, SmiAccelerometerHandler handler);
SMI_API SMI_RESULT SmiAccelerometerUnregisterHandler();

定义是一一对应的。参数入口和返回值都必须完全一致。

初始化指向函数的指针

初始化指向函数的指针的过程也就是动态加载DLL的过程,代码如下:

#define SAMSUNG_SENSOR_DLL      L"SamsungMobilesdk_1.dll"
HMODULE hSensorLib = LoadLibrary (SAMSUNG_SENSOR_DLL);
if (NULL == hSensorLib)
{
printf("Unable to load Samsung Sensor DLL\n");
throw std::runtime_error("Unable to load Samsung Sensor DLL");
}

pfnSmiAccelerometerGetVector = (PFN_SmiAccelerometerGetVector)
GetProcAddress(hSensorLib, L"SmiAccelerometerGetVector");
pfnSmiAccelerometerGetCapabilities = (PFN_SmiAccelerometerGetCapabilities)
GetProcAddress(hSensorLib, L"SmiAccelerometerGetCapabilities");
pfnSmiAccelerometerRegisterHandler = (PFN_SmiAccelerometerRegisterHandler)
GetProcAddress(hSensorLib, L"SmiAccelerometerRegisterHandler");
pfnSmiAccelerometerUnregisterHandler = (PFN_SmiAccelerometerUnregisterHandler)
GetProcAddress(hSensorLib, L"SmiAccelerometerUnregisterHandler");

if (NULL == pfnSmiAccelerometerGetVector)
{
printf("Unable to find entry point of SmiAccelerometerGetVector\n");
throw std::runtime_error("Unable to find entry point of SmiAccelerometerGetVector");
}
if (NULL == pfnSmiAccelerometerGetCapabilities)
{
printf("Unable to find entry point of SmiAccelerometerGetCapabilities\n");
throw std::runtime_error("Unable to find entry point of SmiAccelerometerGetCapabilities");
}
if (NULL == pfnSmiAccelerometerRegisterHandler)
{
printf("Unable to find entry point of SmiAccelerometerRegisterHandler\n");
throw std::runtime_error("Unable to find entry point of SmiAccelerometerRegisterHandler");
}
if (NULL == pfnSmiAccelerometerUnregisterHandler)
{
printf("Unable to find entry point of SmiAccelerometerUnregisterHandler\n");
throw std::runtime_error("Unable to find entry point of SmiAccelerometerUnregisterHandler");
}

LoadLibrary()函数动态加载DLL,GetProcAddress()根据函数的名字 加载函数的入口地址 到指向函数的指针。有点绕口,sorry。如果地址不为空,那么可以根据这个地址调用相应的函数。

调用函数

调用函数的方法和静态加载DLL的方法一样,但是不是直接调用函数的名字,而是使用指向函数的指针来调用,下面的例子可以和静态加载DLL函数调用的例子对比来看。

SmiAccelerometerCapabilities cap; 
if( pfnSmiAccelerometerGetCapabilities(&cap) != SMI_SUCCESS)
{
throw;
}
SmiAccelerometerHandler h = &GetVectorHandler;

if(pfnSmiAccelerometerRegisterHandler(1000, h) != SMI_SUCCESS)
{
throw;
}

动态加载DLL的方法就完成了。


.NET的世界

下面这段表述不对,请看下面的回复。.NET使用DllImport属性进行P/Invoke不应该叫做动态加载,因为不能卸载,应该叫做按需加载,就是在call这个函数的时候才加载,而不是在程序启动的时候加载。按需加载和静态加载的区别是加载的时间不一样。
{

在.NET里面P/Invoke一个DLL里面的函数全部都是动态加载(这是错的,谢谢Wuya指出,这里应该叫做按需加载,动态加载的方法可以见Wuya到回复)的,使用DllImport属性来定义。如果SmiAccelerometerUnregisterHandler()函数使用在.NET下会定义如下:

[DllImport("SamsungMobileSDK_1.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern uint SmiAccelerometerUnregisterHandler();

使用.NET比使用Native C++动态加载相对简单。

}


关于Mobile Sensors API项目

这个项目还是在起步阶段,当前实现了samsung的重力感应器,我把项目host到 Mobile Sensors API - Native unified APIs for Windows Mobile Sensors 了,我会持续改进,把各种sensors的实现到这个项目中。

源代码:http://mobilesensor.codeplex.com/SourceControl/ListDownloadableCommits.aspx

环境:VS2008 + WM 6 professional SDK + Samsung Windows Mobile SDK

作者:Jake LinJake's Blog on 博客园
出处:http://procoder.cnblogs.com

作品Jake Lin创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 任何转载必须保留完整文章,在显要地方显示署名以及原文链接。如您有任何疑问或者授权方面的协商,请给我留言
posted @ 2009-09-25 09:10 Jake Lin 阅读(2688) 评论(24) 编辑 收藏

 回复 引用 查看   
#1楼 2009-09-25 08:19 Wuya      
文章不错,看来在windows mobile里面和以前的windows差不多的实现方式。
不过,楼主的
“在.NET里面P/Invoke一个DLL里面的函数全部都是动态加载的,使用DllImport属性来定义。如果SmiAccelerometerUnregisterHandler()函数使用在.NET下会定义如下:

[DllImport("SamsungMobileSDK_1.dll", CharSet = CharSet.Auto, SetLastError = true)]private static extern uint SmiAccelerometerUnregisterHandler();使用.NET比使用Native C++动态加载相对简单。”
这一段说法不好,这种写法实际上也是静态加载了,因为dll文件名“SamsungMobileSDK_1.dll”在编译时已经确定。.net里面也有动态加载的方法,一时想不起来了。

 回复 引用 查看   
#2楼 2009-09-25 08:32 egmkang      
加载
获取函数入口地址
使用
卸载

 回复 引用 查看   
#3楼 2009-09-25 08:43 Wuya      
.net下动态加载dll的方法找到了,以前用asp.net写的片段,大家凑合着看看:
//声明跟动态加载API相关的函数
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll")]
private extern static IntPtr GetProcAddress(IntPtr lib, String funcName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr hModule);
//为动态dll中的函数定义委托
public delegate int AddDelegate(int a,int b);
//加载test.dll,假设里面有一个类似int Add(int a,int b)的函数
string dllFilename="c:\\test.dll";
IntPtr hDll = LoadLibrary(dllFilename);
Response.Write("hDll:" + hDll.ToInt32() + "<br />");
if (hDll.ToInt32() != 0)
{
Response.Write("dll loaded");
//得到Add函数的地址
IntPtr addProcAddress = GetProcAddress(hDll, "Add");
if (addProcAddress != IntPtr.Zero)
{
//将函数地址转换成代委托
AddDelegate Add = (AddDelegate)Marshal.GetDelegateForFunctionPointer(addProcAddress, typeof(AddDelegate));
Response.Write("dll add:" + Add(1, 2).ToString() + "<br />");
}
}
//释放dll
FreeLibrary(hDll);
上面的方式实际上跟c++的API编程差不多,就多了将函数地址转换成委托这一步。
博客园编辑回复的部分有问题,编辑之后原来的回车符给搞没了。

 回复 引用 查看   
#4楼[楼主] 2009-09-25 08:45 Jake.NET      
@Wuya
谢谢指出,我研究一下。因为我在调用P/Invoke的时候,有时候会抛出"Unable to Load ***.DLL"的异常,所以我认为是动态加载的,否则程序启动的时候就会提示错误了。

 回复 引用 查看   
#5楼[楼主] 2009-09-25 08:53 Jake.NET      
@egmkang
你说的对,Windows Mobile Sensor API库里面我把Sensor实现成Singleton,所以不能卸载了,平常使用还是卸载好。

 回复 引用 查看   
#6楼 2009-09-25 08:55 egmkang      
@Jake.NET
我是在N900上面写程序的时候,不得不去看了一下
人家不给提供lib文件...只能动态调用

 回复 引用 查看   
#7楼 2009-09-25 09:00 Google优化      
重力感知器是什么?新的Place Pages
 回复 引用 查看   
#8楼 2009-09-25 09:11 李森 - listen      
那么我想问一下:如果客户只提供.lib文件,我在.NET CF中这样调用呢?上次接了个项目,是在没办法就用Nactive C++搞定了。
 回复 引用 查看   
#9楼[楼主] 2009-09-25 09:30 Jake.NET      
@egmkang
那是挺惨的,他们提供COM吗?还是普通的DLL,但是你用CF做没大区别呀。

 回复 引用 查看   
#10楼[楼主] 2009-09-25 09:31 Jake.NET      
@Google优化
重力感应器的概念可以参考一下:
Windows Mobile下的重力感应器(Gravitational Sensor)开发

 回复 引用 查看   
#11楼[楼主] 2009-09-25 09:32 Jake.NET      
@李森 - listen
据我所知CF没有直接调用静态库的方法,需要先用c++对该静态库封装成一个动态库,然后再P/Inovke。

 回复 引用 查看   
#12楼 2009-09-25 09:41 Jerry Qian      
是不是win下面的LoadLibrary呢
 回复 引用 查看   
#13楼 2009-09-25 10:06 egmkang      
@Jake.NET
他们做的是动态库,不是COM,只给头文件,有一些因为某种原因连头文件也没得...
他们的lib文件里面没有代码,只有函数的入口点之类的信息....

 回复 引用 查看   
#14楼[楼主] 2009-09-25 10:12 Jake.NET      
@Jerry Qian

 回复 引用 查看   
#15楼[楼主] 2009-09-25 10:12 Jake.NET      
@egmkang
挺麻烦的。

 回复 引用 查看   
#16楼 2009-09-25 14:36 OwnWaterloo      
准确的说,这不叫"动态加载"。
加载一个模块有3个时期:
1. 编译时 —— 静态库
2. 启动时 —— 隐式链接到动态库(implicit linking)
3. 运行时 —— 使用LoadLibrary/FreeLibrary, dlopen/dlclose

LoadLibrary/FreeLibrary就已经是运行时"加载"了。

在其之间的GetProcAddress, dlsymbol叫链接(linking)。
和起来叫explicit linking —— 显示连接到一个动态库。
与其相对的是上面提到的隐式链接到动态库。

所以, "静态加载"也是不准确的。 只有静态库才会在编译时加载。
动态库都是动态加载的 —— 启动时或者运行时。


抬杠到此为止…… 下面说正题。
引用Wuya:
使用.NET比使用Native C++动态加载相对简单。

简单不是无代价的。
在native C/C++中, 如果调用者见到的函数声明与函数实现不一致, 就会出错, 一般来说肯定会导致程序跑崩掉。
最常见的不一致是参数、返回值都相同,但调用约定不一致。

如果仅仅是调用约定不一致,P/invoke不会导致程序崩溃, 不会导致内存泄露, 不会…… 简直就像不存在这回事情一样。
我怀疑.net实现P/invoke时, 是要专门实现一个栈, 去调用非托管代码(是这样称呼的吧?), 并且, 这个栈是每次调用都新建, 调用完毕后收回 —— 否则不匹配的调用约定要么导致内存泄露,要么导致超出栈底。


仅仅是猜测啊, 期待大牛解释。


3楼提到的方式,就没去试过了。
通常我们只需要去学"如何正确使用"。
而上面的"错误使用", 是因为有一个跑得好好的程序, 突然有一天发现其实调用约定没有匹配……
才猜测P/invoke可能会对每次调用都创建一个栈。

如果参数、返回类型不匹配会怎样? 就没试过了。

 回复 引用 查看   
#17楼 2009-09-25 15:07 OwnWaterloo      
引用OwnWaterloo:
加载一个模块有3个时期:
1. 编译时 —— 静态库
2. 启动时 —— 隐式链接到动态库(implicit linking)
3. 运行时 —— 使用LoadLibrary/FreeLibrary, dlopen/dlclose


一时糊涂, 说错了。 其实它们都叫链接, 目的是为了决议符号名的地址。至少有以下4种决议时期:

1. 编译时

static void f() { ... }
void g() { f(); }
g中对f的调用可能在编译时地址就决定了


/* extern */ void f();
void g() { f(); }
g中保留一个符号名引用, 该引用的地址可以在以下时期计算出。

2. 链接时
f来自某个目标文件或者库文件。

3. 加载时(这里的加载是指可执行文件被加载)
隐式链接到动态库就会发生这种情况

4. 运行时
显式链接是会发生这种情况。
使用 Load/Free 加载模块
使用 Get 链接到模块中的函数。



 回复 引用 查看   
#18楼[楼主] 2009-09-25 22:18 Jake.NET      
@OwnWaterloo
谢谢你的回复,我reflect了DllImport属性,可是看不到他怎么实现的,我想P/Invoke应该不是 栈 的方式处理进出口参数。因为我碰到过参数处理不好内存泄漏的问题。

我想你说的四种链接方式和我说的三种加载方式是一样的,你说的是功能函数体连接到程序的时期,而我说的是DLL加载到程序的时期。

编译时,如果函数体在源代码中,根本没有调用DLL的需要,没有DLL加载的过程。

链接时,是指链接静态库文件,功能函数体最终加入到目标文件中。

加载时,也就是我说的静态加载DLL从而实现调用DLL的函数体的功能,程序启动的时候加载。

运行时,也就是我说的动态加载DLL,并调用DLL的函数体,用完可以卸载该DLL。

P/Invoke我认为是按需加载,调用的时候加载才加载DLL,没有卸载过程。

请继续指教,谢谢!

 回复 引用 查看   
#19楼 2009-09-25 22:40 OwnWaterloo      
@Jake.NET

我说了嘛, 那只是抬杠而已-_-
因为"显式链接"经常被说成"动态加载"……
叫什么其实无所谓。
如果能有统一与准确点的术语, 交流会更容易一些。


P/invoke加载的生命周期我也不知道,它会做什么处理我也不知道。
但可以作一些猜测。

比如, 要通过P/invoke调用如下函数:

int __cdecl f(int i) { return i*2; }

假设f被生成为i386下的代码,那它(函数体):
1. 一定会从[esp+4]中取得i
2. 一定会将i*2放入eax
3. 一定会使用ret返回
包含从[esp]中弹出返回地址, 并返回到那个地址去继续执行。
4. 一定会在返回后,在栈中留下4字节的i。

因为C/C++编译器可不会管这段代码会不会被P/invoke


那.net进行P/invoke的时候,在跳转到f入口点之前, 就必须为非托管代码准备好这些条件:
1. [esp+4]中是i
2. [esp]中是返回地址
3. 返回后, 从eax中取结果

平衡栈这一步嘛……

因为我上次发现问题的代码是:
C端使用 __cdecl
C#端使用 Stdcall

C端的 __cdecl每次都会留下4字节的i。
C#端肯定要做点什么事情, 才能将这4字节给清理掉, 比如每次重新设置好一个栈。
这就是我上面猜测P/invoke会在每次调用时, 都创建一个栈的原因。

 回复 引用 查看   
#20楼[楼主] 2009-09-26 17:57 Jake.NET      
@OwnWaterloo
谢谢你很认真的回复,我对P/Invoke内部实现还不是很了解,需要找资料学习学习。我在写文章之前就reflect了一下,想找出原理,可是没找到源码,所以根据自己经验,P/Invoke的时候才检查DLL的存在性,认为是一个按需加载的范畴。但是这方面的细节需要仔细研究才行。

 回复 引用 查看   
#21楼 2009-09-26 18:11 OwnWaterloo      
@Jake.NET
你的方向是对的。

对使用p/invoke来说, 了解什么时候加载、检查、卸载, 比其实现机制来得重要。 重要得多。


我提到那个调用约定不匹配事件,也是想顺便看看有没有人去深究过。
如果有,就捡个现成~_~
没有就算鸟。 只要我保证匹配,就可以使用了。

上次不匹配是从别人那接过来。 根本就没去注意这点。
因为在native c/c++中养成的直觉是:一旦不匹配, 立马就会崩掉。
换句话说, 只要没崩, 调用约定肯定是匹配的。
而且msvc在debug默认配置下, 还会专门作检查, 会报出一个很醒目的错误。
警惕性比较低。


而很久很久之后,我随便翻了翻C#端的代码,心都凉了……
做了个demo, 又发现是虚惊一场。


 回复 引用 查看   
#22楼 2009-09-27 14:05 egmkang      
P/Invoke应该是在用那个DLL的时候,才去加载,这个可以从Managed Ado.NET for SQL CE看出来.

关于卸载,我猜测,注意,是猜测,是发生在程序退出的时候.
程序退出,DLL的引用计数器-1,估计,到0了,OS把他卸载.

之前碰到过,程序被强迫退出(被驱动),没发生任何资源释放,后来再加载那些DLL出错.....

 回复 引用   
#23楼 2009-11-19 20:15 Iameasy_man_01
#pragma comment(lib, "SamsungMobileSDK_1.lib")
其实这个不需要添加,只需要在程序中指明dll的具体位置即可
Jake兄不妨试一下。

 回复 引用 查看   
#24楼[楼主] 2009-11-19 20:37 Jake Lin      
@Iameasy_man_01
这个是为了链接动态库的时候加上的,也可以在项目属性里面配置。
如果动态加载DLL的话,确实不需要连接那个lib了。