Skip to content

A small header-only library which makes it easier to create a custom .NET runtime host

License

Notifications You must be signed in to change notification settings

TheDusty01/NativeHost

Repository files navigation

NativeHost

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).

Setup

Just install the latest release from the Releases tab.

Dependencies

  • coreclr_delegates.h
  • hostfxr.h
  • nethost.h
  • nethost.lib or nethost.dll or nethost.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:

Usage

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).

Creating a CLR host

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;
}

Calling a managed method

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

Creating a managed (.NET) library

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");
        }
    }
}

Creating a runtimeconfig

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.

Calling a native method

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 custom DllImportResolver 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.

Samples

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).

License

NativeHost is licensed under The Unlicense, see LICENSE.txt for more information.

About

A small header-only library which makes it easier to create a custom .NET runtime host

Resources

License

Stars

Watchers

Forks