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

@@ -14,7 +14,11 @@ const char* to_string(InjectStatus status)
case InjectStatus::OpenProcessFailed:
return "OpenProcess failed (try running as administrator)";
case InjectStatus::BitnessMismatch:
return "target is 32-bit; x86 hook not built yet";
return "target is 32-bit and the x86 hook/helper are unavailable";
case InjectStatus::HelperNotFound:
return "target is 32-bit; coop_inject_x86.exe / coop_hook_x86.dll not found next to the host";
case InjectStatus::HelperFailed:
return "the x86 injector helper failed (run the host as administrator?)";
case InjectStatus::DllNotFound:
return "coop_hook.dll not found next to the host";
case InjectStatus::AllocFailed:
@@ -49,6 +53,49 @@ bool is_wow64_process(HANDLE process)
return false; // be permissive if the query is unavailable
}
// Replace the trailing file name of `path` with `name` (same directory).
std::wstring sibling(const std::wstring& path, const wchar_t* name)
{
const std::size_t slash = path.find_last_of(L"\\/");
const std::wstring dir = (slash == std::wstring::npos) ? std::wstring() : path.substr(0, slash + 1);
return dir + name;
}
// Inject into a 32-bit (WOW64) target by spawning the x86 injector helper, which
// loads the x86 hook DLL from the same directory as the x64 dll_path.
InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
{
const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe");
const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll");
if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES ||
GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES)
{
return InjectResult{InjectStatus::HelperNotFound, 0};
}
// coop_inject_x86 <pid> "<x86_dll>"
std::wstring cmd = L"\"" + helper + L"\" " + std::to_wstring(pid) + L" \"" + x86_dll + L"\"";
STARTUPINFOW si{};
si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr,
&si, &pi))
{
return fail(InjectStatus::HelperFailed);
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exit_code = 1;
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
if (exit_code != 0)
{
return InjectResult{InjectStatus::HelperFailed, exit_code};
}
return InjectResult{InjectStatus::Ok, 0};
}
} // namespace
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
@@ -80,7 +127,8 @@ InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
if (is_wow64_process(process))
{
return InjectResult{InjectStatus::BitnessMismatch, 0};
// The x64 host can't inject a 32-bit target directly; delegate to the helper.
return inject_via_helper(pid, dll_path);
}
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);

View File

@@ -11,12 +11,14 @@ enum class InjectStatus
{
Ok,
OpenProcessFailed, // insufficient rights (try running the host as admin)
BitnessMismatch, // 32-bit target; needs the x86 hook (later phase)
BitnessMismatch, // 32-bit target; the x86 hook/helper aren't available
DllNotFound,
AllocFailed,
WriteFailed,
RemoteThreadFailed,
RemoteLoadFailed, // LoadLibraryW returned null inside the target
HelperNotFound, // 32-bit target but coop_inject_x86.exe / coop_hook_x86.dll missing
HelperFailed, // the x86 injector helper ran but reported failure
};
struct InjectResult
@@ -27,8 +29,9 @@ struct InjectResult
const char* to_string(InjectStatus status);
// Injects `dll_path` (absolute) into the process with `pid`. The host and DLL
// must match the target's bitness; 32-bit targets are rejected up front.
// Injects `dll_path` (absolute, the x64 coop_hook.dll) into `pid`. For a 32-bit
// (WOW64) target the x64 host can't inject directly, so it spawns the sibling
// coop_inject_x86.exe to load coop_hook_x86.dll (both expected next to dll_path).
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path);
} // namespace coop