将字符串从VBA传递到C ++ DLL

时间:2016-09-09 04:46:56

标签: c++ excel vba excel-vba

我真的很困惑将字符串从VBA传递给C ++。这是VBA代码:

Private Declare Sub passBSTRVal Lib "foo.dll" (ByVal s As String)
Private Declare Sub passBSTRRef Lib "foo.dll" (ByRef s As String)
Private Declare Sub passByNarrowVal Lib "foo.dll" (ByVal s As String)
Private Declare Sub passByNarrowRef Lib "foo.dll" (ByRef s As String)
Private Declare Sub passByWideVal Lib "foo.dll" (ByVal s As String)
Private Declare Sub passByWideRef Lib "foo.dll" (ByRef s As String)

Sub foobar()
    Dim s As String, str As String
    str = "Hello There, World!"

    s = str
    Call passByBSTRVal(s)
    s = str
    Call passByBSTRRef(s)
    s = str
    Call passByNarrowVal(s)
    s = str
    Call passByNarrowRef(s)
    s = str
    Call passByWideVal(s)
    s = str
    Call passByWideRef(s)
End Sub

和C ++ DLL代码:

void __stdcall passByBSTRVal( BSTR s )
{
    MessageBox(NULL, s, L"Pass BSTR by value", MB_OK | MB_ICONINFORMATION);
}

void __stdcall passByBSTRRef( BSTR *s )
{
    MessageBox(NULL, *s, L"Pass BSTR by ref", MB_OK | MB_ICONINFORMATION);
}

void __stdcall passByNarrowVal( LPCSTR s )
{
    USES_CONVERSION;
    MessageBox(NULL, A2W(s), L"Pass by Narrow Val", MB_OK | MB_ICONINFORMATION);
}

void __stdcall passByNarrowRef( LPCSTR* s )
{
    USES_CONVERSION;
    MessageBox(NULL, A2W(*s), L"Pass by Narrow Ref", MB_OK | MB_ICONINFORMATION);
}

void __stdcall passByWideVal( LPCWSTR s )
{
    MessageBox(NULL, s, L"Pass by Wide Val", MB_OK | MB_ICONINFORMATION);
}

void __stdcall passByWideRef( LPCWSTR* s )
{
    MessageBox(NULL, *s, L"Pass by Wide Ref", MB_OK | MB_ICONINFORMATION);
}

我的期望是前两次调用passByBSTRVal和passByBSTRRef会起作用。为什么?因为VBA字符串是COM BSTR对象。但是,在逐步执行C ++代码时,这两个函数的s值都是垃圾(一堆汉字)。此外,显示的消息框(相同)。我真的很惊讶前两个功能不起作用。

我的下一个期望是第二次调用passByNarrowVal和passByNarrowRef不起作用,因为BSTR定义为“typedef OLECHAR * BSTR”,而OLECHAR是宽字符类型,而LPCSTR是窄字符类型。然而,与我的期望相反,这两个功能确实起作用了。当我逐步完成C ++代码时,参数s正是我所期待的。我的期望再次出错。

最后,我对最后2个函数(通过宽val和ref)的期望是它们可以工作,因为OLECHAR是一串宽字符,所以LPCWSTR应该能够指向BSTR。但与案例#1(我猜这两个案例相同)一样,我的期望是错误的。参数s由垃圾字符组成(并且MessageBox显示相同的垃圾字符。)

为什么我的直觉完全错了?有人可以解释我在这里不理解的内容吗?

5 个答案:

答案 0 :(得分:7)

这种形式的外部函数调用与早期版本的Visual Basic兼容,并且继承了它们的语义。特别是,VB3在16位窗口上运行,仅处理ANSI(即MBCS)字符串。

Declare语法具有相同的限制。 VBA会转换您的字符串,前提是它将其从UTF-16转换为ASCII。这允许用VB3编写的代码在VB4,VB5和VB6中不变。

所以例如" AZ"从\u0041\u005A开始,转换为ANSI并变为\x41\x5A,它被重新解释为\u5A41,即"娄"。

(使用VB4,Microsoft将WordBasic,Excel Basic和Visual Basic合并为单一语言VBA。)

" new"从VBA调用函数的方法是使用MIDL为需要使用的外部函数创建一个类型库,并将其添加为项目的引用。类型库可以描述函数的确切签名(例如BSTRLPCSTRLPCWSTR[out]BSTR*等。)特别是必须将函数包装在COM对象中以便从VBA调用它们(尽管如果你希望从VBScript中调用它们的话)。

