Skip to content

[API Proposal]: Allow runtime-less methods as NativeAOT library entry points #116787

Closed
@TheLeftExit

Description

@TheLeftExit

Background and motivation

As many other users who asking to run things in DllMain under NativeAOT, I'm using NativeAOT to inject code into a game, and I very much want to stay within the .NET ecosystem and avoid writing a separate C++ library shim just for the injection bit.

From what I've read, NativeAOT doesn't support runtime initialization under loader lock. However, I've also seen samples of runtime-less code, which is limited, but still usable for simple tasks with enough boilerplate (https://github.com/MichalStrehovsky/zerosharp).

A minimal hook injection involves calling a few LibraryImport-ed Win32 APIs (GetModuleHandle, VirtualAlloc/Protect), taking an address of an UnmanagedCallersOnly function, and writing to some pointers. All of this is doable without a managed runtime, and it probably doesn't violate loader lock rules.

To facilitate this scenario while sticking to the rules imposed by NativeAOT and the OS, I'm proposing two attributes. The first:

  • Would apply to an UnmanagedCallersOnly method,
  • Would restrict the method's code to no-runtime-compatible operations and calls to other "runtimeless" methods,
  • Would compile the method directly from IL to bytecode (or minimize whatever thunks are usually inserted for reverse P/Invokes, to bring the code down to the bare metal as much as possible),
  • Would allow unmanaged callers to invoke this function directly, without initializing the runtime if it hasn't yet been initialized,

The second:

  • Would apply to a method marked with the first attribute,
  • Would only apply to a single, parameterless method in the module,
  • Would, allow insertion of a call to this method into what's currently a no-op DllMain.

API Proposal

namespace System.Runtime.InteropServices;

[AttributeUsage(AttributeTargets.Method)]
public sealed class UnmanagedMethod : Attribute;

[AttributeUsage(AttributeTargets.Method)]
public sealed class UnmanagedModuleInitializer : Attribute;

API Usage

public static unsafe class Example
{
    [DllImport("kernel32.dll")]
    private static extern uint GetModuleHandleW(char* moduleName);

    [DllImport("kernel32.dll")]
    private static extern uint VirtualProtect(uint address, uint size, uint newProtect, out uint oldProtect);

    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
    [UnmanagedMethod]
    [UnmanagedModuleInitializer]
    public static void OnStartup()
    {
        // Call to PeekMessageA in gta_sa.exe's main loop
        var originalInstructionPtr = GetModuleHandleW(null) + (0x748A57 - 0x400000);
        var newFunctionPtr = (uint)(delegate* unmanaged[Stdcall]<void*, nint, uint, uint, uint, int>)&NotPeekMessageA;
        VirtualProtect(originalInstructionPtr, 6, 0x04, out var oldProtect);
        *(byte*)originalInstructionPtr = 0xE8;
        *(uint*)(originalInstructionPtr + 1) = newFunctionPtr - (originalInstructionPtr + 5);
        VirtualProtect(originalInstructionPtr, 6, oldProtect, out _);

    }

    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
    private static int NotPeekMessageA(void* lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg)
    {
        return 0; // Install/proc custom synchronization context, then return from real PeekMessageA
    }
}

Alternative Designs

It might also be possible to reduce the number of attributes - for instance, add another parameter to UnmanagedCallersOnly instead of designating a separate UnmanagedMethod attribute.

Risks

Users will now be able to cause undefined behavior if their runtimeless code violates loader locks in other ways (e.g., LoadLibrary).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions