Skip to content

Data Marshaling

RoqueDeicide edited this page Dec 19, 2014 · 5 revisions

Data marshaling is a process of moving data between parts of the program, or between the programs that use different memory models. This article focuses on marshaling between CLR (Common Language Run-Time) and CRT (C Run-Time) which are very close (in fact, at their core they are exactly the same). We are going to go through means of converting objects of CIL type into C++ objects and vice versa, either fully or partially.

Accessing Fields and Properties

When working with managed reference-type objects you mostly have to just access their fields and properties.

Accessing Fields

Fields can only be accessed for reference-type objects, It is done by wrapping a reference into IMonoHandle object that provides methods GetField and SetField that allow you to get and set the fields. It is not possible to access fields of value-type objects through these methods, you have to unbox it instead.

You can also use IMonoClass methods of the same name, those just require you to provide a reference to an object of appropriate type, and otherwise are identical to IMonoHandle's.

Sample C# classes which fields we are going to access.

public class FooClass
{
    public string Text;
    public int Number;
}
public class BooClass
{
    public bool BooleanVal;
}

Example of working with fields:

// Get some objects.
mono::object fooObj = GetFooObject();
mono::object booObj = GetBooObject();
// Try setting and getting fields of fooObj through IMonoHandle.
IMonoHandle *foo = MonoEnv->WrapObject(fooObj, false, false);
// Set the fields. Notice how we set value-type field.
foo->SetField("Text", ToMonoString("Some Text"));
int newNumber = 10;
foo->SetField("Number", &newNumber);
// Get fields and print a string. Should print: "Text: Some Text, Number: 10;".
const char *ntValue = ToNativeString(foo->GetField("Text"));
CryLogAlways("Text: %s, Number: %d;", ntValue, Unbox<int>(foo->GetField("Number")));
delete ntValue;

// Set and get the field through IMonoClass.
IMonoClass *booClass = ...(Get the BooClass);
// Set the field.
bool b = true;
booClass->SetField(booObj, "BooleanVal", &b);
if (Unbox<bool>(booClass->GetField(booObj, "BooleanVal")))
{
    CryLogAlways("The field is true.");
}

Accessing Properties

Since properties are essentially methods, working with them has a degree of similarity to method invocation. This means that you can get and set properties not only on managed reference-type objects, but also on C++ objects that are not even defined in CIL! Working with properties for managed reference-type objects is exactly the same as working with fields.

Sample code:

// Get DateTime class.
IMonoClass *dateTime = MonoEnv->CoreLibrary->GetClass("System", "DateTime");
// Create unsigned 64-bit integer.
unsigned __int64 longInteger = 1000000000000;
// Get property Hour from that integer.
int hour = Unbox<int>(dateTime->GetProperty(&longInteger, "Hour"));
// This will print 3.
CryLogAlways("%d", hour);

Direct Memory Transfer

System.Runtime.InteropServices.Marshal

Marshal class still works in Mono, which means you can easily marshal arrays and do any custom data format conversion. It can be done by using IntPtr objects extensively.

using System;
using System.Runtime.InteropServices;

namespace Test
{
    public class Foo
    {
        public ushort[] Numbers;
        public Foo(IntPtr ptr)
        {
            // Let's say we have an object that has int object that represents a number of
            // ushort numbers that are valid in a fixed size array of 1020 bytes that follows.
            int objectCount = Marshal.ReadInt32(ptr);
            this.Numbers = new ushort[objectCount];
            Marshal.Copy(ptr + 4, this.Numbers, 0, objectCount);
        }
    }
}

Pointer Dereferencing

Pointers in C# internally are perfect equivalents of pointers in C/C++. This means that aside from compile-time restrictions, they are essentially the same as native pointers. Hence above code can be written like this (and it might actually be faster):

using System;
using System.Runtime.InteropServices;

namespace Test
{
    public class Foo
    {
        public ushort[] Numbers;
        public Foo(IntPtr ptr)
        {
            // Let's say we have an object that has int object that represents a number of
            // ushort numbers that are valid in a fixed size array of 1020 bytes that follows.
            int objectCount = *((int *)ptr.ToPointer());
            this.Numbers = new ushort[objectCount];
            ushort *validNumbersPtr = (ushort *)(ptr + 4).ToPointer();
            for (int i = 0; i < objectCount; i++)
            {
                this.Numbers[i] = validNumbersPtr[i];
            }
        }
    }
}

Boxing/Unboxing

(Un)Boxing is a process of transfering value-type object (from)to managed memory. In C# it's by simply converting the object (from)to object. In C++ the process is slightly more complicated.

Boxing in C++

Boxing in C++ requires a special code that gets the pointer to IMonoClass that represents the value-type object, then its method Box to actually box the object.

IMonoClass *klass = (some assembly)->GetClass("Whatever", "FooClass");
klass->Box(our object);

IMonoInterface.h defines a number of overloads of global function Box that makes boxing a bit more convenient. It's best to encapsulate the code in such overload to optimize the workflow:

inline mono::object Box(FooClass &obj)
{
    IMonoClass *klass = (some assembly)->GetClass("Whatever", "FooClass");
    return klass->Box(our object);
}

Unboxing in C++

Unboxing is a very simple procedure that only requires usage of one global function Unbox():

FooClass obj = Unbox<FooClass>(something);