Article: Marshaling arrays from C# to C

Home Page


Consultancy

  • Service Vouchers
  • Escrow Service

Shop



Programming
  • Articles
  • Tools
  • Links

Search

 

Contact

 

Chess Puzzles




DWHS

Valid XHTML 1.0 Transitional
Valid CSS!
Mobile-friendly!

An experiment to investigate how well automatic array marshaling works.

category 'experiment', language C#, created 25-Apr-2010, version V1.0, by Luc Pattyn


License: The author hereby grants you a worldwide, non-exclusive license to use and redistribute the files and the source code in the article in any way you see fit, provided you keep the copyright notice in place; when code modifications are applied, the notice must reflect that. The author retains copyright to the article, you may not republish or otherwise make available the article, in whole or in part, without the prior written consent of the author.

Disclaimer: This work is provided “as is”, without any express or implied warranties or conditions or guarantees. You, the user, assume all risk in its use. In no event will the author be liable to you on any legal theory for any special, incidental, consequential, punitive or exemplary damages arising out of this license or the use of the work or otherwise.


This article describes an experiment where managed arrays get marshaled from a C# main program to a native code DLL. The purpose is to investigate how well automatic marshalling works, and whether it differs from using explicit pointers.

The experiment

The experiment consists of a main program in C#, calling on a single native function in C. The function basically returns some pointer values. The main program allocates a pair of arrays and passes them on in one of three ways:

  • without anything special, i.e. relying on automatic marshaling;
  • using pointers, which requires the unsafe and fixed keywords;
  • using GCHandle.

Furthermore, the same experiment is performed for a set of small arrays (1K elements) and for a set of big arrays (1M elements).

The results are always the same; whatever technique is used, the pointer values are the same, which indicates none of the techniques creates a copy of the arrays involved. Here is a typical output of the experiment:

array 10             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
array 1K             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
big array 10         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738
big array 1M         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738

fixed 10             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
fixed 1K             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
big fixed 10         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738
big fixed 1M         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738

GCpin 10             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
GCpin 1K             ar0=018F3828 ar1=018F4834 ar2=04C2E744 ar3=04C2E738
big GCpin 10         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738
big GCpin 1M         ar0=028D8E10 ar1=02CD8E20 ar2=04C2E744 ar3=04C2E738

We see clearly how:

  • the input arrays (ar0 and ar1) never change position, no matter what technique is used;
  • the input arrays (ar0 and ar1) are at entirely different locations from the local arrays (ar2 and ar3) which are allocated on the stack by the native code itself.

The native code

This is a very simple C function that returns some pointer values in a result array; it also fills the arrays to make pretty sure the pointers are valid.

__declspec(dllexport) void ProcessArray(int* array0, int* array1, int fill, int* results) {
	int array2[1], array3[1], i;
	for(i=0; i<fill; i++) {
		array0[i]=i;
		array1[i]=i;
	}
	results[0]=(int)array0;
	results[1]=(int)array1;
	results[2]=(int)array2;
	results[3]=(int)array3;
}

The managed code

This is all the C# code involved in this experiment. The results from the native function are collected in a small array that always used the fixed technique as that was the simplest one that I trusted to work properly.

using System;
using System.Runtime.InteropServices;		// DllImport

namespace MarshalArray1 {
	static class Program {
		[STAThread]
		static void Main() {
			int[] array0=new int[1024];
			int[] array1=new int[1024];
			int[] bigArray0=new int[1024*1024];
			int[] bigArray1=new int[1024*1024];
			testArr("array 10", array0, array1, 10);
			testArr("array 1K", array0, array1, 1024);
			testArr("big array 10", bigArray0, bigArray1, 10);
			testArr("big array 1M", bigArray0, bigArray1, 1024*1024);
			log("");
			unsafe {
				fixed (int* p0=array0) {
					fixed (int* p1=array1) {
						testPtr("fixed 10", p0, p1, 10);
						testPtr("fixed 1K", p0, p1, 1024);
					}
				}
				fixed (int* p0=bigArray0) {
					fixed (int* p1=bigArray1) {
						testPtr("big fixed 10", p0, p1, 10);
						testPtr("big fixed 1M", p0, p1, 1024*1024);
					}
				}
			}
			log("");
			GCHandle handle0=GCHandle.Alloc(array0, GCHandleType.Pinned);
			IntPtr ip0=handle0.AddrOfPinnedObject();
			GCHandle handle1=GCHandle.Alloc(array1, GCHandleType.Pinned);
			IntPtr ip1=handle1.AddrOfPinnedObject();
			testIntPtr("GCpin 10", ip0, ip1, 10);
			testIntPtr("GCpin 1K", ip0, ip1, 1024);
			handle1.Free();
			handle0.Free();
			handle0=GCHandle.Alloc(bigArray0, GCHandleType.Pinned);
			ip0=handle0.AddrOfPinnedObject();
			handle1=GCHandle.Alloc(bigArray1, GCHandleType.Pinned);
			ip1=handle1.AddrOfPinnedObject();
			testIntPtr("big GCpin 10", ip0, ip1, 10);
			testIntPtr("big GCpin 1M", ip0, ip1, 1024*1024);
			handle1.Free();
			handle0.Free();
			log("");

			log("Done");
			Console.ReadKey();
		}

		unsafe private static void testArr(string title, int[] array0, int[] array1, int fill) {
			int[] results=new int[4];
			fixed (int* pResults=results) {
				ProcessArr(array0, array1, fill, pResults);
			}
			show(title, results);
		}

		unsafe private static void testPtr(string title, int* array0, int* array1, int fill) {
			int[] results=new int[4];
			fixed (int* pResults=results) {
				ProcessPtr(array0, array1, fill, pResults);
			}
			show(title, results);
		}

		unsafe private static void testIntPtr(string title, IntPtr array0, IntPtr array1, int fill) {
			int[] results=new int[4];
			fixed (int* pResults=results) {
				ProcessIntPtr(array0, array1, fill, pResults);
			}
			show(title, results);
		}

		private static void show(string title, int[]results) {
			string s=title.PadRight(20);
			s+=" ar0="+results[0].ToString("X8");
			s+=" ar1="+results[1].ToString("X8");
			s+=" ar2="+results[2].ToString("X8");
			s+=" ar3="+results[3].ToString("X8");
			log(s);
		}
		private static void log(string s) {
			Console.WriteLine(s);
		}

		[DllImport("NativeCode.dll", EntryPoint="ProcessArray", CallingConvention=CallingConvention.Cdecl)]
		unsafe public static extern void ProcessArr(int[] array0, int[] array1, int fill, int* results);

		[DllImport("NativeCode.dll", EntryPoint="ProcessArray", CallingConvention=CallingConvention.Cdecl)]
		unsafe public static extern void ProcessPtr(int* array0, int* array1, int fill, int* results);

		[DllImport("NativeCode.dll", EntryPoint="ProcessArray", CallingConvention=CallingConvention.Cdecl)]
		unsafe public static extern void ProcessIntPtr(IntPtr array0, IntPtr array1, int fill, int* results);
	}
}

Conclusion

Apparently arrays can be passed from a C# caller to a native code callee without much ado; automatic marshaling seems to work exactly like explicitly using pointers (which requires both the unsafe and fixed keywords). Obviously automatic marshaling temporarily pins the array down, so it won't be moved around by the garbage collector as long as the native function hasn't returned, just like the fixed keyword does.

This means automatic marshaling and the pointer technique can be used interchangeably; of course the GCHandle technique would still be required when a pointer needs to remain valid after being passed to the native world, e.g. when some native code will asynchronously use a buffer.

History

  • Version 1.0 (25-Apr-2010): Original version


Perceler

Copyright © 2012, Luc Pattyn

Last Modified 02-Sep-2013