CLR 托管 - 自定义 CLR






4.89/5 (35投票s)
我们学习基础知识并创建一个简单的 AppDomainManager。
引言
您是否对 .NET 运行时的工作原理感兴趣,或者需要根据您的需求更改 .NET 运行时的行为?那么这篇文章就是为您准备的。这是第 1 部分,我们将在其中学习基础知识并创建一个简单的 AppDomainManager。在第 2 部分中,我们将实现具有沙盒、异常处理和替代程序集加载功能的 AppDomainManager。
背景
这篇文章是我一次教育尝试的成果。我对调试和测试有非常浓厚的兴趣,所以我大部分文章都与此相关,无论是直接关于如何编写调试器扩展,还是间接关于提高日志记录能力或探索 API。我的上一篇文章是关于构建混合模式采样分析器,那是我需要的东西。当我开始探索与本文相关的 API 时,我并没有特殊的需求,我是出于好奇才这么做的。此时,我看到了几个有趣的观点。
.NET 应用程序如何启动?
现在谈论这个 API 还为时过早。让我们退一步。
Windows 如何知道一个二进制文件是 .NET 应用程序?实际上,答案取决于您运行的 Windows 版本。
通常,Windows 通过查看 PE 头部来执行 .exe 文件。PE 头部说明了如何将其加载到内存中、它有哪些依赖项以及入口点在哪里。
.NET 应用程序的入口点在哪里?嗯,您的应用程序是 IL 代码。直接执行它显然会导致崩溃。不应该是 IL 代码开始执行,而是 .NET 运行时,它最终应该加载 IL 代码并执行它。
在较新版本的 Windows 中,.NET 预安装,并且 Windows 内置了识别 .NET 应用程序的支持。这可以通过简单地查看所有可执行文件和 DLL 中存在的 PE 头部来完成。在较旧版本的 Windows 中,执行会传递到引导程序代码所在的入口点。引导程序是本机代码,它使用非托管 CLR 宿主 API 在当前进程中启动 .NET 运行时并启动真正的程序,即 IL 代码。
CLR 宿主 API
在非托管应用程序中托管 CLR
当您在原生进程中启动 .NET 运行时时,该原生应用程序就成为了运行时的宿主。这允许您将 .NET 功能添加到您的原生应用程序中。
#include <metahost.h>
#include <mscoree.h>
#pragma comment(lib, "mscoree.lib")
ICLRMetaHost *pMetaHost = nullptr;
ICLRRuntimeHost *pRuntimeHost = nullptr;
ICLRRuntimeInfo *pRuntimeInfo = nullptr;
HRESULT hr;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
hr = pMetaHost->GetRuntime(runtimeVersion, IID_PPV_ARGS(&pRuntimeInfo));
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost,IID_PPV_ARGS(&pRuntimeHost));
hr = pRuntimeHost->Start();
现在运行时正在运行,但它还没有加载任何用户代码。一些内部线程调度器和垃圾回收器肯定正在运行,因为它们是 CLR 运行时的一部分。
运行托管代码
要从我们的宿主启动一个托管应用程序,我们想做的是这样的事情:
AppDomain.CurrentDomain.ExecuteAssembly(assemblyName);
旧 API
嗯,在 CLR 宿主接口的早期版本中是可能实现的,通过一个名为 GetDefaultDomain
的 API,它返回一个 AppDomain
。
HRESULT hr = CorBindToRuntimeEx(..., IID_ICorRuntimeHost, (void**)&pRuntimeHost);
hr = pRuntimeHost->Start();
_AppDomain* pCurrentDomain = nullptr;
hr = pRuntimeHost->GetDefaultDomain(&pCurrentDomain);
pCurrentDomain.ExecuteAssembly(assemblyFilename);
但由于充分的理由,此接口已被弃用。旧 API 有很大一部分在非托管代码中,这被证明是一个巨大的缺点。在非托管代码中从 AppDomain 检索值或操作对象会导致大量的封送处理(又名序列化),这严重影响了性能。封送处理也是隐式的,所以并不总是清楚它发生在何处。有时甚至整个 AppDomain 都被封送处理了。所以我们不会使用此接口,而是使用新接口并将所有自定义代码都在 AppDomain 中运行。
新 API
新版本的 CLR 宿主接口已经重新设计。大部分 API 已从非托管代码转移到托管代码。为了获取 AppDomain 实例,必须注册一个 AppDomainManager
实现。好的一面是,用 C# 开发要容易得多也快得多。代码也更清晰,因为不必编写那么多样板代码。
为了能够注册一个新的 AppDomainManager
,我们需要一个名为 ICLRControl
的接口。这个接口包含一个方法 SetAppDomainManagerType
,它加载您对 AppDomainManager
的托管实现。
ICLRControl* pCLRControl = nullptr;
hr = pRuntimeHost->GetCLRControl(&pCLRControl);
LPCWSTR assemblyName = L"SampleAppDomainManager";
LPCWSTR appDomainManagerTypename = L"SampleAppDomainManager.CustomAppDomainManager";
hr = pCLRControl->SetAppDomainManagerType(assemblyName, appDomainManagerTypename);
这就是您需要覆盖它的方法。您只需要一个与之配套的实现。我用托管代码制作了一个基本的实现,名为 CustomAppDomainManager
。下面是我 CustomAppDomainManager
(SampleAppDomainManager.dll) 实现的源代码列表。
[GuidAttribute("0C19678A-CE6C-487B-AD36-0A8B7D7CC035"), ComVisible(true)]
public sealed class CustomAppDomainManager : AppDomainManager, ICustomAppDomainManager
{
public CustomAppDomainManager()
{
System.Console.WriteLine("*** Instantiated CustomAppDomainManager");
}
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
System.Console.WriteLine("*** InitializeNewDomain");
this.InitializationFlags = AppDomainManagerInitializationOptions.RegisterWithHost;
}
public override AppDomain CreateDomain(string friendlyName,
Evidence securityInfo, AppDomainSetup appDomainInfo)
{
var appDomain = base.CreateDomain(friendlyName, securityInfo, appDomainInfo);
System.Console.WriteLine("*** Created AppDomain {0}", friendlyName);
return appDomain;
}
}
现在,您可以,例如,像这样执行程序集中驻留的方法。请注意,由于我们不直接使用 AppDomain,我们将避免所有数据封送。
hr = pRuntimeHost->Start();
DWORD returnValue = 0;
// Executing public static Start(string arg)
hr = pRuntimeHost->ExecuteInDefaultAppDomain(totalPath, L"SampleApp1.Program", L"Start", L"", &returnValue);
hr = pRuntimeHost->Stop();
现在运行示例应用程序会给我们以下输出:
我们真正想要的是通过 ExecuteAssembly
调用直接执行程序集的 Main
方法,就像我们在 AppDomain 上做的那样。有一个小问题。没有 ExecuteAssembly
方法,但我们可以尝试使用 ExecuteApplication
方法。
int retVal = 0;
LPCWSTR dummy = L"";
DWORD dwManifestPaths = 0;
DWORD dwActivationData = 0;
hr = pRuntimeHost->ExecuteApplication(totalPath,
dwManifestPaths,
&dummy,
dwActivationData,
&dummy,
&retVal);
不幸的是,我没有让它工作。文档提到了清单和 Click-Once 部署。我得到的唯一错误是 HRESULT
错误为 E_UNEXPECTED
。这是一个小问题,因为当我们创建 CustomAppDomainManager
实现时,我们可以很容易地解决它,只需添加一个方法,该方法在默认 AppDomain
或新创建的 AppDomain
上调用 ExecuteAssembly
,如下所示:
[GuidAttribute("0C19678A-CE6C-487B-AD36-0A8B7D7CC035"), ComVisible(true)]
public sealed class CustomAppDomainManager : AppDomainManager, ICustomAppDomainManager
{
// ... Rest of class members abbreviated for brevity
public void Run(string assemblyFilename, string friendlyName)
{
AppDomain ad = AppDomain.CreateDomain(friendlyName);
int exitCode = ad.ExecuteAssembly(assemblyFilename);
AppDomain.Unload(ad);
return exitCode;
}
}
修改 SampleApp 以使用此 Run
方法会给我们以下输出:
它执行了程序集的 Main
方法,正如我们所期望的那样。我们还没有完全实现,尽管已经非常接近了。我特意跳过了一步,只是为了向您展示最终结果。缺少的是获取指向我们的 CustomAppDomainManager
的指针的方法。如果您还记得,它不是由我们创建的,而是由 CLR 框架创建的。我们将不得不实现另一个名为 IHostControl
的接口。
IHostControl
这是一个 CLR 将查询替代管理器实现的类,如果我们在任何情况下都有定制版本,我们当然应该实例化它们并返回它们。
可以被用户实现覆盖的处理程序或管理器示例如下所示:
IID_IHostTaskManager
IID_IHostThreadpoolManager
IID_IHostSyncManager
IID_IHostAssemblyManager
IID_IHostGCManager
IID_IHostPolicyManager
在 AppDomainManager
的情况下,CLR 实际上会调用 IHostControl::SetAppDomainManager
,并传入指向我们告诉它创建的类实例的指针。如果您还记得,我们调用了一个名称类似的 方法 ICLRRuntimeHost::SetAppDomainType
。
实现 IHostControl
下面是 IHostControl
接口的最小实现列表。为简洁起见,我删除了样板代码,例如 COM 所需的构造函数、析构函数、AddRef
和 Release
。
class MinimalHostControl : public IHostControl
{
public:
HRESULT STDMETHODCALLTYPE GetHostManager(REFIID riid,void **ppv)
{
*ppv = NULL;
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE SetAppDomainManager(
DWORD dwAppDomainID, IUnknown *pUnkAppDomainManager)
{
HRESULT hr = S_OK;
hr = pUnkAppDomainManager->QueryInterface(__uuidof(ICustomAppDomainManager),
(PVOID*) &m_defaultDomainManager);
return hr;
}
HRESULT STDMETHODCALLTYPE QueryInterface( const IID &iid, void **ppv)
{
if (!ppv) return E_POINTER;
*ppv= this;
AddRef();
return S_OK;
}
// Added in order to get a reference to our AppDomainManager implementation
ICustomAppDomainManager* GetDomainManagerForDefaultDomain()
{
if (m_defaultDomainManager)
{
m_defaultDomainManager->AddRef();
}
return m_defaultDomainManager;
}
private:
ICustomAppDomainManager* m_defaultDomainManager;
};
有了这个最终类,我们就可以通过 AppDomainManager
启动托管程序集了。
运行托管应用程序
现在把所有这些都放在一起。我们将能够启动一个托管应用程序(从我们托管 CLR 运行时的非托管应用程序)。
...
ICLRControl* pCLRControl = nullptr;
hr = pRuntimeHost->GetCLRControl(&pCLRControl);
// Set our own IHostControl implementation
MinimalHostControl* pMyHostControl = pMyHostControl = new MinimalHostControl();
hr = pRuntimeHost->SetHostControl(pMyHostControl);
// Set our own AppDomainManager implementation
LPCWSTR appDomainManagerTypename = L"SampleAppDomainManager.CustomAppDomainManager";
LPCWSTR assemblyName = L"SampleAppDomainManager";
hr = pCLRControl->SetAppDomainManagerType(assemblyName, appDomainManagerTypename);
hr = pRuntimeHost->Start();
// Get a pointer to our CustomAppDomainManager
ICustomAppDomainManager* pAppDomainManager = pMyHostControl->GetDomainManagerForDefaultDomain();
// Invoke the Run method, which in turn invokes the ExecuteAssembly in a new AppDomain
BSTR assemblyFilename = fileName;
BSTR friendlyname = L"TestApp";
hr = pAppDomainManager->Run(assemblyFilename, friendlyname);
hr = pRuntimeHost->Stop();
结论
我们为什么费了这么多周折才执行一个托管应用程序?托管应用程序已经可以通过点击或从命令行启动来执行。
嗯,这只是第一步。我们还没有实现任何有用的东西,但有一个小区别。我们是在一个新的 AppDomain 中执行托管应用程序的,而不是在默认的 AppDomain 中。这样做的好处是您可以创建一个监督启动器。下一步将是实现并替换 CLR 查询 IHostControl
的默认管理器。如果我们不确定应用程序的来源,通过这种类型的托管,我们实际上可以增强应用程序的安全性,并按照我们想要的方式对其进行沙盒化。当然,这是一把双刃剑。它也可以用来消除应用程序的安全性。
我对调试和测试有浓厚的兴趣。自定义运行时将让我能够进行更复杂的日志记录器,而无需修改任何代码。它会正常工作,并且应用程序不会意识到这种变化。
继续 - 第 2 部分
有一篇后续文章,我们将在其中实现具有沙盒、异常处理和替代程序集加载功能的 AppDomainManager。
关注点
主要的文档来源当然是 MSDN 本身,.NET Framework 2.0 宿主接口。
关于 CLR 宿主 API 的一本好书是 自定义 Microsoft® .NET Framework 公共语言运行时。它有点旧,但我认为是最好的(也许是唯一存在的)。