See the question and my original answer on StackOverflow

There are multiple ways to pass an array back from C++.

For example, you can use a raw byte array like you were trying to do. It works but it's not very practical from .NET because it's not a COM automation type which .NET loves.

So, let's say we have this .idl:

interface IBlah : IUnknown
{
    HRESULT GetBytes([out] int *count, [out] unsigned char **bytes);
}

Here is a sample native implementation:

STDMETHODIMP CBlah::GetBytes(int* count, unsigned char** bytes)
{
    if (!count || !bytes)
        return E_INVALIDARG;

    *count = numBytes;
    *bytes = (unsigned char*)CoTaskMemAlloc(*count);
    if (!*bytes)
        return E_OUTOFMEMORY;

    for (unsigned char i = 0; i < *count; i++)
    {
        (*bytes)[i] = i;
    }
    return S_OK;
}

And a sample C# calling code (note the .NET type lib importer doesn't know anything beyond pointers when it's not a COM automation type, so it just blindly defines the argument as an IntPtr):

var obj = (IBlah)Activator.CreateInstance(myType);

// we must allocate a pointer (to a byte array pointer)
var p = Marshal.AllocCoTaskMem(IntPtr.Size);
try
{
    obj.GetBytes(out var count, p);

    var bytesPtr = Marshal.ReadIntPtr(p);
    try
    {
        var bytes = new byte[count];
        Marshal.Copy(bytesPtr, bytes, 0, bytes.Length);
        // here bytes is filled
    }
    finally
    {
        // here, we *must* use the same allocator than used in native code
        Marshal.FreeCoTaskMem(bytesPtr);
    }
}
finally
{
    Marshal.FreeCoTaskMem(p);
}

Note: this won't work in out-of-process scenario as the .idl is not complete to support this, etc.

Or you can use a COM Automation type such as SAFEARRAY (or a wrapping VARIANT). which also would allow you to use it with other languages (such as VB/VBA, Scripting engines, etc.)

So, we could have this .idl:

HRESULT GetBytesAsArray([out] SAFEARRAY(BYTE)* array);

This sample native implementation (a bit more complex, as COM automation was not meant for C/C++, but for VB/VBA/Scripting object...):

STDMETHODIMP CBlah::GetBytesAsArray(SAFEARRAY** array)
{
    if (!array)
        return E_INVALIDARG;

    // create a 1-dim array of UI1 (byte)
    *array = SafeArrayCreateVector(VT_UI1, 0, numBytes);
    if (!*array)
        return E_OUTOFMEMORY;

    unsigned char* bytes;
    HRESULT hr = SafeArrayAccessData(*array, (void**)&bytes); // check errors
    if (FAILED(hr))
    {
        SafeArrayDestroy(*array);
        return hr;
    }

    for (unsigned char i = 0; i < numBytes; i++)
    {
        bytes[i] = i;
    }

    SafeArrayUnaccessData(*array);
    return S_OK;
}

And the sample C# code is much simpler, as expected:

var obj = (IBlah)Activator.CreateInstance(myType);
obj.GetBytesAsArray(out var bytesArray);