Notice: All error handling / checking has been stripped from the code to make things as basic and easy to understand. Majority of the calls made in the below code are to COM objects which use HRESULT returns. Always check your returns before continuing to help prevent crashes!
One of the biggest, I consider, issues with .NET and plugins is that you can’t truly unload a managed module without fully shutting down the application domain it is ran within. Things linger in memory and the garbage collection only does so much. In C/C++ the simple FreeLibrary handles this but we don’t have that luxury in C#. Instead, the easiest way to truly get unloading plugins is by loading each one into it’s own AppDomain.
Doing this with the use of ‘mixed’ code is simple and documented, however doing this in pure native C++ is not. It involves working with the COM interfaces of the CLR and such which does not have much documentation, and after a certain pointer, there is none. There also is a lack of examples showing how to do it online / open source. For someone like me, I personally enjoy both .NET (C#) and C/C++, however, I enjoy them being separate. I do not like VC++, the look and feel of the code is just wrong to me. I do not use it, and do not plan to use it. (This is my opinion, I’m sure yours will differ, that’s not the point of this post.)
Anyway, getting into the code. To start, this article will be based around how I implemented things, and what for. So a little back story. I work on a project that is an injected hook into a popular MMORPG. The purpose of this hook is to extend the ability of the game to make it better enjoyable to play. The focus is not cheating related, but the project does not limit what the users can do with the plugin interface that is exposed. The project is fully coded in C++ and the extension SDK exposes a collection of interfaces that the users can use to interact with the core hook. This is done by the hook loading plugins and passing a core object pointer to them which is casted to the core interface object.
With this I created an extension for our hook called ‘Bootstrap’. The purpose of this extension is to load the CLR into the process as well as creating new appdomains for each C# extension that it loads. I wanted to ensure that these plugins can be fully unloaded during runtime because being an MMO, the game does take up it’s own amount of resources and such. Keeping things clean and limited help out a lot.
So we have:
Hook <- -> Bootstrap Extension <- -> Managed Extension(s)
When Bootstrap first starts, we want to ensure we load the CLR runtime first and immediately.
I do that with the following little bit of code:
// Bind to the given runtime..
ICorRuntimeHost* lpRuntimeHost = NULL;
CorBindToRuntimeEx( L"v4.0.30319", L"wks", 0, CLSID_CorRuntimeHost, IID_PPV_ARGS( &lpRuntimeHost ) );
// Attempt to start the runtime..
lpRuntimeHost->Start();
Next, when a managed plugin is requested to start, Bootstrap will do some minor checking to ensure the file exists and such. Once it ensures the plugins not already loaded and it exists on disk, it will attempt to load it.
The next step is to load this plugin into its own AppDomain. For starters, I wrote a simple wrapper to hold the important interfaces that this plugin created when being loaded like this:
struct managedplugin_t
{
std::wstring m_PluginName;
std::wstring m_PluginPath;
CComPtr<IUnknown> m_AppDomainSetupUnknown;
CComQIPtr<IAppDomainSetup> m_AppDomainSetup;
CComPtr<IUnknown> m_AppDomainUnknown;
CComPtr<_AppDomain> m_AppDomain;
CComPtr<_Assembly> m_AppAssembly;
CComVariant m_AppVariant;
CComPtr<IAshitaExtension> m_AppInstance;
bool m_IsDirect3DReady;
managedplugin_t()
{
this->m_PluginName = L"";
this->m_PluginPath = L"";
this->m_AppDomainSetupUnknown = NULL;
this->m_AppDomainUnknown = NULL;
this->m_AppDomain = NULL;
this->m_AppAssembly = NULL;
this->m_AppVariant = NULL;
this->m_AppInstance = NULL;
this->m_IsDirect3DReady = false;
}
};
Next, the first step is to create a new AppDomain. We want to create a DomainSetup object first to set some properties for the domain, such as where it will resolve its assemblies from and such. Afterward we create the domain with this new setup information:
// Create a new AppDomain setup object for the new domain..
this->m_RuntimeHost->CreateDomainSetup( &plugin->m_AppDomainSetupUnknown );
// Fill some basic structure info about this domain..
plugin->m_AppDomainSetup = plugin->m_AppDomainSetupUnknown;
plugin->m_AppDomainSetup->put_ApplicationBase( CComBSTR( this->m_PluginBasePath.c_str() ) );
plugin->m_AppDomainSetup->put_ShadowCopyFiles( CComBSTR( "true" ) );
plugin->m_AppDomainSetup->put_ApplicationName( CComBSTR( pluginName.c_str() ) );
// Create the new AppDomain..
this->m_RuntimeHost->CreateDomainEx( pluginName.c_str(), plugin->m_AppDomainSetupUnknown, NULL, &plugin->m_AppDomainUnknown );
// Obtain the actual AppDomain object..
plugin->m_AppDomainUnknown->QueryInterface( __uuidof( mscorlib::_AppDomain ), (void**)&plugin->m_AppDomain );
Now we want to load the plugin file into this AppDomain. My method was to have users create their plugins a specific way. Each plugin is loaded by its name, and then it must use a public class named ‘Main’ that inherits my C# implementation of our IAshitaExtension base class. This way I can forward all the C++ plugin calls to the C# plugins. So we load the plugin now like this:
// Load the plugin module into the AppDomain..
plugin->m_AppDomain->Load_2( CComBSTR( mainInterface.c_str() ), &plugin->m_AppAssembly );
// Create instaoce of the 'Main' class object..
plugin->m_AppAssembly->CreateInstance_2( CComBSTR( mainInterface.append( L".Main" ).c_str() ), true, &plugin->m_AppVariant );
// Obtain instance of our base class from this newly created instance..
plugin->m_AppVariant.punkVal->QueryInterface( __uuidof( IAshitaExtension ), (void**)&plugin->m_AppInstance );
One major thing to note here. For some reason CreateInstance_2 does not return properly. If it fails to create an instance of the given object, it will still return S_OK. You need to check the VARIANT object it returns and ensure it is valid. You can do this by checking it like:
if (plugin->m_AppVariant.vt == VT_EMPTY || plugin->m_AppVariant.vt == VT_ERROR)
{
/** Object is invalid.. **/
}
Next is understanding the relationship between C++ and C# in our extensions. As I said above, the C# extensions inherit a managed base class, which looks like this:
namespace ManagedExample
{
public class Main : AshitaBase
{
public override bool Load(IntPtr ashitaCore)
{
}
}
}
The way this works is that AshitaBase is actually an abstract class.
This class inherits a base COM exposed interface, which in turn these look like this:
[ComVisible(true)]
[Guid("7805E68A-A028-433C-BB73-38D34D7208C5")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAshitaExtension
{
[ComVisible(true)]
[return: MarshalAs(UnmanagedType.I1)]
bool Load(IntPtr ashitaCore);
}
public abstract class AshitaBase : IAshitaExtension
{
public virtual bool Load(IntPtr ashitaCore)
{
return false;
}
}
I had to use a COM exposed interface like this in order to be able to call things easily on the C++ side. With it setup like this, and with the above C++ code, we can now tell the C# plugin we loaded to call it’s Load function like this:
bool bReturn = false;
plugin->m_AppInstance->Load( (long)this->m_AshitaCore, (unsigned char*)&bReturn );
And there we have it, CLR hosting in pure native C++ 🙂
There are some things with this that I am still trying to figure out, such as adding an UnhandledException handler to each AppDomain but I am unsure how to properly call the m_AppDomain->add_UnhandledException function at this time.
Feel free to leave comments, suggestions, etc. Hope this helps someone in the future.
Some minor things I forgot to mention, here are the includes and other definitions needed for this stuff:
#pragma comment( lib, "mscoree.lib" )
#include <mscoree.h>
#include <atlbase.h>
#include <atlsafe.h>
#include <metahost.h>
#import "mscorlib.tlb" raw_interfaces_only
// Import of my managed base class information..
#import "../../build/Extensions/Managed/AshitaAPI.tlb" no_namespace named_guids raw_interfaces_only