0

I wrote a C++ COM server (out-of-proc) and client so:

idl (interface is IDispatch):

typedef[uuid(0952A366-20CC-4342-B590-2D8920D61613)]
    struct MyStruct{
    LONG                id;
    BYTE*               data;
    } MyStruct;

[helpstring("")] HRESULT foo([out] MyStruct* pStreamInfo);

server:

STDMETHODIMP foo(MyStruct* myStruct)
{
  myStruct.id = 7;
  myStruct.data = pData; // pData is a pointer to some data of variable length
  return S_OK;
}

client:

MyStruct ms;
hr = comObj->foo(&ms);

The code will work fine except when adding the myStruct.data = pData; line which crashes the server. Assigning memory in the client e.g. ms.data = new BYTE[1000] does not help as the pointer still arrives to foo as NULL.

What would be a solution, 1. preferably most simple one for client side since interface will be used by various users 2. Would there by a different solution if interface is used by C# client 3. If data needs to be out of the struct (I hope not) is there a reference to a complete example.

3
  • 1
    Use a safe array to marshal the data across interfaces. See stackoverflow.com/questions/295067/passing-an-array-using-com Commented Mar 4, 2019 at 13:24
  • 1
    You are using automation, but conformant arrays (pointer-and-length kind) are not automation-compatible. You should use a SAFEARRAY instead, as @RamblinRose correctly notes. Alternatively, instead of a structure, implement a COM object wrapping its fields, and return its interface pointer. data would then be one of the properties on that object. Commented Mar 4, 2019 at 14:41
  • How do I convert the VARIANT back to byte*? Commented Mar 4, 2019 at 15:52

1 Answer 1

0

As others have mentioned in the comments, you cannot pass a raw array this way. Minimally, you have to copy the byte array into a SAFEARRAY of bytes (SAFEARRAY(BYTE) in IDL). I just tested the code below with a custom proxy/stub (compiled from the P/S code generated by midl.exe), and I was able to get my data across the wire.

If you want to use a standard P/S such as PSDispatch ({00020420-0000-0000-C000-000000000046}) or PSOAInterface ({00020424-0000-0000-C000-000000000046}), or if you want to use VBA as a client, then you may have to convert this to a SAFEARRAY(VARIANT) and/or put the resulting safearray into a VARIANT. Try the simplest approach of just using SAFEARRAY(BYTE) first, because that's the one with the least overhead. (A SAFEARRAY(VARIANT) uses 16x more memory than a SAFEARRAY(BYTE) because a VARIANT is 16 bytes long. And you would have to manually convert each byte to/from a VARIANT, as opposed to the simple memcpy calls show below.)

Packing the byte array into a SAFEARRAY: (Note that this copies the byte array into the SAFEARRAY. You could muck around with the internals of the SAFEARRAY struct to prevent the copy, but you'd be doing things in a non-standard way.)

/// <summary>Packs an array of bytes into a SAFEARRAY.</summary>
/// <param name="count">The number of bytes.</param>
/// <param name="pData">A reference to the byte array. Not read if count is 0.</param>
/// <param name="pResult">Receives the packed LPSAFEARRAY on success.</param>
HRESULT PackBytes(ULONG count, const BYTE* pData, /*[ref]*/ LPSAFEARRAY* pResult)
{
    // initialize output parameters
    *pResult = NULL;

    // describe the boundaries of the safearray (1 dimension of the specified length, starting at standard index 1)
    SAFEARRAYBOUND bound{ count, 1 };

    // create the safearray
    LPSAFEARRAY safearray = SafeArrayCreate(VT_UI1, 1, &bound);
    if (!safearray)
        return E_OUTOFMEMORY;

    // when there is actually data...
    if (count > 0)
    {
        // begin accessing the safearray data
        BYTE* safearrayData;
        HRESULT hr = SafeArrayAccessData(safearray, reinterpret_cast<LPVOID*>(&safearrayData));
        if (FAILED(hr))
        {
            SafeArrayDestroy(safearray);
            return hr;
        }

        // copy the data into the safearray
        memcpy(safearrayData, pData, count);

        // finish accessing the safearray data
        hr = SafeArrayUnaccessData(safearray);
        if (FAILED(hr))
        {
            SafeArrayDestroy(safearray);
            return hr;
        }
    }

    // set output parameters
    *pResult = safearray;

    // success
    return S_OK;
}

Unpacking the byte array from the SAFEARRAY: (Note that this copies the byte array from the SAFEARRAY. You could muck around with the internals of the SAFEARRAY struct to prevent the copy, but you'd be doing things in a non-standard way. Also, you could choose to use the data directly from the SAFEARRAY by putting the consuming code between SafeArrayAccessData and SafeArrayUnaccessData.)

/// <summary>Unpacks an array of bytes from a SAFEARRAY.</summary>
/// <param name="safearray">The source SAFEARRAY.</param>
/// <param name="pCount">A pointer to a ULONG that will receive the number of bytes.</param>
/// <param name="ppData">A pointer to a BYTE* that will receive a reference to the new byte array.
/// This array must be deallocated (delete []) when the caller is done with it.</param>
HRESULT UnpackBytes(LPSAFEARRAY safearray, /*[out]*/ ULONG* pCount, /*[out]*/ BYTE** ppData)
{
    // initialize output parameters
    *pCount = 0;
    *ppData = NULL;

    // validate the safearray element type (must be VT_UI1)
    VARTYPE vartype;
    HRESULT hr = SafeArrayGetVartype(safearray, &vartype);
    if (FAILED(hr))
        return hr;
    if (vartype != VT_UI1)
        return E_INVALIDARG;

    // validate the number of dimensions (must be 1)
    UINT dim = SafeArrayGetDim(safearray);
    if (dim != 1)
        return E_INVALIDARG;

    // get the lower bound of dimension 1
    LONG lBound;
    hr = SafeArrayGetLBound(safearray, 1, &lBound);
    if (FAILED(hr))
        return hr;

    // get the upper bound of dimension 1
    LONG uBound;
    hr = SafeArrayGetUBound(safearray, 1, &uBound);
    if (FAILED(hr))
        return hr;

    // if the upper bound is less than the lower bound, it's an empty array
    if (uBound < lBound)
        return S_OK;

    // calculate the count of the bytes
    ULONG count = uBound - lBound + 1;

    // create buffer
    BYTE* pData = new BYTE[count];
    if (!pData)
        return E_OUTOFMEMORY;

    // begin accessing the safearray data
    BYTE* safearrayData;
    hr = SafeArrayAccessData(safearray, reinterpret_cast<LPVOID*>(&safearrayData));
    if (FAILED(hr))
    {
        delete[] pData;
        return hr;
    }

    // copy the data
    memcpy(pData, safearrayData, count);

    // finish accessing the safearray data
    hr = SafeArrayUnaccessData(safearray);
    if (FAILED(hr))
    {
        delete[] pData;
        return hr;
    }

    // set output parameters
    *pCount = count;
    *ppData = pData;

    // success
    return S_OK;
}
Sign up to request clarification or add additional context in comments.

3 Comments

How is cleaning up done? I guess on client side after ppData is used it should be deleted. How about pResult on server side, when to delete it? And safearray on client side?
The client side is the side which packs the bytes -- it needs to destroy the safearray after making the call to the server (and it needs to free the original byte array, if appropriate). The server side is the side which unpacks the bytes -- it needs to delete the data allocated in UnpackBytes. The safearray on the server side will get freed by the marshaler.
@grunt I noticed that you proposed an edit that uses 0 as the starting index instead of 1, and that edit was rejected by other users. When using SAFEARRAY, you must never assume that the starting index is 0. You must use SafeArrayGetLBound. The primary purpose of this is VBA usage, where indices do start at 1. For maximum compatibility with all COM clients, start your SAFEARRAY indices at 1.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.