programing

VBA에서 C++ DLL로 문자열 전달 중

css3 2023. 6. 9. 22:18

VBA에서 C++ DLL로 문자열 전달 중

저는 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);
}

제 예상은 ByBSTRVal과 ByB를 통과하는 첫 두 통화였습니다.STRef라면 효과가 있을 겁니다. 왜죠?VBA 문자열이 COMBSTR 개체이기 때문입니다.그러나 C++ 코드를 밟는 동안 이 두 함수의 s 값은 가비지(한 묶음의 한자)였습니다.또한 표시된 메시지 상자는 동일합니다.처음 두 가지 기능이 작동하지 않아서 정말 놀랐습니다.

다음으로 예상한 것은 BSTR이 "Typedef OLECHAR *BSTR"로 정의되고 OLECHAR이 넓은 문자 유형인 반면 LPCSTR은 좁은 문자 유형이기 때문에 두 번째 두 호출이 ByNarrowVal 및 PassByNarrowRef가 작동하지 않는 것입니다.하지만, 제 예상과는 달리, 이 두 가지 기능은 실제로 작동했습니다.제가 C++ 코드를 밟았을 때, 매개 변수는 정확히 제가 예상했던 것과 같았습니다.제 예상이 또 틀렸습니다.

마지막으로, OLECHAR은 넓은 문자의 문자열이므로 LPCWSTR이 BSTR을 가리킬 수 있어야 하기 때문에, 마지막 두 함수(와이드 밸브 및 레퍼런스 패스)에 대한 저의 기대는 그것들이 작동할 것이라는 것이었습니다.하지만 1번 경우와 마찬가지로 (이 두 경우는 동일한 것 같습니다) 제 예상은 빗나갔습니다.매개 변수는 가비지 문자로 구성되었으며 MessageBox는 동일한 가비지 문자를 표시했습니다.

왜 제 직감이 완전히 틀렸을까요?누가 제가 이해하지 못하는 것을 여기서 설명해 주시겠습니까?

여기 몇 가지 오래된 참조 기사가 있습니다. 모든 문제의 근본 원인을 설명하기 때문에 읽을 가치가 있습니다.

요약:

  • VBA 내부 스토리지는 유니코드 문자가 포함된 BSTR입니다.
  • 또한 VBA는 외부 세계와의 대화를 위해 BSTR을 사용하지만 C/C++에서 BSTR의 포인터 부분만 사용하도록 선택할 수 있기 때문에 원하지 않으면 BSTR을 사용할 필요가 없습니다(BSTR은 LPWSTR이고 LPWSTR은 BSTR이 아닙니다.
  • 밖에서 VBA는 VBA에 대해 BSTR에 합니다.)입니다.String데이터 유형, 외부 세계는 항상 ANSI, ASCIIZ, CodePage 등입니다.따라서 BSTR을 사용하더라도 BSTR에는 내부 Unicode 스토리지에 해당하는 ANSI가 포함되어 있으며 현재 로케일에 모듈화되어 있습니다(BSTR은 데이터 길이와 일치하는 경우 아무 곳에도 문자가 없는 ANSI를 포함하여 모든 것을 포함할 수 있는 엔벨로프와 같습니다).

는 그서사용때는할을 사용합니다.Declare가 론으로 유형인String최종 이진 레이아웃은 항상 C의 ANSI 'char *'(또는 윈도우 매크로 용어로 LPSTR)와 일치합니다.공식적으로 인터op 장벽에 전체 유니코드 문자열을 전달하려면 여전히 VARIANT를 사용해야 합니다(자세한 내용은 링크 참조).

그러나 VBA(VB가 아닌)가 주로 Office 64비트 버전을 지원하기 위해 수년간 약간 개선되었기 때문에 모든 것이 손실되는 것은 아닙니다.

LongPtr 데이터 유형이 도입되었습니다.32비트 시스템에서는 부호 있는 32비트 정수, 64비트 시스템에서는 부호 있는 64비트 정수가 될 유형입니다.

이는 .NET의 IntPtr과 정확히 일치합니다(VBA도 Long은 32비트이고 Integer는 16비트라고 여전히 생각합니다).NET은 64비트의 경우 Long을 사용하고 32비트의 경우 Int를 사용합니다.

