Got ecs and assets imported

master
Louis Pearson 2022-01-14 21:58:57 -07:00
parent 1d430e81f7
commit 0f56ac1f2c
11 changed files with 513 additions and 71 deletions

8
assets/assets-template Normal file
View File

@ -0,0 +1,8 @@
{{#sprites}}
// {{name}}
pub const {{name}}_width = {{width}};
pub const {{name}}_height = {{height}};
pub const {{name}}_flags = {{flags}}; // {{flagsHumanReadable}}
pub const {{name}} = [{{length}}]u8{ {{bytes}} };
{{/sprites}}

2
assets/assets.zig Normal file
View File

@ -0,0 +1,2 @@
// pub const tiles = @import("tiles.zig");
pub usingnamespace @import("sprites.zig");

BIN
assets/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

6
assets/sprites.zig Normal file

File diff suppressed because one or more lines are too long

BIN
assets/tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

6
assets/tiles.zig Normal file

File diff suppressed because one or more lines are too long

4
build-assets.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
w4 png2src --template assets/assets-template --zig assets/tiles.png -o assets/tiles.zig
w4 png2src --template assets/assets-template --zig assets/sprites.png -o assets/sprites.zig

View File

@ -45,9 +45,15 @@ test "stack version check" {
} }
pub fn build(b: *std.build.Builder) !void { pub fn build(b: *std.build.Builder) !void {
const assets = std.build.Pkg{
.name = "assets",
.path = .{ .path = "assets/assets.zig" },
};
const zig_version = @import("builtin").zig_version; const zig_version = @import("builtin").zig_version;
const mode = b.standardReleaseOptions(); const mode = b.standardReleaseOptions();
const lib = b.addSharedLibrary("cart", "src/main.zig", .unversioned); const lib = b.addSharedLibrary("cart", "src/main.zig", .unversioned);
lib.addPackage(assets);
lib.setBuildMode(mode); lib.setBuildMode(mode);
lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
lib.import_memory = true; lib.import_memory = true;

169
src/ecs.zig Normal file
View File

@ -0,0 +1,169 @@
const std = @import("std");
const ArgsTuple = std.meta.Tuple;
const Tuple = std.meta.Tuple;
pub fn World(comptime ComponentBase: type) type {
// Build a component type at comptime based off of ComponentBase. It makes all the fields
// nullable so they are easy to pull out of the store.
const Component = componentConstructor: {
var fields = std.meta.fields(ComponentBase);
var newFields: [fields.len]std.builtin.TypeInfo.StructField = undefined;
inline for (fields) |field, i| {
const T = field.field_type;
const default: ?T = null;
newFields[i] = std.builtin.TypeInfo.StructField{
.name = field.name,
.field_type = ?T,
.default_value = default,
.is_comptime = false,
.alignment = if (@sizeOf(T) > 0) @alignOf(T) else 0,
};
}
break :componentConstructor @Type(.{ .Struct = .{
.layout = .Auto,
.fields = &newFields,
.decls = &[_]std.builtin.TypeInfo.Declaration{},
.is_tuple = false,
} });
};
return struct {
components: ComponentPool,
alloc: std.mem.Allocator,
pub const Query = ComponentQuery;
const ComponentPool = std.MultiArrayList(Component);
const ComponentEnum = std.meta.FieldEnum(Component);
const ComponentSet = std.EnumSet(ComponentEnum);
const ComponentQuery = struct {
required: ComponentSet = ComponentSet.init(.{}),
excluded: ComponentSet = ComponentSet.init(.{}),
pub fn init() @This() {
return @This(){};
}
pub fn query(require_set: []const ComponentEnum, exclude_set: []const ComponentEnum) @This() {
var this = @This(){};
for (require_set) |f| {
this.required.insert(f);
}
for (exclude_set) |f| {
this.excluded.insert(f);
}
return this;
}
pub fn require(set: []const ComponentEnum) @This() {
var this = @This(){};
for (set) |f| {
this.required.insert(f);
}
return this;
}
pub fn exclude(set: []const ComponentEnum) @This() {
var this = @This(){};
for (set) |f| {
this.excluded.insert(f);
}
return this;
}
};
const fields = std.meta.fields(Component);
pub fn init(alloc: std.mem.Allocator) @This() {
return @This(){
.components = ComponentPool{},
.alloc = alloc,
};
}
pub fn create(this: *@This(), component: Component) usize {
const len = this.components.len;
this.components.append(this.alloc, component) catch unreachable;
return len;
}
pub fn destroy(this: *@This(), entity: usize) void {
// TODO
_ = this;
_ = entity;
@compileError("unimplemented");
}
pub fn get(this: *@This(), entity: usize, component: ComponentEnum) *Component {
return this.components.items(component)[entity];
}
fn enum2type(comptime enumList: []const ComponentEnum) []type {
var t: [enumList.len]type = undefined;
inline for (enumList) |e, i| {
const field_type = @typeInfo(fields[@enumToInt(e)].field_type);
t[i] = *field_type.Optional.child;
}
return &t;
}
pub fn process(this: *@This(), dt: f32, comptime comp: []const ComponentEnum, func: anytype) void {
const Args = Tuple([_]type{f32} ++ enum2type(comp));
var i = this.iter(Query.require(comp));
while (i.next()) |e| {
var args: Args = undefined;
args[0] = dt;
inline for (comp) |f, j| {
args[j + 1] = &(@field(e, @tagName(f)).?);
}
@call(.{}, func, args);
}
}
pub fn iterAll(this: *@This()) Iterator {
return Iterator.init(this, ComponentQuery{});
}
pub fn iter(this: *@This(), query: ComponentQuery) Iterator {
return Iterator.init(this, query);
}
const Self = @This();
const Iterator = struct {
world: *Self,
lastComponent: ?Component,
index: usize,
query: ComponentQuery,
pub fn init(w: *Self, q: ComponentQuery) @This() {
return @This(){
.world = w,
.lastComponent = null,
.index = 0,
.query = q,
};
}
pub fn next(this: *@This()) ?*Component {
if (this.lastComponent) |e| this.world.components.set(this.index - 1, e);
if (this.index == this.world.components.len) return null;
var match = false;
while (!match) {
if (this.index == this.world.components.len) return null;
this.lastComponent = this.world.components.get(this.index);
match = true;
inline for (fields) |f| {
const fenum = std.meta.stringToEnum(ComponentEnum, f.name) orelse unreachable;
const required = this.query.required.contains(fenum);
const excluded = this.query.excluded.contains(fenum);
const has = @field(this.lastComponent.?, f.name) != null;
if ((required and !has) or (excluded and has)) {
match = false;
break;
}
}
this.index += 1;
}
return &this.lastComponent.?;
}
};
};
}

View File

@ -1,25 +1,52 @@
const std = @import("std");
const w4 = @import("wasm4.zig"); const w4 = @import("wasm4.zig");
const ecs = @import("ecs.zig");
const assets = @import("assets");
const smiley = [8]u8{ const Vec2f = std.meta.Vector(2, f32);
0b11000011, const Pos = Vec2f;
0b10000001, const Control = enum { player };
0b00100100, const Component = struct {
0b00100100, pos: Pos,
0b00000000, control: Control,
0b00100100,
0b10011001,
0b11000011,
}; };
const World = ecs.World(Component);
const KB = 1024;
var heap: [1 * KB]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&heap);
var world: World = World.init(fba.allocator());
export fn start() void {
_ = world.create(.{ .pos = .{ 76, 76 }, .control = .player });
}
export fn update() void { export fn update() void {
w4.DRAW_COLORS.* = 2; w4.DRAW_COLORS.* = 2;
w4.text("Hello from Zig!", 10, 10); w4.text("Hello from Zig!", .{ 10, 10 });
const gamepad = w4.GAMEPAD1.*; if (w4.GAMEPAD1.button_1) {
if (gamepad & w4.BUTTON_1 != 0) {
w4.DRAW_COLORS.* = 4; w4.DRAW_COLORS.* = 4;
} }
w4.blit(&smiley, 76, 76, 8, 8, w4.BLIT_1BPP); world.process(1, &.{ .pos, .control }, controlProcess);
w4.text("Press X to blink", 16, 90); world.process(1, &.{.pos}, drawProcess);
w4.DRAW_COLORS.* = 2;
// w4.blit(&smiley, .{ 76, 76 }, .{ 8, 8 }, .{ .bpp = .b1 });
w4.text("Press X to blink", .{ 16, 90 });
}
fn drawProcess(_: f32, pos: *Pos) void {
w4.DRAW_COLORS.* = 0x0030;
w4.externs.blitSub(&assets.sprites, @floatToInt(i32, pos.*[0]), @floatToInt(i32, pos.*[1]), 8, 8, 0, 0, 128, assets.sprites_flags);
}
fn controlProcess(_: f32, pos: *Pos, control: *Control) void {
_ = control;
if (w4.GAMEPAD1.button_up) pos.*[1] -= 1;
if (w4.GAMEPAD1.button_down) pos.*[1] += 1;
if (w4.GAMEPAD1.button_left) pos.*[0] -= 1;
if (w4.GAMEPAD1.button_right) pos.*[0] += 1;
// w4.trace("here", .{});
} }

View File

@ -1,13 +1,102 @@
// //! Stolen from pfgithub's wasm4-zig repo
// WASM-4: https://wasm4.org/docs //! https://github.com/pfgithub/wasm4-zig
// const w4 = @This();
// const std = @import("std");
// Platform Constants
//
//
pub const CANVAS_SIZE: u32 = 160; /// PLATFORM CONSTANTS
pub const CANVAS_SIZE = 160;
/// Helpers
pub const Vec2 = @import("std").meta.Vector(2, i32);
pub const x = 0;
pub const y = 1;
pub fn texLen(size: Vec2) usize {
return @intCast(usize, std.math.divCeil(i32, size[x] * size[y] * 2, 8) catch unreachable);
}
pub const Mbl = enum { mut, cons };
pub fn Tex(comptime mbl: Mbl) type {
return struct {
// oh that's really annoying
// ideally there would be a way to have a readonly Tex and a mutable Tex
// and the mutable should implicit cast to readonly
data: switch (mbl) {
.mut => [*]u8,
.cons => [*]const u8,
},
size: Vec2,
pub fn wrapSlice(slice: switch (mbl) {
.mut => []u8,
.cons => []const u8,
}, size: Vec2) Tex(mbl) {
if (slice.len != texLen(size)) {
unreachable;
}
return .{
.data = slice.ptr,
.size = size,
};
}
pub fn cons(tex: Tex(.mut)) Tex(.cons) {
return .{
.data = tex.data,
.size = tex.size,
};
}
pub fn blit(dest: Tex(.mut), dest_ul: Vec2, src: Tex(.cons), src_ul: Vec2, src_wh: Vec2, remap_colors: [4]u3, scale: Vec2) void {
for (range(@intCast(usize, src_wh[y]))) |_, y_usz| {
const yp = @intCast(i32, y_usz);
for (range(@intCast(usize, src_wh[x]))) |_, x_usz| {
const xp = @intCast(i32, x_usz);
const pos = Vec2{ xp, yp };
const value = remap_colors[src.get(src_ul + pos)];
if (value <= std.math.maxInt(u2)) {
dest.rect(pos * scale + dest_ul, scale, @intCast(u2, value));
}
}
}
}
pub fn rect(dest: Tex(.mut), ul: Vec2, wh: Vec2, color: u2) void {
for (range(std.math.lossyCast(usize, wh[y]))) |_, y_usz| {
const yp = @intCast(i32, y_usz);
for (range(std.math.lossyCast(usize, wh[x]))) |_, x_usz| {
const xp = @intCast(i32, x_usz);
dest.set(ul + Vec2{ xp, yp }, color);
}
}
}
pub fn get(tex: Tex(mbl), pos: Vec2) u2 {
if (@reduce(.Or, pos < w4.Vec2{ 0, 0 })) return 0;
if (@reduce(.Or, pos >= tex.size)) return 0;
const index_unscaled = pos[w4.x] + (pos[w4.y] * tex.size[w4.x]);
const index = @intCast(usize, @divFloor(index_unscaled, 4));
const byte_idx = @intCast(u3, (@mod(index_unscaled, 4)) * 2);
return @truncate(u2, tex.data[index] >> byte_idx);
}
pub fn set(tex: Tex(.mut), pos: Vec2, value: u2) void {
if (@reduce(.Or, pos < w4.Vec2{ 0, 0 })) return;
if (@reduce(.Or, pos >= tex.size)) return;
const index_unscaled = pos[w4.x] + (pos[w4.y] * tex.size[w4.x]);
const index = @intCast(usize, @divFloor(index_unscaled, 4));
const byte_idx = @intCast(u3, (@mod(index_unscaled, 4)) * 2);
tex.data[index] &= ~(@as(u8, 0b11) << byte_idx);
tex.data[index] |= @as(u8, value) << byte_idx;
}
};
}
pub fn range(len: usize) []const void {
return @as([*]const void, &[_]void{})[0..len];
}
// pub const Tex1BPP = struct {};
// //
// //
@ -17,26 +106,71 @@ pub const CANVAS_SIZE: u32 = 160;
pub const PALETTE: *[4]u32 = @intToPtr(*[4]u32, 0x04); pub const PALETTE: *[4]u32 = @intToPtr(*[4]u32, 0x04);
pub const DRAW_COLORS: *u16 = @intToPtr(*u16, 0x14); pub const DRAW_COLORS: *u16 = @intToPtr(*u16, 0x14);
pub const GAMEPAD1: *const u8 = @intToPtr(*const u8, 0x16); pub const GAMEPAD1: *const Gamepad = @intToPtr(*const Gamepad, 0x16);
pub const GAMEPAD2: *const u8 = @intToPtr(*const u8, 0x17); pub const GAMEPAD2: *const Gamepad = @intToPtr(*const Gamepad, 0x17);
pub const GAMEPAD3: *const u8 = @intToPtr(*const u8, 0x18); pub const GAMEPAD3: *const Gamepad = @intToPtr(*const Gamepad, 0x18);
pub const GAMEPAD4: *const u8 = @intToPtr(*const u8, 0x19); pub const GAMEPAD4: *const Gamepad = @intToPtr(*const Gamepad, 0x19);
pub const MOUSE_X: *const i16 = @intToPtr(*const i16, 0x1a);
pub const MOUSE_Y: *const i16 = @intToPtr(*const i16, 0x1c);
pub const MOUSE_BUTTONS: *const u8 = @intToPtr(*const u8, 0x1e);
pub const SYSTEM_FLAGS: *u8 = @intToPtr(*u8, 0x1f);
pub const FRAMEBUFFER: *[6400]u8 = @intToPtr(*[6400]u8, 0xA0);
pub const BUTTON_1: u8 = 1; pub const MOUSE: *const Mouse = @intToPtr(*const Mouse, 0x1a);
pub const BUTTON_2: u8 = 2; pub const SYSTEM_FLAGS: *SystemFlags = @intToPtr(*SystemFlags, 0x1f);
pub const BUTTON_LEFT: u8 = 16; pub const FRAMEBUFFER: *[CANVAS_SIZE * CANVAS_SIZE / 4]u8 = @intToPtr(*[6400]u8, 0xA0);
pub const BUTTON_RIGHT: u8 = 32; pub const ctx = Tex(.mut){
pub const BUTTON_UP: u8 = 64; .data = @intToPtr([*]u8, 0xA0), // apparently casting *[N]u8 to [*]u8 at comptime causes a compiler crash
pub const BUTTON_DOWN: u8 = 128; .size = .{ CANVAS_SIZE, CANVAS_SIZE },
};
pub const MOUSE_LEFT: u8 = 1; pub const Gamepad = packed struct {
pub const MOUSE_RIGHT: u8 = 2; button_1: bool,
pub const MOUSE_MIDDLE: u8 = 4; button_2: bool,
_: u2 = 0,
button_left: bool,
button_right: bool,
button_up: bool,
button_down: bool,
comptime {
if (@sizeOf(@This()) != @sizeOf(u8)) unreachable;
}
pub fn format(value: @This(), comptime _: []const u8, _: @import("std").fmt.FormatOptions, writer: anytype) !void {
if (value.button_1) try writer.writeAll("1");
if (value.button_2) try writer.writeAll("2");
if (value.button_left) try writer.writeAll("<"); //"");
if (value.button_right) try writer.writeAll(">");
if (value.button_up) try writer.writeAll("^");
if (value.button_down) try writer.writeAll("v");
}
};
pub const Mouse = packed struct {
x: i16,
y: i16,
buttons: MouseButtons,
pub fn pos(mouse: Mouse) Vec2 {
return .{ mouse.x, mouse.y };
}
comptime {
if (@sizeOf(@This()) != 5) unreachable;
}
};
pub const MouseButtons = packed struct {
left: bool,
right: bool,
middle: bool,
_: u5 = 0,
comptime {
if (@sizeOf(@This()) != @sizeOf(u8)) unreachable;
}
};
pub const SystemFlags = packed struct {
preserve_framebuffer: bool,
hide_gamepad_overlay: bool,
_: u6 = 0,
comptime {
if (@sizeOf(@This()) != @sizeOf(u8)) unreachable;
}
};
pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1; pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1;
pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2;
@ -47,38 +181,68 @@ pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2;
// //
// //
pub const externs = struct {
pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, flags: u32) void;
pub extern fn blitSub(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, src_x: u32, src_y: u32, strie: i32, flags: u32) void;
pub extern fn line(x1: i32, y1: i32, x2: i32, y2: i32) void;
pub extern fn oval(x: i32, y: i32, width: i32, height: i32) void;
pub extern fn rect(x: i32, y: i32, width: i32, height: i32) void;
pub extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: i32, y: i32) void;
/// Draws a vertical line
extern fn vline(x: i32, y: i32, len: u32) void;
/// Draws a horizontal line
extern fn hline(x: i32, y: i32, len: u32) void;
pub extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void;
};
/// Copies pixels to the framebuffer. /// Copies pixels to the framebuffer.
pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, flags: u32) void; pub fn blit(sprite: []const u8, pos: Vec2, size: Vec2, flags: BlitFlags) void {
if (sprite.len * 8 != size[x] * size[y]) unreachable;
externs.blit(sprite.ptr, pos[x], pos[y], size[x], size[y], @bitCast(u32, flags));
}
/// Copies a subregion within a larger sprite atlas to the framebuffer. /// Copies a subregion within a larger sprite atlas to the framebuffer.
pub extern fn blitSub(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, src_x: u32, src_y: u32, stride: i32, flags: u32) void; pub fn blitSub(sprite: []const u8, pos: Vec2, size: Vec2, src: Vec2, strie: i32, flags: BlitFlags) void {
if (sprite.len * 8 >= size[x] * size[y]) trace("Sprite not large enough {}", .{sprite.len});
externs.blitSub(sprite.ptr, pos[x], pos[y], size[x], size[y], @intCast(u32, src[x]), @intCast(u32, src[y]), strie, @bitCast(u32, flags));
}
pub const BLIT_2BPP: u32 = 1; pub const BlitFlags = packed struct {
pub const BLIT_1BPP: u32 = 0; bpp: enum(u1) {
pub const BLIT_FLIP_X: u32 = 2; b1,
pub const BLIT_FLIP_Y: u32 = 4; b2,
pub const BLIT_ROTATE: u32 = 8; },
flip_x: bool = false,
flip_y: bool = false,
rotate: bool = false,
_: u28 = 0,
comptime {
if (@sizeOf(@This()) != @sizeOf(u32)) unreachable;
}
};
/// Draws a line between two points. /// Draws a line between two points.
pub extern fn line(x1: i32, y1: i32, x2: i32, y2: i32) void; pub fn line(pos1: Vec2, pos2: Vec2) void {
externs.line(pos1[x], pos1[y], pos2[x], pos2[y]);
}
/// Draws an oval (or circle). /// Draws an oval (or circle).
pub extern fn oval(x: i32, y: i32, width: i32, height: i32) void; pub fn oval(ul: Vec2, size: Vec2) void {
externs.oval(ul[x], ul[y], size[x], size[y]);
}
/// Draws a rectangle. /// Draws a rectangle.
pub extern fn rect(x: i32, y: i32, width: u32, height: u32) void; pub fn rect(ul: Vec2, size: Vec2) void {
externs.rect(ul[x], ul[y], size[x], size[y]);
}
/// Draws text using the built-in system font. /// Draws text using the built-in system font.
pub fn text(str: []const u8, x: i32, y: i32) void { pub fn text(str: []const u8, pos: Vec2) void {
textUtf8(str.ptr, str.len, x, y); externs.textUtf8(str.ptr, str.len, pos[x], pos[y]);
} }
extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: i32, y: i32) void;
/// Draws a vertical line
pub extern fn vline(x: i32, y: i32, len: u32) void;
/// Draws a horizontal line
pub extern fn hline(x: i32, y: i32, len: u32) void;
// //
// //
@ -87,16 +251,51 @@ pub extern fn hline(x: i32, y: i32, len: u32) void;
// //
/// Plays a sound tone. /// Plays a sound tone.
pub extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void; pub fn tone(frequency: ToneFrequency, duration: ToneDuration, volume: u32, flags: ToneFlags) void {
return externs.tone(@bitCast(u32, frequency), @bitCast(u32, duration), volume, @bitCast(u8, flags));
}
pub const ToneFrequency = packed struct {
start: u16,
end: u16 = 0,
pub const TONE_PULSE1: u32 = 0; comptime {
pub const TONE_PULSE2: u32 = 1; if (@sizeOf(@This()) != @sizeOf(u32)) unreachable;
pub const TONE_TRIANGLE: u32 = 2; }
pub const TONE_NOISE: u32 = 3; };
pub const TONE_MODE1: u32 = 0;
pub const TONE_MODE2: u32 = 4; pub const ToneDuration = packed struct {
pub const TONE_MODE3: u32 = 8; sustain: u8 = 0,
pub const TONE_MODE4: u32 = 12; release: u8 = 0,
decay: u8 = 0,
attack: u8 = 0,
comptime {
if (@sizeOf(@This()) != @sizeOf(u32)) unreachable;
}
};
pub const ToneFlags = packed struct {
pub const Channel = enum(u2) {
pulse1,
pulse2,
triangle,
noise,
};
pub const Mode = enum(u2) {
p12_5,
p25,
p50,
p75,
};
channel: Channel,
mode: Mode = .p12_5,
_: u4 = 0,
comptime {
if (@sizeOf(@This()) != @sizeOf(u8)) unreachable;
}
};
// //
// //
@ -117,10 +316,25 @@ pub extern fn diskw(src: [*]const u8, size: u32) u32;
// //
/// Prints a message to the debug console. /// Prints a message to the debug console.
pub fn trace(x: []const u8) void { /// Disabled in release builds.
traceUtf8(x.ptr, x.len); pub fn trace(comptime fmt: []const u8, args: anytype) void {
if (@import("builtin").mode != .Debug) @compileError("trace not allowed in release builds.");
// stack size is [8192]u8
var buffer: [100]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buffer);
const writer = fbs.writer();
writer.print(fmt, args) catch {
const err_msg = switch (@import("builtin").mode) {
.Debug => "[trace err] " ++ fmt,
else => "[trace err]", // max 100 bytes in trace message.
};
return traceUtf8(err_msg, err_msg.len);
};
traceUtf8(&buffer, fbs.pos);
} }
extern fn traceUtf8(strPtr: [*]const u8, strLen: usize) void; extern fn traceUtf8(str_ptr: [*]const u8, str_len: usize) void;
/// Use with caution, as there's no compile-time type checking. /// Use with caution, as there's no compile-time type checking.
/// ///