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"