From f764b1646575cd42fc6c9c0e73f0d238a052d1ec Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Thu, 26 Mar 2026 09:27:34 +0100 Subject: [PATCH] windows: add DLL init regression tests and probe C# test suite and C reproducer validating DLL initialization. The probe test (DllMainWorkaround_IsStillActive) checks that the CRT workaround is compiled in via ghostty_crt_workaround_active(). When Zig fixes MSVC DLL CRT init, removing the DllMain will make this test fail with instructions on how to verify the fix and clean up. ghostty_init is tested via the C reproducer (test_dll_init.c) rather than C# because the global state teardown crashes the test host on DLL unload. The C reproducer exits without FreeLibrary. --- .github/workflows/test.yml | 6 ++ src/main_c.zig | 13 +++ windows/Ghostty.Tests/Ghostty.Tests.csproj | 30 ++++++ windows/Ghostty.Tests/LibghosttyInitTests.cs | 98 ++++++++++++++++++++ windows/Ghostty.Tests/test_dll_init.c | 54 +++++++++++ 5 files changed, 201 insertions(+) create mode 100644 windows/Ghostty.Tests/Ghostty.Tests.csproj create mode 100644 windows/Ghostty.Tests/LibghosttyInitTests.cs create mode 100644 windows/Ghostty.Tests/test_dll_init.c diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24784a085..0ff3a84ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1128,6 +1128,12 @@ jobs: - name: Test run: zig build -Dapp-runtime=none test + - name: Build ghostty.dll + run: zig build -Dapp-runtime=none -Demit-exe=false + + - name: .NET interop tests + run: dotnet test windows/Ghostty.Tests/Ghostty.Tests.csproj + test-i18n: strategy: fail-fast: false diff --git a/src/main_c.zig b/src/main_c.zig index 953e1f4ec..a131de255 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -24,6 +24,11 @@ const internal_os = @import("os/main.zig"); // 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 { @@ -59,6 +64,14 @@ pub const DllMain = if (builtin.os.tag == .windows and } }.handler else void; +// Probe export: returns 1 when the DllMain CRT workaround above is +// compiled in. The C# test suite checks for this symbol to detect when +// the workaround becomes redundant (when Zig fixes MSVC DLL CRT init). +// Remove this along with the DllMain block above. +pub export fn ghostty_crt_workaround_active() callconv(.c) c_int { + return if (builtin.os.tag == .windows and builtin.abi == .msvc) 1 else 0; +} + // Some comptime assertions that our C API depends on. comptime { // We allow tests to reference this file because we unit test diff --git a/windows/Ghostty.Tests/Ghostty.Tests.csproj b/windows/Ghostty.Tests/Ghostty.Tests.csproj new file mode 100644 index 000000000..632c025b9 --- /dev/null +++ b/windows/Ghostty.Tests/Ghostty.Tests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + latest + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/windows/Ghostty.Tests/LibghosttyInitTests.cs b/windows/Ghostty.Tests/LibghosttyInitTests.cs new file mode 100644 index 000000000..e57e13d55 --- /dev/null +++ b/windows/Ghostty.Tests/LibghosttyInitTests.cs @@ -0,0 +1,98 @@ +using System.Runtime.InteropServices; + +[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling] + +namespace Ghostty.Tests; + +/// +/// Tests that validate libghostty DLL initialization on Windows. +/// +/// Ghostty's main_c.zig declares a DllMain that calls __vcrt_initialize +/// and __acrt_initialize because Zig's _DllMainCRTStartup does not +/// initialize the MSVC C runtime for DLL targets. Without this, any C +/// library function (setlocale, glslang, oniguruma) crashes. +/// +/// See the probe test at the bottom for workaround tracking. +/// +[TestClass] +public partial class LibghosttyInitTests +{ + private const string LibName = "ghostty"; + + [StructLayout(LayoutKind.Sequential)] + private struct GhosttyInfo + { + public int BuildMode; + public nint Version; + public nuint VersionLen; + } + + [LibraryImport(LibName, EntryPoint = "ghostty_info")] + private static partial GhosttyInfo GhosttyInfoNative(); + + [LibraryImport(LibName, EntryPoint = "ghostty_crt_workaround_active")] + private static partial int GhosttyWorkaroundActive(); + + [TestMethod] + public void GhosttyInfo_Works() + { + // Baseline: ghostty_info uses only compile-time constants and + // does not depend on CRT state. This should always work. + var info = GhosttyInfoNative(); + + Assert.IsGreaterThan((nuint)0, info.VersionLen); + Assert.AreNotEqual(nint.Zero, info.Version); + } + + // NOTE: ghostty_init is validated by the C reproducer (test_dll_init.c) + // rather than a C# test because ghostty_init initializes global state + // (glslang, oniguruma, allocators) that crashes during test host + // teardown when the DLL is unloaded. The C reproducer handles this by + // exiting without FreeLibrary. The DLL unload ordering issue is + // separate from the CRT init fix. + + /// + /// PROBE TEST: Detects when our DllMain CRT workaround in main_c.zig + /// is removed, which should happen when Zig fixes _DllMainCRTStartup + /// for MSVC DLL targets. + /// + /// HOW IT WORKS: + /// ghostty_crt_workaround_active() returns 1 when the workaround is + /// compiled in (Windows MSVC), 0 on other platforms. This test + /// asserts that it returns 1. When it returns 0, the workaround was + /// removed. + /// + /// WHEN THIS TEST FAILS: + /// Someone removed the DllMain workaround from main_c.zig. + /// This is the expected outcome when Zig fixes the issue. + /// + /// Step 1: Run test_dll_init.c (the C reproducer) WITHOUT the + /// DllMain workaround. If ghostty_init returns 0, Zig + /// fixed it. Delete the DllMain block in main_c.zig, + /// ghostty_crt_workaround_active(), and this probe test. + /// + /// Step 2: If ghostty_init still crashes without the workaround, + /// restore the DllMain block in main_c.zig. + /// + /// Step 3: To unblock CI while investigating, skip this test: + /// dotnet test --filter "FullyQualifiedName!=Ghostty.Tests.LibghosttyInitTests.DllMainWorkaround_IsStillActive" + /// + /// UPSTREAM TRACKING (as of 2026-03-26): + /// No Zig issue tracks this exact gap. + /// Closest: Codeberg ziglang/zig #30936 (reimplement crt0 code). + /// Related GitHub issues: 7065, 11285, 19672 (link-time, not runtime). + /// + [TestMethod] + public void DllMainWorkaround_IsStillActive() + { + var active = GhosttyWorkaroundActive(); + Assert.AreEqual(1, active, + "ghostty_crt_workaround_active() returned 0. " + + "The DllMain CRT workaround in main_c.zig was removed or disabled. " + + "Run test_dll_init.c without the workaround to check if Zig fixed " + + "the issue. If ghostty_init works, delete the DllMain and this test. " + + "If it crashes, restore the DllMain. " + + "To skip this test: dotnet test --filter " + + "\"FullyQualifiedName!=Ghostty.Tests.LibghosttyInitTests.DllMainWorkaround_IsStillActive\""); + } +} diff --git a/windows/Ghostty.Tests/test_dll_init.c b/windows/Ghostty.Tests/test_dll_init.c new file mode 100644 index 000000000..ccbc49772 --- /dev/null +++ b/windows/Ghostty.Tests/test_dll_init.c @@ -0,0 +1,54 @@ +/* + * 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. + * + * 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 +#include + +typedef struct { + int build_mode; + const char *version; + size_t version_len; +} 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"); + 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) { + ghostty_info_s info = info_fn(); + fprintf(stderr, "ghostty_info: %.*s\n", (int)info.version_len, info.version); + } + + 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; + } + + /* Skip FreeLibrary -- ghostty's global state cleanup and CRT + * teardown ordering is not yet handled. The OS reclaims everything + * on process exit. */ + return 0; +}