Calling Host Native Functions from Dotnet
Dotnet developer, C++ learner, Unreal Engine lover
This is the second entry in the documentation of my C++ journey. Since I work a lot with c#/dotnet on my job, today I would like to write about C++ and dotnet, specifically about hosting/embedding dotnet in a C++ application. In this article I will loosely refer to dotnet and C# interchangeably, as well as use the terms hosting and embedding. Even though there may be some differences, I think it does not really matter for us here.
Embedding a programming language in another can allow us to take advantage of two worlds. A famous example is the game engine Unity that hosts its own fork of the Mono project. Another popular language to embed in game engines is Lua. Embedding C# allows us "script" parts of the C++ application, which can lead to increased productivity in certain areas, and a separation of concerns where some developers build the infrastructure (audio, rendering, physics etc.) while other developers focus on scripting game logic.
I mentioned Mono, and Mono seems to have a very good documentaion on how to do embedding/hosting, but it is also possible to do the same with dotnet core. However, what I found when researching the topic was mostly a sample project from Microsoft and some github issues discussing the question. There is also a lot of documentation regarding P/Invoke that allows us to call native code from C#/dotnet, however I found no documentation on what to do if that code was part of the hosting application, only on how to link to an external library - if I embed dotnet in my game engine I want the C# code to be able to communicate with the engine, right?
Execute Host Native Code
After some reading and experimentation, I have found two ways to go about it (that I'm aware of):
Provide function pointers to dotnet that expose the desired functionality. This was outlined in the discussion here.
Declare functions with internal linkage (
__Internal) and do a P/Invoke into the host binary. See the discussions in this and then this github issue.
Let's see how to do each of these methods in turn! The following discussion draws from my own example repository lambda-snail/dotnet-host-example, which contains a much more detailed discussion in the readme, as well as more code examples.
The examples below also jump right into the code, assuming the reader knows a little about how it works. My project demonstrates what parts are needed to start up the dotnet runtime from C++. It is based on the Dotnet Hosting Samples repository, so the reader is welcome to look into that too, as well as check out the official hosting tutorial. I have also found another repository that demonstrates hosting in C++ - that may be more recommended actually than looking at my code :)
Pass Function Pointers to Hosted Code
Function pointers do not only exist in C++ - in newer C# versions we also have a similar concept (or at least a concept with the same name ...). These are declared in an unsafe context using the syntax delegate*<> and they form the basis of communication when passing function pointers from C++. To use it, we first declare a method like so:
[UnmanagedCallersOnly]
public static unsafe void TestFnPtrWithArgs(delegate* unmanaged<int, double> fn_from_cpp) { ... }
This method will get called by the native host, sending in a C++ function pointer as parameter, which we can then access through the C# function pointer. In the method body we can call this delegate "as usual":
double @return = fn_from_cpp(20);
If everything is set up correctly, this will invoke a function that we have defined in C++ and return a value. To expose a function to be used in this way, we declare a function with the following signature:
typedef void (CORECLR_DELEGATE_CALLTYPE *send_callback_to_dotnet_fn)(double_t(*fn)(int32_t));
send_callback_to_dotnet_fn callback;
Here are juggling two different function pointers; send_callback_to_dotnet_fn is a pointer to a function that takes another function pointer as parameter. This parameter is the callback that we will send to dotnet, callback will after initialization, in effect, store a pointer to the function TestFnPtrWithArgs. CORECLR_DELEGATE_CALLTYPE is defined in coreclr_delegates.h which can be found in the dotnet Hosting Samples. To initialize callback we do the following:
rc = load_assembly_and_get_function_pointer(
dotnetlib_path.c_str(),
dotnet_type,
STR("TestFnPtrWithArgs"),
UNMANAGEDCALLERSONLY_METHOD,
nullptr,
(void**)&callback);
dotnet_type is the assembly qualified name of the type that holds the method we want to call. In this case it is DotNetLib.Lib, DotNetLib. We can now use the newly initialized callback to send a lambda function to dotnet:
auto fn = [](int32_t i) -> double_t
{
std::cout << "[C++] A lambda recieved " << i << " from dotnet!" << std::endl;
return static_cast<double_t>(i) + 3.14;
};
callback(fn);
This idea of passing a function pointer to dotnet and using that to call back into the native host was outlined here, however the discussion didn't give much in terms of code examples on how to actually do it.
P/Invoke Into Host
Another method that we can use to expose functions in the native host to dotnet is to use P/Invoke and link it to the binary that is hosting the dotnet runtime. Funnily enough, I got this idea from studying the Mono documentation, not that of dotnet core. This lead me to the question of how this can be done in dotnet core, and subsequently to this github issue.
So it turns out that we can actually do the same thing in dotnet core. To do this we first declare a function in C++ like so:
extern "C" __declspec(dllexport) void print_simple_message() {...}
In C# we can then declare a method with the same return value and parameters. We then decorate it with the DllImport attribute and specify __Internal as the library name:
[DllImport("__Internal", EntryPoint = "print_simple_message")]
private static extern void PrintSimpleMessage();
This will require us to write some additional code as well for marshalling parameters etc. However, from dotnet 7.0 we can take advantage of source generators to generate the marshalling code. Doing this will change the method definition slightly:
[LibraryImport("__Internal", EntryPoint = "print_simple_message")]
private static partial void PrintSimpleMessage();
We now use the attribute LibraryImport and we also need to make the method partial so that the source generators can do their thing. When testing I also ran into the error SYSLIB1051 which was solved by adding the following attribute to the assembly:
[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute]
The example code in nativehost.cpp in my sample repository contains a few examples of functions that receive data from dotnet and perform some logic (represented by print statements in the example). The examples show how to deal with simple parameters, which is easy to set up thanks to the source generators - we almost don't need to do any thinking at all. Even strings can be handled by the source generators if we wish:
[LibraryImport("__Internal", EntryPoint = "native_log", StringMarshalling = StringMarshalling.Utf8)]
It is also possible to create our own custom marshalling logic if we need to. I have also included an example of a struct parameter in the project:
struct ComplicatedParamStruct
{
int SomeOption;
double ValueOfOption;
bool DoComplicatedThingy;
};
This can be received from dotnet either as a pointer, reference or by copying:
extern "C" __declspec(dllexport) void print_struct_pointer(ComplicatedParamStruct* params);
extern "C" __declspec(dllexport) void print_struct_reference(ComplicatedParamStruct& params);
extern "C" __declspec(dllexport) void print_struct_copy(ComplicatedParamStruct params);
To make this work in C# we need the corresponding struct definition:
[StructLayout(LayoutKind.Sequential)]
struct ComplicatedParamStruct
{
public int SomeOption { get; set; }
public double ValueOfOption { get; set; }
public bool DoComplicatedThingy { get; set; }
};
as well as the method definitions:
[LibraryImport("__Internal", EntryPoint = "print_struct_pointer")]
private static partial void PrintStructPointer(ref ComplicatedParamStruct @params);
[LibraryImport("__Internal", EntryPoint = "print_struct_copy")]
private static partial void PrintStructCopy(ComplicatedParamStruct @params);
[LibraryImport("__Internal", EntryPoint = "print_struct_reference")]
private static partial void PrintStructReference(ref ComplicatedParamStruct @params);
Note that the parameter is declared with the ref keyword for both the pointer and the reference cases.
Wrapping Up
The sample repository also contains functions that deal with strings. The interested reader is welcome to take a look at those functions, and I welcome any feedback that you may have. The code is not perfect and may contain errors, as I am still in the process of learning. In the future I want to write more about the topic of hosting, and then I may include string handling and marshalling as well.
To me, the method of using P/Invoke feels a bit less convoluted than passing function pointers to dotnet and communicating that way. I am not sure if there is any use case that actually requires function pointers, that cannot be done using the P/Invoke method. If you know of such a scenario, feel free to comment :)
Links
lambda-snail/dotnet-host-example: A small example of a dotnet host in c++, based on the official example for native hosting (github.com)
