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)结构实现的,因此利用BSTRstring交互具备天生优势;
  • 最后,C++和VBA都支持Variant类型,因此也可以作为字符串传递的过渡类型

char*返回字符串

由于重点在于 探究VBA接受DLL返回的字符串类型,接下来的实验中统一用char*类型接受VBA传递的字符串,DLL中将其转换为大写字符形式后分别以char*BSTRVariant类型返回给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_sizeout的最大长度(缓冲区大小)。虽然本例返回值的长度就等于输入值,但一般情况下无法预知返回值所占空间,所以需要事先开辟一定长度的缓冲区间,对超出部分则进行截断。函数的返回值是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*BSTRVARIANT三种类型都可以实现传递字符串类型回VBA:

    • char*方式不能直接返回字符串,而是替代以存储目标字符串到预先传入的参数中。因此这种方式的C++接口不能直接用于单元格公式,而是需要包装为VBA函数。
    • BSTR方式注意最后转换为BSTR类型的方法,参考函数SysAllocStringByteLen()SysAllocString()_bstr_t()等。
    • VARIANT方式注意将返回值转换为Unicode编码。

    所以,如果想直接使用DLL中的函数,优先BSTRVARIANT方式;如果考虑VBA再次包装的话,char*的方式更为轻便。

  • VBA封装版本才能正确应用于单元格公式

    上述实例中,三个C++接口函数都有VBA包装版本,例如upper_vba_bstr()是对以BSTR类型返回字符串的upper_bstr()接口的包装。除了upper_char()不是直接返回字符串而无法直接应用于公式函数外,其余两种方式的两个版本(C++、VBA)函数都可以应用于单元格公式。

    然而,在单元格公式中的测试却出现了下图问题:VBA函数工作正常,相应DLL接口都只转换了第一个字符!

    根本原因是VBA宏代码和Excel公式中传递字符串参数的编码方式略有不同,具体参考:

    DLL与VBA之间的字符串传递:传入字符串

最后,本文完整代码:

https://github.com/dothinking/blog/tree/master/samples/vba_cpp_dll/Sample_Return_String


参考