将string从VBA传递给C ++ DLL

我真的很困惑从VBA传递string到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将工作。 为什么? 因为VBAstring是COM BSTR对象。 但是,在遍历C ++代码时,这两个函数的s值是垃圾(一堆汉字)。 此外,显示的消息框是(相同)。 我真的很惊讶,前两个function没有工作。

我的下一个期望是第二次调用passByNarrowVal和passByNarrowRef不起作用,因为BSTR被定义为“typedef OLECHAR * BSTR”,而OLECHAR是宽字符types,而LPCSTR是窄字符types。 但是,与我的预期相反,这两个function确实起作用。 当我通过C ++代码时,参数s正是我所期望的。 我的期望又错了。

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

为什么我的直觉完全错了? 有人能解释一下我不理解吗?

这种forms的外部函数调用存在与早期版本的Visual Basic兼容,并inheritance它们的语义。 特别是,VB3在16位窗口上运行,只处理ANSI(即MBCS)string。

Declare语法具有相同的限制。 VBA转换您的string,假设它将它从UTF-16转换为ASCII。 这使得在VB3中编写的代码可以在VB4,VB5和VB6中保持不变。

因此,例如“AZ”开始为\u0041\u005A ,转换为ANSI并成为\x41\x5A \u5A41 ,它被重新解释为“娄”。

(在VB4中,Microsoft将WordBasic,Excel Basic和Visual basic合并为一种语言VBA。)

从VBA中调用函数的“新”方法是使用MIDL为需要使用的外部函数创build一个types库,并将其添加为项目的引用。 types库可以描述函数的确切签名(例如, BSTRLPCSTRLPCWSTR[out]BSTR*等)。特别是不需要将函数包装在COM对象中,以便从VBA调用它们(尽pipe它如果你想从VBScript调用它们)。

  • 一组DLL函数被描述为一个MIDL module : https : //msdn.microsoft.com/en-us/library/windows/desktop/aa367099(v= vs.85) .aspx

另外,你不能为了单一function而打扰midl ,你可以使用VarPtr / StrPtr / CopyMemory hack。 这几乎等同于PEEKPOKE

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

  • 如何:在Excel中访问DLL
  • 和两个较旧的VB(VBA是一个更新的VB运行时):将string传递给DLL过程 声明一个DLL过程

总结一下:

  • VBA内部存储是BSTR与Unicode字符。
  • VBA也使用BSTR与外部世界进行交stream,但是如果不想使用BSTR,则不必使用BSTR,因为从C / C ++开始,可以select仅使用BSTR的指针部分(BSTR LPWSTR,LPWSTR 不是 BSTR)。
  • VBA用来在外面沟通的BSTR的内容不是Unicode而是ANSI(VBA还活在90年代就认为,对于String数据types,外界总是ANSI,ASCIIZ,CodePage等) 。 因此,即使它仍然使用BSTR,该BSTR包含内部Unicode存储器的ANSI等效值,以当前语言环境为模(BSTR就像一个信封,它可以包含ANSI,包括任何地方的零字符,只要长度匹配数据)。

所以,当你使用DeclareStringtypes的参数时,最终的二进制布局将总是匹配C的ANSI'char *'(或者用macros指令中的LPSTR)。 正式的,你仍然应该使用VARIANTs,如果你想通过完整的Unicodestring跨越互操作障碍(阅读链接了解更多)。

但是,并不是所有的东西都输了,因为VBA(而不是VB)多年来有所改进,主要是为了支持Office 64位版本 。

LongPtr数据types已经被引入。 这是32位系统上有符号的32位整数和64位系统上有符号的64位整数的types。

请注意,这是相当于.NET的IntPtr(VBA还认为Long是32位,Integer是16位,而.NET使用Long作为64位,Int作为32位…)。

现在, LongPtr将无用的W / O的VB的历史未logging函数StrPtr的帮助,该函数接受一个string并返回一个LongPtr 。 这是没有logging,因为正式的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的function,所以它不会支持现在大多数人使用的unicode范围。 它适用于我,因为在IDE中编码的特殊非ASCII'é'字符将在我的ANSI代码页中在我的机器中运行时find对应关系。 但它可能无法运行,取决于它在哪里运行。 用unicode,你没有这种问题。
  • ToUpperBSTR是VBA(COM自动化)客户端的专用。 如果这个函数是从C / C ++客户端调用的,那么C / C ++编码器将不得不创build一个BSTR来使用它,所以它看起来很有趣并且增加了更多的工作。 请注意,它将支持包含零字符的string,这要归功于BSTR的工作方式。 例如,传递字节数组或特殊string可能有用。

好的,所以我知道我给了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的macros可设置的工作簿放在与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 

然后,我添加一个工具引用到新创build的types库,现在我可以通过添加一个标准模块并添加以下内容来完成

 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。我可以在这里find有关C ++新手的更丰富的指令。 使用IDL为模块声明函数 。

巨大的注意:我不是一个程序员,我真的很喜欢编程,所以请对我好。 我想改进,所以比我更熟练的人(基本上每个人)的build议和意见都非常受欢迎!

本,如果你正在读这本书,我想你睁开了眼睛看看发生了什么事。 MIDL听起来像是这样做的正确方式,我打算学习它,但这似乎是一个很好的学习机会,我从来不让那些通过我!

我认为发生的事情是狭隘的angular色正在被编组成一个宽广的angular色存储。 例如,用窄字符存储的string“hello”看起来像:

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

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

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

但是当你从VBA传递一个string到C ++时,会发生一些奇怪的事情。 你把狭窄的字符编成宽字符,就像这样:

 |he |ll |o \0 | | | | 

这就是使用LPCSTR / LPCSTR *的原因。 是的,BSTR使用一个wchar_tstring,但是这个编组使得它看起来像一串char。 用char *访问交替地指向wchar_t(h,然后e。l,然后l。o,然后\ 0)的每一半中的第一个和第二个字符。 尽pipechar *和wchar_t *的指针算术是不同的,但它的作用是因为字符被编组的有趣的方式。 实际上,我们传递了一个指向数据string的指针,但是如果你想要访问BSTR的长度,在数据string之前的4个字节,你可以用指针algorithm来玩游戏,以得到你想要去的地方。 假设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]; 

当然,如果这个string是作为LPCSTR *传入的,那么当然你需要首先通过类似下面的方式访问:

 ptrToChar = (char *)(*s); 

等等。

如果要使用LPCWSTR或BSTR接收VBAstring,则必须在此编组之间跳舞。 例如,为了创build一个将VBAstring转换为大写的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, // eg 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 *接收。

好吧,我百分之百确定这里有很多错误,但是我相信这些观点是正确的。 如果有错误或者更好的思考方式,我会很乐意接受更正/解释,并为Google,后代和未来的程序员修正。

听起来最好的办法是用Ben的MIDLbuild议(也许MIDL会使Safearrays和Variants变得更加复杂?),当我进入后,我将开始学习这个方法。 但是这个方法也是可行的,对我来说是一个很好的学习机会。