mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-12-28 17:14:39 +00:00
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...
This commit is contained in:
committed by
GitHub
parent
3548acfac6
commit
9dc2e5978f
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user