或者,您无法为单个功能启动midl,您可以使用VarPtr / StrPtr / CopyMemory黑客。这几乎相当于PEEKPOKE

答案 1 :(得分:4)

以下是一些旧的参考文章,值得一读,因为它解释了我们所有问题的根本原因:

总结一下:

  • VBA内部存储是BSTR,其中包含unicode字符。
  • VBA也使用BSTR与外部世界交谈,但如果你不想使用BSTR,则不必使用BSTR,因为从C / C ++开始,你可以选择只使用BSTR的指针部分(a BSTR 是LPWSTR,LPWSTR 不是 BSTR。)
  • VBA用于在其世界之外进行通信的BSTR的内容不是unicode而是ANSI(VBA仍然生活在90年代,并认为,对于String数据类型,外部世界始终是ANSI, ASCIIZ,CodePage等)。因此,即使它仍然使用BSTR,该BSTR包含内部Unicode存储的ANSI等价物,以当前语言环为模(BSTR就像一个可以包含任何内容的信封,包括ANSI,包括任何地方的零字符,前提是长度与数据)。

因此,当您使用Declare类型为String的参数时,最终的二进制布局将始终与C的ANSI'char *'(或Windows宏用语中的LPSTR)相匹配。正式地说,如果你想通过互操作障碍传递完整的unicode字符串,你仍然应该使用VARIANTs(阅读链接了解更多信息)。

但是,并非所有事情都失败了,因为VBA(不是VB)多年来有所改善,主要是为了支持Office 64-bit versions

引入了LongPtr数据类型。它是32位系统上带符号的32位整数和64位系统上带符号的64位整数的类型。

注意它与.NET的IntPtr完全等价(VBA也认为Long是32位而Integer是16位,而.NET使用Long代表64位而Int代表32位......)

现在,LongPtr在VB的历史未记录函数StrPtr的帮助下无用,它接受一个字符串并返回LongPtr。它没有记录,因为正式的VB不知道指针是什么(实际上,要小心,因为如果使用不当,这会在运行时使程序崩溃)。

所以,让我们假设这个C代码:

  STDAPI ToUpperLPWSTR(LPCWSTR in, LPWSTR out, int cch)
  {
    // unicode version
    LCMapStringW(LOCALE_USER_DEFAULT, LCMAP_LINGUISTIC_CASING | LCMAP_UPPERCASE, in, lstrlenW(in), out, cch);
    return S_OK;
  }

  STDAPI ToUpperBSTR(BSTR in, BSTR out, int cch)
  {
    // unicode version
    // note the usage SysStringLen here. I can do it because it's a BSTR
    // and it's slightly faster than calling lstrlen...
    LCMapStringW(LOCALE_USER_DEFAULT, LCMAP_LINGUISTIC_CASING | LCMAP_UPPERCASE, in, SysStringLen(in), out, cch);
    return S_OK;
  }

  STDAPI ToUpperLPSTR(LPCSTR in, LPSTR out, int cch)
  {
    // ansi version
    LCMapStringA(LOCALE_USER_DEFAULT, LCMAP_LINGUISTIC_CASING | LCMAP_UPPERCASE, in, lstrlenA(in), out, cch);
    return S_OK;
  }

然后你可以用这些VBA声明来调用它(注意这段代码是32位和64位兼容的):

  Private Declare PtrSafe Function ToUpperLPWSTR Lib "foo.dll" (ByVal ins As LongPtr, ByVal out As LongPtr, ByVal cch As Long) As Long
  Private Declare PtrSafe Function ToUpperBSTR Lib "foo.dll" (ByVal ins As LongPtr, ByVal out As LongPtr, ByVal cch As Long) As Long
  Private Declare PtrSafe Function ToUpperLPSTR Lib "foo.dll" (ByVal ins As String, ByVal out As String, ByVal cch As Long) As Long

  Sub Button1_Click()

      Dim result As String
      result = String(256, 0)

      // note I use a special character 'é' to make sure it works
      // I can't use any unicode character because VBA's IDE has not been updated and does not suppport the
      // whole unicode range (internally it does, but you'll have to store the texts elsewhere, and load it as an opaque thing w/o the IDE involved)

      ToUpperLPWSTR StrPtr("héllo world"), StrPtr(result), 256
      MsgBox result
      ToUpperBSTR StrPtr("héllo world"), StrPtr(result), 256
      MsgBox result
      ToUpperLPSTR "héllo world", result, 256
      MsgBox result
  End Sub

