C++ DLL与VBA之间的字符串传递:返回字符串¶
发布于:2019-08-18 | 分类:python/vba/cpp
VBA调用C++动态链接库介绍了VBA调用C++ DLL的基础,例如基本参数类型的对应关系,这涉及到传参和返回值的正确获取。其中逻辑、数值类型的传递相对简单,但字符串类型不同的存储形式带来了一定的障碍,尤其是从C++返回字符类型给VBA。本文从实例出发,探索C++ DLL和VBA之间字符串类型参数传递的基本姿势。
基本类型¶
- 我们会毫不犹豫地想到
char*
与VBAString
对应的可能性; - 另外字符串类型在VBA内部是由
COM BSTR
(Basic string)结构实现的,因此利用BSTR
与string
交互具备天生优势; - 最后,C++和VBA都支持
Variant
类型,因此也可以作为字符串传递的过渡类型
以char*
返回字符串¶
由于重点在于 探究VBA接受DLL返回的字符串类型,接下来的实验中统一用char*
类型接受VBA传递的字符串,DLL中将其转换为大写字符形式后分别以char*
、BSTR
和Variant
类型返回给VBA。
首先实现基本的大写转换过程。为了成功返回转换后的字符串,需要在堆上开辟永久空间;否则该函数调用结束后即销毁了栈上的变量,从而无法获取返回值。但这样做的话,注意在使用结束后显式销毁开辟的空间。因此这个函数并不适合作为VBA的接口,因为一旦被调用后很难再从VBA中销毁res
。
char* __stdcall upper_heap(const char* str)
{
char* res = new char[strlen(str)+1];
size_t i = 0;
for (; str[i]; i++) {
res[i] = toupper(str[i]);
}
res[i] = '\0';
// res should be released out of this function
return res;
}
如果强行这么做的话,直接导致了Excel的崩溃
Declare Function upper_heap Lib "Sample.dll" (ByVal str$) As String
Sub test()
Debug.Print upper_heap("abC123e")
End Sub
在C语言中一个常用的做法是通过传址参数来返回值,于是得到另一个版本:
int __stdcall upper_char(const char* str, char* out, int n_size)
{
char* res = upper_heap(str);
// for safe, the max buffer n_size is used
size_t n = strlen(res) < n_size ? strlen(res) : n_size;
strncpy(out, res, n);
delete[] res;
return n;
}
参数out
即用来存放目标字符串,n_size
是out
的最大长度(缓冲区大小)。虽然本例返回值的长度就等于输入值,但一般情况下无法预知返回值所占空间,所以需要事先开辟一定长度的缓冲区间,对超出部分则进行截断。函数的返回值是out
的真实大小。
在VBA中重新封装成一个Function
,于是可以用于VBA代码或者单元格函数中:
Declare Function upper_char Lib "Sample.dll" (ByVal str$, ByVal out$, ByVal n As Long) As Long
Function upper_vba_arg(str)
Dim n&, out$
n = Len(str)
out = Space(n)
Call upper_char(str, out, n)
upper_vba_arg = out
End Function
以BSTR
返回字符串¶
还是使用前面upper_heap()
的基本实现,然后以BSTR
类型返回即可。这里貌似有些问题,因为BSTR
类型的参数也需要显式释放内存却没有在C++代码中处理。不过无需担心,BSTR
类型的内存释放可以由VBA自动完成。
The important point is the use of the OLE SysAllocStringByteLen() function to allocate new space for the string. This enables VBA to free the string whenit is done with it.
#include <OAIdl.h> // for VARIANT, BSTR etc
BSTR __stdcall upper_bstr(const char* str)
{
char* res = upper_heap(str);
// This function takes an ANSI string as input, and returns an allocated string.
// This function does not perform ANSI to Unicode translation. It is valid only for 32-bit systems.
BSTR bstr = SysAllocStringByteLen(res, strlen(res));
delete[] res;
return bstr;
}
以上接口函数upper_bstr()
在VBA宏代码和单元格公式中都可以直接使用,下面用VBA看似“多此一举”地包装一下(后文阐述其用意):
Declare Function upper_bstr Lib "Sample.dll" (ByVal str$) As String
Function upper_vba_bstr(str) As String
upper_vba_bstr = upper_bstr(str)
End Function
以VARIANT
返回字符串¶
同理在C++中以VARIANT
类型返回即可:
#include <OAIdl.h> // for VARIANT, BSTR etc
VARIANT __stdcall upper_var(const char* str)
{
BSTR bstr = upper_bstr(str);
VARIANT var;
var.vt = VT_BSTR;
var.bstrVal = bstr;
return var;
VariantClear(&var);
}
相应地,VBA中需要将函数返回值声明为Variant
类型,并且在重新包装VBA函数时需要转换编码为Unicode:
Declare Function upper_var Lib "Sample.dll" (ByVal str$) As Variant
Function upper_vba_var(str)
upper_vba_var = StrConv(upper_var(str), vbUnicode) ' unicode encoding
End Function
对比总结¶
-
char*
、BSTR
、VARIANT
三种类型都可以实现传递字符串类型回VBA:char*
方式不能直接返回字符串,而是替代以存储目标字符串到预先传入的参数中。因此这种方式的C++接口不能直接用于单元格公式,而是需要包装为VBA函数。BSTR
方式注意最后转换为BSTR
类型的方法,参考函数SysAllocStringByteLen()
、SysAllocString()
、_bstr_t()
等。VARIANT
方式注意将返回值转换为Unicode编码。
所以,如果想直接使用DLL中的函数,优先
BSTR
和VARIANT
方式;如果考虑VBA再次包装的话,char*
的方式更为轻便。 -
VBA封装版本才能正确应用于单元格公式
上述实例中,三个C++接口函数都有VBA包装版本,例如
upper_vba_bstr()
是对以BSTR
类型返回字符串的upper_bstr()
接口的包装。除了upper_char()
不是直接返回字符串而无法直接应用于公式函数外,其余两种方式的两个版本(C++、VBA)函数都可以应用于单元格公式。然而,在单元格公式中的测试却出现了下图问题:VBA函数工作正常,相应DLL接口都只转换了第一个字符!
根本原因是VBA宏代码和Excel公式中传递字符串参数的编码方式略有不同,具体参考:
最后,本文完整代码:
https://github.com/dothinking/blog/tree/master/samples/vba_cpp_dll/Sample_Return_String