A small header-only crossplatform library which makes it easier to host the CoreCLR runtime inside of an native app. For more information on this topic check out the .NET Documentation and this article on the dotnet/runtime repo about the API design.
The goal of this repo is to also show off the common use cases with the CLR host and should act as a small showcase of the current possibilities with it (since the documentation on this is pretty narrow at the moment).
Just install the latest release from the Releases tab.
coreclr_delegates.h
hostfxr.h
nethost.h
nethost.lib
ornethost.dll
ornethost.so
Make sure to include the needed headers and libraries from NativeHost.Native/include and NativeHost.Native/lib.
You can also get the headers from the dotnet runtime repo:
coreclr_delegates.h
hostfxr.h
nethost.h
nethost.lib
,nethost.dll
,nethost.so
- Can be found in your local dotnet installation
Don't forget to checkout the example project which contains various samples like calling a managed method with parameters like strings, calling an unmanaged method back from the managed context using function pointers and much more (check Samples).
To create a CLR host you just need the path to the runtimeconfig (more info on this below) and to the managed assembly (DLL).
std::filesystem::path runtimeConfigPath = "Path" / "To" / "The" / "AssemblyName.runtimeconfig.json";
std::filesystem::path assemblyPath = "Path" / "To" / "The" / "AssemblyName.dll";
CoreCLRResult rc = CoreCLRHost::Create(runtimeConfigPath, assemblyPath);
if (rc != CoreCLRResult::Success)
{
std::cout << "RC Create: " << (uint32_t)rc << std::endl;
return;
}
The following code calls the method public static void Main()
inside of the NativeHost.ManagedLib.ManagedApi
class (in the NativeHost.ManagedLib.dll
assembly).
const char_t* dotnetType = STR("NativeHost.ManagedLib.ManagedApi, NativeHost.ManagedLib"); // Namespace.Class, AssemblyName
const char_t* methodName = STR("Main");
using ClrMainFn = void (CORECLR_DELEGATE_CALLTYPE*)();
// Create a function pointer with the specified signature
ClrMainFn method = nullptr;
CoreCLRResult rc = CoreCLRHost::GetMethodFunctionPointer<ClrMainFn>(dotnetType, methodName, &method);
if (rc != CoreCLRResult::Success)
{
std::cout << "RC GetMethodFunctionPointer: " << (uint32_t)rc << std::endl;
return;
}
// Call the method
method();
Check below if you want to call a native method from .NET
Make sure to mark your methods with the UnmanagedCallersOnly
attribute so they can be called from your host.
namespace NativeHost.ManagedLib
{
public class ManagedApi
{
[UnmanagedCallersOnly]
public static void Main()
{
Console.WriteLine($"C# Main");
}
}
}
Before running your own CLR host, make sure to create a runtimeconfig in the same directory as your managed DLL. The naming scheme for this file is pretty straight forward, you just call it like your DLL but instead of .dll
, you append .runtimeconfig.json
at the end.
In the example from before you would call the file NativeHost.ManagedLib.runtimeconfig.json
.
With the following contents you configure the runtime to use .NET 6 as the target framework:
{
"runtimeOptions": {
"tfm": "net6.0",
"rollForward": "LatestMinor",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
}
}
}
For more information about the options, check out the .NET Documentation on this topic: https://docs.microsoft.com/en-us/dotnet/core/runtime-config/threading.
To call a native method from .NET you have 2 options:
- Use function pointers with delegates (check the Samples section)
- Use the
DllImport
attribute with a customDllImportResolver
to call an exported function (recommended)
The following will show how to use the DllImport
attribute way.
namespace NativeHost.ManagedLib
{
public class ManagedApi
{
public const string InternalDllImport = "__Internal";
public static IntPtr MainProgramHandle = IntPtr.Zero;
[UnmanagedCallersOnly]
public static void Main(IntPtr mainProgramHandle)
{
Console.WriteLine($"C# Main");
MainProgramHandle = mainProgramHandle;
NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), (string libraryName, Assembly assembly, DllImportSearchPath? searchPath) =>
{
// If the library name equals our internal resolver name return the base address of the host
if (libraryName == InternalDllImport)
return MainProgramHandle;
// Otherwise try to load the library
NativeLibrary.TryLoad(libraryName, assembly, searchPath, out IntPtr handle);
return handle;
});
}
[DllImport(InternalDllImport)]
internal static extern void SomeExportedFunction();
}
}
Call the Main
function from the host:
extern "C" __declspec(dllexport) void SomeExportedFunction()
{
std::cout << "C++ SomeExportedFunction" << std::endl;
}
void CallMain()
{
const char_t* dotnetType = STR("NativeHost.ManagedLib.ManagedApi, NativeHost.ManagedLib"); // Namespace.Class, AssemblyName
const char_t* methodName = STR("Main");
using ClrMainFn = void (CORECLR_DELEGATE_CALLTYPE*)(void*);
// Create a function pointer with the specified signature
ClrMainFn method = nullptr;
CoreCLRResult rc = CoreCLRHost::GetMethodFunctionPointer<ClrMainFn>(dotnetType, methodName, &method);
if (rc != CoreCLRResult::Success)
{
std::cout << "RC GetMethodFunctionPointer: " << (uint32_t)rc << std::endl;
return;
}
#if WINDOWS
void* mainProgramHandle = GetModuleHandleW(NULL);
#else
void* mainProgramHandle = dlopen(nullptr);
#endif
// Call the method
method(mainProgramHandle);
}
Upon calling the Main
function of the managed assembly, the managed assembly calls the native SomeExportedFunction
function.
More samples like calling a managed method with parameters like strings, calling an unmanaged method back from the managed context using function pointers and much more can be found in the unamanged C++ project (NativeHost.Native/NativeHost.Native.cpp) and the managed C# library (NativeHost.ManagedLib/ManagedApi.cs).
NativeHost is licensed under The Unlicense, see LICENSE.txt for more information.