然而,它们都有效,

  • ToUpperLPSTR是ANSI功能,因此它不支持大多数人现在使用的unicode范围。它适用于我,因为当我在我的机器上使用我的ANSI代码页运行时,IDE中编码的特殊非ASCII“é”字符将找到对应关系。但它可能无法运行取决于它运行的位置。使用unicode,你没有那种问题。
  • ToUpperBSTR专门用于VBA(COM自动化)客户端。如果从C / C ++客户端调用此函数,则C / C ++编码器必须创建一个BSTR才能使用它,因此它看起来很有趣并且可以添加更多工作。但请注意,由于BSTR的工作方式,它将支持包含零字符的字符串。例如,传递字节数组或特殊字符串有时很有用。

答案 2 :(得分:1)

巨大的注意:我不是程序员,我真的很喜欢编程,所以请善待我。我想改进,所以非常欢迎来自比我更熟练的人(基本上每个人)的建议和评论!

Ben,如果你正在阅读这篇文章,我想你会睁开眼睛看看发生了什么。 MIDL听起来像这样做的正确方法,我打算学习它,但这似乎是一个很好的学习机会,我从来没有让那些人过去!

我认为正在发生的事情是,狭窄的角色正在被编组到一个宽广的角色存储中。例如,字符串" hello"用窄字符存储看起来像:

|h |e |l |l |o |\0 |

并以宽字符存储,如下所示:

|h   |e   |l   |l   |o   |\0   |

但是当你将一个字符串从VBA传递给C ++时,会发生一些非常奇怪的事情。你将狭窄的角色编组成一个宽广的角色,如下所示:

|h e |l l |o \0 |    |    |    |

这就是使用LPCSTR / LPCSTR *的原因。是的,BSTR使用一串wchar_t,但是这种编组使它看起来像一串char。使用char *交替指向wchar_t的每一半中的第一个和第二个字符(h,然后是e.l,然后是l.o,然后是\ 0)。尽管char *和wchar_t *的指针算法不同,但它的工作原理是因为字符编组的有趣方式。事实上,我们传递了一个指向数据字符串的指针,但是如果你想访问数据字符串前4个字节的BSTR长度,你可以使用指针算法来玩游戏以获得你想去的地方。假设BSTR作为LPCSTR传入,

char* ptrToChar;      // 1 byte
wchar_t* ptrToWChar;  // 2 bytes
int* ptrToInt;        // 4 bytes
size_t strlen;

ptrToChar = (char *) s;
strlen = ptrToChar[-4];

ptrToWChar = (wchar_t *) s;
strlen = ptrToWChar[-2];

ptrToInt = (int *) s;
strlen = ptrToInt[-1];

当然,如果字符串作为LPCSTR *传入,那么你当然需要首先通过以下方式进行解除引用:

ptrToChar = (char *)(*s);

等等。

如果想要使用LPCWSTR或BSTR接收VBA字符串,您必须围绕此编组跳舞。例如,要创建一个将VBA字符串转换为大写的C ++ DLL,我执行了以下操作:

BSTR __stdcall pUpper( LPCWSTR* s )
{
    // Get String Length (see previous discussion)
    int strlen = (*s)[-2];

    // Allocate space for the new string (+1 for the NUL character).
    char *dest = new char[strlen + 1];

    // Accessing the *LPCWSTR s using a (char *) changes what we mean by ptr arithmetic,
    // e.g. p[1] hops forward 1 byte.  s[1] hops forward 2 bytes.
    char *p = (char *)(*s);

    // Copy the string data
    for( int i = 0; i < strlen; ++i )
        dest[i] = toupper(p[i]);

    // And we're done!
    dest[strlen] = '\0';

    // Create a new BSTR using our mallocated string.
    BSTR bstr = SysAllocStringByteLen(dest, strlen);

    // dest needs to be garbage collected by us.  COM will take care of bstr.
    delete dest;
    return bstr;
}

据我所知,将BSTR作为BSTR接收相当于将其作为LPCWSTR接收,并将其作为BSTR *接收相当于将其作为LPCWSTR *接收。

好的,我100%肯定这里有很多错误,但我相信潜在的想法是正确的。如果有错误或更好的思考方式,我会很乐意接受更正/解释,并为谷歌,后代和未来的程序员修复它们。