지금이다,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 기능이므로 오늘날 대부분의 사용자가 사용하는 유니코드 범위를 지원하지 않습니다.IDE에서 코드화된 특수 비 ASCII 'é' 문자는 ANSI 코드 페이지를 사용하여 컴퓨터에서 실행할 때 해당 문자를 찾기 때문에 유용합니다.하지만 어디를 달리느냐에 따라 작동하지 않을 수도 있습니다.유니코드를 사용하면 그런 문제가 없습니다.
  • ToUpperBSTR은 COM 자동화(VBA) 클라이언트에 특화되어 있습니다.이 함수가 C/C++ 클라이언트에서 호출되면 C/C++ 코더는 이 함수를 사용하기 위해 BSTR을 만들어야 하므로 재미있어 보이고 작업을 추가할 수 있습니다.그러나 BSTR이 작동하는 방식 덕분에 0 문자가 포함된 문자열을 지원합니다.예를 들어 바이트 배열이나 특수 문자열을 전달하는 것이 유용할 수 있습니다.

이 형식의 외부 함수 호출은 이전 버전의 Visual Basic과 호환되며 해당 의미를 상속합니다.특히 VB3는 16비트 창에서 실행되었으며 ANSI(즉, MBCS) 문자열만 처리했습니다.

Declare구문에도 동일한 제한이 있습니다. VBA는 문자열을 UTF-16에서 ASCII로 변환한다고 가정하여 변환합니다.이를 통해 VB3로 작성된 코드가 VB4, VB5 및 VB6에서 변경되지 않고 작동할 수 있습니다.

예를 들어 "AZ"는 "AZ"로 시작합니다.\u0041\u005A되어 ANSI 변환다됩니다이가 됩니다.\x41\x5A로 재해석되는.\u5A41그것은 "유연한" 것입니다.

(VB4를 통해 마이크로소프트는 워드 베이직, 엑셀 베이직, 비주얼 베이직을 하나의 언어인 VBA로 통합했습니다.)

VBA에서 함수를 호출하는 "새로운" 방법은 MIDL을 사용하여 사용해야 하는 외부 함수에 대한 유형 라이브러리를 만들고 프로젝트에 대한 참조로 추가하는 것입니다.할 수 형라이러리예함서정설수있명습할다니명을한확는브수식의예(▁can▁type:있,다:BSTR,LPCSTR,LPCWSTR,[out]BSTR*등) 특히 VBA에서 호출하기 위해 함수를 COM 개체로 래핑할 필요는 없습니다(VBScript에서 호출하려는 경우에도 마찬가지입니다).

를 내는 도 없어요.midl 함수의에는 단함일수에, 당은사수있다습니용할을 할 수 .VarPtr/StrPtr/CopyMemoryhack.은 킹해와 . 이것은 꽤나 상당합니다.PEEK그리고.POKE.

중요한 참고 사항:저는 프로그래머가 아닙니다, 그냥 프로그래밍을 정말 즐기니까 친절하게 대해주세요.저는 발전하고 싶기 때문에 저보다 더 숙련된 사람들(기본적으로 모두)의 제안과 의견을 매우 환영합니다!

벤, 만약 당신이 이것을 읽고 있다면, 나는 당신이 무슨 일이 일어나고 있는지에 대해 눈을 떴다고 생각합니다. 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*의 포인터 산술은 다르지만, 문자들이 정리된 재미있는 방식 때문에 작동합니다.사실, 우리는 데이터 문자열에 대한 포인터를 전달받았지만, 만약 당신이 BSTR의 길이, 즉 데이터 문자열 앞의 4바이트에 접근하고 싶다면, 당신은 포인터 산술로 당신이 원하는 곳으로 게임을 할 수 있습니다.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이 Safarray와 Variants를 덜 복잡하게 만들 것인가요?). 그리고 Enter를 누른 후에, 저는 그 방법을 배우기 시작할 것입니다.하지만 이 방법도 효과가 있고 저에게 훌륭한 학습 기회였습니다.

좋아요, 그래서 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라는 매크로 지원 워크북에 있습니다.debug 출력 Dll과 동일한 디렉토리에 xlsm 배치 이 워크북 모듈에 다음과 같이 기록합니다.

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++ newbies에 대한 자세한 지침은 모듈에 대한 IDL과 함께 선언 함수를 버립니다.

이것은 사이먼의 대답에 대한 예시일 뿐입니다.은 매개 인 네이티브 .LPWSTR예로 간한예로, 는저를 합니다.GetWindowsDirectoryW사이먼이 지적했듯이 항상 기본 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

언급URL : https://stackoverflow.com/questions/39404028/passing-strings-from-vba-to-c-dll