smx-smx/EzDotnet

GitHub: smx-smx/EzDotnet

一个跨平台的 C/C++ 库,用于从原生代码中加载和运行托管 .NET 程序集,支持多种 .NET 运行时后端。

Stars: 16 | Forks: 4

# EzDotNet 用于从 C/C++ 代码轻松加载和运行托管 .NET 程序集的库和工具。 ## 项目结构: EzDotnet 为以下运行时实现了 host: | 名称 | 运行时 | 操作系统 | |------|---------|----| | CLRHost | .NET Framework v4.x | 仅限 Windows | | MonoHost | [mono](https://github.com/mono/mono) / [wine-mono](https://gitlab.winehq.org/mono/mono) | 跨平台 | | CoreCLR | [.NET Core](https://dotnet.microsoft.com/en-us/download) | 跨平台 | | MonoCoreClr | [.NET Core Mono](https://github.com/dotnet/runtime/tree/eb1c0ab314ef67bc31d85a5bee8a9a36fca84b93/src/mono) | 跨平台,参见 [Microsoft.NETCore.App.Runtime.Mono](https://www.nuget.org/packages?page=2&q=Microsoft.NETCore.App.Runtime.Mono&sortBy=relevance) | 各后端暴露了相同的接口,因此可以在保持代码不变的情况下互换使用它们。 要加载托管程序集,我们需要将 EzDotnet API 引入我们的项目中。 可以通过以下任一方式实现: - 静态链接以下后端之一:`coreclrhost`、`monohost` 或 `clrhost` - 动态链接(`dlopen`/`LoadLibrary`) - 使用示例动态辅助库(`ezdotnet_shared`) ## C# 项目设置 首先,创建一个新的控制台应用程序: ``` dotnet new console -o ManagedSample ``` 然后,添加 `Microsoft.NETCore.DotNetAppHost` nuget 包,例如通过 `dotnet` cli: ``` dotnet add package Microsoft.NETCore.DotNetAppHost ``` 现在使用以下代码作为起点,为 native 代码创建一个 EntryPoint: ``` 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 ``` 以下段落说明了如何设置 native 加载器: ## Native 设置 提供了一个示例动态辅助工具,以简化加载 .NET 和调用程序集入口点的过程。 否则,请参阅 [API 文档](#api-documentation) 以自行使用静态/动态链接。 ### 命令行 如果你的 C# 代码使用了示例项目中的 Entry Point 格式(需要字符串参数),你可以使用 `ezdotnet` CLI 工具来运行你的程序集。 这也使你能够在 Cygwin 中运行 C# 程序,并轻松实现 Cygwin/C# 之间的互操作。 构建并安装项目后,你可以在 `CMAKE_INSTALL_PREFIX` 的 `bin` 文件夹下找到该 CLI。 用法如下: ``` Usage: ezdotnet [loaderPath] [asmPath] [className] [methodName] ``` 其中: - `loaderPath`:你想使用的一个 .NET Host/后端的路径(作为 EzDotNet 的一部分构建) - `asmPath`:你要加载的已发布托管程序集的完整路径(`dotnet publish` 的输出) - `className`:包含 EntryPoint 方法的完全限定类名(包括命名空间) - `methodName`:该类中 EntryPoint 方法的名称 ### 动态辅助工具 如果你决定使用动态辅助工具,你必须加载 `ezdotnet_shared` 并解析 `int main(int argc, char *argv[])` 方法(通过 `dlsym` 或 `GetProcAddress`)。 详情请参阅以下示例: ``` 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 文档 各后端共享一个通用接口: #### clrInit - `ASMHANDLE clrInit(const char *assemblyPath, const char *baseDir, bool enableDebug)` 返回:指向已加载程序集的句柄 #### clrDeInit - `bool clrDeInit(ASMHANDLE handle)` 取消初始化执行环境。 #### runMethod - `int runMethod(ASMHANDLE handle, const char *typeName, const char *methodName)` 在给定先前 `clrInit` 调用加载的程序集 `handle` 的情况下,运行类 `typeName` 中的 `methodName` 方法。 C# 方法应具有以下签名: ``` private static int Entry(IntPtr args, int sizeBytes) { string[] argv = ReadArgv(args, sizeBytes); Main(argv); return 0; } ``` ## 用例 ### 可执行文件或库 你可以在可执行文件或库中使用 EzDotnet。 你可以静态链接到单个加载器,也可以使用动态链接(例如 `dlopen`),以便在运行时选择要使用的引擎(CLR/CoreCLR/Mono)。 **警告** 如果你正在从 DLL 加载 EzDotnet,请避免在类似 `DllMain` 的库构造函数中加载 CLR。这样做会导致 `clrInit` 中出现死锁。 相反,应创建一个单独的线程并使用它来加载 CLR,以便 `DllMain` 能够顺利返回。 ### Cygwin 互操作性 此项目使你能够从 .NET 调用 Cygwin 代码。 对于这种用例,.NET host/loader(例如 `samples/cli/ezdotnet` 或 `libcoreclrhost`)**必须**在 Cygwin 下编译。 换句话说,只有当你从 Cygwin 进程启动,随后再加载 .NET 时,才能从 .NET 调用 Cygwin 代码。 从 Win32 启动并调用 Cygwin **无法**工作 因此,如果你想构建一个具有 Cygwin 功能的典型 CLI 或 Windows Forms 应用程序,你需要使用 `ezdotnet` CLI 来启动该应用程序才能使其正常工作。 **注意**:`ezdotnet` CLI **必须**被编译为 Cygwin 应用程序。 ### 进程注入 如果你正在构建一个共享库,你可以将其注入到另一个进程中,以使其能够运行 .NET 代码。 对于这种用例,你将需要使用库注入器。 有多种工具和方法可以实现这一点,例如: #### Windows - [Detours](https://github.com/microsoft/Detours) 提供了一个使用 DLL 生成进程的 API - [SetSail](https://github.com/TheAssemblyArmada/SetSail) 可以在 EXE Entrypoint 处注入 DLL #### 类 Unix 系统 - [LD_PRELOAD](https://man7.org/linux/man-pages/man8/ld.so.8.html)(Linux、FreeBSD 等)可用于在可执行文件中(在启动时)预加载库 #### 通用 - [ezinject](https://github.com/smx-smx/ezinject) 可以将库注入到正在运行的可执行文件中 - 或者使用你喜欢的注入器 ### 关于 MonoCoreClr 的说明 MonoCoreClr 需要一个特定的 Mono 运行时包,其中包含一个“伪装”成 `coreclr.[dll|so|dylib]` 的 Mono 构建版本。 设置此运行时的过程如下(主要由构建过程执行,灵感来自 https://github.com/lambdageek/monovm-embed-sample)。 阅读它以了解其工作原理及涉及的文件非常重要: 1. CMake 根据操作系统和位数(例如 `win-x86`)启发式地确定运行时 ID (RID) 2. CMake 调用 `GetRuntimePack.csproj` 下载相应的运行时包,并将其写入本地 NuGet 缓存。我们可以通过自定义的 MSBuild target 读取此位置,并将其写入 `runtime-pack-dir.txt` 供 CMake 使用。 3. CMake 从 txt 文件中读取此位置,并调用 `copy_runtime.cmake` 将运行时复制到暂存目录(相对于构建目录) 4. 我们的 `MonoCoreClr` host 需要在 `coreclr.dll/so` 中调用 Mono 的嵌入 API,但该运行时不提供我们可以链接的 `.a` 或 `.lib` 接口库。 \ 对于可以直接链接到共享库的平台(如 GNU/Linux),这不是问题,但对于 Cygwin/Mingw 和 MSVC 来说是一个阻碍,因为它们需要专用的 `.dll.a`/`.lib` 文件。 对于这些平台,我们可以在构建时使用 [gendef](https://www.mingw-w64.org/tools/gendef/) 和 [dlltool](https://sourceware.org/binutils/docs/binutils/dlltool.html) 从 `.dll` 生成它们。 \ 生成的文件仅用于构建 `MonoCoreClr`,在运行时不需要。 5. 除了 `coreclr.dll`,我们还需要复制: - Mono 使用的其他 native 库(`hostfxr`、`hostpolicy`、`System.Private.CoreLib` 等),它们是运行时包的一部分 - 编译后的框架,其中还包含 Mono 特定的托管程序集,例如 `System.Runtime` CMake 通过自定义安装脚本执行所有这些操作,你可以通过运行 `cmake --install` 来调用它。 `bin` 文件夹的最终结构应如下所示(以 Windows 为例): - `ezdotnet.exe` - `MonoHost_CoreClr.dll` - `coreclr.dll` - `hostfxr.dll`、`System.Private.CoreLib.dll` 等... - `publish-monocoreclr` **重要提示** 为了让 `MonoHost_CoreClr.dll` 正常工作,你**必须**将 `MONO_PATH` 环境变量设置为指向 `publish-monocoreclr` 目录(即 Mono 框架所在的目录)。 这必须在运行 `ezdotnet` cli **之前**完成。 如果不这样做,或者使用非 Mono 的 CoreClr 框架,将会由于运行时库不兼容而导致断言失败和其他难以调试的问题。 示例: ``` 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" ```
标签:Bash脚本