听起来最好的方法是使用Ben的MIDL建议(也许MIDL会让Safearrays和Variants变得不那么复杂?),在我进入后,我将开始学习那种方法。但这种方法也适用,对我来说是一个很好的学习机会。

答案 3 :(得分:1)

好的,所以我知道我设定赏金以便对IDL的想法作出更全面的回应,但我自己已经开始了。所以我打开了一个ATL项目,将idl更改为以下

// IDLForModules.idl : IDL source for IDLForModules
//

// This file will be processed by the MIDL tool to
// produce the type library (IDLForModules.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
    helpstring("Idl For Modules"),
    uuid(EA8C8803-2E90-45B1-8B87-2674A9E41DF1),
    version(1.0),
]
library IDLForModulesLib
{
    importlib("stdole2.tlb");

    [
        /* dllname attribute https://msdn.microsoft.com/en-us/library/windows/desktop/aa367099(v=vs.85).aspx */
        dllname("IdlForModules.dll"),
        uuid(4C1884B3-9C24-4B4E-BDF8-C6B2E0D8B695)
    ]
    module Math{
        /* entry attribute https://msdn.microsoft.com/en-us/library/windows/desktop/aa366815(v=vs.85).aspx */
        [entry(656)] /* map function by entry point ordinal */
        Long _stdcall Abs([in] Long Number);
    }
    module Strings{
        [entry("pUpper")] /* map function by entry point name */
        BSTR _stdcall Upper([in] BSTR Number);
    }
};

然后在主cpp文件中我添加了

#include <string>
#include <algorithm>

INT32 __stdcall _MyAbs(INT32 Number) {
    return abs(Number);
}

BSTR __stdcall pUpper(BSTR sBstr)
{
    // Get the BSTR into the wonderful world of std::wstrings immediately
    std::wstring sStd(sBstr);

    // Do some "Mordern C++" iterator style op on the string
    std::transform(sStd.begin(), sStd.end(), sStd.begin(), ::toupper);

    // Dig out the char* and pass to create a return BSTR
    return SysAllocString(sStd.c_str());
}

在DEF文件中我将其编辑为

; MidlForModules.def : Declares the module parameters.

LIBRARY

EXPORTS
    DllCanUnloadNow     PRIVATE
    DllGetClassObject   PRIVATE
    DllRegisterServer   PRIVATE
    DllUnregisterServer PRIVATE
    DllInstall      PRIVATE
    _MyAbs @656
    pUpper

在一个名为TestClient.xlsm的宏可编译工作簿中放置在与Debug输出Dll相同的目录中,我在ThisWorkbook模块中编写以下内容

Option Explicit

Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long

Private Sub Workbook_Open()
    '* next line establishes relative position of Dll
    Debug.Assert Dir(ThisWorkbook.Path & "\IDLForModules.dll") = "IDLForModules.dll"

    '* next line loads the Dll so we can avoid very long Lib "c:\foo\bar\baz\barry.dll"
    LoadLibrary ThisWorkbook.Path & "\IDLForModules.dll"

    '* next go to  Tools References are check "Idl For Modules"
    '* "Idl For Modules" Iis set in the IDL with helpstring("Idl For Modules")

End Sub

然后我将一个工具参考添加到新创建的类型库中,现在我可以通过添加标准模块并添加以下内容来完成

Option Explicit

Sub TestAbs()
    Debug.Print IDLForModulesLib.Math.Abs(-5)
End Sub

Sub TestUpper()
    Debug.Print IDLForModulesLib.Strings.Upper("foobar")
End Sub

这适用于Windows 8.1 Professional 64位,VS2013,Excel 15.我可以在此处找到有关C ++新手的富勒指令Throw away Declare Function with IDL for Modules

答案 4 :(得分:0)

这只是西蒙回答的一个例证。它显示了如何调用参数类型为LPWSTR的本机DLL。举一个简单的例子,我使用GetWindowsDirectoryW。正如Simon所指出的,请始终使用本机DLL的“ W”版本。

Declare PtrSafe Function GetWindowsDirectoryW Lib "kernel32" _ 
   (ByVal lpBuffer As LongPtr, ByVal nSize As Long) As Long

Sub TestGetWindowsDirectoryW()
  Dim WindowsDir As String
  WindowsDir = Space$(256)
  GetWindowsDirectoryW StrPtr(WindowsDir), 256
  MsgBox WindowsDir
End Sub