11

My C declarations are as follows:

int myData(uint myHandle, tchar *dataName, long *Time, uint *maxData, DATASTRUCT **data);

typedef struct {
  byte Rel;
  __int64 Time;
  char Validated;
  unsigned char Data[1];
} DATASTRUCT;

My C# declarations are as follows:

[DllImport("myData.dll", EntryPoint = "myData")]
public static extern int myData(uint myHandle, [MarshalAs(UnmanagedType.LPTStr)] string dataName, out long Time, out uint maxData, ref DATASTRUCT[] data);

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DATASTRUCT
{
    public sbyte Rel;
    public long Time;
    public byte Validated;
    public double Data;
}

I then call the managed function as follows:

string dataToShow = "description";
long Time;
uint maxData; // How many structs will be returned, i.e., how much data is available?
uint myHandle = 1;

DATASTRUCT[] dataInformation = new DATASTRUCT[3]; // Doesn't it matter what I specify as the array size?

myData(myHandle, dataToShow, out Time, out maxData, ref dataInformation);

Upon execution the above function will return successfully with only one structure even though there are 3 to return. Why is this so?

Additional information; I have tried passing the pointer to a pointer of an array of structs the following ways:

  • ref DATASTRUCT[] data; // It works, but it only returns one struct
  • [Out, MarshalAs(UnmanagedType.LPArray)] DATASTRUCT[] data; // returns the number of defined structs with garbage

As I understand it I might need to do some manual marshalling using IntPtr, I do not know how to implement this however, so any advice would be appreciated.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user1470994
  • 301
  • 1
  • 2
  • 9
  • How is the C function allocating the structs? Does it use the input array as a buffer, or generate new structs with malloc? – Jason Larke Jun 21 '12 at 05:06
  • Oh, and also you're C# declaration looks wrong. An unsigned char is not the same size as a double (1 vs 8 bytes) – Jason Larke Jun 21 '12 at 05:08
  • @JasonLarke, Unfortunately the DLL library documentation does not state whether it allocates memory for the struct or not. I am assuming that it doesn't. I did not provide you with the full deceleration for `Data` it is in fact a variable char that can take a maximum of 8 bytes and hence the reason behind marshalling it as a double, this variable definitely returns the correct results each time I have tested it. – user1470994 Jun 21 '12 at 05:52
  • Try this [Out, MarshalAs(UnmanagedType.LPArray,SizeParamIndex:=3)] out DATASTRUCT[] data; – user629926 Jun 21 '12 at 09:00
  • @user629926, I get an exception when calling this as `myData(myHandle, dataToShow, out Time, out maxData, out dataInformation);` removing the `out` returns a similar output as `[Out, MarshalAs(UnmanagedType.LPArray)] DATASTRUCT[] data;` – user1470994 Jun 21 '12 at 10:32
  • Forgot SizeParamIndex doesent work with ref,out Check this http://stackoverflow.com/a/188396/629926 – user629926 Jun 21 '12 at 10:53
  • @JasonLarke, upon further investigation it seems as though myData.dll allocates the memory for the structs. – user1470994 Jun 22 '12 at 00:20
  • In that case, simply change the parameter to an IntPtr and marshal the return. See my answer. – Jason Larke Jun 22 '12 at 04:25

2 Answers2

9

Okay, it seems as though your native library does the allocation, so really all you need to do is provide a pointer through which you can access the allocated data.

