Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork630
Description
📝 Feature Description
Separate the ASI into native C++ loader and a managed C# runtime, so we can run test code for some part of managed runtime, and add support for reloading the native ASI.
I found that we can have SHVRELOAD the ASI by manually hosting .NET Framework runtime, rather than having our ASI being a mixed assembly. Still, we have to have at least 1 managed .dll assembly get pinned for the SHVDN runtime. however, which means we can't swap all part of the runtime. This is because at least 1 managed one needs to be executed in the defaultAppDomain, which locks grabbed assemblies and can't be disposed by anything other than exiting the process.
🔧 Feature Workflow
- Separate the ASI into native C++ loader and a managed C# runtime
- Have the native C++ loader ASI invokes a tick method and a keyboard message method of managed C# runtime
PoC code:
Place these pieces of code in 2 different projects. You can use the projects in the src archive to build, or you can put the .asi and .dll files of the bin archive to the main directory of the game.
ScriptHookVDotNet-NativeLoader-bin.zip
ScriptHookVDotNet-NativeLoader-src.zip
Native C++ Code
#include"main.h"#include<mscoree.h>#include<corerror.h>#include<metahost.h>#include<fstream>#pragma comment (lib, "mscoree.lib")staticbool called_script_main =false;staticvoidLog(constchar* msg){std::ofstreamfile("ScriptHookVDotNet-NativeLoader.log", std::ios_base::app);file << msg << std::endl;file.close();}static ICLRRuntimeHost*GetClr45RuntimeHost(){constexpr WCHAR VER_STR_FOR_CLR45[] =L"v4.0.30319"; ICLRMetaHost* pMetaHost =nullptr; ICLRRuntimeInfo* pRuntimeInfo =nullptr; ICLRRuntimeHost* pRuntimeHost =nullptr;// Build runtime. Looks like calling `CLRCreateInstance` multiple times causes no problems...if (CLRCreateInstance(CLSID_CLRMetaHost,IID_PPV_ARGS(&pMetaHost)) != S_OK) {Log("CLRCreateInstance failed");returnnullptr; }if (pMetaHost->GetRuntime(VER_STR_FOR_CLR45,IID_PPV_ARGS(&pRuntimeInfo)) != S_OK) {Log("ICLRMetaHost::GetRuntime failed");returnnullptr; }if (pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost,IID_PPV_ARGS(&pRuntimeHost)) != S_OK) {Log("ICLRRuntimeInfo::GetInterface failed");returnnullptr; }return pRuntimeHost;}static ICLRRuntimeHost*StartClr45RuntimeHost(){ ICLRRuntimeHost* pRuntimeHost =GetClr45RuntimeHost();// build runtimeif (pRuntimeHost ==nullptr) {Log("Failed to start CLR Runtime For .NET Framework 4.5+");returnnullptr; }// it looks like we need to explicitly call `Start` for our case pRuntimeHost->Start();return pRuntimeHost;}staticvoidScriptMain(){while (true) {if (!called_script_main) { called_script_main =true; ICLRRuntimeHost *runtimeHost =StartClr45RuntimeHost();// Execute our managed assembly. We shouldn't execute our code in a main script func for SHV// because of damn SHV fibers (all C++ exceptions would fall through and the process gets killed),// but we call a managed method with a fiber *for this demo*.//// Static variables in managed code do NOT reset to the default after returning to the caller. DWORD pReturnValue; runtimeHost->ExecuteInDefaultAppDomain(L"ScriptHookVDotNet_ManagedRuntime.dll",L"ScriptHookVDotNet_ManagedRuntime.Program",L"Main",L"", &pReturnValue);}WAIT(0);}}staticvoidScriptKeyboardMessage(DWORD key, WORD repeats, BYTE scanCode, BOOL isExtended, BOOL isWithAlt, BOOL wasDownBefore, BOOL isUpNow){if (isUpNow || wasDownBefore) {return; } ICLRRuntimeHost* runtimeHost =StartClr45RuntimeHost();// execute our managed assembly DWORD pReturnValue; runtimeHost->ExecuteInDefaultAppDomain(L"ScriptHookVDotNet_ManagedRuntime.dll",L"ScriptHookVDotNet_ManagedRuntime.Program",L"SendKeyboardMessage",L"", &pReturnValue);}BOOL APIENTRYDllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH:DisableThreadLibraryCalls(hModule);scriptRegister(hModule, ScriptMain);keyboardHandlerRegister(ScriptKeyboardMessage);break;case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:scriptUnregister(hModule);keyboardHandlerUnregister(ScriptKeyboardMessage);break; }returnTRUE;}
C# Code (.NET Framework)
usingSystem;usingSystem.Globalization;usingSystem.IO;namespaceScriptHookVDotNet_ManagedRuntime{publicclassProgram{staticobject_lockObj=newobject();staticint_callCount=0;staticintMain(stringargs){// the count has to be incremented within a lock to avoid data race because SHV calls the main function// and the keyboard message function in different threadslock(_lockObj){_callCount++;}using(StreamWritersw=newStreamWriter("ScriptHookVDotNet-Managed-test.txt")){stringdtStr=$"[{DateTime.Now.ToString()}]";sw.WriteLine($"{dtStr}: Woah! Loader ASI and Managed Runtime DLL were loaded separately! "+$"Managed Runtime Call Count:{_callCount}");}return0;}staticintSendKeyboardMessage(stringargs){lock(_lockObj){_callCount++;}using(StreamWritersw=newStreamWriter("ScriptHookVDotNet-Managed-test.txt")){sw.WriteLine("Managed Runtime Call Count is incremented by one. Current Managed Runtime Call Count: "+_callCount.ToString());}return0;}}}