Reloaded-Project / Reloaded.Hooks

Advanced native function hooks for x86, x64. Welcome to the next level!
GNU Lesser General Public License v3.0
213 stars 33 forks source link

Using hooks from a native AOT library #25

Closed valters-tomsons closed 3 months ago

valters-tomsons commented 3 months ago

Description

I'm trying to use Reloaded.Hooks in a project with Ahead-Of-Time (AOT) compilation, but it appears that hook generation needs runtime reflection. Additionally, the trimmer seems to strip away important types like Reloaded.Memory.Sources.IMemory. This issue complicates the use of Reloaded.Hooks in AOT scenarios.

Would it be possible to provide reflection-less & trimming-friendly hooks?

Use Case

I am running the hooker outside the regular Reloaded-II runtime/injector, and instead, compiling as a native dinput8.dll proxy that's placed in the target folder. See here

Expected Behavior:

An AOT compiled project depending on Reloaded.Hooks should build without warnings and install hooks without runtime exceptions.

Actual Behavior

The application builds with warnings, and at runtime, it fails to load Reloaded.Memory.Sources.IMemory on new Hook<T>() due to the trimming process removing necessary components.

Sewer56 commented 3 months ago

Note: This library will be deprecated in a couple of months; I've written ~80% of a Rust successor already. And will provide a backcompat layer that implementes IReloadedHooks It's just on a small hiatus while I write the Reloaded3 docs; so that may take around half a year.


That said, I'm still happy to help nonetheless.

Here's some quick questions:

A few years ago I went through a lot of pain to get this annotated for trimming (there's almost 100 trim annotations), in an era where Source Generators still weren't really a thing. Although Reloaded.Memory 7.x is quite dated, that's also trimming annotated.

I've used this library trimmed before (with JIT though), so it's kind of a bummer to see it fail in a specific use case :sweat_smile:

Sewer56 commented 3 months ago

It's also a bit weird that it would trim away Reloaded.Memory.Sources.IMemory. There shouldn't be any reflection involved there, or up to the path of new Hook<T>(). I do find that quite odd.

valters-tomsons commented 3 months ago

Hey, thanks for the reply! Great to see you working on more stuff!

There are some warnings on the Hook constructor:

warning IL2026: Using member 'System.Delegate.CreateDelegate(Type, Object, String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The target method might be removed.
warning IL2026: Using member 'System.Delegate.CreateDelegate(Type, Object, String, Boolean)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The target method might be removed.
warning IL2026: Using member 'System.Delegate.CreateDelegate(Type, Object, String, Boolean, Boolean)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The target method might be removed.
warning IL2111: Method 'System.Delegate.CreateDelegate(Type, Type, String)' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.
warning IL2111: Method 'System.Delegate.CreateDelegate(Type, Type, String, Boolean)' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.
warning IL2111: Method 'System.Delegate.CreateDelegate(Type, Type, String, Boolean, Boolean)' with parameters or return value with `DynamicallyAccessedMembersAttribute` is accessed via reflection. Trimmer can't guarantee availability of the requirements of the method.

Here's the runtime exception too:

Unhandled exception. System.TypeLoadException: Could not load type 'Reloaded.Memory.Sources.IMemory' from assembly 'Reloaded.Memory, Version=9.4.1.0, Culture=neutral, PublicKeyToken=null'.
   at Internal.Runtime.CompilerHelpers.ThrowHelpers.ThrowTypeLoadException(ExceptionStringID, String, String) + 0x10
   at Reloaded.Hooks.Tools.Utilities.CreateJump(UIntPtr, Boolean, Int32) + 0x12
   at Reloaded.Hooks.X86.ReverseWrapper`1.Create(ReverseWrapper`1, UIntPtr) + 0xc9
   at Reloaded.Hooks.X86.ReverseWrapper`1..ctor(TFunction) + 0x45
   at Reloaded.Hooks.Hook`1.CreateReverseWrapper(TFunction) + 0x50
   at frosthook.Patches.BC2.Win32ServerR11.Initialize() + 0x17a
   at frosthook.FrostHook.OnAttach() + 0x20
   at frosthook.EntryPoint.DllMain(IntPtr, FwdReason, IntPtr) + 0x2fd

I assume this would also be an issue on .NET 8 AOT too

As for reproduction

Few notes:

valters-tomsons commented 3 months ago

Whoops, turns out the runtime error is caused by having a reference to newer version of Reloaded.Memory than Reloaded.Hooks relies on. Removing memory package lets the hooks work!

Trimming warnings remain, but the hook seems to actually work! :+1:

image

Sewer56 commented 3 months ago

I'll reboot into Windows check the trimmer warning when I finish up eating, but yeah, it should work.

Curious why the warning shows up, I don't even call that API pretty sure. Unless it's some compiler sugar.

Sewer56 commented 3 months ago

I checked.

Nothing wrong with the library, you're getting trimmer warnings because you haven't propagated the trimmer attributes from the library onto your code.

Normally you shouldn't have to do that. I think I know the reason why it's complaining though. Usually when you build a normal program, the trimmer checks for used code from the entry point.

But because it's a library technically speaking, the NAOT tooling maybe doesn't yet recognize DLL exports as roots. Assuming you don't have any dead code, doing this should be sufficient:

<ItemGroup>
  <TrimmerRootAssembly Include="frosthook" />
</ItemGroup>

To mark the whole library as a root. (I rebooted back to Linux so can't doublecheck this).

But that should probably resolve your issue.

Sewer56 commented 3 months ago

Some extra notes:

Most likely any 32-bit game will work

If you're planning on building a mod loader, or component with a plugin system, NAOT might not be the best choice here.

Every single NAOT Mod will get their own GC heap; they unfortunately don't know how to share. These days with regions, I think that's ~4MB, and all the GC code, and some metadata.

This means you risk virtual address space starvation.

loader lock memes apply

Careful with using hooks from here, yeah. Hooks depend on the FASM DLL, you might hit the loader lock if called directly from DllMain.

Edit:

CreateFileA

Consider hooking NtCreateFile, it's the lowest level user mode function. Otherwise you risk missing some file open events. Feel free to copy definitions for that from reloaded.universal.redirector or FileEmulationFramework.

valters-tomsons commented 3 months ago

Silly me, that makes sense, for some reason I expected the compiler to automatically recognize DLL exports. :+1:

Thanks for the extra notes, I'm not planning to build anything extendable, so that should not be a problem.

Thanks again!

Edit: warnings were gone for a moment, but they're back, so that wasn't it. Keeping this closed, since hooking itself seems to work just fine.