VBA调用C++动态链接库

发布于:2017-11-30 | 分类:python/vba/cpp


Excel的VBA可以使用Declare Function语句直接调用C++动态链接库,本文介绍此过程中可能涉及的参数传递、导入路径、链接库位数等方面的问题。

基本语句

' 64 bit dll
Private Declare PtrSafe Function [function_name] Lib ["path_to_dll"] ([parameters_list]) As [type]

' 32 bit dll
Private Declare Function [function_name] Lib ["path_to_dll"] ([parameters_list]) As [type]
  • 以上示例单个函数的导入,同理导入同一动态链接库中的其他函数
  • Private限定在module中使用动态链接库函数,不允许直接在公式栏使用;否则可以直接应用于单元格公式
  • PtrSafe表明针对64-bit版本Excel软件
  • path_to_dll指定加载DLL的路径,可以有两种方式:
    • 使用绝对路径,必须为字符串常量,不允许拼接动态路径
    • 写明DLL文件名,然后从默认位置加载

DLL加载路径

为避免硬编码绝对路径,采用Declare方式引用动态链接库函数时一般只指定其文件名,然后尽量将其所在目录加载到默认搜索路径中去。例如引用Excel同级bin文件夹下的动态链接库文件bar.dll

Private Declare PtrSafe Function foo Lib "bar.dll" () As Double

方式一:当前工作目录

Excel 当前工作目录 并不一定是Excel文件所在目录,而是通过ChDir()函数来动态设置。需要注意的是,所有打开的Excel工作簿共享同一个工作目录,并且某些手动操作例如浏览文件可能改变当前工作目录,

因此,这种方式存在一定风险。我们只能通过每次激活当前工作簿时都重置当前工作目录到目标位置来尽可能避免不期望的工作目录改变:

Private Sub Workbook_Activate()
    Dim DLLPath As String
    ' set current path to load DLL
    DLLPath = ThisWorkbook.Path & "\bin"
    ChDrive Left$(ThisWorkbook.Path, 1)
    If Dir(DLLPath, 16) <> Empty Then ChDir DLLPath
End Sub

以上代码中ChDrive进入单个字符表示的盘符,例如C盘、D盘;一旦不满足此条件,例如处在网络文件夹\\server\this\that,则会出错。所以,要么避免网络路径,要么将网络路径映射到单字符的盘符。

方式二:PATH环境变量

更稳定的方式是将动态链接库所在路径加到PATH环境变量中去。好处是无需增加额外的代码,不足是需要设置环境变量后才能正确使用。

方式三:LoadLibrary动态加载

Declare声明中不能使用动态路径,但我们可以借助Windows API LoadLibrary来实现。主要步骤为:

  1. 调用LoadLibrary加载动态拼接路径表示的DLL
  2. 按照常规方式Declare声明和使用DLL中的API
  3. 调用FreeLibrary释放DLL
' --------
' module: ThisWorkbook: load/free DLL when open/close workbook
' --------
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long

Public hModule As Long

' load DLL when open workbook
Private Sub Workbook_Open()
    Dim DLLPath As String
    DLLPath = ThisWorkbook.path & "\bin\bar.dll"
    hModule = LoadLibrary(DLLPath)    
    If hModule <= 0 Then MsgBox "load dll error"   
End Sub

' release DLL whe close
Private Sub Workbook_BeforeClose(Cancel As Boolean)
    If hModule > 0 Then FreeLibrary hModule
End Sub

通过上面的代码正确加载了bar.dll,因此其他模块中即可按常规方式使用函数foo

Private Declare Function foo Lib "bar.dll" () As Double

Sub test()
    Debug.Print foo()
End Sub

参数类型对应

VBAC++中基本参数类型的对应关系:

C++ VBA
int Long
bool Integer
float Single
double Double
char* String

需要注意的是,对于C++中数组类型的变量,VBA中需要声明为引用Byref,使用时传入数组参数的第一个元素即可,参考例子:

// C++ test.dll
int __stdcall foo(float*, float[]);
' VBA
Declare Function foo lib "path/to/test.dll" (Byref a!, Byref b!) As Long

Dim a(2), b(3)
call foo(a(0), b(0))

其中!VBAfloat类型的简写,其他的还有:

Integer(%)
Long(&)
float(!)
double(#)
string($)

函数名称

C++中函数名混肴机制将导致编译后的函数名与源码中的不同,为了避免此类转换,可以设置.def文件,基本格式参考:

LIBRARY [library name]
EXPORTS
[user_defined_exported_name_1] = [function_name_in_source_code_1]
[user_defined_exported_name_2] = [function_name_in_source_code_2]

以上内容中[]表示需要填入的内容([]本身并不需要)

然后在Visual Studio中编译前通过Configuration->Properties->Linker->InputModule Definition File指定自定义的.def文件即可。

Excel版本与动态链接库版本

Excel软件(而非操作系统)位数必须与编译动态链接库时选择的平台位数一致,否则导入函数时将会提示:

Error 48: file can not be found.

此前一个案例涉及Qt库的QNetwork模块,相关的两点延伸:

  • 为了成功编译出32-bit和64-bit的动态链接库,需要同时安装32-bit和64-bit的Qt库,在编译时指定相应版本的库

  • QNetwork的https访问需要用到SSL认证,因此需要提供相应库函数,例如libeay32.dllssleay32.dll,同时要求相应的位数必须一致

另外,32位/64位Excel之前迁移代码需要注意:

  • 32位的指针地址类型Long对应64位的LongPtr

  • 对于32位Excel,Declare声明的API同时会VBA和工作表单元格有效;对64位Excel,直接声明的API需要经过VBA函数的包装才能正确应用于单元格公式


参考资料