hudhook

repository·main·Indexed 43 days ago

https://github.com/veeenu/hudhook

A Rust library for creating in-game overlays by hooking into graphics APIs on Windows and Wine/Proton. It supports DirectX 9, 11, 12, and OpenGL 3, providing a render loop for the Dear ImGui UI toolkit. Version 0.9.3 includes tools for DLL injection, proxy DLL creation, and the ImguiRenderLoop trait for custom UI rendering.

Tokens
9.7K
Snippets
40
Records
46
Agent score
56%

What's inside hudhook

  1. Overview of hudhook

    main

    hudhook is a Rust library designed for creating in-game overlays (similar to the Steam Overlay). It enables you to create injectable DLLs that hook into an application's rendering loop to draw a UI on top of the game. This allows developers to control application state or display information every time a frame is rendered.

    Supported Rendering Engines

    hudhook can target applications using:

    • DirectX 9
    • DirectX 11
    • DirectX 12
    • OpenGL 3

    UI Toolkit

    Currently, the only supported UI toolkit is dear imgui. Future support for egui is planned to provide a native Rust development experience.

  2. How hudhook hooks into rendering engines

    main
    hudhook works by detouring calls to specific rendering functions within the host application. For example, it intercepts calls to IDXGISwapChain::Present. By detouring these calls, hudhook can introduce custom logic and draw calls before yielding control back to the host application, ensuring the overlay is drawn on top of the current frame.
  3. Implement the ImguiRenderLoop trait

    main

    To create an overlay, you must define a struct and implement the ImguiRenderLoop trait. The render method provides access to a &mut imgui::Ui object, where you can define your UI elements using the Dear ImGui API.

    use hudhook::ImguiRenderLoop;
    
    pub struct MyRenderLoop;
    
    impl ImguiRenderLoop for MyRenderLoop {
        fn render(&mut self, ui: &mut imgui::Ui) {
            ui.window("My first render loop")
                .position([0., 0.], imgui::Condition::FirstUseEver)
                .size([320., 200.], imgui::Condition::FirstUseEver)
                .build(|| {
                    ui.text("Hello, hello!");
                });
        }
    }
  4. Use the hudhook! macro for a simple entry point

    main

    To allow your code to be executed upon injection in Windows, you must implement a DllMain entry point. The simplest way to do this is using the hudhook! macro, which handles the boilerplate for you.

    The macro requires two arguments:

    1. The type of hook you are targeting (e.g., ImguiDx12Hooks for DirectX 12 host applications).
    2. An instance of a struct that implements the ImguiRenderLoop trait.

    After adding the macro, build your library using cargo build --release to generate the .dll file.

    use hudhook::hooks::dx12::ImguiDx12Hooks;
    
    hudhook!(ImguiDx12Hooks, HelloHud::new());
  5. Roll your own DLL entry point with Hudhook::builder()

    main

    If you need to execute custom logic during library initialization or manage multiple hooks, you can manually implement DllMain instead of using the macro.

    To do this, use the Hudhook::builder() pattern. You should typically spawn a new thread within DLL_PROCESS_ATTACH to run the hook application logic to avoid blocking the loader lock.

    Key builder methods:

    • .with::<T>(instance): Registers a hook type T with a provided instance implementing ImguiRenderLoop.
    • .with_hmodule(hmodule): Provides the library's module handle.
    • .build(): Finalizes the builder into a Hudhook instance.
    • .apply(): Executes the hook application process.

    If .apply() returns an error, you can call eject() to remove the library.

    use hudhook::hooks::dx12::ImguiDx12Hooks;
    use hudhook::tracing::*;nuse hudhook::windows::Win32::Foundation::HINSTANCE;
    use hudhook::windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
    use hudhook::*;
    
    #[no_mangle]
    pub unsafe extern "system" fn DllMain(
        hmodule: HINSTANCE,
        reason: u32,
        _: *mut std::ffi::c_void,
    ) {
        if reason == DLL_PROCESS_ATTACH {
            trace!("DllMain()");
            let hmodule_raw = hmodule.0 as usize;
            std::thread::spawn(move || {
                let hmodule = HINSTANCE(hmodule_raw as _);
                if let Err(e) = Hudhook::builder()
                    .with::<ImguiDx12Hooks>(HelloHud::new())
                    .with_hmodule(hmodule)
                    .build()
                    .apply()
                {
                    error!("Couldn't apply hooks: {e:?}");
                    eject();
                }
            });
        }
    }
  6. Implement the ImguiRenderLoop trait for custom UI rendering

    main

    To display a UI using dear imgui within hudhook, you must implement the ImguiRenderLoop trait on your state structure. This trait requires a single method, render, which receives a mutable reference to an imgui::Ui object. hudhook calls this method every frame, providing the context needed to execute your rendering logic.

    use hudhook::ImguiRenderLoop;
    use imgui::*;
    
    // Your state structure
    struct HelloHud {
        start_time: std::time::Instant,
    }
    
    impl ImguiRenderLoop for HelloHud {
        fn render(&mut self, ui: &mut Ui) {
            ui.window("##hello")
                .size([320., 200.], Condition::Always)
                .build(|| {
                    ui.text("Hello, world!");
                    ui.text(format!("Elapsed: {:?}", self.start_time.elapsed()));
                });
        }
    }
  7. Build a proxy DLL for injection

    main

    DLL proxying is an injection technique where you place a custom DLL with a specific name (e.g., dinput8.dll) in the application's search path. When the application calls LoadLibrary, your DLL is loaded instead of the system version. To ensure the application continues to function, your DLL must export the same functions as the original and forward calls to the real system DLL.

    1. Configure Cargo.toml

    Set the crate type to cdylib and name the library after the DLL you are proxying.

    2. Define Exports

    Create an exports.def file listing the functions you need to export. This ensures the linker exports them correctly.

    Use a build.rs script to pass the .def file to the linker.

    4. Implement Function Forwarding

    In your Rust code:

    • Define a type for the original function signature.
    • Use a static LazyLock to load the real system DLL (e.g., from C:\Windows\System32\) and retrieve the address of the original function using GetProcAddress.
    • Implement the exported function with #[no_mangle] and the correct calling convention (e.g., extern "stdcall").
    • Inside the exported function, perform your custom logic (like calling patch()) and then call the original function pointer.

    5. Define DllMain

    Implement DllMain to initialize your state (e.g., by forcing the LazyLock) when the DLL is attached to the process.

    6. Compile

    Build the release version of your library.

    # Configure Cargo.toml
    [lib]
    name = "dinput8"
    crate-type = ["cdylib"]
    
    # Create exports.def
    EXPORTS
      DirectInput8Create
    
    # build.rs
    fn main() {
        println!("cargo:rustc-cdylib-link-arg=/DEF:exports.def");
    }
    
    # Build the DLL
    cargo build --release
  8. Set up a new hudhook library project

    main

    To create a library using hudhook, you must configure your Rust project as a Windows dynamically-linked library (DLL).

    1. Initialize a new Rust library project.
    2. Add hudhook and imgui as dependencies.
    3. Configure the crate-type in Cargo.toml to include cdylib.
    # Create the project
    cargo init --lib hello-hud
    cd hello-hud
    
    # Add dependencies
    cargo add hudhook@0.9 imgui@0.12

    Configure Cargo.toml

    [lib] crate-type = ["cdylib", "rlib"] name = "hello_hud"

  9. Inject a DLL into a process using the inject module

    main

    If you have the inject feature enabled, you can use the Process struct to inject your compiled DLL into a running application by its name.

    Example workflow:

    1. Locate the path to your compiled .dll.
    2. Use Process::by_name("AppName.exe") to find the target.
    3. Call .inject(path) on the process handle.
    use hudhook::inject::Process;
    
    fn main() {
        let mut cur_exe = std::env::current_exe().unwrap();
        cur_exe.push("..");
        cur_exe.push("libmyhook.dll");
    
        let cur_dll = cur_exe.canonicalize().unwrap();
    
        Process::by_name("MyTargetApplication.exe")
            .unwrap()
            .inject(cur_dll)
            .unwrap();
    }
  10. Implement function forwarding in a proxy DLL

    main

    To prevent the host application from crashing, your proxy DLL must intercept calls to the original function, execute your custom logic, and then pass the arguments to the real function located in the system DLL.

    Example implementation for DirectInput8Create:

    type FDirectInput8Create = unsafe extern "stdcall" fn(
        hinst: HINSTANCE,
        dwversion: u32,
        riidltf: *const GUID,
        ppvout: *mut *mut c_void,
        punkouter: HINSTANCE,
    ) -> HRESULT;
    
    struct State {
        directinput8create: FDirectInput8Create,
    }
    
    unsafe impl Send for State {}
    unsafe impl Sync for State {}
    
    static STATE: LazyLock<State> = LazyLock::new(|| unsafe {
        let dinput8 = LoadLibraryA(PCSTR(b"C:\\Windows\\System32\\dinput8.dll\0".as_ptr())).unwrap();
        let directinput8create = 
            std::mem::transmute(GetProcAddress(dinput8, PCSTR(b"DirectInput8Create\0".as_ptr())));
        
        State { directinput8create }
    });
    
    #[no_mangle]
    unsafe extern "stdcall" fn DirectInput8Create(
        hinst: HINSTANCE,
        dwversion: u32,
        riidltf: *const GUID,
        ppvout: *mut *mut c_void,
        punkouter: HINSTANCE,
    ) -> HRESULT {
        patch(); // Your custom logic
    
        (STATE.directinput8create)(hinst, dwversion, riidltf, ppvout, punkouter)
    }
    
    #[no_mangle]
    #[allow(non_snake_case, unused_variables)]
    extern "system" fn DllMain(dll_module: HINSTANCE, call_reason: u32, reserved: *mut c_void) -> BOOL {
        match call_reason {
            DLL_PROCESS_ATTACH => LazyLock::force(&STATE),
            DLL_PROCESS_DETACH => (),
            _ => (),
        }
        BOOL::from(true)
    }