Marshalling Output Arrays

I recently ran into an issue getting values out of a function that you pass an int* to get an array out. The C function looks like this:

int __stdcall UP_GetProgList(int prog_type, int *sn_list, int count, int *count_returned)

Naively, I wrote the following P/Invoke call:

[DllImport(@"the.dll")]
private static extern int UP_GetProgList(int prog_type, out int[] sn_list, int count, out int count_returned);

Turns out that when I called that, I got an access violation.

I tried searching for the solution, I even asked on StackOverflow, but no one seemed to have the answer in an easy-to-digest format.

So I did some digging. My first attempt used out int instead of the array. That didn't give me an exception, but it would only allow me to get one value out.

I eventually found the IntPtr type. My first attempt at using it looked something like this:

void Main()
{
	var listP = Marshal.AllocHGlobal(20);
	UP_GetProgList(2, listP, 20, out int countReturned);
	
	for (int i = 0; i < countReturned; i++)
	{
		unsafe
		{
			Console.Out.WriteLine(*(int*)(listP + i));
		}
	}
}

[DllImport(@"the.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int UP_GetProgList(int prog_type, IntPtr sn_list, int count, out int count_returned);

That worked, and gave me the results I was expecting. There were two problems: 1. It used unsafe which I didn't like the look of, and, as a StackOverflow user pointed out, it had a memory leak.

More digging was required. The solution was Marshal.Copy, which I had looked at, but rejected because I'd only seen the version for copying an array into a pointer. Turns out there's an overload that does the opposite. Adding a try and finally block fixed the memory leak, so I ended up with the following code, which works, and shouldn't leak memory:
void Main()
{
	Console.Out.WriteLine(GetSerialNumbers());
}

int[] GetSerialNumbers()
{
	IntPtr listPtr = IntPtr.Zero;

	try
	{
		listPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int)) * 20);
		var result = UP_GetProgList(PROG_FORTE, listPtr, 20, out int countReturned);
		var sns = new int[countReturned];
		Marshal.Copy(listPtr, sns, 0, countReturned);
		return sns;
	}
	finally
	{
		Marshal.FreeHGlobal(listPtr);
	}
}

This gave me the expected results, shouldn't leak memory, and also doesn't use unsafe. I hope this can serve as a template for someone else who encounters this problem.

Comments

Popular posts from this blog

RoadieRichStateMachine

Radio nets for the non-amateur