diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6520a9ed5..ef03c5f32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -204,6 +204,7 @@ jobs: aarch64-linux, x86_64-linux, x86_64-windows, + wasm32-freestanding, ] runs-on: namespace-profile-ghostty-sm needs: test diff --git a/build.zig b/build.zig index 7836b5c0d..68dc0028b 100644 --- a/build.zig +++ b/build.zig @@ -101,10 +101,19 @@ pub fn build(b: *std.Build) !void { ); // libghostty-vt - const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( - b, - &mod, - ); + const libghostty_vt_shared = shared: { + if (config.target.result.cpu.arch.isWasm()) { + break :shared try buildpkg.GhosttyLibVt.initWasm( + b, + &mod, + ); + } + + break :shared try buildpkg.GhosttyLibVt.initShared( + b, + &mod, + ); + }; libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); diff --git a/src/build/Config.zig b/src/build/Config.zig index 124cf7299..e88213d71 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -173,7 +173,13 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { bool, "simd", "Build with SIMD-accelerated code paths. Results in significant performance improvements.", - ) orelse true; + ) orelse simd: { + // We can't build our SIMD dependencies for Wasm. Note that we may + // still use SIMD features in the Wasm-builds. + if (target.result.cpu.arch.isWasm()) break :simd false; + + break :simd true; + }; config.wayland = b.option( bool, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 590792ef3..d1ab5d1ba 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -1,6 +1,7 @@ const GhosttyLibVt = @This(); const std = @import("std"); +const assert = std.debug.assert; const RunStep = std.Build.Step.Run; const Config = @import("Config.zig"); const GhosttyZig = @import("GhosttyZig.zig"); @@ -17,7 +18,35 @@ artifact: *std.Build.Step.InstallArtifact, /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, -pkg_config: std.Build.LazyPath, +pkg_config: ?std.Build.LazyPath, + +pub fn initWasm( + b: *std.Build, + zig: *const GhosttyZig, +) !GhosttyLibVt { + const target = zig.vt.resolved_target.?; + assert(target.result.cpu.arch.isWasm()); + + const exe = b.addExecutable(.{ + .name = "ghostty-vt", + .root_module = zig.vt_c, + .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, + }); + + // Allow exported symbols to actually be exported. + exe.rdynamic = true; + + // There is no entrypoint for this wasm module. + exe.entry = .disabled; + + return .{ + .step = &exe.step, + .artifact = b.addInstallArtifact(exe, .{}), + .output = exe.getEmittedBin(), + .dsym = null, + .pkg_config = null, + }; +} pub fn initShared( b: *std.Build, @@ -82,9 +111,11 @@ pub fn install( ) void { const b = step.owner; step.dependOn(&self.artifact.step); - step.dependOn(&b.addInstallFileWithDir( - self.pkg_config, - .prefix, - "share/pkgconfig/libghostty-vt.pc", - ).step); + if (self.pkg_config) |pkg_config| { + step.dependOn(&b.addInstallFileWithDir( + pkg_config, + .prefix, + "share/pkgconfig/libghostty-vt.pc", + ).step); + } } diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index bcd7f9dcc..ccea7ae29 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -20,11 +20,17 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { // If we're given an allocator, use it. if (c_alloc_) |c_alloc| return c_alloc.zig(); + // Tests always use the test allocator so we can detect leaks. + if (comptime builtin.is_test) return testing.allocator; + // If we have libc, use that. We prefer libc if we have it because // its generally fast but also lets the embedder easily override // malloc/free with custom allocators like mimalloc or something. if (comptime builtin.link_libc) return std.heap.c_allocator; + // Wasm + if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator; + // No libc, use the preferred allocator for releases which is the // Zig SMP allocator. return std.heap.smp_allocator; diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 1df8330ea..322f391ab 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -9,6 +9,9 @@ //! this in the future. const lib = @This(); +const std = @import("std"); +const builtin = @import("builtin"); + // The public API below reproduces a lot of terminal/main.zig but // is separate because (1) we need our root file to be in `src/` // so we can access other directories and (2) we may want to withhold @@ -126,6 +129,26 @@ comptime { } } +pub const std_options: std.Options = options: { + if (builtin.target.cpu.arch.isWasm()) break :options .{ + // Wasm builds we specifically want to optimize for space with small + // releases so we bump up to warn. Everything else acts pretty normal. + .log_level = switch (builtin.mode) { + .Debug => .debug, + .ReleaseSmall => .warn, + else => .info, + }, + + // Wasm doesn't have access to stdio so we have a custom log function. + .logFn = @import("os/wasm/log.zig").log, + }; + + // For everything else we currently use defaults. Longer term I'm + // SURE this isn't right (e.g. we definitely want to customize the log + // function for the C lib at least). + break :options .{}; +}; + test { _ = terminal; _ = @import("lib/main.zig"); diff --git a/src/os/wasm.zig b/src/os/wasm.zig index 73a5922cf..3d0b90e9a 100644 --- a/src/os/wasm.zig +++ b/src/os/wasm.zig @@ -23,93 +23,3 @@ pub const alloc = if (builtin.is_test) std.testing.allocator else std.heap.wasm_allocator; - -/// For host-owned allocations: -/// We need to keep track of our own pointer lengths because Zig -/// allocators usually don't do this and we need to be able to send -/// a direct pointer back to the host system. A more appropriate thing -/// to do would be to probably make a custom allocator that keeps track -/// of size. -var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{}; - -/// Allocate len bytes and return a pointer to the memory in the host. -/// The data is not zeroed. -pub export fn malloc(len: usize) ?[*]u8 { - return alloc_(len) catch return null; -} - -fn alloc_(len: usize) ![*]u8 { - // Create the allocation - const slice = try alloc.alloc(u8, len); - errdefer alloc.free(slice); - - // Store the size so we can deallocate later - try allocs.putNoClobber(alloc, slice.ptr, slice.len); - errdefer _ = allocs.remove(slice.ptr); - - return slice.ptr; -} - -/// Free an allocation from malloc. -pub export fn free(ptr: ?[*]u8) void { - if (ptr) |v| { - if (allocs.get(v)) |len| { - const slice = v[0..len]; - alloc.free(slice); - _ = allocs.remove(v); - } - } -} - -/// Convert an allocated pointer of any type to a host-owned pointer. -/// This pushes the responsibility to free it to the host. The returned -/// pointer will match the pointer but is typed correctly for returning -/// to the host. -pub fn toHostOwned(ptr: anytype) ![*]u8 { - // Convert our pointer to a byte array - const info = @typeInfo(@TypeOf(ptr)).pointer; - const T = info.child; - const size = @sizeOf(T); - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - - // Store the information about it - try allocs.putNoClobber(alloc, casted, size); - errdefer _ = allocs.remove(casted); - - return casted; -} - -/// Returns true if the value is host owned. -pub fn isHostOwned(ptr: anytype) bool { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - return allocs.contains(casted); -} - -/// Convert a pointer back to a module-owned value. The caller is expected -/// to cast or have the valid pointer for alloc calls. -pub fn toModuleOwned(ptr: anytype) void { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - _ = allocs.remove(casted); -} - -test "basics" { - const testing = std.testing; - const buf = malloc(32).?; - try testing.expect(allocs.size == 1); - free(buf); - try testing.expect(allocs.size == 0); -} - -test "toHostOwned" { - const testing = std.testing; - - const Point = struct { x: u32 = 0, y: u32 = 0 }; - const p = try alloc.create(Point); - errdefer alloc.destroy(p); - const ptr = try toHostOwned(p); - try testing.expect(allocs.size == 1); - try testing.expect(isHostOwned(p)); - try testing.expect(isHostOwned(ptr)); - free(ptr); - try testing.expect(allocs.size == 0); -} diff --git a/src/os/wasm/log.zig b/src/os/wasm/log.zig index d81571229..1aac8c4e7 100644 --- a/src/os/wasm/log.zig +++ b/src/os/wasm/log.zig @@ -1,15 +1,12 @@ const std = @import("std"); const builtin = @import("builtin"); const wasm = @import("../wasm.zig"); -const wasm_target = @import("target.zig"); // Use the correct implementation -pub const log = if (wasm_target.target) |target| switch (target) { - .browser => Browser.log, -} else @compileError("wasm target required"); +pub const log = Freestanding.log; -/// Browser implementation calls an extern "log" function. -pub const Browser = struct { +/// Freestanding implementation calls an extern "log" function. +pub const Freestanding = struct { // The function std.log will call. pub fn log( comptime level: std.log.Level, diff --git a/src/os/wasm/target.zig b/src/os/wasm/target.zig index cd8b2dd33..a6a29e208 100644 --- a/src/os/wasm/target.zig +++ b/src/os/wasm/target.zig @@ -10,7 +10,7 @@ pub const Target = enum { }; /// Our specific target platform. -pub const target: ?Target = if (!builtin.target.isWasm()) null else target: { +pub const target: ?Target = if (!builtin.target.cpu.arch.isWasm()) null else target: { const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target))); // This maybe isn't necessary but I don't know if enums without a specific // tag type and value are guaranteed to be the same between build.zig diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 96754d884..f5f6ff054 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -123,7 +123,9 @@ pub fn encode( encoder_.?.opts, ) catch unreachable; - out_written.* = discarding.count; + // Discarding always uses a u64. If we're on 32-bit systems + // we cast down. We should make this safer in the future. + out_written.* = @intCast(discarding.count); return .out_of_memory; }, };