Skip to content

Instantly share code, notes, and snippets.

@aont
Last active October 26, 2025 11:03
Show Gist options
  • Select an option

  • Save aont/0aa173b5a317ff9eff0d100b846172ee to your computer and use it in GitHub Desktop.

Select an option

Save aont/0aa173b5a317ff9eff0d100b846172ee to your computer and use it in GitHub Desktop.

A Tiny CRT-Free Windows “Wrapper” Launcher (MSYS-friendly)

This article walks through a minimal Windows launcher executable—useful as a wrapper that forwards your process’s command line to a target program after adjusting environment variables (e.g., MSYS2/MSYS paths), while avoiding any dependency on the C/C++ runtime (CRT). The code uses only Win32 APIs and a hand-rolled zero-fill to keep the binary lean and linkable with -nostdlib.

What This Launcher Does

  • Skips argv[0]: Parses the raw process command line (GetCommandLineA) and returns a pointer to the first real argument. This means you can invoke the wrapper as wrapper.exe git status and have the child process receive just git status.

  • MSYS-centric PATH: Prepends a curated MSYS2/MSYS path segment to the existing PATH so tools like bash, perl, or git resolve as expected.

  • Environment hint: Sets MSYSTEM=MSYS to guide MSYS’s runtime behavior.

  • Ctrl-C handling: Installs a console control handler that ignores CTRL_C_EVENT and CTRL_BREAK_EVENT so the wrapper doesn’t die; the child process handles them.

  • No CRT required: Replaces typical helpers (memset, formatted I/O) with tiny custom routines:

    • ZeroBuffer writes zeros without pulling in the CRT.
    • PrintErrorMessage builds a compact ASCII error line and writes to STD_ERROR_HANDLE directly.
  • Spawns and mirrors exit code: Launches the child with CreateProcessA, waits with WaitForSingleObject, then returns the child’s exit code via ExitProcess.

Key Pieces Explained

1) Command-line shifting (ShiftCmdline)

Windows provides the full command line as a single string. To avoid CRT parsing, ShiftCmdline walks it character-by-character, respecting quotes and backslashes:

  • Tracks whether it’s inside a double-quoted segment.
  • Treats spaces/tabs outside quotes as delimiters.
  • When it finds the boundary after argv[0], it returns the pointer to the first “real” argument.
  • If there are no arguments, the program exits cleanly.

This behavior makes the wrapper transparent: whatever you pass after the wrapper’s name is forwarded unchanged (including proper quote semantics).

2) PATH construction (BuildNewPath)

The launcher constructs a new PATH:

  1. A fixed prefix of MSYS locations (e.g., C:\msys64\ucrt64\bin, C:\msys64\usr\bin, etc.).
  2. The current PATH (if present).

It allocates buffers with LocalAlloc, concatenates with lstrcpyA/lstrcatA, then updates the process environment via SetEnvironmentVariableA("PATH", ...).

3) Minimal setup and process creation

  • STARTUPINFOA and PROCESS_INFORMATION are zero-initialized using ZeroBuffer to avoid dragging in memset.
  • The child process is created by CreateProcessA(NULL, shiftedCmd, ...) so the child’s image is resolved from the new PATH.
  • Standard handles are inherited (bInheritHandles=TRUE) to keep I/O flowing naturally.
  • After the child exits, the launcher fetches and returns the child’s exit code.

4) Error reporting without CRT (PrintErrorMessage)

To keep the binary small, error messages are built by hand:

  • Appends " (Error NNN)" to a static buffer.
  • Writes directly with WriteFile to STD_ERROR_HANDLE.
  • Avoids printf, fprintf, or FormatMessage.

Building the Wrapper

Because there’s no CRT, you must provide your own entry point and link only against kernel32:

x86_64-w64-mingw32-g++ msys_launcher.cpp -nostdlib -fno-exceptions -fno-rtti \
  -Wl,-e,WinMainCRTStartup -lkernel32
  • -nostdlib cuts out CRT initialization.
  • -Wl,-e,WinMainCRTStartup sets the raw entry point to our function.
  • -fno-exceptions -fno-rtti keeps C++ overhead out (the code uses only C-style constructs).
  • -lkernel32 is enough for the Win32 APIs used here.

When to Use This Pattern

  • You need a tiny wrapper that adjusts environment or arguments and then launches another tool.
  • You’re operating in portable or constrained environments where pulling in the CRT is undesirable.
  • You want predictable quoting/escaping behavior that mirrors Windows’ native command line semantics.