Change your API definition to (note, I changed the maxData param to uint, long is 64 bits in .NET and 32 bits in native.

[DllImportAttribute("myData.dll", EntryPoint = "myData")]
public static extern int myData(uint myHandle, [MarshalAs(UnmanagedType.LPTStr)] string dataName, out uint Time, out uint maxData, out IntPtr pData);

Off the top of my head I can't quite remember if you need the out keyword for the final parameter, but I think so.

Then, call myData:

uint nAllocs = 0, time = 0;
IntPtr pAllocs = IntPtr.Zero;
myData(1, "description", out time, out nAllocs, out pAllocs);

Now, pAllocs should point to unmanaged memory, to marshal these into managed memory isn't too difficult:

[StructLayoutAttribute(LayoutKind.Sequential, Pack = 1)]
public struct DATASTRUCT
{
    public byte Rel;
    public long Time;
    public byte Validated;
    public IntPtr Data; //pointer to unmanaged string.
}


int szStruct = Marshal.SizeOf(typeof(DATASTRUCT));
DATASTRUCT[] localStructs = new DATASTRUCT[nAllocs];
for(uint i = 0; i < nallocs; i++)
    localStructs[i] = (DATASTRUCT)Marshal.PtrToStructure(new IntPtr(pAllocs.ToInt32() + (szStruct * i)), typeof(DATASTRUCT));

And now you should have an array of local structs.

A point to note You may need to set your project to compile as x86, to standardize the size of an IntPtr to 4 bytes (DWORD) instead of AnyCPU's default 8.

Jason Larke
  • 5,289
  • 25
  • 28
  • Thank you Jason, so I believe we are getting somewhere. I now get the correct amount of structs returned as well as data filling the structs. When using `public IntPtr Data;` the data returned for `data` seems like garbage, if i change it back to `public double Data;` it returns the data correctly. – user1470994 Jun 22 '12 at 06:08
1

A pointer to a pointer could be represented in your dllimport declaration as ref IntPtr data, so your declaration would become:

[DllImportAttribute("myData.dll", EntryPoint = "myData")]
public static extern int myData(uint myHandle, [MarshalAs(UnmanagedType.LPTStr)] string dataName, out long Time, out uint maxData, ref IntPtr data);

(As an aside, I think a long in C is just equivalent to an int in C#. Long in C# is an Int64, which would be a long long in C)

Marshalling your DATASTRUCT[] to an IntPtr can be done using the GCHandle class

DATASTRUCT [] dataInformation = new DATASTRUCT[3];
GCHandle gch = GCHandle.Alloc(dataInformation , GCHandleType.Pinned);
IntPtr ptr = gch.AddrOfPinnedObject();
myData(myHandle, dataToShow, out Time, out maxData, ref ptr);
//It's absolutely essential you do this next bit so the object can be garbage collected again, 
//but it should only be done once the unmanaged code is definitely done with the reference.    
gch.Free(); 

Using the Marshal class and the StructureToPtr or Copy methods of it would also be an option but for the purposes of proving the concept at least the GCHandle should do the trick, it's just not ideal for scenarios where the unmanaged code does long running operations because you've pinned this object in place and the GC can't move it until you free it.

Nanhydrin
  • 4,332
  • 2
  • 38
  • 51
  • I attempted the above, it returns the 3 structures in the dataInformation array, however, they are all empty. I have also edited my original post to reference the Int64 appropriately as it is in fact an __int64. – user1470994 Jun 21 '12 at 10:37
  • Have you updated the c# struct definition too? The unsigned char in the original would not translate to a double as @JasonLarke observed in the other comments. If you get the size of the struct does it match the expected size as per the C code? – Nanhydrin Jun 21 '12 at 10:57
  • It might be worth trying Marshal and just passing in a pointer to a pointer to an array of byte arrays rather than an array of DATASTRUCTS. At least then you should be able to see if there are any bytes at all in there. – Nanhydrin Jun 21 '12 at 10:58
  • Yes I have also updated the struct, the `unsigned char Data[1];` is returning correct values I believe. How would I check that the current struct size matches the C code? Like I said there is definitely data there as it returns 1 struct ok when using `ref DATASTRUCT[] data;` – user1470994 Jun 21 '12 at 13:09
  • In C# Marshal.SizeOf(typeof(DATASTRUCT)) will give you the size in bytes required for the whole struct. I don't know how you'd get that in C, probably just manually calculate it from the definition. I make it 7 bytes in the C code. – Nanhydrin Jun 21 '12 at 14:03
  • This will return 18bytes. So, as I see it the `public double Data; ` is assumed as incorrect? If so, what are your recommendations for marshalling this type, keeping in mind that the DLL Library docmentation states that it is "variable size which is equal to 'datalength'" in my case, datalength is equal to 8bytes. Also, `unsigned char Data[1];` could well be a pointer to the start of the character string of `datalength` length, terminating at NULL. – user1470994 Jun 21 '12 at 23:10
  • My maths was off there, the C struct is 11 bytes. The double is definitely wrong. You could try a byte rather than a double, or perhaps [MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst=1)] public string Data; but you'd also possibly need to change your struct attributes to include CharSet = CharSet.Ansi. – Nanhydrin Jun 22 '12 at 09:04