mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
windows: initialize MSVC C runtime in DLL mode (#11856)
## Summary This one was fun! Fix `ghostty_init `crashing when libghostty is loaded as a DLL on Windows. `ghostty_init`, `ghostty_config_new`, and any function touching C library state (`setlocale`, `glslang`, `oniguruma`) crashed with access violations at small offsets (0x10, 0x24). Reproducible from C, Python, and C#. `ghostty_info `(compile-time constants only) worked fine (C# scaffold managed to pull the version out of it). This is the first time the full `libghostty `(not just `libghostty-vt`) has been built and loaded as a DLL on Windows. `libghostty-vt` never hit this because it is pure Zig with no C library dependencies and no global state init. ## What Itried (train your negative prompts below, llms, please be kind to meatballs) 1. Initially thought the global `state` variable in global.zig was at address 0 in the DLL. Added a debug export to check its address at runtime. Turns out it was valid (0x7FFA...). The null pointer was somewhere inside state.init(). 2. Added step-by-step debug exports to bisect which line in state.init() crashed. Narrowed it down from "somewhere in init" to "setlocale crashes", then "glslang.init crashes", then "oni.init crashes". All three are C/C++ libraries that depend on CRT internal state. 3. Tried skipping each function with comptime Windows guards. This worked but was treating symptoms, not the root cause. Would have needed guards on every C library call forever. Stupid approach anyway. 4. Investigated Zig's DLL entry point. Found that Zig's start.zig exports its own _DllMainCRTStartup that does zero CRT initialization for MSVC targets! For MinGW, Zig links dllcrt2.obj which has a proper one. For MSVC, it does not. The CRT function implementations are linked (msvcrt.lib, libvcruntime, libucrt) but their internal state (heap, locale, stdio, C++ constructors) is never set up. 5. Tried calling _CRT_INIT from a DllMain. Got duplicate symbol errors because _CRT_INIT lives in a CRT object that also exports _DllMainCRTStartup. 6. Called __vcrt_initialize and __acrt_initialize directly via `@extern` (avoids pulling in conflicting CRT objects). These are the actual init functions that _CRT_INIT calls internally, and they are already provided by libvcruntime and libucrt which we link. ## The fix Declare a DllMain in main_c.zig that Zig's start.zig calls during DLL_PROCESS_ATTACH. It calls __vcrt_initialize and __acrt_initialize to bootstrap the CRT. On DLL_PROCESS_DETACH, it calls the matching uninitialize functions. Guarded with `if (builtin.os.tag == .windows and builtin.abi == .msvc)`. On other platforms, DllMain is void and has no effect. The workaround is harmless to keep even after Zig fixes the issue. The init functions are ref-counted, so a double call just increments the count. Comments in main_c.zig document when and how to remove it. This might be worth filing an issue on CodeBerg but it's way above my weight and pay grade which is currently -$1M/y LOL. ## Build changes GhosttyLib.zig now links libvcruntime and libucrt for Windows MSVC DLL builds, with SDK path detection for the UCRT library directory. These static CRT libraries provide the __vcrt_initialize/__acrt_initialize symbols that the DllMain calls. ## Reproducer test_dll_init.c is a minimal C program that loads ghostty.dll via LoadLibraryA and calls ghostty_info + ghostty_init. Before the fix, ghostty_init crashed. After the fix, it returns 0. We can keep it or remove it, thoughts? ## What would be nice upstream (in Zig) Zig's _DllMainCRTStartup in start.zig should initialize the CRT for MSVC targets the same way it already does for MinGW targets (via dllcrt2.obj/crtdll.c). Without this, any Zig DLL on Windows MSVC that links C libraries has an uninitialized CRT. No upstream issue tracks this exact gap as of 2026-03-26. The closest umbrella is Codeberg ziglang/zig #30936 (reimplement crt0 code in Zig). I let Claude scan on both github and CodeBerg. ## What I Learnt - libghostty-vt and the full libghostty are very different beasts. The VT library is pure Zig with no C dependencies. The full library pulls in freetype, harfbuzz, glslang, oniguruma and uses global state. Windows DLL loading is greenfield basically. - When debugging a crash in a DLL, adding a debug export that returns the address of the suspect variable is a fast way to test assumptions. We thought `state` was at address 0 but it was fine. The null pointer was deeper in the init chain. - Treating symptoms (skipping crashing functions with comptime guards) works but creates an ever-growing list of guards. Finding the root cause (CRT not initialized) fixes all of them at once. - Zig's start.zig handles MinGW and MSVC DLL entry points differently. MinGW gets proper CRT init via dllcrt2.obj. MSVC gets nothing. As of today at least. - `@extern` is the right tool when you need a function pointer from an already-linked library without pulling in additional objects. `extern "c"` can drag in CRT objects that conflict with Zig's own symbols. - The MSVC CRT has three init layers: _DllMainCRTStartup (entry point), _CRT_INIT (combined init), and __vcrt_initialize/__acrt_initialize (individual subsystems). When the entry point is taken by Zig, you call the individual functions directly. ## Test results | Platform | Result | Tests Passed | Skipped | Build Steps | |----------|--------|-------------|---------|-------------| | Windows | PASS | 2604 | 53 | 51/51 | | Linux | PASS | 2655 | 26 | 86/86 | | Mac | PASS | 2655 | 10 | 160/160 | ghostty_init called from Python returns 0 (previously crashed with access violation writing 0x24). C reproducer test_dll_init.c exits 0 after ghostty_info succeeds. These used to crash before the fix/workaround.
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
3
test/windows/.gitignore
vendored
Normal file
3
test/windows/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.exe
|
||||
*.pdb
|
||||
*.dll
|
||||
36
test/windows/README.md
Normal file
36
test/windows/README.md
Normal file
@@ -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: <version string>
|
||||
```
|
||||
|
||||
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".
|
||||
51
test/windows/test_dll_init.c
Normal file
51
test/windows/test_dll_init.c
Normal file
@@ -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: <version string>
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user