Skip to content

Add HybridCLR runtime compatibility support#251

Open
SNWCreations wants to merge 30 commits into
BepInEx:masterfrom
SNWCreations:master
Open

Add HybridCLR runtime compatibility support#251
SNWCreations wants to merge 30 commits into
BepInEx:masterfrom
SNWCreations:master

Conversation

@SNWCreations

@SNWCreations SNWCreations commented Feb 15, 2026

Copy link
Copy Markdown

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:

Fatal error. System.AccessViolationException: Attempted to read or write protected memory.
   at ...MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.Hook(Int32)

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 IsInterpreterIndex check at the start of GetTypeInfoFromTypeDefinitionIndex (in my case):

cmp ecx, -1
jne short +3      ; <-- this short jump is the problem
xor eax, eax
ret
test ecx, 0xf0000000   ; IsInterpreterIndex check
jne ...

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, 0xf0000000 instruction is. This skips the problematic short jump entirely.

To find the right spot, we use a signature pattern:

F7 C1 00 00 00 F0 0F 85 ?? ?? ?? ?? 48 8B 05

This matches test ecx, 0xf0000000; jne ...; mov rax, [rip+...] which uniquely identifies our target function.

Since we skip the -1 check in the prologue, the hook handles it manually.

What this PR adds

Runtime Layer

  • HybridCLR detection: Auto-detect HybridCLR runtime via icalls (e.g., HybridCLR.RuntimeApi::LoadMetadataForAOTAssembly)
  • Hotfix assembly support: RefreshImageCache() to detect newly loaded hotfix assemblies at runtime
  • Interpreter method detour: Prepare interpreter methods for Harmony hooks by copying bridge functions and managing invoker_method
  • Cross-platform memory allocation: VirtualAlloc (Windows) and mmap (Linux/macOS, experimental)

Generator Layer

  • Null safety: Fix KeyNotFoundException when mscorlib is not in source assemblies (hotfix-only generation)
  • Signature-ignoring assembly resolution: Handle signature mismatches between hotfix DLLs and AOT DLLs
  • Type relocation fallback: HybridCLRMetadataResolver handles IL2CPP moving types from System.dll to mscorlib
  • IsHybridCLREnvironment option: For BepInEx integration

HarmonySupport Layer

  • HybridCLRMethodInfo struct: Access extended MethodInfo fields (interpData, methodPointerCallByInterp, etc.)
  • Interpreter method hook flow: Detect → copy bridge function → clear isInterpterImpl → apply detour → restore invoker_method

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:

  1. Copy the bridge function to new executable memory
  2. Update method pointers to the copied bridge
  3. Clear isInterpterImpl flag so Harmony treats it as native
  4. After detour, restore invoker_method for calling original code

Detection

HybridCLR is detected by checking for its registered icalls:

  • HybridCLR.RuntimeApi::LoadMetadataForAOTAssembly
  • HybridCLR.RuntimeApi::GetRuntimeOption
  • etc.

If none are found, standard behavior is used - non-HybridCLR games are unaffected.

Tested with

  • Astral Party (Unity IL2CPP + HybridCLR)
  • Windows platform

Limitations & call for feedback

  • Linux/macOS memory allocation is experimental (untested)
  • HybridCLR MethodInfo structure layout may vary between versions

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.

@SNWCreations SNWCreations marked this pull request as draft February 17, 2026 01:12
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
@SNWCreations SNWCreations marked this pull request as ready for review February 21, 2026 09:07
- 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
@Xkein

Xkein commented Feb 22, 2026

Copy link
Copy Markdown

Nice work! Just a reminder: we should version HybridCLRMethodInfo because the IsInterpterImpl declaration has changed (bit field vs. full boolean) and Unity has updated the MethodInfo struct(see Runtime/VersionSpecific/MethodInfo).

@SNWCreations

SNWCreations commented Feb 22, 2026 via email

Copy link
Copy Markdown
Author

@SNWCreations SNWCreations marked this pull request as draft February 22, 2026 09:53
- 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
@SNWCreations SNWCreations marked this pull request as ready for review February 23, 2026 04:36
@SNWCreations

Copy link
Copy Markdown
Author

Ready for review. I think. :P

@SNWCreations

Copy link
Copy Markdown
Author

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.
@Glyceryl6

Copy link
Copy Markdown

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!

@SNWCreations

Copy link
Copy Markdown
Author

@Glyceryl6: 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.
A tutorial is planned, probably got it started when I'm available (maybe this weekend). The mentioned library will be uploaded to a GitHub repo, I will reference it here when I think it is ready.

@Glyceryl6

Copy link
Copy Markdown

@Glyceryl6: 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. A tutorial is planned, probably got it started when I'm available (maybe this weekend). The mentioned library will be uploaded to a GitHub repo, I will reference it here when I think it is ready.

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.
@SNWCreations

SNWCreations commented Jun 13, 2026

Copy link
Copy Markdown
Author

@Glyceryl6: 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. A tutorial is planned, probably got it started when I'm available (maybe this weekend). The mentioned library will be uploaded to a GitHub repo, I will reference it here when I think it is ready.

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!

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
Only tested on Unity 2021.3 Windows x64. x86 and other OS not explicitly supported yet.
Please let me know if there is any fault in it or the code from this branch.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants