0

I would like to pass a SAFEARRAY of BSTR from a C++ DLL to VB6. This is what my current implementation of the DLL looks like (compiled with VC2013U5 on Win7 x64 SP1):

sabstrtovb6.h:

#ifdef SABSTRTOVB6_EXPORTS
#define SABSTRTOVB6_API __declspec(dllexport)
#else
#define SABSTRTOVB6_API __declspec(dllimport)
#endif

#include <OAIdl.h>

SABSTRTOVB6_API HRESULT __stdcall FnSABSTRToVB6(LPSAFEARRAY* ppSABSTR);

sabstrtovb6.cpp (Note: I'm allocating ANSI strings with SysAllocStringByteLen() because VB6 always performs a wide-char / ANSI and vice versa translation of strings when interfacing with DLLs. When the function returns, VB6 performs a wide-char conversion for each string in the array.):

#include "stdafx.h"
#include "sabstrtovb6.h"

#include <atlsafe.h>

SABSTRTOVB6_API HRESULT __stdcall FnSABSTRToVB6(LPSAFEARRAY* ppSABSTR)
{
    if (!ppSABSTR)
        return E_POINTER;

    if (*ppSABSTR)
        return DISP_E_BADINDEX;

    HRESULT hr = SafeArrayDestroy(*ppSABSTR);
    if FAILED(hr)
        return hr;

    CComSafeArray<BSTR> cSABSTR(ULONG(0), LONG(0));

    cSABSTR.Add(SysAllocStringByteLen("Apple\0", strlen("Apple\0")));
    cSABSTR.Add(SysAllocStringByteLen("Orange\0", strlen("Orange\0")));
    cSABSTR.Add(SysAllocStringByteLen("Wall\0", strlen("Wall\0")));
    cSABSTR.Add(SysAllocStringByteLen("Bananas\0", strlen("Bananas\0")));
    cSABSTR.Add(SysAllocStringByteLen("Fax\0", strlen("Fax\0")));

    *ppSABSTR = cSABSTR.Detach();

    return S_OK;
}

All works well, except that the length of some strings is mismatched by an offset of a NULL character. When executing the following code in VB6...

Option Explicit

Private Declare Function FnSABSTRToVB6 Lib "sabstrtovb6" (ByRef sa() As String) As Long

Private Sub Form_Load()

ChDrive App.Path: ChDir App.Path

Dim sa() As String
Dim res As Long: res = FnSABSTRToVB6(sa)

If res >= 0 Then
    Dim i As Long
    For i = 0 To UBound(sa)
        Debug.Print (sa(i) + ":" + CStr(Len(sa(i))))
    Next
End If

End Sub

... this is what the debug window display as an output:

Apple :6
Orange:6
Wall:4
Bananas :8
Fax :4

Clearly, the length indicator of Apple, Bananas, and Fax is wrong. I get the feeling that all strings with an odd-numbered length are padded with an additional byte. What is going on here? Can someone help me fix this behavior?

UPDATE: It seems that the CComSafeArray wrapper is causing the trouble here. When I allocate the BSTRs in the old fashioned way, as in...

SAFEARRAYBOUND bounds;
bounds.cElements = 5;
bounds.lLbound = 0;
*ppSABSTR = SafeArrayCreate(VT_BSTR, 1, &bounds);

SafeArrayLock(*ppSABSTR);

BSTR *SABSTRArray = (BSTR *)(*ppSABSTR)->pvData;
SABSTRArray[0] = SysAllocStringByteLen("Apple\0", strlen("Apple\0"));
SABSTRArray[1] = SysAllocStringByteLen("Orange\0", strlen("Orange\0"));
SABSTRArray[2] = SysAllocStringByteLen("Wall\0", strlen("Wall\0"));
SABSTRArray[3] = SysAllocStringByteLen("Bananas\0", strlen("Bananas\0"));
SABSTRArray[4] = SysAllocStringByteLen("Fax\0", strlen("Fax\0"));

SafeArrayUnlock(*ppSABSTR);

... Len in VB6 will display correct values:

Apple:5
Orange:6
Wall:4
Bananas:7
Fax:3

Is this a known behavior of CComSafeArray when dealing with BSTRs?

Aurora
  • 1,334
  • 8
  • 21
  • 2
    From MSDN about SysAllocStringByteLen: _The string psz can contain embedded null characters, and does not need to end with a Null_. Actual length is then right because you add an extra nil character at the end. – Adriano Repetti Oct 07 '15 at 12:09
  • 2
    Yes, that cannot work. The `CComSafeArray::SetAt()` accessor function dutifully creates a copy of the BSTR before storing it in the array. That cannot work correctly on these weirdo 8-bit BSTRs, it has no way to find out that they are special. It uses SysAllocString() to create the copy and passes the Chinese, turning 5 chars into 6. You can't CComSafeArray, it is too safe :) – Hans Passant Oct 07 '15 at 15:48
  • What if I'd omit the copy operation by using `CComSafeArray's` overloaded `Add` method and specifying `FALSE` for the `bCopy` parameter? Would this cause any trouble later on regarding reference counting of the managed BSTR inside the array? – Aurora Oct 07 '15 at 16:04
  • Yes, that would be good. You were leaking those BSTRs, they are not reference-counted. – Hans Passant Oct 07 '15 at 17:07
  • You don't need \0 inside those strings as there already is one provided by the compiler. Just use L"apple" and `SysAllocString` as BSTR are unicode by design. What do you use for "length indicator in VB6", `LenB`? Stop it and just use `Len` and see that marshalling is ok, your initialization makes no sense. – wqw Oct 07 '15 at 18:37
  • @wqw `SysAllocString` won't work, since VB6 expects ANSI strings when the function returns (see this [post](https://stackoverflow.com/questions/6134666/pass-bstr-from-c-dll-function-to-vb6-application)). Also, I'm using `Len` for calculating the string length, as indicated in my VB6 code snippet. – Aurora Oct 07 '15 at 19:00
  • What's going on here and how to fix it is longer than a comment can fit. Yes, by using a `Declare` in VB6 you are suffering from the automagic ANSI<->Unicode translation VB6 does on each call. – wqw Oct 07 '15 at 19:25
  • @wqw Are you implying using a type library? That would require me to perform a registry entry, a step I'd like to avoid. – Aurora Oct 07 '15 at 19:37
  • `ByVal param As String` on a `Declare` is not BSTR but LPSTR. There is no win32 API function with `param() As String` parameters. This would be treated as a safe array of LPSTRs which is weird. I would simplify function signature to a simple string and then use `Split(result, Chr$(1))` on the client side. That or use a typelib with proper string array for out param. – wqw Oct 07 '15 at 20:12

0 Answers0