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,
|
||||
x86_64-linux,
|
||||
x86_64-windows,
|
||||
wasm32-freestanding,
|
||||
]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
|
|||
17
build.zig
17
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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue