build: fix C++ linking and enum signedness on MSVC (#11812)

> [!WARNING]
> Review/approve this AFTER #11807 and #11810 (this PR includes their
commits)

## Summary

### **And `run test ghostty-test` finally runs on Windows! 🎉almost
there!**

- Skip `linkLibCpp()` on MSVC for dcimgui, spirv-cross, and harfbuzz
(same fix already applied upstream to highway, simdutf, utfcpp, glslang,
SharedDeps, GhosttyZig)
- Fix freetype C enum signedness: MSVC translates C enums as signed
`int`, while GCC/Clang uses unsigned `int`. Add `@intCast` at call sites
and `@bitCast` for bit-shift operations on glyph format tags.

## Context
Zig unconditionally passes `-nostdinc++` and adds its bundled
libc++/libc++abi include paths, which conflict with MSVC's own C++
runtime headers. The MSVC SDK directories (added via `linkLibC`) already
contain both C and C++ headers, so `linkLibCpp` is not needed.

The freetype enum issue is a different facet of the same MSVC vs
GCC/Clang divide: `FT_Render_Mode` and `FT_Glyph_Format` are C enums
that get different signedness on different compilers.

## Stack
Stacked on 015-windows/fix-ssize-t-msvc.

## Test plan

### Cross-platform results (`zig build test` / `zig build
-Dapp-runtime=none test` on Windows)

| | Windows | Linux | Mac |
|---|---|---|---|
| **BEFORE** (015, a35f84db3) | FAIL - 48/51, 1 failed (compile
ghostty-test) | PASS - 86/86, 2655/2678, 23 skipped | PASS - 160/160,
2655/2662, 7 skipped |
| **AFTER** (016, ce9930051) | FAIL - 49/51, 2630/2654 tests passed, 1
failed, 23 skipped | PASS - 86/86, 2655/2678, 23 skipped | PASS -
160/160, 2655/2662, 7 skipped |

### Windows: what changed (48 -> 49 steps, tests now run)

**Fixed by this PR:**
- `compile test ghostty-test` - was `3 errors` (libcxxabi conflicts +
freetype type mismatches) -> `success`
- `run test ghostty-test` - now actually runs: 2630 passed, 23 skipped,
1 failed

**Remaining test failure (pre-existing, unrelated):**
- `ghostty.h MouseShape` - `checkGhosttyHEnum` cannot find
`GHOSTTY_MOUSE_SHAPE_*` constants in the translate-c output. This is a
translate-c issue with how MSVC enum constants are exposed, not related
to C++ linking or enum signedness.

### Linux/macOS: no regressions
Identical pass counts and test results before and after.

## Discussion

### Grep wider: other unconditional linkLibCpp calls
`pkg/breakpad/build.zig` still calls `linkLibCpp()` unconditionally but
is behind sentry and not in the Windows build path. Noted for
completeness.

### Freetype enum signedness
The freetype Zig bindings define `RenderMode = enum(c_uint)` and
`Encoding = enum(u31)`. On MSVC, C enums are `int` (signed), so the
translated C functions expect `c_int` parameters. The fix adds
`@intCast` to convert between signed and unsigned at call sites. This is
safe because the enum values are small positive integers that fit in
both types.

Also, not sure if there's a better way to make this change more
elegantly. The comments are replicated in each instance, probably
overkill but I have seen this same pattern elsewhere in the codebase.

## What I Learnt
- More of the same
This commit is contained in:
Mitchell Hashimoto
2026-03-24 10:29:26 -07:00
committed by GitHub
5 changed files with 33 additions and 9 deletions

View File

@@ -26,7 +26,14 @@ pub fn build(b: *std.Build) !void {
.linkage = .static,
});
lib.linkLibC();
lib.linkLibCpp();
// On MSVC, we must not use linkLibCpp because Zig unconditionally
// passes -nostdinc++ and then adds its bundled libc++/libc++abi
// include paths, which conflict with MSVC's own C++ runtime headers.
// The MSVC SDK include directories (added via linkLibC) contain
// both C and C++ headers, so linkLibCpp is not needed.
if (target.result.abi != .msvc) {
lib.linkLibCpp();
}
b.installArtifact(lib);
// Zig module

View File

@@ -52,7 +52,7 @@ pub const Face = struct {
/// Select a given charmap by its encoding tag (as listed in freetype.h).
pub fn selectCharmap(self: Face, encoding: Encoding) Error!void {
return intToError(c.FT_Select_Charmap(self.handle, @intFromEnum(encoding)));
return intToError(c.FT_Select_Charmap(self.handle, @intCast(@intFromEnum(encoding))));
}
/// Call FT_Request_Size to request the nominal size (in points).
@@ -99,7 +99,7 @@ pub const Face = struct {
pub fn renderGlyph(self: Face, render_mode: RenderMode) Error!void {
return intToError(c.FT_Render_Glyph(
self.handle.*.glyph,
@intFromEnum(render_mode),
@intCast(@intFromEnum(render_mode)),
));
}

View File

@@ -103,7 +103,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
.linkage = .static,
});
lib.linkLibC();
lib.linkLibCpp();
// On MSVC, we must not use linkLibCpp because Zig unconditionally
// passes -nostdinc++ and then adds its bundled libc++/libc++abi
// include paths, which conflict with MSVC's own C++ runtime headers.
// The MSVC SDK include directories (added via linkLibC) contain
// both C and C++ headers, so linkLibCpp is not needed.
if (target.result.abi != .msvc) {
lib.linkLibCpp();
}
if (target.result.os.tag.isDarwin()) {
try apple_sdk.addPaths(b, lib);

View File

@@ -58,7 +58,14 @@ fn buildSpirvCross(
.linkage = .static,
});
lib.linkLibC();
lib.linkLibCpp();
// On MSVC, we must not use linkLibCpp because Zig unconditionally
// passes -nostdinc++ and then adds its bundled libc++/libc++abi
// include paths, which conflict with MSVC's own C++ runtime headers.
// The MSVC SDK include directories (added via linkLibC) contain
// both C and C++ headers, so linkLibCpp is not needed.
if (target.result.abi != .msvc) {
lib.linkLibCpp();
}
if (target.result.os.tag.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, lib);

View File

@@ -679,13 +679,16 @@ pub const Face = struct {
else => |f| {
// Glyph formats are tags, so we can
// output a semi-readable error here.
// Use @bitCast to u32 because MSVC translates C enums
// as signed int, while GCC/Clang uses unsigned int.
const tag: u32 = @bitCast(f);
log.err(
"Can't render glyph with unsupported glyph format \"{s}\"",
.{[4]u8{
@truncate(f >> 24),
@truncate(f >> 16),
@truncate(f >> 8),
@truncate(f >> 0),
@truncate(tag >> 24),
@truncate(tag >> 16),
@truncate(tag >> 8),
@truncate(tag >> 0),
}},
);
return error.UnsupportedGlyphFormat;