C++如何测试dll_VB.NET动态静态加载Win32 C++ DLL并调用API函数优雅方法总结

a2b5604b75debca7b1f79c58593c88ed.png

本文亮点:无需使用反射,无需动态生成方法,无需CallWindowProc!

本文毒点:如果你不知道要调用的API函数原型(函数名、参数类型、返回值类型),本文不能帮助你,建议关注API Monitor、反向工程等话题。

静态加载

所谓静态加载,是指你在编码时就知道DLL文件的位置,用户有义务保证运行时可以在那个固定的、不允许变更的位置找到DLL文件。这种情况可以使用简单的静态调用。

静态调用十分简单,官网文档就有详解,但是出于完整性考虑还是在这里解说一下。

Imports 

最典型的两种静态加载调用的方法:

  • Declare Function/Sub 函数名 Lib DLL路径 Alias 入口点 (参数列表) As 返回类型。如果指定Sub则必须去掉As 返回类型。这是一种比较简单的声明方法,但是无法实现某些特殊功能。如果在Public类或模块中声明,应当加上Private访问限制。如果参数列表中有String,应当用MarshalAs特性注明函数原型接受的是LPWStr还是LPStr。参数类型是重要的,参数名称可以任意。如果你声明的函数名就是其原名,Alias 入口点可以省略。
  • <DllImport(DLL路径, 其它属性列表)>一般的函数声明。这是一种复杂的声明方法,可以实现一些高级功能。例子中指定了BestFitMapping字段,因为GetProcAddress接受的是LPStr类型而不是LPWStr类型的String。还有一个EntryPoint字段,这是因为我想在我的程序中用Fg_InitLibrariesExGpa这个名字而不是原名调用它。如果使用原名,则无需指定EntryPoint。注意尽管这只是一个函数声明没有定义内容,End Function不能省略。此外如果该声明出现在类中,必须加上Private Shared修饰。

有一些要注意澄清的地方。

  • 在你的代码中应该使用你定义的函数名而不是函数原名,如果你声明了不同于原名的函数名的话。
  • 永远不要将这些外部过程声明为Public访问。如果必须要公开这些功能,请另外定义函数间接调用。
  • 不能在泛型类中声明这些方法,因为Declare和DllImport方法都不支持类型参数。
  • Sub必须没有返回类型,Function必须声明返回类型且不能为Object。
  • C:WindowsSystem32中的DLL,以及和exe可执行文件同目录的DLL,可以只提供文件名,否则应当提供绝对路径。
  • 如果函数原型接受LPStr类型的String,不要用Declare声明,请使用DllImport。如果为LPWStr,可以使用Declare,但需要紧接那个参数前面加上<MarshalAs(UnmanagedType.LPWStr)>特性。这两种都可以用DllImport声明,都需要设置BestFitMapping字段为False。
  • 编译时,编译器是不会去检查你的声明是否符合目标DLL的要求的,所以即使你声明不符合函数原型也能通过编译,但是运行时可能会出错
  • 以上提到的编码要求,有些是通过编译所必需的,有些则是建议的最好做法。为什么这样建议,官网文档都有详细解释。

动态加载

所谓动态加载,是指你在编码时不知道DLL文件的位置,需要在运行时搜索,或者由用户提供自己的DLL。尽管如此,你仍然需要在编码时知道函数原型,否则就超出本文讨论范围了。

动态调用通常采用kernel32.dll提供的LoadLibraryW、GetProcAddress工具链。前者可以在运行时加载DLL,后者可以根据函数名返回函数所在的内存指针。

但是,http://VB.NET不支持调用函数指针,所以GetProcAddress返回的函数指针通常无法直接调用。官方提供的解决方案是用C++编写一个Windows运行时组件.winmd,网上其它的办法还包括CallWindowProc、反射动态生成方法等。这些方法无一例外都是迂回婉转的,而且有许多缺点和限制。本人亲测有效一种找遍全网都没人贴出、官方也没有提供支持文档的优雅简洁直观高效方法。

Imports 

如果你去看网上搜到的其他动态教程,就会发现我的方法是何等简洁优雅。这个方法最天才的地方就在于,巧妙地违背了按照函数原型进行声明的一般原则,让这个声明为自己的目的服务。之前提到编译器是不会检查声明是否符合要求的,但即使不符合也可能正常运行!这里就是一个鲜活的例子。其原理是GetProcAddress返回的是函数指针,http://VB.NET虽然表面上不支持函数指针,但其委托类型本质上就是个限定了签名的函数指针。由于托管平台无法识别非托管代码返回的二进制数据,只能根据开发者指定的类型进行解析,所以某种意义上强类型的http://VB.NET在这里出现了破绽——这个返回值,你说是啥类型就是啥类型!在托管平台内部IntPtr是无法转换为委托类型的,但是在托管与非托管的交界地带,巧妙实现了强制转换。

这个方法也有一定的缺陷,就是如果你需要从动态库中加载多个函数,你必须要为每个函数声明一个Delegate和一个GetProcAddress,这些GetProcAddress必须声明为不同的函数名,利用DllImport的EntryPoint字段将它们指向同一个实际的外部过程,并返回各自定义的委托。下面给出一个实际应用的例子:

Public 

可见上述代码中,SisoHal类对应的库,我们需要调用其中4个外部过程,因此必须声明4个委托、4个GetProcAddress、4个委托方法属性,并在构造函数中一一加载验证。可以取巧的是,如果有几个外部过程具有相同的签名,即各个输入参数的类型和返回类型都相同,只有过程名不同,它们可以共用一个委托和GetProcAddress。但是本例中4个外部过程签名各不相同,所以只能分别定义4个。这种繁杂的定义,归根到底是因为DllImport不支持泛型……

如果你的程序中需要动态加载多个外部DLL库,建议的做法就是按照上述设计模式,每个库定义一个类,共同继承自一个MustInherit基类,在基类中完成库的加载和释放过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值