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.
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!