Notes & Variations

  • Adjust the prefix list for different MSYS2 subsystems (e.g., mingw64\bin) or other toolchains.
  • If you want the wrapper to launch a specific program rather than rely on PATH lookup, pass that program as the lpApplicationName to CreateProcessA and keep shiftedCmd as the command line.
  • To forward signals differently, invert or customize IgnoreCtrlHandler.

That’s it—a compact, CRT-free launcher that makes MSYS-based workflows (and other toolchains) easier to bootstrap on Windows.

#include <windows.h>
// Minimal zero-fill to avoid memset
static void ZeroBuffer(void* p, SIZE_T n) {
BYTE* b = (BYTE*)p;
while (n--) *b++ = 0;
}
LPSTR ShiftCmdline(LPSTR cmdline) {
BOOL backslashPreceding = FALSE, insideDoubleQuote = FALSE, afterArgv0 = FALSE;
for (INT i = 0;; ++i) {
CHAR c = cmdline[i];
if (afterArgv0) {
if (c == ' ' || c == '\t') continue;
return cmdline + i;
} else if (insideDoubleQuote) {
if (c == '\\') backslashPreceding = !backslashPreceding;
else if (c == '"') insideDoubleQuote = backslashPreceding ? FALSE : !insideDoubleQuote;
else if (c == '\0') return cmdline + i;
} else {
if (c == '\\') backslashPreceding = !backslashPreceding;
else if (c == '"') insideDoubleQuote = TRUE;
else if (c == '\t' || c == ' ') afterArgv0 = TRUE;
else if (c == '\0') return cmdline + i;
}
}
}
BOOL WINAPI IgnoreCtrlHandler(DWORD type) {
return (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT);
}
LPSTR BuildNewPath() {
LPCSTR prefix =
"C:\\msys64\\ucrt64\\bin;"
"C:\\msys64\\usr\\local\\bin;"
"C:\\msys64\\usr\\bin;"
"C:\\msys64\\usr\\bin\\vendor_perl;"
"C:\\msys64\\usr\\bin\\core_perl;";
DWORD oldLen = GetEnvironmentVariableA("PATH", NULL, 0);
DWORD total = lstrlenA(prefix) + oldLen + 1;
LPSTR buf = (LPSTR)LocalAlloc(LMEM_FIXED, total);
if (!buf) return NULL;
lstrcpyA(buf, prefix);
if (oldLen > 1) {
LPSTR old = (LPSTR)LocalAlloc(LMEM_FIXED, oldLen);
if (old) {
GetEnvironmentVariableA("PATH", old, oldLen);
lstrcatA(buf, old);
LocalFree(old);
}
}
return buf;
}
VOID PrintErrorMessage(LPCSTR msg, DWORD code) {
CHAR out[128];
CHAR* p = out;
while (*msg) *p++ = *msg++;
*p++ = ' ';
*p++ = '('; *p++ = 'E'; *p++ = 'r'; *p++ = 'r'; *p++ = 'o'; *p++ = 'r'; *p++ = ' ';
CHAR num[16]; INT n = 0; DWORD t = code;
do { num[n++] = '0' + (t % 10); t /= 10; } while (t);
while (n--) *p++ = num[n];
*p++ = ')'; *p++ = '\r'; *p++ = '\n'; *p = 0;
DWORD w;
WriteFile(GetStdHandle(STD_ERROR_HANDLE), out, lstrlenA(out), &w, NULL);
}
extern "C" void __stdcall WinMainCRTStartup(void) {
LPSTR fullCmdline = GetCommandLineA();
LPSTR shiftedCmd = ShiftCmdline(fullCmdline);
if (!shiftedCmd || *shiftedCmd == '\0')
ExitProcess(0);
LPSTR newPath = BuildNewPath();
if (newPath) {
SetEnvironmentVariableA("PATH", newPath);
LocalFree(newPath);
}
SetEnvironmentVariableA("MSYSTEM", "MSYS");
SetConsoleCtrlHandler(IgnoreCtrlHandler, TRUE);
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroBuffer(&si, sizeof(si));
ZeroBuffer(&pi, sizeof(pi));
si.cb = sizeof(si);
if (!CreateProcessA(NULL, shiftedCmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
PrintErrorMessage("Failed to create process", GetLastError());
ExitProcess(1);
}
WaitForSingleObject(pi.hProcess, INFINITE);
SetConsoleCtrlHandler(IgnoreCtrlHandler, FALSE);
DWORD code = 0;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
ExitProcess(code);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment