Skip to content

Calling Managed Code

RoqueDeicide edited this page Dec 15, 2014 · 2 revisions

Calling managed code from unmanaged one always requires getting a pointer to a wrapper object that represents the Mono method that needs to be invoked, after that there are two options for the actual invocation: using unamanged thunk, or using Invoke method.

Getting pointer to method

To get the wrapper for the method, you need to get a pointer to the class where the method is defined, for that you will also need a pointer to the assembly wrapper where the class is defined.

Getting the class wrapper is quite straight forward, however getting a pointer to the assembly may not be as easy. IMonoInterface implementation does provide you with pointers to Cryambly and mscorlib, however getting any other assembly might need some unorthodox measures.

IMonoInterface has LoadAssembly method that loads a specified library into memory and returns a wrapper for it, however it only works for unloaded assemblies, which means a number of assemblies that are preloaded when initializing CryCIL are not so easy to get.

Your only option for already loaded assembly is using IMonoInterface::WrapAssembly method that requires a full assembly name, which you can get only by using an initialization stage method that informs native code about its declaring assembly's full name.

Once you've got the assembly wrapper, you can get the class wrapper, from which you can get the method wrapper by using one of the overloads of GetMethod function.

You can also bypass getting the class by using MethodFromDescription method of IMonoAssembly, however you will have less options there.

Examples

  • Getting Int32.Parse method.

This method is defined in mscorlib, which means we need to use IMonoInterface::CoreLibrary property.

// Get the class.
IMonoClass *int32Class = MonoEnv->CoreLibrary->GetClass("System", "Int32");
// Since Int32.Parse only has one overload that takes 1 parameter, so we can use fastest method
// of getting the method wrapper.
IMonoMethod *parseMethod = int32Class->GetMethod("Parse", 1);
  • Getting ConsoleLogWriter.Write(int) method.

This method is defined in ConsoleLogWriter which is defined in Cryambly, so we can us IMonoInterface::Cryambly property.

// Get the class.
IMonoClass *consoleLogWriterClass =
    MonoEnv->Cryambly->GetClass("CryCil.RunTime.Logging", "ConsoleLogWriter");
// Since ConsoleLogWriter.Write has multiple overloads that take 1 parameter, so we have to use a method
// that allows us to specify the exact signature. We will use one that uses full type names.
IMonoMethod *writeMethod = consoleLogWriterClass->GetMethod("Write", "System.Int32");
  • Getting a method from assembly that is not loaded

Since the assembly is not yet loaded, we have to use IMonoInterface::LoadAssembly method.

// Load the assembly.
IMonoAssembly *assembly = MonoEnv->LoadAssembly(Path to assembly);
// Get the class.
IMonoClass *fooClass = assembly->GetClass("SomeNamespace", "FooClass");
// Our method has a number of overloads with the same param count, so lets use the method
// that allows us to specify the exact signature by using an array of parameters.
// So lets create an array of parameters.
IMonoArray *params = MonoEnv->CreateArray(3, true);
// Our method is supposed to accept int, string and Vector3 as arguments.
params->At(0) = Box(10);
params->At(1) = ToMonoString("Some text.");
params->At(2) = Box(Vec3(10.0f, 10.0f, 10.0f));
// Now we can get the method.
IMonoMethod *booMethod = fooClass->GetMethod("Boo", params);
  • Getting a method from assembly that is already loaded

This situation is the most awkward one, since we need a full assembly name, which means, that we have to use initialization stages to make that assembly inform native code about its name (This will allow you to not have to bother with constantly updating the code whenever the name of the assembly changes).

First, we need an Interop that will allow our assembly to provide the name:

// .h file

struct AssemblyNameInterop : IMonoInterop
{
    virtual const char *GetNameSpace() { return "Native"; }
    virtual const char *GetName()      { return "AssemblyNameInterop"; }
    virtual void OnRuntimeInitialized()
    {
        REGISTER_METHOD();
    }
    static void ProvideAssemblyName(mono::string name);
    static const char *fullAssemblyName;
}

// .cpp file
const char * AssemblyNameInterop::fullAssemblyName = nullptr;

void AssemblyNameInterop::ProvideAssemblyName(mono::string name)
{
    fullAssemblyName = ToNativeString(name);
}

We will have to register the interop as a standard listener.

