Передача строк из VBA в С++ DLL

Я действительно запутался в передаче строк из VBA на С++. Здесь код 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

И код 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. Однако, переходя через код С++, значение s для обеих этих функций было мусором (кучей кандзи). Кроме того, отображаемое окно сообщения (то же самое). Я действительно удивлен, что первые две функции не работают.

Мое следующее ожидание было для вторых двух вызовов passByNarrowVal и passByNarrowRef, чтобы они не работали, потому что BSTR определяется как "typedef OLECHAR * BSTR", а OLECHAR - это широкий тип символов, а LPCSTR - узкий характер. Однако, вопреки моему ожиданиям, эти две функции действительно работали. Когда я перешагнул код С++, параметр s был именно тем, чего я ожидал. Мое ожидание снова было неправильным.

Наконец, мое ожидание последних двух функций (pass by wide val и ref) состояло в том, что они будут работать, поскольку OLECHAR - это строка с широкими символами, поэтому LPCWSTR должен иметь возможность указывать на BSTR. Но как с случаем № 1 (я думаю, эти два случая идентичны), мое ожидание было неправильным. Параметр s состоял из символов мусора (и MessageBox отображал одни и те же символы мусора).

Почему моя интуиция совершенно неправа? Может кто-нибудь объяснить, что я здесь не понимаю?

Ответ 1

Эта форма внешнего вызова функции существует для совместимости с более ранними версиями 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.)

"Новый" способ вызова функций из VBA - создать библиотеку типов для внешних функций, которые вам нужно использовать, используя MIDL и добавить их в качестве ссылки на проект. Библиотеки типов могут описывать точную подпись функции (например, BSTR, LPCSTR, LPCWSTR, [out]BSTR* и т.д.). В частности, нет необходимости обертывать функции в COM-объекте, чтобы вызвать их из VBA (хотя вы хотите называть их из VBScript).

В качестве альтернативы вы не можете беспокоиться о том, чтобы запустить midl для одной функции, вы можете использовать хакеры VarPtr/StrPtr/CopyMemory. Это в значительной степени эквивалентно PEEK и POKE.

Ответ 2

Вот несколько старых справочных статей, это стоит прочитать, потому что это объясняет коренные причины всех наших проблем:

Подводя итог:

  • Внутреннее хранилище VBA - это BSTR с символами Unicode.
  • VBA также использует BSTR для общения с внешним миром, но вам не нужно использовать BSTR, если вы этого не хотите, потому что из C/С++ вы можете использовать только часть указателя BSTR (a BSTR является LPWSTR, LPWSTR не является BSTR).
  • Содержимое BSTR, которое VBA использует для связи за пределами своего мира, не является unicode, но ANSI (VBA все еще живет в 90-х годах и считает, что в отношении типа данных String внешний мир всегда имеет ANSI, ASCIIZ, CodePage и т.д.). Таким образом, даже если он все еще использует BSTR, этот BSTR содержит эквивалент ANSI внутреннего хранилища Unicode по модулю текущего языка (BSTR похож на конверт, который может содержать что угодно, включая ANSI, включая нулевые символы в любом месте, при условии, что длина соответствует данные).

Поэтому, когда вы используете use Declare с аргументом типа String, окончательный двоичный макет всегда будет соответствовать C ANSI 'char *' (или LPSTR в макросе окна windows). Официально, вы все еще должны использовать VARIANT, если хотите передать полную строку unicode поверх барьеров interop (прочитайте ссылки для получения дополнительной информации об этом).

Но не все потеряно, поскольку VBA (а не VB) за последние годы немного улучшилось, в основном для поддержки 64-разрядные версии Office.

Был введен тип данных LongPtr. Это тип, который будет подписанным 32-битным целым числом в 32-разрядной системе и подписанным 64-битным целым числом в 64-разрядной системе.

Обратите внимание на точный эквивалент .NET IntPtr (VBA также по-прежнему считает, что Long - 32-разрядный, а Integer - 16 бит, тогда как .NET использует Long для 64-битных и Int для 32-битных...).

Теперь LongPtr будет бесполезным без помощи VOB-времени бездокументальной функции 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 a BSTR
    // and it 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 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, который большинство людей использует в наши дни. Он работает для меня, потому что специальный символ не ASCII 'é', закодированный в среде IDE, найдет соответствие, когда я запустил его на своей машине с моей кодовой страницей ANSI. Но он может не работать в зависимости от того, где он работает. С unicode у вас нет таких проблем.
  • ToUpperBSTR является специализированным для клиентов VBA (COM автоматизации). Если эта функция вызывается с клиентом C/С++, кодер C/С++ должен будет создать BSTR, чтобы использовать его, поэтому может показаться забавным и добавить больше работы. Обратите внимание, что он поддерживает строки, содержащие нулевой символ в нем, благодаря тому, как работает BSTR. Иногда это может быть полезно, например, для передачи массива байтов или специальных строк.

Ответ 3

Хорошо, поэтому я знаю, что я заработал щедрость, чтобы получить более полный отклик на идею 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 output 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. Более полные инструкции для новичков С++ можно найти здесь Отбросить Declare Function с IDL для Модули.

Ответ 4

БОЛЬШОЕ ОГРОМНОЕ ПРИМЕЧАНИЕ: Я не программист, мне просто нравится программировать, поэтому, пожалуйста, будьте добры ко мне. Я хочу улучшить, поэтому предложения и комментарии от людей, более опытных, чем я (в основном, все) ОЧЕНЬ приветствуются!

Бен, если вы читаете это, я думаю, вы открыли глаза на то, что происходит. MIDL звучит как правильный способ сделать это, и я намереваюсь его изучить, но это казалось хорошей возможностью для изучения, и я никогда не позволял этим проходить мимо меня!

Я думаю, что происходит то, что узкие персонажи попадают в широкое хранилище символов. Например, строка "привет", хранящаяся с узкими символами, выглядит так:

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

и хранится с широкими символами, выглядит так:

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

Но когда вы передаете строку из VBA на С++, происходит что-то действительно странное. Вы получаете узких символов, распределенных в широкий характер, например:

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

Вот почему работает LPCSTR/LPCSTR *. Да, BSTR использует строку wchar_t, но эта сортировка делает ее похожей на строку char. Доступ с помощью char * попеременно указывает на первый и второй символы в каждой половине wchar_t (h, затем e. L, затем l. O, then\0). Несмотря на то, что арифметика указателя для char * и wchar_t * отличается, она работает из-за забавного способа сортировки символов. Фактически, мы передаем указатель на строку данных, но если вы хотите получить доступ к длине BSTR, за 4 байта до строки данных, вы можете играть в игры с арифметикой указателя, чтобы получить то место, куда вы хотите идти. Предполагая, что BSTR передается как LPCSTR s,

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 * s, то, конечно, вам нужно сначала разыменовать s, обратившись через что-то вроде:

ptrToChar = (char *)(*s);

и т.д.

Если вы хотите использовать LPCWSTR или BSTR для получения строки VBA, вы должны танцевать вокруг этого сортировки. Так, например, для создания С++ DLL, которая преобразует строку VBA в верхний регистр, я сделал следующее:

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% уверен, что здесь много ошибок, но я считаю, что основные идеи верны. Если есть ошибки или даже лучшие способы думать о чем-то, я с радостью принимаю исправления/объяснения и исправляю их для Google, потомков и будущих программистов.

Похоже, что ЛУЧШИЙ способ сделать это с предложением Бена MIDL (и, может быть, MIDL сделает Safierrays и варианты менее сложными?), и после того, как я нажму "enter", я начну изучать этот метод. Но этот метод тоже работает и был отличной возможностью обучения для меня.