Add HybridCLR runtime compatibility support#251
Conversation
Generator layer: - AssemblyRewriteContext: Fix mscorlib KeyNotFoundException by using TryGetAssemblyByName with fallback to CorLibTypeFactory.Object - RewriteGlobalContext: Remove CurrentInstance static property - HybridCLRMetadataResolver: Simplify GetCorLibAssembly() to use AssemblyResolver directly instead of global state - Delete Generator/HybridCLRCompat.cs (extension methods superseded by null safety changes) - GeneratorOptions: Add IsHybridCLREnvironment property for BepInEx integration Runtime layer: - HybridCLRCompat: Add cross-platform memory allocation support - Windows: VirtualAlloc (tested, production-ready) - Linux/macOS: mmap (experimental, untested)
Generator layer: - Il2CppAssemblyResolver: Add assembly cache for signature-ignoring lookups (handles hotfix DLL signature mismatches) - Pass11ComputeTypeSpecifics: Add null checks for unresolvable types - Pass61ImplementAwaiters: Add CorLib null check for hotfix-only generation - Pass80UnstripMethods: Add null safety for type resolution - DeobfuscationMapGenerator: Add null checks for type context HarmonySupport layer: - Il2CppDetourMethodPatcher: Add HybridCLR interpreter method hook support - HybridCLRMethodInfo struct for accessing extended MethodInfo fields - Detect and prepare interpreter methods for detouring - Copy bridge functions and restore invoker_method after detour Runtime layer: - IL2CPP: Add RefreshImageCache() for detecting newly loaded hotfix assemblies at runtime
- Add ExistingInteropDir and SkipExistingAssemblies options to GeneratorOptions - Support incremental mode in InteropAssemblyGenerator: generates interop only for new assemblies while referencing existing interop as dependencies - Add HotfixInteropSubdir constant to HybridCLRCompat for standard directory naming
- Load existing interop assemblies as reference-only contexts - Register assemblies under both Il2Cpp-prefixed and original names (e.g., Il2Cppmscorlib -> mscorlib) for lookup compatibility - Register types under both Il2Cpp-prefixed and original names (e.g., Il2CppSystem.Type -> System.Type) - Add RegisterTypeByAlternativeName method for alias registration - Update CorLib and GetContextForNewAssembly to check reference assemblies
|
Nice work! Just a reminder: we should version HybridCLRMethodInfo because the |
|
Thanks for hint! I had forgot about that. I will handle it tonight -- currently outside.
|
- Add IHybridCLRMethodInfoStruct interface and HybridCLRMethodInfoWrapper - Auto-detect HybridCLR MethodInfo layout (NEW bitfield vs LEGACY) - PrepareMethodForDetour now requires detourAddress parameter - Allocate executable memory within ±2GB of original bridge - Update Il2CppDetourMethodPatcher to use new API
|
Ready for review. I think. :P |
|
Resolved conflict due to #254 |
PrepareMethodForDetour only needs to duplicate the bridge code and update methodPointer/virtualMethodPointer. Also remove unused hotfix assembly management APIs and the inlined HybridCLRMethodInfo struct from Il2CppDetourMethodPatcher (now uses the shared WrapMethodInfo API).
The bridge contains rel32 calls (e.g. to Interpreter::Execute) that are encoded as offsets from the instruction address. After copying to a new location, these offsets point to wrong addresses and must be adjusted.
|
What a great help! I happen to want modding a HybridCLR Game recently. However, I haven't found any tutorials about it, and I only found some old tutorial of BepInEx 5. Do you have any ideas to write a tutorial? Thanks! |
This PR provides fix for making IL2CPPInterop lib runnable in HybridCLR environment, and proper API for generating interop dll files for late loaded DLLs by reusing existing interop. The new methods have detailed documents and should be good to go. Modding a HybridCLR game requires some hacks into the HybridCLR interpreter for getting callbacks when the execution stack calls the methods from late loaded DLLs, which is not included in this PR but a standalone library which I'm working on. |
It's great to hear that a dedicated library for HybridCLR modding callbacks is in the works. As a newcomer to BepInEx and HybridCLR modding, I'm currently exploring what's possible with the current setup. I plan to focus on basic Unity Engine hooks and assembly analysis while waiting for the upcoming library and tutorial. Please keep me posted when the repository is ready. I'm looking forward to testing it and providing feedback. Thanks again for your efforts! |
Three fixes to resolve types through reference (existing interop) assemblies: 1. Il2CppAssemblyResolver: register unprefixed assembly names in cache 2. HybridCLRMetadataResolver: try Il2Cpp-prefixed namespace fallback 3. AssemblyRewriteContext: fall back to reference assembly type context in RewriteTypeRef
Object-identity lookup fails because the resolved TypeDefinition lives in the raw dependency DLL (e.g., mscorlib loaded by metadata resolver), while the registered context is for the interop DLL (Il2Cppmscorlib). Switch to: 1. TryGetAssemblyByName for assembly lookup (matches by name) 2. TryGetTypeByName for type lookup (matches registered alternative names)
Add isHybridCLREnvironment parameter to AssemblyMetadataAccess. When true, uses HybridCLRMetadataResolver which handles Il2Cpp-prefixed namespace lookup in reference assemblies. Enabled when ExistingInteropDir is set.
Root cause: source assemblies' HybridCLRMetadataResolver could not resolve types like System.MulticastDelegate because reference assemblies (Il2Cppmscorlib) were in a separate resolver instance. Generator: Add AddReferenceAssemblies() to share reference assemblies with the source resolver so type references can be resolved across both. Runtime: Skip delegate .ctor call when MethodPointer is null (HybridCLR interpreter methods have no native body). Fall back to il2cpp_object_new + direct field writes.
…r check HybridCLR sets a bridge stub on MethodPointer for interpreter methods, so checking for IntPtr.Zero is insufficient. Use the HybridCLR-specific IsInterpterImpl flag to correctly detect interpreter methods.
…untime Non-HybridCLR games don't have extension fields after MethodInfo struct, reading them would produce garbage. Early-return true when not HybridCLR.
Allocate extended buffer with HybridCLR extension fields (methodPointerCallByInterp, virtualMethodPointerCallByInterp) and set klass pointer. The HybridCLR interpreter reads past the standard struct size when invoking delegates, causing crashes on heap garbage.
Now it lives. https://github.com/SNWCreations/IL2CppSharp.Hooking , also stands as the example usage of this fork branch, available on NuGet as IL2CppSharp.Hooking |
Merge BepInEx upstream/master at dbda1cb into the local SNW master branch. This brings in upstream IllGames-related fixes and the upstream version bump while preserving the local HybridCLR support carried on master. Resolved the conflict in Il2CppInterop.HarmonySupport/Il2CppDetourMethodPatcher.cs by keeping the local HybridCLR hotfix method preparation flow: detect interpreter-backed HybridCLR methods, call HybridCLRCompat.PrepareMethodForDetour before applying the native detour, and call HybridCLRCompat.RestoreInvokerMethod afterwards so original-method calls keep the original invoker_method. At the same time, kept upstream's revert of the managed MonoMod Detour path. The removed managed Detour/DetourCache logic rewrites the managed Original body and upstream reverted it because it breaks repeated Harmony patches on the same target when CopyOriginal expects the original method body. Also dropped the now-unused detourAddress local and updated the nearby comment because PrepareMethodForDetour no longer consumes a detour target address.
Add HybridCLR Runtime Compatibility Support
Inspired by https://zhuanlan.zhihu.com/p/1962949427217015984 , a successful attempt on introducing HybridCLR env support to BepInEx by modding Il2CppInterop generator, written by Xkein
Probably relates to #138 #214 #230 #231, hope this helps.
HybridCLR (formerly Huatuo) is a popular hot-update solution for Unity IL2CPP games, especially in the Chinese game dev community. It modifies the IL2CPP runtime to support interpreter execution for hot-loaded assemblies.
Currently, Il2CppInterop crashes on HybridCLR games with:
Why bother?
I had seen this and that's fair. Sorry if you think this reference is aggressive.
But HybridCLR isn't some niche hack anymore. It's widely adopted, especially for mobile games and games from Chinese studios. More and more games are shipping with it, and modders are hitting this crash with no workaround.
This fix doesn't change how Il2CppInterop works for normal games. It just adds a fallback path for HybridCLR games so they don't crash on startup. Seems like a reasonable tradeoff.
Why does it crash?
HybridCLR adds an
IsInterpreterIndexcheck at the start ofGetTypeInfoFromTypeDefinitionIndex(in my case):The detour library can't handle the short jump (
jne short +3) properly when creating the trampoline - it corrupts the instruction and causes the crash.The fix
Instead of hooking at the function start, we hook at offset +08 where the
test ecx, 0xf0000000instruction is. This skips the problematic short jump entirely.To find the right spot, we use a signature pattern:
This matches
test ecx, 0xf0000000; jne ...; mov rax, [rip+...]which uniquely identifies our target function.Since we skip the
-1check in the prologue, the hook handles it manually.What this PR adds
Runtime Layer
HybridCLR.RuntimeApi::LoadMetadataForAOTAssembly)RefreshImageCache()to detect newly loaded hotfix assemblies at runtimeinvoker_methodGenerator Layer
KeyNotFoundExceptionwhen mscorlib is not in source assemblies (hotfix-only generation)HybridCLRMetadataResolverhandles IL2CPP moving types from System.dll to mscorlibIsHybridCLREnvironmentoption: For BepInEx integrationHarmonySupport Layer
HybridCLRMethodInfostruct: Access extended MethodInfo fields (interpData, methodPointerCallByInterp, etc.)Why do interpreter methods need special handling?
HybridCLR interpreter methods share bridge functions across multiple methods. Direct hooking would affect all methods using the same bridge. The solution:
isInterpterImplflag so Harmony treats it as nativeinvoker_methodfor calling original codeDetection
HybridCLR is detected by checking for its registered icalls:
HybridCLR.RuntimeApi::LoadMetadataForAOTAssemblyHybridCLR.RuntimeApi::GetRuntimeOptionIf none are found, standard behavior is used - non-HybridCLR games are unaffected.
Tested with
Limitations & call for feedback
This was developed by debugging a forked BepInEx with a single HybridCLR game. The signature pattern and detection logic work for my test case, but different games might use different HybridCLR versions or compiler settings that produce slightly different code.
If you're working with other HybridCLR games and run into issues (or it works fine!), I'd love to hear about it. Comparing implementations across different games would help make this more robust.