mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'wasm'
This commit is contained in:
20
build.zig
20
build.zig
@ -73,6 +73,9 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
"Build and install test executables with 'build'",
|
"Build and install test executables with 'build'",
|
||||||
) orelse false;
|
) orelse false;
|
||||||
|
|
||||||
|
// We can use wasmtime to test wasm
|
||||||
|
b.enable_wasmtime = true;
|
||||||
|
|
||||||
// Add our benchmarks
|
// Add our benchmarks
|
||||||
try benchSteps(b, target, mode);
|
try benchSteps(b, target, mode);
|
||||||
|
|
||||||
@ -106,11 +109,11 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns");
|
b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns");
|
||||||
}
|
}
|
||||||
|
|
||||||
// term.wasm
|
// wasm
|
||||||
{
|
{
|
||||||
const wasm = b.addSharedLibrary(
|
const wasm = b.addSharedLibrary(
|
||||||
"ghostty-term",
|
"ghostty-wasm",
|
||||||
"src/terminal/main_wasm.zig",
|
"src/main_wasm.zig",
|
||||||
.{ .unversioned = {} },
|
.{ .unversioned = {} },
|
||||||
);
|
);
|
||||||
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
||||||
@ -122,8 +125,17 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
wasm.addPackage(utf8proc.pkg);
|
wasm.addPackage(utf8proc.pkg);
|
||||||
_ = try utf8proc.link(b, wasm);
|
_ = try utf8proc.link(b, wasm);
|
||||||
|
|
||||||
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
const step = b.step("wasm", "Build the wasm library");
|
||||||
step.dependOn(&wasm.step);
|
step.dependOn(&wasm.step);
|
||||||
|
|
||||||
|
// We support tests via wasmtime. wasmtime uses WASI so this
|
||||||
|
// isn't an exact match to our freestanding target above but
|
||||||
|
// it lets us test some basic functionality.
|
||||||
|
const test_step = b.step("test-wasm", "Run all tests for wasm");
|
||||||
|
const main_test = b.addTest("src/main_wasm.zig");
|
||||||
|
main_test.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .wasi });
|
||||||
|
main_test.addOptions("build_options", exe_options);
|
||||||
|
test_step.dependOn(&main_test.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
|
@ -13,21 +13,23 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('ghostty-term.wasm').then(response =>
|
fetch('ghostty-wasm.wasm').then(response =>
|
||||||
response.arrayBuffer()
|
response.arrayBuffer()
|
||||||
).then(bytes =>
|
).then(bytes =>
|
||||||
WebAssembly.instantiate(bytes, importObject)
|
WebAssembly.instantiate(bytes, importObject)
|
||||||
).then(results => {
|
).then(results => {
|
||||||
const {
|
const {
|
||||||
terminal_new,
|
atlas_new,
|
||||||
terminal_free,
|
atlas_free,
|
||||||
terminal_print,
|
atlas_reserve,
|
||||||
|
free,
|
||||||
|
memory,
|
||||||
} = results.instance.exports;
|
} = results.instance.exports;
|
||||||
|
|
||||||
const term = terminal_new(80, 80);
|
const atlas = atlas_new(512, 0);
|
||||||
console.log(term);
|
const reg = atlas_reserve(atlas, 10, 10);
|
||||||
terminal_free(term);
|
free(reg);
|
||||||
terminal_print('a');
|
atlas_free(atlas);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
, gdb
|
, gdb
|
||||||
, glxinfo
|
, glxinfo
|
||||||
|
, nodejs
|
||||||
, parallel
|
, parallel
|
||||||
, pkg-config
|
, pkg-config
|
||||||
, python
|
, python
|
||||||
@ -64,6 +65,9 @@ in mkShell rec {
|
|||||||
zig
|
zig
|
||||||
zip
|
zip
|
||||||
|
|
||||||
|
# For web and wasm stuff
|
||||||
|
nodejs
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
gdb
|
gdb
|
||||||
parallel
|
parallel
|
||||||
|
@ -49,10 +49,10 @@ modified: bool = false,
|
|||||||
/// updated in-place.
|
/// updated in-place.
|
||||||
resized: bool = false,
|
resized: bool = false,
|
||||||
|
|
||||||
pub const Format = enum {
|
pub const Format = enum(u8) {
|
||||||
greyscale,
|
greyscale = 0,
|
||||||
rgb,
|
rgb = 1,
|
||||||
rgba,
|
rgba = 2,
|
||||||
|
|
||||||
pub fn depth(self: Format) u8 {
|
pub fn depth(self: Format) u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@ -76,7 +76,7 @@ pub const Error = error{
|
|||||||
|
|
||||||
/// A region within the texture atlas. These can be acquired using the
|
/// A region within the texture atlas. These can be acquired using the
|
||||||
/// "reserve" function. A region reservation is required to write data.
|
/// "reserve" function. A region reservation is required to write data.
|
||||||
pub const Region = struct {
|
pub const Region = extern struct {
|
||||||
x: u32,
|
x: u32,
|
||||||
y: u32,
|
y: u32,
|
||||||
width: u32,
|
width: u32,
|
||||||
@ -298,6 +298,80 @@ pub fn clear(self: *Atlas) void {
|
|||||||
self.nodes.appendAssumeCapacity(.{ .x = 1, .y = 1, .width = self.size - 2 });
|
self.nodes.appendAssumeCapacity(.{ .x = 1, .y = 1, .width = self.size - 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wasm-compatible API. This lacks documentation unless the API differs
|
||||||
|
/// from the standard Zig API. To learn what a function does, just look one
|
||||||
|
/// level deeper to what Zig function is called and read the documentation there.
|
||||||
|
///
|
||||||
|
/// To export this from Zig, use `usingnamespace Wasm` in some top-level
|
||||||
|
/// space and it will be exported.
|
||||||
|
pub const Wasm = struct {
|
||||||
|
// If you're copying this file (Atlas.zig) out to a separate project,
|
||||||
|
// just replace this with the allocator you want to use.
|
||||||
|
const wasm = @import("../wasm.zig");
|
||||||
|
const alloc = wasm.alloc;
|
||||||
|
|
||||||
|
export fn atlas_new(size: u32, format: u8) ?*Atlas {
|
||||||
|
const atlas = init(
|
||||||
|
alloc,
|
||||||
|
size,
|
||||||
|
@intToEnum(Format, format),
|
||||||
|
) catch return null;
|
||||||
|
const result = alloc.create(Atlas) catch return null;
|
||||||
|
result.* = atlas;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The return value for this should be freed by the caller with "free".
|
||||||
|
export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region {
|
||||||
|
return atlas_reserve_(self, width, height) catch return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atlas_reserve_(self: *Atlas, width: u32, height: u32) !*Region {
|
||||||
|
const reg = try self.reserve(alloc, width, height);
|
||||||
|
const result = try alloc.create(Region);
|
||||||
|
errdefer alloc.destroy(result);
|
||||||
|
_ = try wasm.toHostOwned(result);
|
||||||
|
result.* = reg;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn atlas_set(self: *Atlas, reg: *Region, data: [*]const u8, len: usize) void {
|
||||||
|
self.set(reg.*, data[0..len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn atlas_grow(self: *Atlas, size_new: u32) bool {
|
||||||
|
self.grow(alloc, size_new) catch return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn atlas_clear(self: *Atlas) void {
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn atlas_free(ptr: ?*Atlas) void {
|
||||||
|
if (ptr) |v| {
|
||||||
|
v.deinit(alloc);
|
||||||
|
alloc.destroy(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "happy path" {
|
||||||
|
var atlas = atlas_new(512, @enumToInt(Format.greyscale)).?;
|
||||||
|
defer atlas_free(atlas);
|
||||||
|
|
||||||
|
const reg = atlas_reserve(atlas, 2, 2).?;
|
||||||
|
defer alloc.destroy(reg);
|
||||||
|
try testing.expect(wasm.isHostOwned(reg));
|
||||||
|
defer wasm.toModuleOwned(reg);
|
||||||
|
try testing.expect(reg.width > 0);
|
||||||
|
|
||||||
|
const data = &[_]u8{ 1, 2, 3, 4 };
|
||||||
|
try testing.expect(!atlas.modified);
|
||||||
|
atlas_set(atlas, reg, data, data.len);
|
||||||
|
try testing.expect(atlas.modified);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
test "exact fit" {
|
test "exact fit" {
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border
|
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border
|
||||||
|
@ -68,6 +68,8 @@ pub fn deinit(self: *DeferredFace) void {
|
|||||||
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
|
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
|
||||||
.coretext => if (self.ct) |*ct| ct.deinit(),
|
.coretext => if (self.ct) |*ct| ct.deinit(),
|
||||||
.freetype => {},
|
.freetype => {},
|
||||||
|
// TODO
|
||||||
|
.web_canvas => unreachable,
|
||||||
}
|
}
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
@ -90,6 +92,9 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
.freetype => {},
|
.freetype => {},
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
.web_canvas => unreachable,
|
||||||
}
|
}
|
||||||
|
|
||||||
return "TODO: built-in font names";
|
return "TODO: built-in font names";
|
||||||
@ -122,6 +127,9 @@ pub fn load(
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
.web_canvas => unreachable,
|
||||||
|
|
||||||
// Unreachable because we must be already loaded or have the
|
// Unreachable because we must be already loaded or have the
|
||||||
// proper configuration for one of the other deferred mechanisms.
|
// proper configuration for one of the other deferred mechanisms.
|
||||||
.freetype => unreachable,
|
.freetype => unreachable,
|
||||||
@ -245,6 +253,9 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
.web_canvas => unreachable,
|
||||||
|
|
||||||
.freetype => {},
|
.freetype => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@ const builtin = @import("builtin");
|
|||||||
const options = @import("main.zig").options;
|
const options = @import("main.zig").options;
|
||||||
const freetype = @import("face/freetype.zig");
|
const freetype = @import("face/freetype.zig");
|
||||||
const coretext = @import("face/coretext.zig");
|
const coretext = @import("face/coretext.zig");
|
||||||
|
const web_canvas = @import("face/web_canvas.zig");
|
||||||
|
|
||||||
/// Face implementation for the compile options.
|
/// Face implementation for the compile options.
|
||||||
pub const Face = switch (options.backend) {
|
pub const Face = switch (options.backend) {
|
||||||
.fontconfig_freetype => freetype.Face,
|
.fontconfig_freetype => freetype.Face,
|
||||||
.coretext => freetype.Face,
|
.coretext => freetype.Face,
|
||||||
//.coretext => coretext.Face,
|
//.coretext => coretext.Face,
|
||||||
|
.web_canvas => web_canvas.Face,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
11
src/font/face/web_canvas.zig
Normal file
11
src/font/face/web_canvas.zig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const font = @import("../main.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.font_face);
|
||||||
|
|
||||||
|
pub const Face = struct {
|
||||||
|
// TODO
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
|
|
||||||
pub const Atlas = @import("Atlas.zig");
|
pub const Atlas = @import("Atlas.zig");
|
||||||
@ -16,16 +17,15 @@ pub const Sprite = sprite.Sprite;
|
|||||||
pub const Descriptor = discovery.Descriptor;
|
pub const Descriptor = discovery.Descriptor;
|
||||||
pub const Discover = discovery.Discover;
|
pub const Discover = discovery.Discover;
|
||||||
|
|
||||||
|
pub usingnamespace if (builtin.target.isWasm()) struct {
|
||||||
|
pub usingnamespace Atlas.Wasm;
|
||||||
|
} else struct {};
|
||||||
|
|
||||||
/// Build options
|
/// Build options
|
||||||
pub const options: struct {
|
pub const options: struct {
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
} = .{
|
} = .{
|
||||||
.backend = if (build_options.coretext)
|
.backend = Backend.default(),
|
||||||
.coretext
|
|
||||||
else if (build_options.fontconfig)
|
|
||||||
.fontconfig_freetype
|
|
||||||
else
|
|
||||||
.freetype,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Backend = enum {
|
pub const Backend = enum {
|
||||||
@ -38,14 +38,37 @@ pub const Backend = enum {
|
|||||||
/// CoreText for both font discovery and rendering (macOS).
|
/// CoreText for both font discovery and rendering (macOS).
|
||||||
coretext,
|
coretext,
|
||||||
|
|
||||||
|
/// Use the browser font system and the Canvas API (wasm). This limits
|
||||||
|
/// the available fonts to browser fonts (anything Canvas natively
|
||||||
|
/// supports).
|
||||||
|
web_canvas,
|
||||||
|
|
||||||
|
/// Returns the default backend for a build environment. This is
|
||||||
|
/// meant to be called at comptime.
|
||||||
|
pub fn default() Backend {
|
||||||
|
// Wasm only supports browser at the moment.
|
||||||
|
if (builtin.target.isWasm()) return .web_canvas;
|
||||||
|
|
||||||
|
return if (build_options.coretext)
|
||||||
|
.coretext
|
||||||
|
else if (build_options.fontconfig)
|
||||||
|
.fontconfig_freetype
|
||||||
|
else
|
||||||
|
.freetype;
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper that just returns true if we should be using freetype. This
|
/// Helper that just returns true if we should be using freetype. This
|
||||||
/// is used for tests.
|
/// is used for tests.
|
||||||
pub fn freetype(self: Backend) bool {
|
pub fn freetype(self: Backend) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.freetype, .fontconfig_freetype => true,
|
.freetype, .fontconfig_freetype => true,
|
||||||
.coretext => false,
|
.coretext, .web_canvas => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "default can run at comptime" {
|
||||||
|
_ = comptime default();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The styles that a family can take.
|
/// The styles that a family can take.
|
||||||
@ -66,5 +89,11 @@ pub const Presentation = enum(u1) {
|
|||||||
pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
|
pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
// For non-wasm we want to test everything we can
|
||||||
|
if (!comptime builtin.target.isWasm()) {
|
||||||
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Atlas;
|
||||||
}
|
}
|
||||||
|
5
src/main_wasm.zig
Normal file
5
src/main_wasm.zig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// This is the main file for the WASM module. The WASM module has to
|
||||||
|
// export a C ABI compatible API.
|
||||||
|
|
||||||
|
pub usingnamespace @import("wasm.zig");
|
||||||
|
pub usingnamespace @import("font/main.zig");
|
114
src/wasm.zig
Normal file
114
src/wasm.zig
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
//! This file contains helpers for wasm compilation.
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
if (!builtin.target.isWasm()) {
|
||||||
|
@compileError("wasm.zig should only be analyzed for wasm32 builds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The allocator to use in wasm environments.
|
||||||
|
///
|
||||||
|
/// The return values of this should NOT be sent to the host environment
|
||||||
|
/// unless toHostOwned is called on them. In this case, the caller is expected
|
||||||
|
/// to call free. If a pointer is NOT host-owned, then the wasm module is
|
||||||
|
/// expected to call the normal alloc.free/destroy functions.
|
||||||
|
///
|
||||||
|
/// TODO: we should NOT be using page_allocator because we're getting
|
||||||
|
/// full 64kb pages for every allocation. I plan on changing this to the
|
||||||
|
/// new stdlib wasm allocator once that is merged and available.
|
||||||
|
pub const alloc = if (builtin.is_test)
|
||||||
|
std.testing.allocator
|
||||||
|
else
|
||||||
|
std.heap.page_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 = @intToPtr([*]u8, @ptrToInt(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 = @intToPtr([*]u8, @ptrToInt(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 = @intToPtr([*]u8, @ptrToInt(ptr));
|
||||||
|
_ = allocs.remove(casted);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "basics" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var 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 };
|
||||||
|
var 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);
|
||||||
|
}
|
Reference in New Issue
Block a user