Skip to main content

Crate lazy_importer

Crate lazy_importer 

Source
Expand description

§lazy_importer

A Rust rewrite of Justas Masiulis’s original C++ lazy_importer with enhancements.

Rust License crates.io docs.rs

A no_std Rust library that resolves Windows API functions at runtime — without linking to import libraries. It walks the PEB loader list and PE export tables directly, so function names never appear in your binary’s import table.

§Quick Start

# Cargo.toml
[dependencies]
lazy_importer = "0.1"
// 1. Declare the function signature
type MessageBoxA = unsafe extern "system" fn(
    hwnd: *mut core::ffi::c_void,
    text: *const u8,
    caption: *const u8,
    utype: u32,
) -> i32;

// 2. Resolve + call — nothing appears in the import table
let msgbox: MessageBoxA = unsafe {
    lazy_importer::li_fn!("MessageBoxA")
        .get::<MessageBoxA>()
        .expect("MessageBoxA should resolve")
};

unsafe {
    msgbox(
        core::ptr::null_mut(),
        b"Hello\0".as_ptr(),
        b"lazy_importer\0".as_ptr(),
        0,
    );
}

§Usage

§Resolve a function globally

li_fn! takes a string literal. get casts the resolved address to your function pointer type and follows forwarded exports.

type GetCurrentProcessId = unsafe extern "system" fn() -> u32;

let f: GetCurrentProcessId = unsafe {
    lazy_importer::li_fn!("GetCurrentProcessId")
        .get::<GetCurrentProcessId>()
        .expect("GetCurrentProcessId should resolve")
};

Use .address() when you only need the raw pointer (still follows forwarded exports):

let addr = lazy_importer::li_fn!("GetCurrentProcessId")
    .address()
    .expect("GetCurrentProcessId should resolve");

§Resolve without following forwarded exports

raw_address returns the export RVA directly — forwarded exports are not resolved to their final target.

let raw = lazy_importer::li_fn!("GetCurrentProcessId").raw_address();

§Resolve a loaded module

let kernel32 = lazy_importer::li_module!("KERNEL32.DLL")
    .get()
    .expect("kernel32 should be loaded");

// Use the handle directly
use lazy_importer::ModuleHandle;
let handle: ModuleHandle = kernel32;
let ptr: *mut core::ffi::c_void = handle.as_ptr();

§Resolve inside a known module

When you already have a ModuleHandle, resolve the export in that specific module. address_in follows forwarded exports; raw_address_in does not.

let kernel32 = lazy_importer::li_module!("KERNEL32.DLL")
    .get()
    .expect("kernel32 should be loaded");

// With forwarded-export resolution
let addr = lazy_importer::li_fn!("GetCurrentProcessId")
    .address_in(kernel32)
    .expect("export should resolve in kernel32");

// Raw — no forwarded-export resolution
let raw = lazy_importer::li_fn!("GetCurrentProcessId")
    .raw_address_in(kernel32)
    .expect("export should exist in kernel32");

// Cast to a function pointer type in one step
type GetCurrentProcessId = unsafe extern "system" fn() -> u32;
let f: GetCurrentProcessId = unsafe {
    lazy_importer::li_fn!("GetCurrentProcessId")
        .get_in::<GetCurrentProcessId>(kernel32)
        .expect("export should resolve in kernel32")
};

§Caching

By default every lookup walks the PEB again. Chain .cached() to store the first successful result in a process-wide lock-free table, keyed by the name you passed to the macro.

// Module cache — first .get() resolves; subsequent calls hit the cache
let kernel32 = lazy_importer::li_module!("KERNEL32.DLL")
    .cached()
    .get()
    .expect("kernel32 should be loaded");

// Function cache — works for .address() and .get()
type GetCurrentProcessId = unsafe extern "system" fn() -> u32;
let f: GetCurrentProcessId = unsafe {
    lazy_importer::li_fn!("GetCurrentProcessId")
        .cached()
        .get::<GetCurrentProcessId>()
        .expect("GetCurrentProcessId should resolve")
};
  • Modules and functions use separate caches.
  • Cache keys are derived from the name at compile time; the cache stores only resolved pointers, never strings.
  • The cache is a fixed-size no_std table (256 slots, linear probing). If full, the lookup still succeeds — it just isn’t stored.
  • raw_address and raw_address_in never use the cache.

§Case-insensitive hashing

Enable the case-insensitive feature to fold ASCII case when hashing module and export names:

[dependencies]
lazy_importer = { version = "0.1", features = ["case-insensitive"] }
// With case-insensitive enabled, both resolve to the same module
let a = lazy_importer::li_module!("KERNEL32.DLL").cached().get();
let b = lazy_importer::li_module!("kernel32.dll").cached().get();
assert_eq!(a.unwrap().as_ptr(), b.unwrap().as_ptr());

§API Reference

§Macros

MacroReturnsDescription
li_fn!("Name")LazyFunction<HASH>Resolves the named export across all loaded modules
li_module!("NAME")LazyModule<HASH>Resolves the named module in the PEB loader list

§LazyFunction<OHP>

MethodReturnsFollows forwarded?Uses cache?
.address()Option<NonNull<c_void>>YesIf .cached()
.raw_address()Option<NonNull<c_void>>NoNever
.get::<F>()Option<F>YesIf .cached()
.address_in(m)Option<NonNull<c_void>>YesNever
.raw_address_in(m)Option<NonNull<c_void>>NoNever
.get_in::<F>(m)Option<F>YesNever
.cached()SelfEnables global cache

§LazyModule<OHP>

MethodReturnsUses cache?
.get()Option<ModuleHandle>If .cached()
.cached()SelfEnables global cache

§ModuleHandle

MethodReturns
.from_ptr(p)Option<Self>
.as_ptr()*mut c_void

§How It Works

┌─────────────────────────────────────────────────┐
│                 Your Process                      │
│                                                   │
│  li_fn!("VirtualAlloc").get::<F>()                │
│  ├─ hash("VirtualAlloc") at compile time          │
│  ├─ walk PEB → InLoadOrderModuleList              │
│  │   ├─ kernel32.dll → parse PE export directory  │
│  │   ├─ ntdll.dll    → parse PE export directory  │
│  │   └─ ...                                       │
│  ├─ compare export name hashes                    │
│  ├─ follow forwarded exports (up to 32 hops)      │
│  ├─ resolve API-set contracts (api-ms-win-*)      │
│  └─ return function pointer                       │
│                                                   │
│  • No LoadLibrary / GetProcAddress calls          │
│  • No entries in the binary's import table        │
│  • Compile-time hash offsets (const-random)        │
│    randomize the FNV-1a seed per call site        │
└─────────────────────────────────────────────────┘

§Platform Support

ArchitectureStatus
x86_64-pc-windows-msvc✅ Full support
i686-pc-windows-msvc✅ Full support
aarch64-pc-windows-msvc✅ Full support
Non-Windows❌ Compile-time error

Requires Rust 1.85+ (edition 2024).

§Building

Release builds enable LTO (lto = true) so that resolution code is inlined directly into call sites — no separate function shows up in the disassembly for li_fn! or li_module! lookups.

cargo build --release

Set CONST_RANDOM_SEED for deterministic (reproducible) builds:

CONST_RANDOM_SEED=0xDEAD_BEEF cargo build --release

§Testing

# Unit tests (requires Windows)
cargo test

# With case-insensitive feature
cargo test --features case-insensitive --test '*' --lib

§License

Apache 2.0 — see LICENSE.

Macros§

li_fn
li_module

Structs§

LazyFunction
LazyModule
ModuleHandle