diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index 9ec9147fa..4e15fbbf4 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -94,6 +94,44 @@ pub fn initShared( }); _ = try deps.add(lib); + // On Windows with MSVC, building a DLL requires the full CRT library + // chain. linkLibC() (called via deps.add) provides msvcrt.lib, but + // that references symbols in vcruntime.lib and ucrt.lib. Zig's library + // search paths include the MSVC lib dir and the Windows SDK 'um' dir, + // but not the SDK 'ucrt' dir where ucrt.lib lives. + if (deps.config.target.result.os.tag == .windows and + deps.config.target.result.abi == .msvc) + { + // The CRT initialization code in msvcrt.lib calls __vcrt_initialize + // and __acrt_initialize, which are in the static CRT libraries. + lib.linkSystemLibrary("libvcruntime"); + + // ucrt.lib is in the Windows SDK 'ucrt' dir. Detect the SDK + // installation and add the UCRT library path. + const arch = deps.config.target.result.cpu.arch; + const sdk = std.zig.WindowsSdk.find(b.allocator, arch) catch null; + if (sdk) |s| { + if (s.windows10sdk) |w10| { + const arch_str: []const u8 = switch (arch) { + .x86_64 => "x64", + .x86 => "x86", + .aarch64 => "arm64", + else => "x64", + }; + const ucrt_lib_path = std.fmt.allocPrint( + b.allocator, + "{s}\\Lib\\{s}\\ucrt\\{s}", + .{ w10.path, w10.version, arch_str }, + ) catch null; + + if (ucrt_lib_path) |path| { + lib.addLibraryPath(.{ .cwd_relative = path }); + } + } + } + lib.linkSystemLibrary("libucrt"); + } + // Get our debug symbols const dsymutil: ?std.Build.LazyPath = dsymutil: { if (!deps.config.target.result.os.tag.isDarwin()) { diff --git a/src/main_c.zig b/src/main_c.zig index 9d48f376d..ef8d9ec7e 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -156,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 new file mode 100644 index 000000000..d9047d841 --- /dev/null +++ b/test/windows/README.md @@ -0,0 +1,36 @@ +# Windows Tests + +Manual test programs for Windows-specific functionality. + +## test_dll_init.c + +Regression test for the DLL CRT initialization fix. Loads ghostty.dll +at runtime and calls ghostty_info + ghostty_init to verify the MSVC C +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 +``` + +Expected output (after the CRT fix): + +``` +ghostty_info: +``` + +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 new file mode 100644 index 000000000..68363304f --- /dev/null +++ b/test/windows/test_dll_init.c @@ -0,0 +1,51 @@ +/* + * Minimal reproducer for the libghostty DLL CRT initialization issue. + * + * Before the fix (DllMain calling __vcrt_initialize / __acrt_initialize), + * 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: + */ + +#include +#include + +typedef struct { + int build_mode; + const char *version; + size_t version_len; +} ghostty_info_s; + +typedef ghostty_info_s (*ghostty_info_fn)(void); + +int main(void) { + HMODULE dll = LoadLibraryA("ghostty.dll"); + if (!dll) { + fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError()); + return 1; + } + + ghostty_info_fn info_fn = (ghostty_info_fn)GetProcAddress(dll, "ghostty_info"); + if (!info_fn) { + fprintf(stderr, "GetProcAddress(ghostty_info) failed: %lu\n", GetLastError()); + 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 + * on process exit. */ + return 0; +}