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脚本