Skip to content

Commit 9dc2e59

Browse files
authored
lib-vt: enable freestanding wasm builds (#9301)
This makes `libghostty-vt` build for freestanding wasm targets (aka a browser) and produce a `ghostty-vt.wasm` file. This exports the same C API that libghostty-vt does. This commit specifically makes the changes necessary for the build to build properly and for us to run the build in CI. We don't yet actually try using it...
1 parent 3548acf commit 9dc2e59

File tree

10 files changed

+94
-109
lines changed

10 files changed

+94
-109
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ jobs:
204204
aarch64-linux,
205205
x86_64-linux,
206206
x86_64-windows,
207+
wasm32-freestanding,
207208
]
208209
runs-on: namespace-profile-ghostty-sm
209210
needs: test

build.zig

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,19 @@ pub fn build(b: *std.Build) !void {
101101
);
102102

103103
// libghostty-vt
104-
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
105-
b,
106-
&mod,
107-
);
104+
const libghostty_vt_shared = shared: {
105+
if (config.target.result.cpu.arch.isWasm()) {
106+
break :shared try buildpkg.GhosttyLibVt.initWasm(
107+
b,
108+
&mod,
109+
);
110+
}
111+
112+
break :shared try buildpkg.GhosttyLibVt.initShared(
113+
b,
114+
&mod,
115+
);
116+
};
108117
libghostty_vt_shared.install(libvt_step);
109118
libghostty_vt_shared.install(b.getInstallStep());
110119

src/build/Config.zig

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,13 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
173173
bool,
174174
"simd",
175175
"Build with SIMD-accelerated code paths. Results in significant performance improvements.",
176-
) orelse true;
176+
) orelse simd: {
177+
// We can't build our SIMD dependencies for Wasm. Note that we may
178+
// still use SIMD features in the Wasm-builds.
179+
if (target.result.cpu.arch.isWasm()) break :simd false;
180+
181+
break :simd true;
182+
};
177183

178184
config.wayland = b.option(
179185
bool,

src/build/GhosttyLibVt.zig

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const GhosttyLibVt = @This();
22

33
const std = @import("std");
4+
const assert = std.debug.assert;
45
const RunStep = std.Build.Step.Run;
56
const Config = @import("Config.zig");
67
const GhosttyZig = @import("GhosttyZig.zig");
@@ -17,7 +18,35 @@ artifact: *std.Build.Step.InstallArtifact,
1718
/// The final library file
1819
output: std.Build.LazyPath,
1920
dsym: ?std.Build.LazyPath,
20-
pkg_config: std.Build.LazyPath,
21+
pkg_config: ?std.Build.LazyPath,
22+
23+
pub fn initWasm(
24+
b: *std.Build,
25+
zig: *const GhosttyZig,
26+
) !GhosttyLibVt {
27+
const target = zig.vt.resolved_target.?;
28+
assert(target.result.cpu.arch.isWasm());
29+
30+
const exe = b.addExecutable(.{
31+
.name = "ghostty-vt",
32+
.root_module = zig.vt_c,
33+
.version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 },
34+
});
35+
36+
// Allow exported symbols to actually be exported.
37+
exe.rdynamic = true;
38+
39+
// There is no entrypoint for this wasm module.
40+
exe.entry = .disabled;
41+
42+
return .{
43+
.step = &exe.step,
44+
.artifact = b.addInstallArtifact(exe, .{}),
45+
.output = exe.getEmittedBin(),
46+
.dsym = null,
47+
.pkg_config = null,
48+
};
49+
}
2150

