3

I'm experiencing some strange behaviour when marshalling a C# byte[] array to C++.

When the byte[] is passed as an argument I get the expected data in C++. (See ReportData)

When the byte[] is wrapped in a struct I get strange values out. (See ReportBuffer)

What is causing this difference in behaviour and is there anyway to correct it as I need to have the data wrapped in a more complex use case?

C# Calling Code

public struct Buffer
{
    public int DataLength;
    public byte[] Data;
    
    public Buffer(byte[] data) : this()
    {
        Data = data;
        DataLength = data.Length;
    }
}

internal class Program
{
    [DllImport(@"C:\Users\lawsm\source\repos\MarshallingTest\Debug\Test.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern void ReportBuffer(Buffer buffer);

    [DllImport(@"C:\Users\lawsm\source\repos\MarshallingTest\Debug\Test.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern void ReportData(byte[] data, int dataCount);


    private static void Main(string[] args)
    {
        byte[] data = new byte[] {0, 1, 2, 3, 4, 5};
        Buffer buffer = new Buffer(data);

        Console.WriteLine("Report Buffer");
        ReportBuffer(buffer);

        Console.WriteLine("\n\nReport Data");
        ReportData(data, data.Length);

        Console.ReadKey();
    }
}

C++ Dll Code

#include <cstdint>
#include <iostream>

struct Buffer
{
public:
    int DataLength;
    uint8_t* Data;  
};


extern "C"
{
    _declspec(dllexport) void ReportBuffer(const Buffer& buffer)
    {
        for (int i = 0; i < buffer.DataLength; i++)
        {
            std::cout << (int)buffer.Data[i] << std::endl;
        }
    }

    _declspec(dllexport) void ReportData(uint8_t* data, int dataLength)
    {
        for (int i = 0; i < dataLength; i++)
        {
            std::cout << (int)data[i] << std::endl;
        }
    }
}

Console Output

Report Buffer
1
0
128
0
1
0


Report Data
0
1
2
3
4
5

1 Answers1

-1

I discovered a solution by changing the byte[] array to an IntPtr, allocating the space and copying the data.

[StructLayout(LayoutKind.Sequential)]
public struct Buffer
{
    public int DataLength;
    public IntPtr Data;
    
    public Buffer(byte[] data) : this()
    {
        Data = Marshal.AllocHGlobal(data.Length);
        Marshal.Copy(data, 0, Data, data.Length);
        DataLength = data.Length;
    }
}

[DllImport("Buffer.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern void ReportBuffer(Buffer buffer);

The C++ code remains the same:

struct Buffer
{
public:
    int DataLength;
    uint8_t* Data;  
};

extern "C"
{
    _declspec(dllexport) void ReportBuffer(const Buffer& buffer)
    {
        std::cout << buffer.DataLength << std::endl;
        for (int i = 0; i < buffer.DataLength; i++)
        {
            std::cout << (int)buffer.Data[i] << std::endl;
        }
    }
}
  • Note that you still need `[StructLayout(LayoutKind.Sequential)]` or the compiler is allowed to hose you in the future. Having worked you what you're asking, my solution is somewhat similar to yours. – Joshua Apr 16 '21 at 15:49
  • You're very correct, I'll add that to the awnser. – Michael Laws Apr 16 '21 at 16:39
  • This is pretty miserable compared to the pattern that passes the length and array as separate parameters. You now have a memory leak that wasn't there before. – Ben Voigt Apr 16 '21 at 16:49
  • @BenVoigt: lol I missed it he forgot to free the memory. If I'm not transferring ownership to the native code I'll pin and unpin the array but forgetting to unpin is about as bad. – Joshua Apr 16 '21 at 16:55
  • @Joshua: Right. And if you are transferring ownership to the native code, you can't use `Marshal.AllocHGlobal`, you have to use the allocation function matching the deallocation that the native code does later. – Ben Voigt Apr 16 '21 at 16:58
  • @BenVoigt: If you allocate with `Marshal.AllocHGlobal`, you can free it with `GlobalFree` in native code. – Joshua Apr 16 '21 at 17:02
  • @Joshua: You can't, because `AllocHGlobal` does not actually allocate or return an `HGLOBAL`. [When `AllocHGlobal` calls `LocalAlloc`, it passes a `LMEM_FIXED` flag, which causes the allocated memory to be locked in place](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.allochglobal?view=net-5.0) – Ben Voigt Apr 16 '21 at 17:03
  • @BenVoigt: lol it calls localalloc. Why would they name it that badly? – Joshua Apr 16 '21 at 17:08
  • @Joshua: I can't help there, but to me that kind of major design flaw justifies (1) never using it and (2) describing all code that does use it as smelly – Ben Voigt Apr 16 '21 at 17:10