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 |
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 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:
unsafe
and fixed
keywords;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:
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;
}
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);
}
}
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.
Perceler |
Copyright © 2012, Luc Pattyn |
Last Modified 20-Dec-2024 |