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