Now lets declare it on C# side:

using System.Runtime.CompilerServices;

namespace Native
{
    internal static class AssemblyNameInterop
    {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern void ProvideAssemblyName(string text);
    }
}

Now we need an initialization stage that invokes those methods.

using Native;
using CryCil.RunTime;
using System.Reflection;

namespace Code
{
    [InitializationClass]
    public static class NameProvider
    {
        [InitializationStage(0)]
        public static void FetchAssemblyName(int stageIndex)
        {
            // Get this assembly.
            AssemblyNameInterop.ProvideAssemblyName(Assembly.GetAssembly(typeof(NameProvider)).FullName);
            // Above algorithm works if we need an assembly that is preloaded by CryCIL.
            // Sometimes, however, assembly is loaded due to being referenced by another.
            // You can register those assemblies by registering name of each of them from a single
            // initialization stage.
            AssemblyNameInterop.ProvideAssemblyName(Assembly.GetAssembly(typeof(TypeFromReferencedAssembly1)).FullName);
            AssemblyNameInterop.ProvideAssemblyName(Assembly.GetAssembly(typeof(TypeFromReferencedAssembly2)).FullName);
            AssemblyNameInterop.ProvideAssemblyName(Assembly.GetAssembly(typeof(TypeFromReferencedAssembly3)).FullName);
            AssemblyNameInterop.ProvideAssemblyName(Assembly.GetAssembly(typeof(TypeFromReferencedAssembly4)).FullName);
        }
    }
}

Now that we have a name, we can just use IMonoInterface::WrapAssembly to get the wrapper.

// Wrap the assembly.
IMonoAssembly *assembly = MonoEnv->WrapAssembly(AssemblyNameInterop::fullAssemblyName);
// Get the method.
IMonoMethod *booMethod =
    assembly->MethodFromDescription
    (
        "SomeNamespace",
        "FooClass",
        "Boo",
        "System.Int32,System.String,CryCil.Mathematics.Vector3"
    );

Invocation

IMonoMethod::Invoke methods

Somewhat slow way, but the most dynamic one: you can invoke different functions using the same code. There are two overloads of Invoke method, one uses a Mono array for parameters, another uses unmanaged array of pointers to parameters. Also this is the only way of invocation of virtual methods.

Sample code:

  • With Mono array.
// Create the list of parameters, if we got the method wrapper using one, then we can use it.
IMonoArray *params = MonoEnv->CreateArray(3, true);
// Our method is supposed to accept int, string and Vector3 as arguments.
params->At(0) = Box(10);
params->At(1) = ToMonoString("Some text.");
params->At(2) = Box(Vec3(10.0f, 10.0f, 10.0f));
// Invoke the method, our method is static, so we pass null pointer as first parameter.
mono::object result = booMethod->Invoke(nullptr, params);
  • With unmanaged array.
// Create the list of parameters, if we got the method wrapper using one, then we can use it.
void *params[3];
// Our method is supposed to accept int, string and Vector3 as arguments.
int number(10);
const char *text = "Some text";
Vec3 vector(10.0f, 10.0f, 10.0f);
// As you can see, no boxing, which means this way somewhat quicker.
params[0] = &number;
params[1] = text;
params[2] = &vector;
// Invoke the method, our method is static, so we pass null pointer as first parameter.
mono::object result = booMethod->Invoke(nullptr, params);

Unmanaged thunks

Unmanaged thunks a standard function pointers, which means they are invoked in the same way as any C/C++ function, they skip a lot of code, however they may require a lot of boxing of value type objects. It is recommended to use thunks unless you may have to invoke different methods or you need to invoke virtual methods, since attempting to get unmanaged thunk for virtual method is guaranteed to throw an error.

Sample code:

// Get unmanaged thunk.
void *thunkPtr = booMethod->UnmanagedThunk;
// We need an appropriate signature to be able to invoke it. Read IMonoMethod::UnmanagedThunk documentation to 
// learn about signatures.
void(*booThunk)(int,mono::string,mono::vector3,mono::exception *) = 
    (void(*)(int,mono::string,mono::vector3,mono::exception *))thunkPtr;
// Invoke it.
booThunk(10, ToMonoString("Some Text"), Box(Vec3(10.0f, 10.0f, 10.0f)));