Marshalling Complicated Structures using PInvoke
This article talks about marshaling structures using PInvoke which has a pointer to an array of another structure as a member.
When calling native functions in a managed application, a frequent question that comes to mind is how to marshal a nested complicated structure. Recently I came across a scenario where we have to marshal a structure which contains a pointer to an array of another structure as a member.
When doing PInvoke, it’s essential to know what is the layout of the native structure and how it can be marshaled. During marshaling, one of the most important steps is converting unmanaged types to managed types. You can use the below link for conversion table to match basic data types.
msdn.microsoft.com/en-us/library/aa719104(VS.71).aspx#docum_topic3
Here is an example of C struct definitions that need to be marshaled and a function to marshal them
// NativeDLL.cpp : Defines the exported functions for the DLL application.
#include "stdafx.h"
#include <strsafe.h>
#include <stdio.h>
#define _EXPORTS_API __declspec(dllexport)
#define HASHSIZE 151
typedef struct CELL
{
char *name;
char *label;
char *fmt;
double amount;
unsigned int precision;
unsigned int flags;
unsigned int rank;
} CELL;
typedef struct CELLTABLE
{
CELL *table[HASHSIZE];
void *userData; // reserved for future use
} CELLTABLE;
extern "C" _EXPORTS_API void NativeFunction(CELLTABLE * pFinance)
{
char buffer[1024];
for ( int ix = 0 ; pFinance->table[ix] != NULL ; ix++ )
{
CELL * pCell = pFinance->table[ix] ;
StringCbPrintf(buffer, sizeof(buffer), "%d:\n%f\n%s\n%u\n%u\n%u\n",ix, pCell->amount, pCell->name, pCell->precision, pCell->flags, pCell->rank);
MessageBox(NULL,buffer,"Test",MB_OK);
}
}
Note: Char set of native application is set to Multi-Byte Character Set.
Managed Structure equivalent to Native Structure:
//MyManagedApp.cs file
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace MyManagedAPP
{
public class MyClass
{
private const int HASHSIZE = 151;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public class CELL
{
public string name;
public string label;
public string fmt;
public double amount;
public UInt32 precision;
public UInt32 flags;
public UInt32 rank;
};
[StructLayout(LayoutKind.Sequential)]
public class CELLTABLE
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = HASHSIZE)]
public IntPtr[] table;
public IntPtr userData;
}
[DllImport("PInvokeDll.DLL")]
public static extern void NativeFunction(IntPtr finance);
public void Test()
{
CELL[] cells = new CELL[HASHSIZE];
cells[0] = new CELL();
cells[0].amount = 123.45;
cells[0].name = "First Record";
cells[0].precision = 50;
cells[0].flags = 0;
cells[0].rank = 0;
cells[1] = new CELL();
cells[1].amount = 9876.54;
cells[1].name = "Second Record";
cells[1].precision = 3;
cells[1].flags = 0;
cells[1].rank = 1;
cells[2] = new CELL();
cells[2].amount = 24680.1357;
cells[2].name = "Third Record";
cells[2].precision = 2;
cells[2].flags = 255;
cells[2].rank = 4;
CELLTABLE ct = new CELLTABLE();
ct.table = new IntPtr[HASHSIZE];
for (int ix = 0; ix < HASHSIZE && cells[ix] != null; ix++)
{
int nSizeCell = Marshal.SizeOf(cells[ix]);
ct.table[ix] = Marshal.AllocHGlobal(nSizeCell);
Marshal.StructureToPtr(cells[ix], ct.table[ix], false);
}
int nSizeTable = Marshal.SizeOf(ct);
IntPtr pCellTable = Marshal.AllocHGlobal(nSizeTable);
Marshal.StructureToPtr(ct, pCellTable, false);
NativeFunction(pCellTable);
}
static void Main()
{
MyClass md = new MyClass();
md.Test();
}
}
}
Note: Marshaling the structure in C#, it’s essential to set CharSet to Ansi if the C++ application expects an Ansi type string. If it’s not set to Ansi, output can be unexpected (Mostly first character of string passed). If you don't explicitly set the CharSet property, then its default is CharSet.Ansi. If the native application is built with UNICODE defined, the CharSet should be set to CharSet.Unicode.
Jyoti Patel
Developer Support VC++ and C#
Anonymous
October 30, 2011
Good article. I have a double pointer in my structure like CELL **pcell.Please help me how to declare in c# Thanks in advanceAnonymous
March 31, 2014
If I am not mistaken: "IntPtr pCellTable = Marshal.AllocHGlobal(nSizeTable);" will cause memory to be leaked since memory allocated by 'Marshal.AllocHGlobal' is unmanaged and CLR will not clean it up. 'Marshal.FreeHGlobal();' must be called.Anonymous
March 31, 2014
@Nickolay , agreed I would suggest you to free up the memory using FreeHGlobal. How ever in my experience with clients & discussions there is consensus around , when allocating memory with (Marshal.AllocHGlobal), and the size of the process heap grows, it will never be released completely so that .NET can use it again. Be careful not to grow your process heap out of bounds and avoid having your .NET applications run out of memory space.