smx-smx/EzDotnet

GitHub: smx-smx/EzDotnet

Stars: 16 | Forks: 4

# EzDotNet Libraries and tools to easily load and run managed .NET assemblies from C/C++ code. ## Project structure: EzDotnet implements hosts for the following runtimes: | Name | Runtime | OS | |------|---------|----| | CLRHost | .NET Framework v4.x | Windows only | | MonoHost | [mono](https://github.com/mono/mono) / [wine-mono](https://gitlab.winehq.org/mono/mono) | Multi-Platform | | CoreCLR | [.NET Core](https://dotnet.microsoft.com/en-us/download) | Multi-Platform | | MonoCoreClr | [.NET Core Mono](https://github.com/dotnet/runtime/tree/eb1c0ab314ef67bc31d85a5bee8a9a36fca84b93/src/mono) | Multi-Platform, see [Microsoft.NETCore.App.Runtime.Mono](https://www.nuget.org/packages?page=2&q=Microsoft.NETCore.App.Runtime.Mono&sortBy=relevance) | The backends expose the same interface so that it's possible to swap them while keeping the same code. To load a managed assembly, we need to pull the EzDotnet APIs into our project. This can be done in one of the following ways: - Statically linking against one of the backends: `coreclrhost`, `monohost` or `clrhost` - Dynamic linking (`dlopen`/`LoadLibrary`) - Using the sample dynamic helper library (`ezdotnet_shared`) ## C# Project setup First, create a new console application: dotnet new console -o ManagedSample Then, add the `Microsoft.NETCore.DotNetAppHost` nuget package, for example via the `dotnet` cli: dotnet add package Microsoft.NETCore.DotNetAppHost Now create a EntryPoint for the native code, using the following code as a starting point: namespace ManagedSample { public class EntryPoint { private static string[] ReadArgv(IntPtr args, int sizeBytes) { int nargs = sizeBytes / IntPtr.Size; string[] argv = new string[nargs]; for(int i=0; i The following paragraph explains how to setup the native loader: ## Native setup A sample dynamic helper is provided to ease the process of loading .NET and calling the entry point of an assembly. Otherwise, refer to the [API Documentation](#api-documentation) to use static/dynamic linking yourself. ### Command Line If your C# code uses the Entry Point format from the example project, which expects string arguments, you can use the `ezdotnet` CLI tool to run your assembly. This also enables you to run a C# program from Cygwin, and interop between Cygwin/C# easily. You can find the CLI after building and installing the project, under the `bin` folder of `CMAKE_INSTALL_PREFIX`. The usage is the following: Usage: ezdotnet [loaderPath] [asmPath] [className] [methodName] where: - `loaderPath`: the path to one of the .NET Hosts/backends you wish to use (built as part of EzDotNet) - `asmPath`: the full path to the published Managed assembly you wish to load (the output of `dotnet publish`) - `className`: the fully qualified class name which contains the EntryPoint method (including the Namespace) - `methodName`: name of the EntryPoint method within the class ### Dynamic helper If you decide to use the dynamic helper, you have to load `ezdotnet_shared` and resolve the `int main(int argc, char *argv[])` method (via `dlsym` or `GetProcAddress`). Refer ot the following sample for details: typedef int (*pfnEzDotNetMain)(int argc, const char *argv[]); HMODULE ezDotNet = LoadLibraryA("libezdotnet_shared.dll"); pfnEzDotNetMain main = reinterpret_cast(GetProcAddress(ezDotNet, "main")); const char *argv[] = { // name of the program (argv0) - unused (can be set to anything) "ezdotnet", // path of the .NET backend to use "libcoreclrhost.dll", // path of the .NET assembly to load "bin/x86/Debug/net7.0/publish/ManagedSample.dll", // fully qualified class name to invoke "ManagedSample.EntryPoint", // name of the entry method inside the class (can be private) "Entry" }; // call main(argc, argv) pfnMain(5, argv); ### API documentation The backends share a common interface: #### clrInit - `ASMHANDLE clrInit(const char *assemblyPath, const char *baseDir, bool enableDebug)` Returns: a handle to the loaded assembly #### clrDeInit - `bool clrDeInit(ASMHANDLE handle)` Deinitializes the execution environment. #### runMethod - `int runMethod(ASMHANDLE handle, const char *typeName, const char *methodName)` Runs the method `methodName` inside the class `typeName`, given a `handle` to an assembly loaded by a previous `clrInit` call. The C# method is expected to have the following signature: private static int Entry(IntPtr args, int sizeBytes) { string[] argv = ReadArgv(args, sizeBytes); Main(argv); return 0; } ## Use cases ### Executable or Library You can use EzDotnet inside an executable or a library. You can either link statically against a single loader or use dynamic linking (e.g. `dlopen`) so that the engine to use (CLR/CoreCLR/Mono) can be chosen at runtime. **WARNING** If you're loading EzDotnet from a DLL, avoid loading the CLR from library constructors like `DllMain`. Doing so will cause a deadlock in `clrInit`. Instead, create a separate thread and use it to load the CLR, so that `DllMain` is free to return. ### Cygwin Interoperability This project enables you to call Cygwin code from .NET. For this use case, the .NET host/loader (for example `samples/cli/ezdotnet`, or `libcoreclrhost`) **MUST** be compiled under Cygwin. In other words, you can call code Cygwin code from .NET only if you're starting with a Cygwin process, and you load .NET afterwards. Starting from Win32 and calling into Cygwin will **NOT** work Therefore, if you want to build a typical CLI or Windows Forms application with Cygwin features, you will need to start the application with the `ezdotnet` CLI for it to work properly. **NOTE**: The `ezdotnet` CLI **MUST** be compiled as a Cygwin application. ### Process Injection If you're building a shared library, you can inject it into another process to enable it to run .NET code. For this use case you will need to use a library injector. There are several tools and ways to achieve this, for example: #### Windows - [Detours](https://github.com/microsoft/Detours) has an API to spawn a process with a DLL - [SetSail](https://github.com/TheAssemblyArmada/SetSail) can inject a DLL at the EXE Entrypoint #### Unix-like - [LD_PRELOAD](https://man7.org/linux/man-pages/man8/ld.so.8.html) (Linux, FreeBSD, and others) can be used to preload a library in a executable (at launch time) #### Universal - [ezinject](https://github.com/smx-smx/ezinject) can inject a library in a running executable - or use your favorite injector ### Notes for MonoCoreClr MonoCoreClr requires a specific Mono runtime pack, which also includes a build of Mono itself "disguised" as `coreclr.[dll|so|dylib]`. The process for setting up this runtime is the following (mostly performed by the build process and inspired by https://github.com/lambdageek/monovm-embed-sample). It's important you read it to understand how it works, and the files involved: 1. CMake heuristically determines the Runtime ID (RID) based on the Operating System and bitness (e.g. `win-x86`) 2. CMake invokes `GetRuntimePack.csproj` to download the appropriate Runtime Pack, which gets written to the local NuGet cache. We can read this location with a custom MSBuild target, and write it to `runtime-pack-dir.txt` for CMake to use. 3. CMake reads this location from the txt file, and calls `copy_runtime.cmake` to copy the runtime to a staging directory (relative to the build directory) 4. Our `MonoCoreClr` host needs to call Mono's embedding APIs within `coreclr.dll/so`, but the runtime doesn't ship `.a` or `.lib` interface libraries that we can link against. \ This is not a problem for platforms that can directly link against shared libraries, like GNU/Linux, but it's a blocker for Cygwin/Mingw and MSVC, which require dedicated `.dll.a`/`.lib` files. For those platforms, we can generate them from the `.dll` by using [gendef](https://www.mingw-w64.org/tools/gendef/) and [dlltool](https://sourceware.org/binutils/docs/binutils/dlltool.html), at build time. \ The resulting files are only required to build `MonoCoreClr` and are not needed at runtime. 5. Besides `coreclr.dll`, we also need to copy: - The other native libraries used by Mono (`hostfxr`, `hostpolicy`, `System.Private.CoreLib`, etc.), which are part of the Runtime Pack - The compiled framework, which also includes Mono-specific Managed assemblies such as `System.Runtime` CMake performs all of this with a custom install script, which you can invoke by running `cmake --install`. The final structure of the `bin` folder should then look like this (example for Windows): - `ezdotnet.exe` - `MonoHost_CoreClr.dll` - `coreclr.dll` - `hostfxr.dll`, `System.Private.CoreLib.dll`, etc... - `publish-monocoreclr` **IMPORTANT** In order for `MonoHost_CoreClr.dll` to work, you **MUST** set the `MONO_PATH` environment variable to point to the `publish-monocoreclr` directory, where the Mono Framework is located. This must be done **BEFORE** running `ezdotnet` cli. Failing to do this, or using the non-Mono CoreClr framework, will result in assertion failures and other hard to debug issues due to incompatible runtime libraries. Example: set MONO_PATH=%CD%\publish-monocoreclr ezdotnet.exe MonoHost_CoreClr.dll ^ %SAMPLE_DIR%\Sample\net8.0\Sample.dllSample.dll ^ "ManagedSample.EntryPoint" "Entry" "arg1" "arg2" "arg3" "arg4" "arg5"