本文亮点:无需使用反射,无需动态生成方法,无需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基类,在基类中完成库的加载和释放过程。