2251
pub fn initShared(
2352
b: *std.Build,
@@ -82,9 +111,11 @@ pub fn install(
82111
) void {
83112
const b = step.owner;
84113
step.dependOn(&self.artifact.step);
85-
step.dependOn(&b.addInstallFileWithDir(
86-
self.pkg_config,
87-
.prefix,
88-
"share/pkgconfig/libghostty-vt.pc",
89-
).step);
114+
if (self.pkg_config) |pkg_config| {
115+
step.dependOn(&b.addInstallFileWithDir(
116+
pkg_config,
117+
.prefix,
118+
"share/pkgconfig/libghostty-vt.pc",
119+
).step);
120+
}
90121
}

src/lib/allocator.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator {
2020
// If we're given an allocator, use it.
2121
if (c_alloc_) |c_alloc| return c_alloc.zig();
2222

23+
// Tests always use the test allocator so we can detect leaks.
24+
if (comptime builtin.is_test) return testing.allocator;
25+
2326
// If we have libc, use that. We prefer libc if we have it because
2427
// its generally fast but also lets the embedder easily override
2528
// malloc/free with custom allocators like mimalloc or something.
2629
if (comptime builtin.link_libc) return std.heap.c_allocator;
2730

31+
// Wasm
32+
if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator;
33+
2834
// No libc, use the preferred allocator for releases which is the
2935
// Zig SMP allocator.
3036
return std.heap.smp_allocator;

src/lib_vt.zig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
//! this in the future.
1010
const lib = @This();
1111

12+
const std = @import("std");
13+
const builtin = @import("builtin");
14+
1215
// The public API below reproduces a lot of terminal/main.zig but
1316
// is separate because (1) we need our root file to be in `src/`
1417
// so we can access other directories and (2) we may want to withhold
@@ -126,6 +129,26 @@ comptime {
126129
}
127130
}
128131

132+
pub const std_options: std.Options = options: {
133+
if (builtin.target.cpu.arch.isWasm()) break :options .{
134+
// Wasm builds we specifically want to optimize for space with small
135+
// releases so we bump up to warn. Everything else acts pretty normal.
136+
.log_level = switch (builtin.mode) {
137+
.Debug => .debug,
138+
.ReleaseSmall => .warn,
139+
else => .info,
140+
},
141+
142+
// Wasm doesn't have access to stdio so we have a custom log function.
143+
.logFn = @import("os/wasm/log.zig").log,
144+
};
145+
146+
// For everything else we currently use defaults. Longer term I'm
147+
// SURE this isn't right (e.g. we definitely want to customize the log
148+
// function for the C lib at least).
149+
break :options .{};
150+
};
151+
129152
test {
130153
_ = terminal;
131154
_ = @import("lib/main.zig");

src/os/wasm.zig

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -23,93 +23,3 @@ pub const alloc = if (builtin.is_test)
2323
std.testing.allocator
2424
else
2525
std.heap.wasm_allocator;
26-
27-
/// For host-owned allocations:
28-
/// We need to keep track of our own pointer lengths because Zig
29-
/// allocators usually don't do this and we need to be able to send
30-
/// a direct pointer back to the host system. A more appropriate thing
31-
/// to do would be to probably make a custom allocator that keeps track
32-
/// of size.
33-
var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{};
34-
35-
/// Allocate len bytes and return a pointer to the memory in the host.
36-
/// The data is not zeroed.
37-
pub export fn malloc(len: usize) ?[*]u8 {
38-
return alloc_(len) catch return null;
39-
}
40-
41-
fn alloc_(len: usize) ![*]u8 {
42-
// Create the allocation
43-
const slice = try alloc.alloc(u8, len);
44-
errdefer alloc.free(slice);
45-
46-
// Store the size so we can deallocate later
47-
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
48-
errdefer _ = allocs.remove(slice.ptr);
49-
50-
return slice.ptr;
51-
}
52-
53-
/// Free an allocation from malloc.
54-
pub export fn free(ptr: ?[*]u8) void {
55-
if (ptr) |v| {
56-
if (allocs.get(v)) |len| {
57-
const slice = v[0..len];
58-
alloc.free(slice);
59-
_ = allocs.remove(v);
60-
}
61-
}
62-
}
63-
64-
/// Convert an allocated pointer of any type to a host-owned pointer.
65-
/// This pushes the responsibility to free it to the host. The returned
66-
/// pointer will match the pointer but is typed correctly for returning
67-
/// to the host.
68-
pub fn toHostOwned(ptr: anytype) ![*]u8 {
69-
// Convert our pointer to a byte array
70-
const info = @typeInfo(@TypeOf(ptr)).pointer;
71-
const T = info.child;
72-
const size = @sizeOf(T);
73-
const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr)));
74-
75-
// Store the information about it
76-
try allocs.putNoClobber(alloc, casted, size);
77-
errdefer _ = allocs.remove(casted);
78-
79-
return casted;
80-
}
81-
82-
/// Returns true if the value is host owned.
83-
pub fn isHostOwned(ptr: anytype) bool {
84-
const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr)));
85-
return allocs.contains(casted);
86-
}
87-
88-
/// Convert a pointer back to a module-owned value. The caller is expected
89-
/// to cast or have the valid pointer for alloc calls.
90-
pub fn toModuleOwned(ptr: anytype) void {
91-
const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr)));
92-
_ = allocs.remove(casted);
93-
}
94-
95-
test "basics" {
96-
const testing = std.testing;
97-
const buf = malloc(32).?;
98-
try testing.expect(allocs.size == 1);
99-
free(buf);
100-
try testing.expect(allocs.size == 0);
101-
}
102-
103-
test "toHostOwned" {
104-
const testing = std.testing;
105-
106-
const Point = struct { x: u32 = 0, y: u32 = 0 };
107-
const p = try alloc.create(Point);
108-
errdefer alloc.destroy(p);
109-
const ptr = try toHostOwned(p);
110-
try testing.expect(allocs.size == 1);
111-
try testing.expect(isHostOwned(p));
112-
try testing.expect(isHostOwned(ptr));
113-
free(ptr);
114-
try testing.expect(allocs.size == 0);
115-
}

src/os/wasm/log.zig

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
const std = @import("std");
22
const builtin = @import("builtin");
33
const wasm = @import("../wasm.zig");
4-
const wasm_target = @import("target.zig");
54

65
// Use the correct implementation
7-
pub const log = if (wasm_target.target) |target| switch (target) {
8-
.browser => Browser.log,
9-
} else @compileError("wasm target required");
6+
pub const log = Freestanding.log;
107

11-
/// Browser implementation calls an extern "log" function.
12-
pub const Browser = struct {
8+
/// Freestanding implementation calls an extern "log" function.
9+
pub const Freestanding = struct {
1310
// The function std.log will call.
1411
pub fn log(
1512
comptime level: std.log.Level,

src/os/wasm/target.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub const Target = enum {
1010
};
1111

1212
/// Our specific target platform.
13-
pub const target: ?Target = if (!builtin.target.isWasm()) null else target: {
13+
pub const target: ?Target = if (!builtin.target.cpu.arch.isWasm()) null else target: {
1414
const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target)));
1515
// This maybe isn't necessary but I don't know if enums without a specific
1616
// tag type and value are guaranteed to be the same between build.zig

src/terminal/c/key_encode.zig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ pub fn encode(
123123
encoder_.?.opts,
124124
) catch unreachable;
125125

126-
out_written.* = discarding.count;
126+
// Discarding always uses a u64. If we're on 32-bit systems
127+
// we cast down. We should make this safer in the future.
128+
out_written.* = @intCast(discarding.count);
127129
return .out_of_memory;
128130
},
129131
};

0 commit comments

Comments
 (0)