调用动态链接库
背景知识
动态链接库的英文是:Dynamic Linkable Library,简称 DLL。从字面上理解,它是一种“程序库”。库内存放的是可供应用程序使用的函数、变量等。
“动态”是与“静态”相对应而来的。这里的动态和静态是指链接库中代码与使用它们的应用程序之间的链接方式。如果采用静态链接库,在生成应用程序时,库中的函数等都会被直接放入最终生成的可执行文件中;而使用动态链接库时,库中的函数等不会被放到可执行文件中去,而是仍然保留在 DLL 文件内。当程序被运行时,再链接到动态链接库中的函数和变量等内容。
静态库的局限性比较大,C 语言编写的静态库只能在 C 语言中使用,LabVIEW 无法调用。而动态链接库则可以在多种编程语言中通用:使用某一种语言编写出来的 DLL 可以在另一种语言编写的程序中使用。比如,使用 C 语言编写的 DLL 可以在 LabVIEW 中使用,反之亦可。
动态链接库的加载方式又分“动态”与“静态”两种。这里的动态和静态是指应用程序运行时,动态链接库代码被载入内存的方式。常用的方式是静态加载,指动态链接库在应用程序启动时,随应用程序一起被载入内存。而动态加载方式是指,应用程序启动时,并不载入动态链接库。只有在使用到动态链接库中某个函数时,才把动态链接库载入内存。
DLL 最大的优势在于代码共享,只要某一功能以 DLL 的形式提供出来了,其它的应用程序就可以直接使用这一功能,而不必再实现一份相同的代码了。
DLL 的使用非常普遍。比如 Windows 操作系统提供给应用程序调用的功能,就是以 DLL 的形式公布出来的。LabVIEW 中若需使用某个系统功能,如读写注册表等,即可通过调用 Windows 提供的 DLL 函数来完成。Window 提供的这些完成系统功能的函数也被称为 Windows API。在常用的几个系统 DLL 中,kernel32.dll 提供了内存管理和进程调度相关的函数,user32.dll 中的函数则主要用于控制用户界面,gdi32.dll 中的函数则负责图形方面的操作。在 32 位操作系统中,这些 Windows API 的 DLL 都被保存在 system32 目录下。
很多硬件设备的驱动程序也往往是以 DLL 方式提供的。此外,在互联网上还可以找到各种各样的 DLL。如果需要解析某种文件,需要使用某些常用的算法等等,都可以先去互联网上搜索一下,是否有相关的 DLL 库可供直接使用。
在 LabVIEW 中,经常会遇到需要使用 DLL 的情况,比如,在程序中使用到某个以 DLL 方式提供的第三方的驱动程序或算法。再比如,在一个大项目的开发中,出于效率和开发人员喜好等因素的考虑,可能会使用 C++ 语言实现软件的运算部分,并把这些功能构建在 DLL 文件中,再使用 LabVIEW 编写程序的界面部分,并通过调用编写好的 DLL 来调用运算部分的功能。
LabVIEW 程序员在使用 DLL、ActiveX 控件等之前,至少先要了解它们的功能和用法。比如初次使用某个 DLL 时,常常会遇到这样的情况:调用了一个函数但没有得到正确的运行结果。遇到这类问题,首先要查清是自己不了解 LabVIEW 调用 DLL 函数的用法,还是不了解那个 DLL 的使用方法。熟悉 C 语言的用户,可以先在 C 语言下尝试能否正确使用这个 DLL。如果在 C 语言下可以,但在 LabVIEW 下却不能,那说明是在 LabVIEW 调用 DLL 时错了。学习完本章的内容应该可以解决这个问题了。如果在 C 语言下,也不能正确调用这个 DLL,那说明是对这个 DLL 的使用方法还没理解,就应当先学习弄懂这个 DLL 的有关资料。
CLN 和 CIN 节点
在 LabVIEW 中,通过“互联接口 - > 库与可执行程序 - > 调用库函数”节点来调用 DLL 中的函数。调用库函数节点常被简称为 CLN 节点,它是英文 Call Library Function Node 的缩写。
在同一函数选板上,它旁边的一个节点是“代码接口”节点(Code Interface Node),简称 CIN 节点。在 CLN 节点出现以前,LabVIEW 只能通过 CIN 节点调用 C 语言编写的函数。现在有了 CLN,可以不再考虑使用 CIN 了。CIN 节点不能调用动态链接库中的函数,它只能调用按照特定方式编译出来的程序代码。稍有差错,程序就无法正常运行。CIN 所调用的程序模块不通用,而且限制颇多,CLN 节点出现之后,很少有人再使用 CIN 节点了。
在 LabVIEW 中调用 DLL 中的函数,最大的困难在于把函数参数的数据类型映射为相应的 LabVIEW 中的数据类型。在着手手工设置 CLN 节点前,可以优先考虑使用导入共享库工具用以自动生成配置 CLN 节点。这个工具在菜单“工具 - > 导入 - > 共享库”中。它专门用于把 DLL 中的函数包装成 VI,生成的每个 VI 中最主要的部分就是一个 CLN 节点,它能够自动设置函数的参数。这个工具在大多数情况下,都能够把 DLL 中的函数包装成可以正确运行的 VI。如果你有现成的 DLL,打算在 LabVIEW 中使用,可以考虑首先用这个工具,把 DLL 中所有的函数都包装成 VI。再在其结果上继续改进,就方便多了。这个工具可能无法直接处理一些非常特殊的数据类型(如字符串数组)和函数(如回调函数),所以,即便有工具我们还是需要了解一下 LabVIEW 是如何调用 DLL 中的函数、以及如何手工配置 CLN 节点的。
需要注意的是,如果 DLL 使用了 C++ 的类作为接口,这样的 DLL 是没办法在 LabVIEW 中直接调用的。CLN 节点只能调用符合标准 C 语言函数接口的 DLL。若项目中必须使用某个 C++ DLL 时,可以在其上再用 C 语言写一个 C 接口 DLL,作为它和 LabVIEW 之间的中间层。LabVIEW 调用这个中间层 DLL 提供的函数,中间层函数再调用 C++ 接口的 DLL 函数。
DLL 的加载方式
一个 CLN 节点刚被拖到程序框图上的时候,外观如下图所示:
还需要对它进行配置后,才能够使用。双击这个 CLN 节点,就会出现它的配置对话框。这个对话框有四页。第一页是被调用函数的信息:
“库名或路径”栏,用于填写 DLL 文件名和 DLL 的全路径。在系统路径下的 DLL,直接输入文件名即可,否则需要全路径。若没有勾选“在程序框图中指定路径”,则 DLL 是被 LabVIEW 静态加载到程序中的,也可被称为 LabVIEW 程序静态调用了这个 DLL。在调用了这个 DLL 的 VI 被装入内存时,DLL 也同时被装入内存,虽然这时,这个 VI 也许还没有被运行。在这个 VI 运行完毕后,也不会把 DLL 卸载出内存。要一直等到所有使用了这个 DLL 的 VI 被关闭后,这个 DLL 才会被卸载出内存。
若勾选了“在程序框图中指定路径”选项,那么对话框中配置的 DLL 就是无效的。CLN 节点会多出两个 "路径" 接线端,以便在程序框图中输入 DLL 的路径:
此时,LabVIEW 是动态加载 DLL 的,或者说 LabVIEW 是动态调用 DLL 的。因为在 VI 运行到这个 CLN 节点之前,都不能确定输入的“路径”是什么,所以自然也无法加载相应的 DLL。只有当运行到这个 CLN 节点时,LabVIEW 才把要用到的 DLL 装入内存。在这个 CLN 节点运行结束后,LabVIEW 并不会立即把 DLL 卸载出内存。如果后续的 CLN 节点也使用了这个 DLL 文件,就不需要再重新加载了。只有当程序传一个空路径给 CLN 节点时,LabVIEW 才会把已经加载的 DLL 文件卸载。
加载 DLL 文件通常是一个比较耗时的工作。采用静态加载方式,程序所用到的 DLL 在程序启动时都被装入内存,程序启动时间会比较长。而使用动态加载方式,程序运行到需要用到 DLL 的时候才被加载,缩短了程序的启动时间,把这部分时间转移到了程序运行中。
还有一种情况,使用动态加载的优势比较明显。假如程序功能很多,调用了多个 DLL 文件。而程序每次运行时,往往只用到其中部分 DLL。如果使用静态加载,不管有没有用到,所有的 DLL 都要被装入内存。如果此时缺失了某个 DLL,即便程序运行时不需要用到它,程序也会因为在启动时找不到它而无法运行。采用动态加载方式,只是把那些程序运行时用到的 DLL 装进来。这样,提高了程序效率,又不会因为缺少某个暂时用不到的 DLL 而影响程序运行。
函数的配置
CLN 节点配置对话框中,“函数名”一项用于输入需要调用的 DLL 中的函数。如果是选用静态加载方式,并已输入了正确的 DLL 文件全路径,这里会列出 DLL 中所有允许被外部调用的函数,用户只要在下拉框中选取一个即可。
“线程”选项用于选择被调用的 DLL 函数在何线程内运行。CLN 节点的线程选项只有两项:“在 UI 线程中运行”和“在任一线程中运行”。在程序框图上直接就可以看出一个 CLN 节点是选用的什么线程。如果是“在 UI 线程中运行”,节点颜色是较深的桔黄色;如果是“在任一线程中运行”,则节点是比较淡的黄色:
“在 UI 线程中运行”是指在 UI 线程(即界面线程)中运行被调用的函数。LabVIEW 程序不论多么复杂,都只会有一个界面线程。这个线程用于处理所有与界面相关的工作,如显示一个数据,产生一个用户事件等。由于程序中只有一个界面线程,如果把多个被调用的函数都设置为在界面线程中运行,就可以确保这些函数在同一线程内运行。
LabVIEW 除了界面线程之外,还有多个其它执行线程,用于执行程序框图中的代码。如果选择“在任一线程中运行”,就不能确定 LabVIEW 会在哪个线程内运行这个 DLL 函数。
可以按照以下判断方法,选择 CLN 节点中的线程设置:如果被调用的动态链接库是多线程安全的,就选择“在任一线程中运行”;否则,动态链接库就不是多线程安全的,就得选择“在 UI 线程中运行”。选择在任一线程中运行一个 DLL 函数,程序的运行效率比较高。因为 LabVIEW 可以把 DLL 函数放在与其前后程序代码相同的线程内执行,这样就省去了线程切换的开销。并且,该设置允许 LabVIEW 在不同的线程内同时调用同一个 DLL 函数,并行执行的速度通常比串行高一些。但是,如果 DLL 不是多线程安全的,也就意味着在不同线程内同时调用 DLL 中的函数可能会出现错误,那么必须禁止这种情况的出现。这时,只能把 CLN 节点设置为“在 UI 线程中运行”,以确保所有 DLL 函数都只在一个线程内运行。
判断一个动态链接库是否是为线程安全的,也需要费一番心思。如果这个动态链接库文档中没有明确说明它是多线程安全的,那么,为稳妥起见,应该把它当成非多线程安全的。熟悉 C 语言的用户可以查看一下动态链接库的源代码,若代码中存在全局变量、静态变量或者代码中看不到有使用信号量,关键区等保护措施的,这个动态链接库也肯定不是多线程安全的。
关于 LabVIEW 中几种不同线程的详细介绍可以参考 多线程编程 一节。
“调用规范”(Calling convention),用于指明被调用函数的参数压栈规范。CLN 节点支持两种规范:stdcall 和 C call。它们之间的区别在于,stdcall 由被调用者负责清理堆栈,C call 由调用者清理堆栈。如果调用规范设置错误,可能会引起 LabVIEW 崩溃,所以一定要小心。反过来说,如果 LabVIEW 调用 DLL 函数时出现异常,首先就应该考虑这个设置是否正确。
作为 DLL 的使用者,往往不需要关心调用规范的实现细节,只要知道如何判断被调用的 DLL 采用哪种规范就可以了。一个简单的判断规则如下:Windows API 一般使用 stdcall,标准 C 的库函数大多使用 C call。如果函数声明中有类似 " _ _ stdcall" 这样的关键字,它就是 stdcall 的。
简单数据类型参数的设置
CLN 节点配置对话框的第二页是配置参数页:
使用 CLN 节点,最困难的部分就是把函数参数的数据类型映射为相应的 LabVIEW 中的数据类型。在 DLL 和 LabVIEW 之间传递参数,最常用的三种数据类型是:数值、字符串和数值型数组。这几种类型的参数配置还是比较简单的。
数值类型
如果掌握一点 C 语言的背景知识,就会发现 LabVIEW 多种不同精度的数值类型与 C 语言中的数值类型的匹配还是相当直观的。比如“4 字节单精度”数据类型对应 C 语言中的 float 数据类型。LabVIEW 自带的例子 " [ LabVIEW ] \ examples \ dll \ data passing \ Call Native Code.llb" 中详细地列出了简单数据类型在 LabVIEW 与 C 之间的对应关系。
C 语言中经常在函数间传递指针或者数据的地址。在 32 位的程序中,可以使用 int32 数值来表示指针。因此,当需要在 LabVIEW 中传递指针数据时,可以使用 I32 或 U32 数值类型来表示这个地址类型的数据。但是,在 64 位的程序中,数据的地址只能使用 I64 或 U64 来表示。这样在一个调用了 DLL 函数、并且函数参数中有地址型数据的 VI,如果使用固定数据类型的数值来表示地址的话,就要准备两份代码。解决此问题的方法就是在该页的“数据类型”栏选择使用 LabVIEW 中的新数据类型:有符号或无符号 “指针大小整形”。这个数据类型的长度在不同的平台上会自动使用 32 位或 64 位长度。
如果在 C 语言函数参数声明中有 const 关键字,可以选中“常量”选项。LabVIEW 把一个数据传递给 DLL 中的函数时,通常需要为数据生成一份拷贝,让 DLL 函数使用这个拷贝数据。这样做的目的,是为了防止数据在 DLL 中被修改,外部程序又不知道,因而引起错误。设置“常量”属性,就说明这个参数一定不会在 DLL 中被改动,LabVIEW 可以不为它生成一份拷贝,以节约内存。
下表列举了 DLL 函数的数值型数据在 C 语言和 LabVIEW 中的设置方法:
输入 / 输出 | 输入 | 输出或兼作输入输出 |
C 语言声明 |
float red;
|
float* red;
|
LabVIEW 中的配置 | ||
LabVIEW 的使用 |
布尔类型
在 DLL 函数和 VI 之间并没有专门的、用于传递布尔值的数据类型,它也是利用数值类型来传递的。输入时先把布尔值转换为数值,再传递给 DLL 函数;输出时再把数值转换为布尔值。
在 C 语言中,有多种表示布尔类型的数据类型,如“bool”、“BOOL”等。它们的存储长度可能不相同,有的用一个字节表示,有的用四个字节表示。在使用时需要查看一下在被调用的 DLL 文件中布尔类型是以何种长度存储的,再使用对应的数值数据来表示它。
下表列举了布尔型数据的设置:
输入 / 输出 | 输入 | 输出或兼作输入输出 |
C 语言声明 |
bool visible;
|
bool* visible;
|
LabVIEW 中的配置 | ||
LabVIEW 的使用 |
数值型数组
对于数组的传递,LabVIEW 只支持 C 数据类型中的数值型数组。在 CLN 节点中配置数组型参数时,选择“类型”为“数组”,这是指参数的类型;然后还要选择“数据类型”,这是指数组元素的数据类型,它可以是任何一种数值类型。
“数组格式”一般都是使用“数组数据指针”,它对应于 C 语言中用指针表示的数组。“数组格式”还有其它两个选项:“数组句柄”和“数组句柄指针”。像这种带有“句柄”的参数类型都是表示 LabVIEW 定义的特殊类型的,通常只有 NI 公司产品中的 DLL 或使用 LabVIEW 生成的 DLL 中才会使用这些数据类型,在第三方的 DLL 中是不会用到的。
“最小尺寸”参数可以给一维数组参数设置一个最小长度,当用户传给 CLN 节点的数组长度小于这个值时,LabVIEW 会将其扩充至最小长度,再传递给 DLL 函数。
当函数的输出参数为数组时,一定要为输出的数组数据开辟存储空间。开辟数据空间的方法有两种:
第一种方法是创建一个长度满足要求的数组,作为初始值传递给参数。这个输入数组的内容对程序而言是无效的,它只被用来表示应该为输出数组分配的内存空间的大小,以确保输出数据都被保存在这个合法的内存空间中。
第二种方法是直接在参数配置面板上进行设置。在“最小尺寸”中写入一个固定的数值,LabVIEW 会按此大小为输出的数组开辟内存空间。在“最小尺寸”栏中参数可以输入一个固定数值。此外,如果该 CLN 节点调用的参数中有整型参数,这些参数的名称就会出现在该栏的选择项中,也就是说,也可以选择某个参数作为“最小尺寸”。这样 LabVIEW 会按照所选参数运行时的输入值来开辟相应大小的空间。
如果没有给输出数据分配内存,或分配的空间不够大,程序运行时就可能出现数组越界的运行错误,LabVIEW 会莫名其妙崩溃。更糟糕的是,LabVIEW 也许并不是在出现数组越界错误的瞬间崩溃的,而往往是在其之后的某一个不确定的时刻崩溃。如果意识不到程序中有这种错误,或者程序中有很多个类似的 CLN 节点,那么,调试并查找排除这一错误可能要花费大量的时间和精力。
下表列举了数组型数据的设置:
输入 / 输出 | 输入 | 输出或兼作输入输出 |
C 语言声明 |
int values [];
|
int values [];
|
LabVIEW 中的配置 | ||
LabVIEW 的使用 |
字符串类型
字符串的使用与数组是非常类似的。实际上,在 C 语言中字符串就是一个 I8 数组。
输入 / 输出 | 输入 | 输出或兼作输入输出 |
C 语言声明 |
char* name;
|
char* name;
|
LabVIEW 中的配置 | ||
LabVIEW 的使用 |
下表列举了字符串型数据的设置:
结构型参数的设置
C 语言中的结构(struct),在一些简单情况下,可以和 LabVIEW 中的“簇”相对应。但是,对于比较复杂的情况,LabVIEW 中的簇要做适当调整,才能够对应起来。
在讨论结构型参数的映射前,一定要先了解一下字节对齐的概念。在这里只做一个简单介绍,其详细内容可以搜索数据对齐相关的专题文章。
C 语言中的一个结构:
typedef struct {
char a;
int b
} MyStct;
显然,元素 a 只占用一个字节,而元素 b 占用四个字节。假设结构中的元素 a 所在的地址是 0xAAAA0000,那么,元素 b 占用四个字节的存放地址是与结构的字节对齐设置相关的。如果采用 1 字节对齐,则 b 是紧挨着 a 存放的,b 的地址就是:0xAAAA0001;如果采用 2 字节对齐,b 的存放