Section: External versus in-process.
An external tool uses Windows to cross a process boundary. An in-process library is loaded into the target and shares its virtual address space.
In brief: Sharing the game’s address space removes a copying boundary, but object lifetimes and valid function signatures still matter.
The previous lesson reconstructed a D L L, dynamic link library, function’s contract: how arguments arrive, what the result means, and who owns pointed-to data. The preceding chapter showed how code and imports become a running image. Here we create the other side of that boundary: a Rust library loaded as a guest inside one exact game build. We will follow its loading and cleanup, learn how detours and import hooks redirect calls, and give the tool input and menus. The chapter closes by putting those pieces into an architecture with one owner for each resource and an explicit cleanup path.
The two routes reach the same bytes through different doors:
That makes pointer access direct, but not automatically safe. An address discovered in a debugger can still be null, stale, misaligned, or wrong for this game version.
Sharing an address space removes one copying boundary; it does not create shared ownership. The game still owns its objects, decides when they move or disappear, and may update them from other threads. Your D L L is a guest that can observe an address only while the assumptions that justified it remain true.

Before dereferencing, state the contract in four parts:
which exact build and game state define the layout;
which object owns the bytes;
which thread may change or destroy them;
how long this particular pointer remains valid.
Resolve short-lived pointers near the point of use. Copy the few fields you need into a local snapshot, validate them, then release the borrowed view of game memory.
The safe path has distinct stages instead of one giant startup function:
Each stage earns the capabilities used by the next one, and shutdown releases them in a controlled order.
Section: Create a D L L project.
The first artifact is a library that Windows can load, not a standalone executable with a main function. Start with Cargo’s library template; the following configuration then asks Rust to emit a C-compatible dynamic library and includes the Windows types used at its boundary:
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Configure Cargo:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
cdylib tells the compiler to build a Windows D L L with a C-compatible boundary.
Section: Keep DllMain tiny.
Windows calls DllMain while holding the loader lock. Many operations are unsafe there: starting complicated work, loading other DLLs, waiting on threads, or calling code that might do those things.
Code example. The code’s comments explain: 🛡️ SAFETY: Windows supplied the module that it is currently attaching.
Normally the compiler mangles a function’s name so it can tell apart two functions with the same short name in different modules. #[no_mangle] turns that off and exports the bare name DllMain instead. If another object linked into the same D L L also exports a symbol named DllMain, or if some other code calls this export assuming a different signature, the linker or loader cannot catch the mismatch the way Rust’s type checker normally would. Rust 2024 marks #[unsafe(no_mangle)] to make that unchecked, program-wide promise visible at the call site instead of leaving it implicit.
Section: Export an explicit start function.
Let the host or lab loader call a separate function after loading:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
This separates loading from initialization and prevents accidental double-starts.
Alternative: Separate opening the workshop from starting its machinery: loading makes the D L L available, while the explicit start function begins its work.
Section: Read known in-process data.
Wrap a verified field rather than exposing raw pointers everywhere:
Code example. The code’s comments explain: / # Safety / The address must refer to a live, aligned `u32` gold field in the / documented target build for the duration of this call. 🛡️ SAFETY: upheld by the caller after version and pointer-chain checks.
An even better design resolves the object each time from a module-relative path and checks readable regions before dereferencing.
Section: Make the Wesnoth gold D L L actually change gold.
For Wesnoth 1.14.9, 32-bit Windows, use the same chain as the external tool. Only the two offsets and the root address are fixed; the two pointer values in between are whatever Wesnoth allocated this run:
The resolver below reads both live pointer values in that order. The constants identify the known root and field offsets; the returned gold pointer belongs only to the current verified match, and the worker uses it for one write.
Code example. The code’s comments explain: / Resolves the live gold pointer in the documented Wesnoth build. / # Safety / The caller must first verify the exact 32-bit target build and that a / local match is active. Each intermediate pointer must be readable. 🛡️ SAFETY: target/build/state checks are the caller's responsibility. 🛡️ SAFETY: the verified player object contains a 32-bit game pointer here. 🛡️ SAFETY: `gha_start` enables this only after the build and match checks. ✅ SAFETY: the resolver returned the live, aligned four-byte gold field.
Call set_wesnoth_gold(999) from the worker started by gha_start. Return to the local match and trigger a UI refresh. If no match is active, the function must return an error instead of dereferencing zero.
This is a real internal memory hack: the D L L shares Wesnoth’s address space and writes the live gold field directly. The three raw dereferences are isolated and explained instead of being scattered through the thread loop.
Section: Load it and call the exported start function.
The matching injector performs two remote calls. First it runs LoadLibraryW in Wesnoth. Then it maps the same D L L locally without running it, finds the relative offset of gha_start, adds that offset to the D L L base returned by Wesnoth, and starts a second remote thread at that export.
That second step is important. LoadLibraryW returning means Windows finished the loader-lock portion of loading. Only then does gha_start create the worker. The complete implementation is in injector.rs, and the D L L entry points are in dll.rs.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
With a local match already active, the worker resolves the chain and writes 999. This is the proof for the lesson: the injected library has executed inside Wesnoth and changed a live, spendable game value.
Section: Do not hide the target assumptions.
Put version-specific facts together:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
At startup:
confirm the module exists;
confirm the supported build fingerprint;
resolve the pointer path;
check each pointer;
only then enable the lab feature.
Section: Unloading is a protocol.
Background threads must stop before the D L L unloads. Use an atomic stop flag or a channel, wait for workers to exit from outside DllMain, restore patches, and then unload.
Narration: If a worker returns into D L L code after that D L L unloads, even a correctly calculated feature can crash the host.
The important lesson is architectural: loading, running, stopping, and unloading are separate states. Model them deliberately instead of relying on global booleans and timing luck.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The type system cannot make an unknown game address safe, but it can make the rest of the lifecycle much harder to mix up.
Lifecycle states should control capabilities. Loaded may allow only bookkeeping, Running may own workers and patches, Stopping may request cleanup but reject new work, and Stopped must own nothing that points into executable code. This is stronger than a single started: bool, which cannot distinguish “never started” from “cleanup still in progress.”