Section: Loading a D L L is a coordinated operation.
When Windows loads a D L L, it does more than copy bytes. The loader maps P E, portable executable, sections, applies relocations when needed, resolves imports, prepares thread-local storage, updates module bookkeeping, and calls initialization entry points.
Other threads must not observe a half-initialized module. Windows serializes important parts of loading with a process-wide synchronization mechanism commonly called the loader lock.
That creates a special rule: Windows calls DllMain while the loader lock is held.
Section: Initialization is a dependency-order problem.
Each initialization step depends on other services already being ready. Import resolution depends on module loading; logging may depend on files, locks, or other DLLs; a hook may depend on the target module and worker state. DllMain runs inside the mechanism that is still satisfying some of those dependencies.
Alternative: Initialization is a dependency ladder: logging, workers, and hooks belong on steps whose supporting services are already ready.
Draw the order before writing startup code:
The arrows are dependency claims: a step may begin only after the step above it has completed.
Shutdown should reverse ownership, not merely reverse the list of function calls:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
An explicit lifecycle makes partially initialized states visible. If verification fails before a patch is installed, cleanup should not pretend the patch exists.
Section: What DllMain notifications mean.
The entry point can receive reasons such as:
DLL_PROCESS_ATTACH: the D L L is being attached to the process;
DLL_THREAD_ATTACH: a new thread is being announced to DLLs;
DLL_THREAD_DETACH: a thread is ending;
DLL_PROCESS_DETACH: the D L L or process is being unloaded.
These are loader notifications, not general application events. The code is running in the context of whichever thread caused the load or unload, and notifications are serialized.
Section: Why ordinary-looking work can deadlock.
Imagine two locks:
Neither thread can continue. The dangerous second lock may be hidden inside a function that loads a D L L, initializes COM, sends a blocking cross-thread message, starts complex runtime work, or waits for another thread.

The version that catches most game tools is subtler, because you never write the second lock yourself:
CreateThread returns success, so nothing appears to have gone wrong. The new thread then blocks before executing a single line of your code, and the wait never finishes. This is precisely why the shutdown sequence above joins worker threads outside the loader lock, and why starting real work belongs in an explicit start() that the host calls once loading has completed.
That is why DllMain should not:
load another D L L;
initialize COM;
create a window or call complex User32/Shell APIs;
wait for a worker thread;
acquire a lock also used by normal game code;
parse configuration, scan memory, install hooks, or run a hotkey loop;
perform heavy allocation or logging whose dependencies are unclear.
“It worked on my machine” does not prove the lock ordering is safe. Deadlocks depend on timing.
A deadlock needs a cycle of waits. Write each lock edge as “while holding A, this path may wait for B.” If another path can hold B and wait for A, no timeout makes the ordering correct. Remove the wait from the loader callback or establish one global lock order that every path follows.
Section: The course entry point stays tiny.
The course D L L does one documented optimization during process attach: it disables per-thread attach/detach notifications that the library does not use.
Code example. The code’s comments explain: 🛡️ SAFETY: instance is the module Windows is attaching now. This A P I, application programming interface, only disables notifications; it does not allocate, wait, or scan while the loader lock is held.
It does not start the game lab, read memory, scan modules, or wait.
Section: Stage two begins after loading finishes.
The D L L exports a separate function named gha_start. The injector waits for its LoadLibraryW thread to finish first. Only then does it call gha_start, after the initial loader work has returned.
In brief: Begin the D L L’s real work only after loading returns, using an explicit entry point with its own result.
The exported function uses the Windows thread-procedure A B I, application binary interface, because the course injector calls it as a separate thread entry:
Code example. The code’s comments explain: 🔒 Publish startup exactly once. A second injector call must not create a second worker that races over the same hooks and cleanup state. 🔁 Release ordering publishes the new run state before the worker observes it. 🧭 Complex work begins on this ordinary worker only after LoadLibraryW and DllMain have returned, so it is outside the loader-lock boundary.
OnceLock prevents duplicate startup. AtomicBool lets the exported stop function ask the worker loop to finish without blocking inside DllMain.
Section: Separate “request stop” from “wait until stopped”.
The stop export sets a flag and returns:
Code example. The code’s comments explain: 🧹 Request-only A P I: the worker owns restoration and exits at its next safe checkpoint. Waiting here could recreate the loader-lock deadlock.
The worker notices the flag, leaves its loop, releases any pressed input, and drops owned patches so their original bytes are restored. A loader callback should not wait for that worker while holding the loader lock.
Narration: A stop flag can be set quickly, but waiting for the worker inside a loader callback can trap both sides behind the loader lock.
Section: Why creating a thread in DllMain is still the wrong design here.
You may see examples that call CreateThread directly from DLL_PROCESS_ATTACH. The new thread cannot safely finish D L L-dependent initialization until loader notifications are complete. Waiting for it inside DllMain can deadlock, and returning immediately creates a fragile ordering problem.
The explicit gha_start contract is easier to reason about and test:
The original loader callback starts a worker and installs hooks during DLL_PROCESS_ATTACH. That puts initialization under the loader lock, where a worker that needs D L L loading can deadlock.
Before: start work during the loader callback
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Move that work to an explicit startup boundary. The injector waits for loading to finish, then calls gha_start; its result reports whether course initialization succeeded.
Improved code
Code example. The code’s comments explain: The injector calls this only after its LoadLibraryW thread has finished.
Show the diff
Code example. The code’s comments explain: The injector calls this only after its LoadLibraryW thread has finished.
Takeaway: finish loader notification before starting work that may depend on other DLLs.
Section: Why this version?.
The boundary is visible. A failure in LoadLibraryW is different from a failure in course initialization, and each stage returns its own result.
Section: Test the lifecycle, not just startup.
For each exact game D L L profile:
inject the exact 32-bit build;
verify gha_start returns zero;
enable one lab feature;
press End or call gha_stop;
confirm patches restore and input is released;
close the game normally;
repeat to catch one-run initialization assumptions.
If the game hangs while loading or unloading, inspect lock order and work performed by entry points before adding timeouts. A timeout hides a deadlock; it does not repair one.
The complete entry-point implementation is dll.rs, and the two-stage caller is injector.rs.
References: DllMain, dynamic-link library best practices, and preventing hangs.