Phase 3: x86 (32-bit) game support via injector helper

Drive a nested Win32 sub-build (CMake ExternalProject, re-entrant via
COOP_X86_HELPER_BUILD) from the normal x64 build to produce coop_hook_x86.dll and
a 32-bit coop_inject_x86.exe, staged next to the x64 binaries. The host detects a
WOW64 target with IsWow64Process2 and spawns the helper to load the x86 DLL, since
a 64-bit process can't cleanly inject a 32-bit one. The shared-memory IPC is
fixed-width / bitness-stable, so the x64 host and x86 hook interoperate.

Validated end-to-end against Slaps and Beans (32-bit D3D11): all 15 hooks
installed, heartbeat advancing, the Present hook engaged (shared a 1920x1080
backbuffer -- the real-game video-hook proof Phantom Brave's D3D9 couldn't give),
and status/audio/video/log IPC all crossed the x64<->x86 boundary. coop_audio_probe
now also delegates to the helper for WOW64 targets. All 5 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:42:42 +02:00
parent 36b861d167
commit b1f8783321
8 changed files with 261 additions and 15 deletions

View File

@@ -0,0 +1,82 @@
// coop_inject_x86 -- a 32-bit injector helper the (x64) host spawns to load the
// x86 hook DLL into a 32-bit (WOW64) game. A 64-bit process can't cleanly
// CreateRemoteThread(LoadLibraryW) into a 32-bit target (its LoadLibraryW lives
// in the 32-bit kernel32 at an address the 64-bit host doesn't have), so the
// host shells out to this same-bitness helper instead.
//
// coop_inject_x86 <pid> <dll_path>
//
// Exit code 0 = injected, 1 = failure, 2 = bad arguments.
#include <cstdio>
#include <string>
#include <windows.h>
namespace
{
int inject(unsigned long pid, const std::wstring& dll_path)
{
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
{
std::fprintf(stderr, "coop_inject_x86: dll not found: %ls\n", dll_path.c_str());
return 1;
}
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
PROCESS_VM_WRITE | PROCESS_VM_READ;
HANDLE process = OpenProcess(access, FALSE, pid);
if (process == nullptr)
{
std::fprintf(stderr, "coop_inject_x86: OpenProcess(%lu) failed (%lu)\n", pid, GetLastError());
return 1;
}
int result = 1;
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remote != nullptr && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr))
{
// In a 32-bit process kernel32 is mapped at the same base as in this 32-bit
// helper, so LoadLibraryW's address here is valid as the remote start routine.
auto load_library = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr);
if (thread != nullptr)
{
WaitForSingleObject(thread, INFINITE);
DWORD exit_code = 0;
GetExitCodeThread(thread, &exit_code);
CloseHandle(thread);
result = (exit_code != 0) ? 0 : 1; // LoadLibraryW returns the module handle
}
else
{
std::fprintf(stderr, "coop_inject_x86: CreateRemoteThread failed (%lu)\n", GetLastError());
}
}
if (remote != nullptr)
{
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
}
CloseHandle(process);
return result;
}
} // namespace
int wmain(int argc, wchar_t** argv)
{
if (argc < 3)
{
std::printf("usage: coop_inject_x86 <pid> <dll_path>\n");
return 2;
}
const unsigned long pid = std::wcstoul(argv[1], nullptr, 10);
if (pid == 0)
{
std::fprintf(stderr, "coop_inject_x86: invalid pid\n");
return 2;
}
return inject(pid, argv[2]);
}