mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
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.
This commit is contained in:
committed by
Mitchell Hashimoto
parent
a0785710bb
commit
f764b16465
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
30
windows/Ghostty.Tests/Ghostty.Tests.csproj
Normal file
30
windows/Ghostty.Tests/Ghostty.Tests.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" Version="4.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Copy ghostty.dll from the Zig build output so P/Invoke can find it.
|
||||
Build libghostty first: zig build -Dapp-runtime=none -Demit-exe=false
|
||||
-->
|
||||
<Target Name="CopyGhosttyDll" AfterTargets="Build"
|
||||
Condition="Exists('$(MSBuildThisFileDirectory)..\..\zig-out\lib\ghostty.dll')">
|
||||
<Copy SourceFiles="$(MSBuildThisFileDirectory)..\..\zig-out\lib\ghostty.dll"
|
||||
DestinationFolder="$(OutputPath)"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
98
windows/Ghostty.Tests/LibghosttyInitTests.cs
Normal file
98
windows/Ghostty.Tests/LibghosttyInitTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
|
||||
|
||||
namespace Ghostty.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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.
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[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\"");
|
||||
}
|
||||
}
|
||||
54
windows/Ghostty.Tests/test_dll_init.c
Normal file
54
windows/Ghostty.Tests/test_dll_init.c
Normal file
@@ -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: <version string>
|
||||
* ghostty_init: 0
|
||||
*/
|
||||
|
||||
#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);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user