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...pull/8912/head^2
parent
3548acfac6
commit
9dc2e5978f
|
|
@ -204,6 +204,7 @@ jobs:
|
||||||
aarch64-linux,
|
aarch64-linux,
|
||||||
x86_64-linux,
|
x86_64-linux,
|
||||||
x86_64-windows,
|
x86_64-windows,
|
||||||
|
wasm32-freestanding,
|
||||||
]
|
]
|
||||||
runs-on: namespace-profile-ghostty-sm
|
runs-on: namespace-profile-ghostty-sm
|
||||||
needs: test
|
needs: test
|
||||||
|
|
|
||||||
17
build.zig
17
build.zig
|
|
@ -101,10 +101,19 @@ pub fn build(b: *std.Build) !void {
|
||||||
);
|
);
|
||||||
|
|
||||||
// libghostty-vt
|
// libghostty-vt
|
||||||
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
|
const libghostty_vt_shared = shared: {
|
||||||
b,
|
if (config.target.result.cpu.arch.isWasm()) {
|
||||||
&mod,
|
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(libvt_step);
|
||||||
libghostty_vt_shared.install(b.getInstallStep());
|
libghostty_vt_shared.install(b.getInstallStep());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,13 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
|
||||||
bool,
|
bool,
|
||||||
"simd",
|
"simd",
|
||||||
"Build with SIMD-accelerated code paths. Results in significant performance improvements.",
|
"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(
|
config.wayland = b.option(
|
||||||
bool,
|
bool,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const GhosttyLibVt = @This();
|
const GhosttyLibVt = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
const RunStep = std.Build.Step.Run;
|
const RunStep = std.Build.Step.Run;
|
||||||
const Config = @import("Config.zig");
|
const Config = @import("Config.zig");
|
||||||
const GhosttyZig = @import("GhosttyZig.zig");
|
const GhosttyZig = @import("GhosttyZig.zig");
|
||||||
|
|
@ -17,7 +18,35 @@ artifact: *std.Build.Step.InstallArtifact,
|
||||||
/// The final library file
|
/// The final library file
|
||||||
output: std.Build.LazyPath,
|
output: std.Build.LazyPath,
|
||||||
dsym: ?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(
|
pub fn initShared(
|
||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
|
|
@ -82,9 +111,11 @@ pub fn install(
|
||||||
) void {
|
) void {
|
||||||
const b = step.owner;
|
const b = step.owner;
|
||||||
step.dependOn(&self.artifact.step);
|
step.dependOn(&self.artifact.step);
|
||||||
step.dependOn(&b.addInstallFileWithDir(
|
if (self.pkg_config) |pkg_config| {
|
||||||
self.pkg_config,
|
step.dependOn(&b.addInstallFileWithDir(
|
||||||
.prefix,
|
pkg_config,
|
||||||
"share/pkgconfig/libghostty-vt.pc",
|
.prefix,
|
||||||
).step);
|
"share/pkgconfig/libghostty-vt.pc",
|
||||||
|
).step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,17 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator {
|
||||||
// If we're given an allocator, use it.
|
// If we're given an allocator, use it.
|
||||||
if (c_alloc_) |c_alloc| return c_alloc.zig();
|
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
|
// If we have libc, use that. We prefer libc if we have it because
|
||||||
// its generally fast but also lets the embedder easily override
|
// its generally fast but also lets the embedder easily override
|
||||||
// malloc/free with custom allocators like mimalloc or something.
|
// malloc/free with custom allocators like mimalloc or something.
|
||||||
if (comptime builtin.link_libc) return std.heap.c_allocator;
|
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
|
// No libc, use the preferred allocator for releases which is the
|
||||||
// Zig SMP allocator.
|
// Zig SMP allocator.
|
||||||
return std.heap.smp_allocator;
|
return std.heap.smp_allocator;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
//! this in the future.
|
//! this in the future.
|
||||||
const lib = @This();
|
const lib = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
// The public API below reproduces a lot of terminal/main.zig but
|
// 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/`
|
// 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
|
// 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 {
|
test {
|
||||||
_ = terminal;
|
_ = terminal;
|
||||||
_ = @import("lib/main.zig");
|
_ = @import("lib/main.zig");
|
||||||
|
|
|
||||||
|
|
@ -23,93 +23,3 @@ pub const alloc = if (builtin.is_test)
|
||||||
std.testing.allocator
|
std.testing.allocator
|
||||||
else
|
else
|
||||||
std.heap.wasm_allocator;
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const wasm = @import("../wasm.zig");
|
const wasm = @import("../wasm.zig");
|
||||||
const wasm_target = @import("target.zig");
|
|
||||||
|
|
||||||
// Use the correct implementation
|
// Use the correct implementation
|
||||||
pub const log = if (wasm_target.target) |target| switch (target) {
|
pub const log = Freestanding.log;
|
||||||
.browser => Browser.log,
|
|
||||||
} else @compileError("wasm target required");
|
|
||||||
|
|
||||||
/// Browser implementation calls an extern "log" function.
|
/// Freestanding implementation calls an extern "log" function.
|
||||||
pub const Browser = struct {
|
pub const Freestanding = struct {
|
||||||
// The function std.log will call.
|
// The function std.log will call.
|
||||||
pub fn log(
|
pub fn log(
|
||||||
comptime level: std.log.Level,
|
comptime level: std.log.Level,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub const Target = enum {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Our specific target platform.
|
/// 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)));
|
const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target)));
|
||||||
// This maybe isn't necessary but I don't know if enums without a specific
|
// 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
|
// tag type and value are guaranteed to be the same between build.zig
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,9 @@ pub fn encode(
|
||||||
encoder_.?.opts,
|
encoder_.?.opts,
|
||||||
) catch unreachable;
|
) 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;
|
return .out_of_memory;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue