From ca08ab861913b7c43bf0d5b21b2392fa1edc5438 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Fri, 27 Mar 2026 04:40:18 +0100 Subject: [PATCH] windows: simplify DLL init test and improve README --- src/main_c.zig | 93 ++++++++++++++++++------------------ test/windows/.gitignore | 3 ++ test/windows/README.md | 10 +++- test/windows/test_dll_init.c | 27 +++++------ 4 files changed, 70 insertions(+), 63 deletions(-) create mode 100644 test/windows/.gitignore diff --git a/src/main_c.zig b/src/main_c.zig index 2f9e45b5f..ef8d9ec7e 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -17,53 +17,6 @@ const state = &@import("global.zig").state; const apprt = @import("apprt.zig"); const internal_os = @import("os/main.zig"); -// On Windows, Zig's _DllMainCRTStartup does not initialize the MSVC C -// runtime when targeting MSVC ABI. Without initialization, any C library -// function that depends on CRT internal state (setlocale, malloc from C -// dependencies, C++ constructors in glslang) crashes with null pointer -// dereferences. Declaring DllMain causes Zig's start.zig to call it -// during DLL_PROCESS_ATTACH/DETACH, and we forward to the CRT bootstrap -// functions from libvcruntime and libucrt (already linked). -// -// This is a workaround. Zig handles MinGW DLLs correctly (via dllcrt2.obj) -// but not MSVC. No upstream issue tracks this exact gap as of 2026-03-26. -// Closest: Codeberg ziglang/zig #30936 (reimplement crt0 code). -// Remove this DllMain when Zig handles MSVC DLL CRT init natively. -pub const DllMain = if (builtin.os.tag == .windows and - builtin.abi == .msvc) struct -{ - const BOOL = std.os.windows.BOOL; - const HINSTANCE = std.os.windows.HINSTANCE; - const DWORD = std.os.windows.DWORD; - const LPVOID = std.os.windows.LPVOID; - const TRUE = std.os.windows.TRUE; - const FALSE = std.os.windows.FALSE; - - const DLL_PROCESS_ATTACH: DWORD = 1; - const DLL_PROCESS_DETACH: DWORD = 0; - - const __vcrt_initialize = @extern(*const fn () callconv(.c) c_int, .{ .name = "__vcrt_initialize" }); - const __vcrt_uninitialize = @extern(*const fn (c_int) callconv(.c) c_int, .{ .name = "__vcrt_uninitialize" }); - const __acrt_initialize = @extern(*const fn () callconv(.c) c_int, .{ .name = "__acrt_initialize" }); - const __acrt_uninitialize = @extern(*const fn (c_int) callconv(.c) c_int, .{ .name = "__acrt_uninitialize" }); - - pub fn handler(_: HINSTANCE, fdwReason: DWORD, _: LPVOID) callconv(.winapi) BOOL { - switch (fdwReason) { - DLL_PROCESS_ATTACH => { - if (__vcrt_initialize() < 0) return FALSE; - if (__acrt_initialize() < 0) return FALSE; - return TRUE; - }, - DLL_PROCESS_DETACH => { - _ = __acrt_uninitialize(1); - _ = __vcrt_uninitialize(1); - return TRUE; - }, - else => return TRUE, - } - } -}.handler else void; - // Some comptime assertions that our C API depends on. comptime { // We allow tests to reference this file because we unit test @@ -203,6 +156,52 @@ pub export fn ghostty_string_free(str: String) void { str.deinit(); } +// On Windows, Zig's _DllMainCRTStartup does not initialize the MSVC C +// runtime when targeting MSVC ABI. Without initialization, any C library +// function that depends on CRT internal state (setlocale, malloc from C +// dependencies, C++ constructors in glslang) crashes with null pointer +// dereferences. Declaring DllMain causes Zig's start.zig to call it +// during DLL_PROCESS_ATTACH/DETACH, and we forward to the CRT bootstrap +// functions from libvcruntime and libucrt (already linked). +// +// This is a workaround. Zig handles MinGW DLLs correctly (via dllcrt2.obj) +// but not MSVC. No upstream issue tracks this exact gap as of 2026-03-26. +// Closest: Codeberg ziglang/zig #30936 (reimplement crt0 code). +// Remove this DllMain when Zig handles MSVC DLL CRT init natively. +pub const DllMain = if (builtin.os.tag == .windows and + builtin.abi == .msvc) struct { + const BOOL = std.os.windows.BOOL; + const HINSTANCE = std.os.windows.HINSTANCE; + const DWORD = std.os.windows.DWORD; + const LPVOID = std.os.windows.LPVOID; + const TRUE = std.os.windows.TRUE; + const FALSE = std.os.windows.FALSE; + + const DLL_PROCESS_ATTACH: DWORD = 1; + const DLL_PROCESS_DETACH: DWORD = 0; + + const __vcrt_initialize = @extern(*const fn () callconv(.c) c_int, .{ .name = "__vcrt_initialize" }); + const __vcrt_uninitialize = @extern(*const fn (c_int) callconv(.c) c_int, .{ .name = "__vcrt_uninitialize" }); + const __acrt_initialize = @extern(*const fn () callconv(.c) c_int, .{ .name = "__acrt_initialize" }); + const __acrt_uninitialize = @extern(*const fn (c_int) callconv(.c) c_int, .{ .name = "__acrt_uninitialize" }); + + pub fn handler(_: HINSTANCE, fdwReason: DWORD, _: LPVOID) callconv(.winapi) BOOL { + switch (fdwReason) { + DLL_PROCESS_ATTACH => { + if (__vcrt_initialize() < 0) return FALSE; + if (__acrt_initialize() < 0) return FALSE; + return TRUE; + }, + DLL_PROCESS_DETACH => { + _ = __acrt_uninitialize(1); + _ = __vcrt_uninitialize(1); + return TRUE; + }, + else => return TRUE, + } + } +}.handler else void; + test "ghostty_string_s empty string" { const testing = std.testing; const empty_string = String.empty; diff --git a/test/windows/.gitignore b/test/windows/.gitignore new file mode 100644 index 000000000..5b32b4cb4 --- /dev/null +++ b/test/windows/.gitignore @@ -0,0 +1,3 @@ +*.exe +*.pdb +*.dll diff --git a/test/windows/README.md b/test/windows/README.md index ed0500fb0..d9047d841 100644 --- a/test/windows/README.md +++ b/test/windows/README.md @@ -10,12 +10,17 @@ runtime is properly initialized. ### Build +First build ghostty.dll, then compile the test: + ``` +zig build -Dapp-runtime=none -Demit-exe=false zig cc test_dll_init.c -o test_dll_init.exe -target native-native-msvc ``` ### Run +From this directory: + ``` copy ..\..\zig-out\lib\ghostty.dll . && test_dll_init.exe ``` @@ -24,5 +29,8 @@ Expected output (after the CRT fix): ``` ghostty_info: -ghostty_init: 0 ``` + +The ghostty_info call verifies the DLL loads and the CRT is initialized. +Before the fix, loading the DLL would crash with "access violation writing +0x0000000000000024". diff --git a/test/windows/test_dll_init.c b/test/windows/test_dll_init.c index ccbc49772..68363304f 100644 --- a/test/windows/test_dll_init.c +++ b/test/windows/test_dll_init.c @@ -2,16 +2,19 @@ * Minimal reproducer for the libghostty DLL CRT initialization issue. * * Before the fix (DllMain calling __vcrt_initialize / __acrt_initialize), - * ghostty_init crashed with "access violation writing 0x0000000000000024" - * because Zig's _DllMainCRTStartup does not initialize the MSVC C runtime - * for DLL targets. + * loading ghostty.dll and calling any function that touches the C runtime + * crashed with "access violation writing 0x0000000000000024" because Zig's + * _DllMainCRTStartup does not initialize the MSVC C runtime for DLL targets. + * + * This test loads the DLL and calls ghostty_info, which exercises the CRT + * (string handling, memory). If it returns a version string without + * crashing, the CRT is properly initialized. * * Build: zig cc test_dll_init.c -o test_dll_init.exe -target native-native-msvc * Run: copy ..\..\zig-out\lib\ghostty.dll . && test_dll_init.exe * * Expected output (after fix): * ghostty_info: - * ghostty_init: 0 */ #include @@ -24,7 +27,6 @@ typedef struct { } ghostty_info_s; typedef ghostty_info_s (*ghostty_info_fn)(void); -typedef int (*ghostty_init_fn)(size_t, char **); int main(void) { HMODULE dll = LoadLibraryA("ghostty.dll"); @@ -34,18 +36,13 @@ int main(void) { } ghostty_info_fn info_fn = (ghostty_info_fn)GetProcAddress(dll, "ghostty_info"); - if (info_fn) { - ghostty_info_s info = info_fn(); - fprintf(stderr, "ghostty_info: %.*s\n", (int)info.version_len, info.version); + if (!info_fn) { + fprintf(stderr, "GetProcAddress(ghostty_info) failed: %lu\n", GetLastError()); + return 1; } - ghostty_init_fn init_fn = (ghostty_init_fn)GetProcAddress(dll, "ghostty_init"); - if (init_fn) { - char *argv[] = {"ghostty"}; - int result = init_fn(1, argv); - fprintf(stderr, "ghostty_init: %d\n", result); - if (result != 0) return 1; - } + ghostty_info_s info = info_fn(); + fprintf(stderr, "ghostty_info: %.*s\n", (int)info.version_len, info.version); /* Skip FreeLibrary -- ghostty's global state cleanup and CRT * teardown ordering is not yet handled. The OS reclaims everything