Mapping a Struct to Unmanaged Memory
A struct with a StructLayout
of Sequential
or Explicit
can be mapped directly into unmanaged memory. Consider the following struct:
[StructLayout (LayoutKind.Sequential)]
unsafe struct MySharedData
{
public int Value;
public char Letter;
public fixed float Numbers [50];
}
The fixed
directive allows us to define fixed-length value-type arrays inline, and it is what takes us into the unsafe
realm. Space in this struct is allocated inline for 50 floating-point numbers. Unlike with standard C# arrays, Numbers
is not a reference to an array—it is the array. If we run the following:
static unsafe void Main() => Console.WriteLine (sizeof (MySharedData));
the result is 208: 50 4-byte floats, plus the 4 bytes for the Value
integer, plus 2 bytes for the Letter
character. The total, 206, is rounded to 208 due to the floats
being aligned on 4-byte boundaries (4 bytes being the size of a float
).
We can demonstrate MySharedData
in an unsafe
context, most simply, with stack-allocated memory:
MySharedData d;
MySharedData* data = &d; // Get the address of d
data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
or:
// Allocate the array on the stack:
MySharedData* data = stackalloc MySharedData[1];
data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
Of course, we’re not demonstrating anything that couldn’t otherwise be achieved in a managed context. Suppose, however, that we want to store an instance of MySharedData
on the unmanaged heap, outside the realm of the CLR’s garbage collector. This is where pointers become really useful:
MySharedData* data = (MySharedData*)
Marshal.AllocHGlobal (sizeof (MySharedData)).ToPointer();
data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
Marshal.AllocHGlobal
allocates memory on the unmanaged heap. Here’s how to later free the same memory:
Marshal.FreeHGlobal (new IntPtr (data));
(The result of forgetting to free the memory is a good old-fashioned memory leak.)
In keeping with its name, we’ll now use MySharedData
in conjunction with the SharedMem
class we wrote in the preceding section. The following program allocates a block of shared memory and then maps the MySharedData
struct into that memory:
static unsafe void Main()
{
using (SharedMem sm = new SharedMem ("MyShare", false, 1000))
{
void* root = sm.Root.ToPointer();
MySharedData* data = (MySharedData*) root;
data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
Console.WriteLine ("Written to shared memory");
Console.ReadLine();
Console.WriteLine ("Value is " + data->Value);
Console.WriteLine ("Letter is " + data->Letter);
Console.WriteLine ("11th Number is " + data->Numbers[10]);
Console.ReadLine();
}
}
Note
You can use the built-in MemoryMappedFile
class instead of SharedMem
as follows:
using (MemoryMappedFile mmFile =
MemoryMappedFile.CreateNew ("MyShare", 1000))
using (MemoryMappedViewAccessor accessor =
mmFile.CreateViewAccessor())
{
byte* pointer = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer
(ref pointer);
void* root = pointer;
...
}
Here’s a second program that attaches to the same shared memory, reading the values written by the first program. (It must be run while the first program is waiting on the ReadLine
statement, since the shared memory object is disposed upon leaving its using
statement.)
static unsafe void Main()
{
using (SharedMem sm = new SharedMem ("MyShare", true, 1000))
{
void* root = sm.Root.ToPointer();
MySharedData* data = (MySharedData*) root;
Console.WriteLine ("Value is " + data->Value);
Console.WriteLine ("Letter is " + data->Letter);
Console.WriteLine ("11th Number is " + data->Numbers[10]);
// Our turn to update values in shared memory!
data->Value++;
data->Letter = '!';
data->Numbers[10] = 987.5f;
Console.WriteLine ("Updated shared memory");
Console.ReadLine();
}
}
The output from each of these programs is as follows:
// First program:
Written to shared memory
Value is 124
Letter is !
11th Number is 987.5
// Second program:
Value is 123
Letter is X
11th Number is 1.45
Updated shared memory
Don’t be put off by the pointers: C++ programmers use them throughout whole applications and are able to get everything working. At least most of the time! This sort of usage is fairly simple by comparison.
As it happens, our example is unsafe—quite literally—for another reason. We’ve not considered the thread-safety (or more precisely, process-safety) issues that arise with two programs accessing the same memory at once. To use this in a production application, we’d need to add the volatile
keyword to the Value
and Letter
fields in the MySharedData
struct to prevent fields from being cached in CPU registers. Furthermore, as our interaction with the fields grew beyond the trivial, we would most likely need to protect their access via a cross-process Mutex
, just as we would use lock
statements to protect access to fields in a multithreaded program. We discussed thread safety in detail in Chapter 22.
fixed and fixed {...}
One limitation of mapping structs directly into memory is that the struct can contain only unmanaged types. If you need to share string data, for instance, you must use a fixed character array instead. This means manual conversion to and from the string
type. Here’s how to do it:
[StructLayout (LayoutKind.Sequential)]
unsafe struct MySharedData
{
...
// Allocate space for 200 chars (i.e., 400 bytes).
const int MessageSize = 200;
fixed char message [MessageSize];
// One would most likely put this code into a helper class:
public string Message
{
get { fixed (char* cp = message) return new string (cp); }
set
{
fixed (char* cp = message)
{
int i = 0;
for (; i < value.Length && i < MessageSize - 1; i++)
cp [i] = value [i];
// Add the null terminator
cp [i] = ' ';
}
}
}
}
Note
There’s no such thing as a reference to a fixed array; instead, you get a pointer. When you index into a fixed array, you’re actually performing pointer arithmetic!
With the first use of the fixed
keyword, we allocate space, inline, for 200 characters in the struct. The same keyword (somewhat confusingly) has a different meaning when used later in the property definition. It tells the CLR to pin an object, so that should it decide to perform a garbage collection inside the fixed
block, it doesn’t move the underlying struct about on the memory heap (since its contents are being iterated via direct memory pointers). Looking at our program, you might wonder how MySharedData
could ever shift in memory, given that it lives not on the heap, but in the unmanaged world, where the garbage collector has no jurisdiction. The compiler doesn’t know this, however, and is concerned that we might use MySharedData
in a managed context, so it insists that we add the fixed
keyword to make our unsafe
code safe in managed contexts. And the compiler does have a point—here’s all it would take to put MySharedData
on the heap:
object obj = new MySharedData();
This results in a boxed MySharedData
—on the heap and eligible for transit during garbage collection.
This example illustrates how a string can be represented in a struct mapped to unmanaged memory. For more complex types, you also have the option of using existing serialization code. The one proviso is that the serialized data must never exceed, in length, its allocation of space in the struct; otherwise, the result is an unintended union with subsequent fields.