Search code examples
c#arrayspointerspinvokemarshalling

How to marshal a pointer to array of structs which contains an unsigned char array?


I'm sorry if this sounds too specific, but I need to get this done exactly this way and I'm stuck at it for days. In my real scenario, I have a MyStruct **ppObject which address must be passed to a third-party dll so that it will point to an array of structs. Afterwards, I must p/Invoke this same pointer to array, but I'm having problems with the contents of the struct. Here's a MCVE:

Unmanaged.h:

#ifndef _NATIVELIB_H_
#define _NATIVELIB_H_

#ifndef MCVE
#define MCVE
#endif

struct PlcVarValue
{
    unsigned char   byData[8];
};

#ifdef __cplusplus
extern "C" {
#endif
    MCVE __declspec(dllexport) void FillArray(void);
    MCVE __declspec(dllexport) void Clear(void);
    MCVE __declspec(dllexport) PlcVarValue** GetValues(void);

#ifdef __cplusplus
}
#endif

#endif // _NATIVELIB_H_

Unmanaged.cpp

#include "stdafx.h"
#include "Unmanaged.h"
#include <iostream>

PlcVarValue** values;

MCVE __declspec(dllexport) void FillArray(void) {
    values = (PlcVarValue**)malloc(sizeof(PlcVarValue*) * 5);

    for (int i = 0; i < 5; i++) 
    {
        values[i] = new PlcVarValue();
        *values[i]->byData = i;
    }
}

MCVE __declspec(dllexport) void Clear(void) {
    delete *values;
    free(values);
}

MCVE __declspec(dllexport) PlcVarValue** GetValues(void) {
    return values;
}

PlcVarValue.cs

using System.Runtime.InteropServices;

namespace Managed
{
    [StructLayout(LayoutKind.Sequential)]
    public class PlcVarValue
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public byte[] Data;
    }
}

ManagedClass.cs

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace Managed
{
    public class ManagedClass
    {
        public ManagedClass()
        {
            FillArray();
        }

        [DllImport("Unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern void FillArray();

        [DllImport("Unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr GetValues();

        [DllImport("Unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern void Clear();

        public List<PlcVarValue> GetList()
        {
            int size = 5;
            var list = new List<PlcVarValue>();
            IntPtr ptr = GetValues();

            var gch = GCHandle.Alloc(ptr, GCHandleType.Pinned);

            try
            {
                int memSize = Marshal.SizeOf(typeof(PlcVarValue));
                for (int i = 0; i < size; i++)
                {
                    //I know this is wrong, it would work for a PlcVarValue* but I need it to work with a PlcVarValue**
                    list.Add((PlcVarValue)Marshal.PtrToStructure(new IntPtr(ptr.ToInt32() + memSize * i), typeof(PlcVarValue)));
                }
            }
            finally
            {
                gch.Free();
            }

            return list;
        }

        public void FreeMemory()
        {
            Clear();
        }
    }
}

Program.cs

using Managed;
using System;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            var managed = new ManagedClass();
            var list = managed.GetList();
            foreach(var value in list)
                Console.WriteLine(BitConverter.ToInt32(value.Data, 0));

            managed.FreeMemory();
            Console.ReadKey();
        }
    }
}

I hope that code is self-explanatory, I'll provide more information if needed. My problem is that the byte array (which I'm trying to marshal from the aforementioned unsigned char array) in the Program class prints different random data every time I run the program but when I check the C++ side with the debugger (before marshalling takes place), everything is fine. Like I mentioned in a comment in the code above, I believe the problem lies in this line in the ManagedClass class:

list.Add((PlcVarValue)Marshal.PtrToStructure(new IntPtr(ptr.ToInt32() + memSize * i), typeof(PlcVarValue)));

I know for a fact that this works fine with MyStruct *pObject where *pObject is an array of the same struct, but in this case I need a pointer to a pointer since what the third-party dll requires is actually a MyStruct ***pppObject (beats me, but that's what I have to deal with). I have tried to copy data from ppObject to a single pointer but, even though it worked, I was not satisfied with the result as it had some undesired side-effects in the real application. Also, should my usage of GCHandle be wrong, I'd gladly accept advice on how to fix it, but that's not the main focus of this question.


Solution

  • The C# side:

    public List<PlcVarValue> GetList()
    {
        int size = 5;
        var list = new List<PlcVarValue>(size);
        IntPtr ptr = GetValues();
    
        for (int i = 0; i < size; i++)
        {
            IntPtr ptrPlc = Marshal.ReadIntPtr(ptr, i * IntPtr.Size);
            var plc = (PlcVarValue)Marshal.PtrToStructure(ptrPlc, typeof(PlcVarValue));
            list.Add(plc);
        }
    
        return list;
    }
    

    and the C++ side (the only error was in the Clear()):

    __declspec(dllexport) void Clear(void)
    {
        for (int i = 0; i < 5; i++)
        {
            delete values[i];
        }
    
        free(values);
    }
    

    BUT you shouldn't mix malloc and new!

    If you need an explanation about what I'm doing, remember that you are returning a pointer to an array of pointers!