replace zig-js-runtime

This commit is contained in:
Karl Seguin
2025-04-02 10:30:59 +08:00
parent 25dcae7648
commit b8d7744563
88 changed files with 5933 additions and 4124 deletions

229
src/runtime/generate.zig Normal file
View File

@@ -0,0 +1,229 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// ----
const Type = std.builtin.Type;
// Union
// -----
// Generate a flatten tagged Union from a Tuple
pub fn Union(comptime interfaces: anytype) type {
// @setEvalBranchQuota(10000);
const tuple = Tuple(interfaces){};
const fields = std.meta.fields(@TypeOf(tuple));
const tag_type = switch (fields.len) {
0 => unreachable,
1 => u0,
2 => u1,
3...4 => u2,
5...8 => u3,
9...16 => u4,
17...32 => u5,
33...64 => u6,
65...128 => u7,
129...256 => u8,
else => @compileError("Too many interfaces to generate union"),
};
// second iteration to generate tags
var enum_fields: [fields.len]Type.EnumField = undefined;
for (fields, 0..) |field, index| {
const member = @field(tuple, field.name);
const full_name = @typeName(member);
const separator = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse unreachable;
const name = full_name[separator + 1 ..];
enum_fields[index] = .{
.name = name ++ "",
.value = index,
};
}
const enum_info = Type.Enum{
.tag_type = tag_type,
.fields = &enum_fields,
.decls = &.{},
.is_exhaustive = true,
};
const enum_T = @Type(.{ .@"enum" = enum_info });
// third iteration to generate union type
var union_fields: [fields.len]Type.UnionField = undefined;
for (fields, enum_fields, 0..) |field, e, index| {
var FT = @field(tuple, field.name);
if (@hasDecl(FT, "Self")) {
FT = *(@field(FT, "Self"));
}
union_fields[index] = .{
.type = FT,
.name = e.name,
.alignment = @alignOf(FT),
};
}
return @Type(.{ .@"union" = .{
.layout = .auto,
.tag_type = enum_T,
.fields = &union_fields,
.decls = &.{},
} });
}
// Tuple
// -----
// Flattens and depuplicates a list of nested tuples. For example
// input: {A, B, {C, B, D}, {A, E}}
// output {A, B, C, D, E}
pub fn Tuple(comptime args: anytype) type {
@setEvalBranchQuota(100000);
const count = countInterfaces(args, 0);
var interfaces: [count]type = undefined;
_ = flattenInterfaces(args, &interfaces, 0);
const unfiltered_count, const filter_set = filterMap(count, interfaces);
var field_index: usize = 0;
var fields: [unfiltered_count]Type.StructField = undefined;
for (filter_set, 0..) |filter, i| {
if (filter) {
continue;
}
fields[field_index] = .{
.name = std.fmt.comptimePrint("{d}", .{field_index}),
.type = type,
// has to be true in order to properly capture the default value
.is_comptime = true,
.alignment = @alignOf(type),
.default_value_ptr = @ptrCast(&interfaces[i]),
};
field_index += 1;
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,
} });
}
fn countInterfaces(args: anytype, count: usize) usize {
var new_count = count;
for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
const member = @field(args, f.name);
if (@TypeOf(member) == type) {
new_count += 1;
} else {
new_count = countInterfaces(member, new_count);
}
}
return new_count;
}
fn flattenInterfaces(args: anytype, interfaces: []type, index: usize) usize {
var new_index = index;
for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
const member = @field(args, f.name);
if (@TypeOf(member) == type) {
interfaces[new_index] = member;
new_index += 1;
} else {
new_index = flattenInterfaces(member, interfaces, new_index);
}
}
return new_index;
}
fn filterMap(comptime count: usize, interfaces: [count]type) struct { usize, [count]bool } {
var map: [count]bool = undefined;
var unfiltered_count: usize = 0;
outer: for (interfaces, 0..) |iface, i| {
for (interfaces[i + 1 ..]) |check| {
if (iface == check) {
map[i] = true;
continue :outer;
}
}
map[i] = false;
unfiltered_count += 1;
}
return .{ unfiltered_count, map };
}
test "generate.Union" {
const Astruct = struct {
pub const Self = Other;
const Other = struct {};
};
const Bstruct = struct {
value: u8 = 0,
};
const Cstruct = struct {
value: u8 = 0,
};
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
const ti = @typeInfo(value).@"union";
try std.testing.expectEqual(3, ti.fields.len);
try std.testing.expectEqualStrings("*runtime.generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type));
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
try std.testing.expectEqual(Bstruct, ti.fields[1].type);
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
try std.testing.expectEqual(Cstruct, ti.fields[2].type);
try std.testing.expectEqualStrings(ti.fields[2].name, "Cstruct");
}
test "generate.Tuple" {
const Astruct = struct {};
const Bstruct = struct {
value: u8 = 0,
};
const Cstruct = struct {
value: u8 = 0,
};
{
const tuple = Tuple(.{ Astruct, Bstruct }){};
const ti = @typeInfo(@TypeOf(tuple)).@"struct";
try std.testing.expectEqual(true, ti.is_tuple);
try std.testing.expectEqual(2, ti.fields.len);
try std.testing.expectEqual(Astruct, tuple.@"0");
try std.testing.expectEqual(Bstruct, tuple.@"1");
}
{
// dedupe
const tuple = Tuple(.{ Cstruct, Astruct, .{Astruct}, Bstruct, .{ Astruct, .{ Astruct, Bstruct } } }){};
const ti = @typeInfo(@TypeOf(tuple)).@"struct";
try std.testing.expectEqual(true, ti.is_tuple);
try std.testing.expectEqual(3, ti.fields.len);
try std.testing.expectEqual(Cstruct, tuple.@"0");
try std.testing.expectEqual(Astruct, tuple.@"1");
try std.testing.expectEqual(Bstruct, tuple.@"2");
}
}

2200
src/runtime/js.zig Normal file

File diff suppressed because it is too large Load Diff

429
src/runtime/loop.zig Normal file
View File

@@ -0,0 +1,429 @@
// Copyright 2023-2024 Lightpanda (Selecy SAS)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const std = @import("std");
const MemoryPool = std.heap.MemoryPool;
pub const IO = @import("tigerbeetle-io").IO;
const JSCallback = @import("../browser/env.zig").Env.Callback;
const log = std.log.scoped(.loop);
// SingleThreaded I/O Loop based on Tigerbeetle io_uring loop.
// On Linux it's using io_uring.
// On MacOS and Windows it's using kqueue/IOCP with a ring design.
// This is a thread-unsafe version without any lock on shared resources,
// use it only on a single thread.
// The loop provides I/O APIs based on callbacks.
// I/O APIs based on async/await might be added in the future.
pub const Loop = struct {
alloc: std.mem.Allocator, // TODO: unmanaged version ?
io: IO,
js_events_nb: usize,
zig_events_nb: usize,
cbk_error: bool = false,
// js_ctx_id is incremented each time the loop is reset for JS.
// All JS callbacks store an initial js_ctx_id and compare before execution.
// If a ctx is outdated, the callback is ignored.
// This is a weak way to cancel all future JS callbacks.
js_ctx_id: u32 = 0,
// zig_ctx_id is incremented each time the loop is reset for Zig.
// All Zig callbacks store an initial zig_ctx_id and compare before execution.
// If a ctx is outdated, the callback is ignored.
// This is a weak way to cancel all future Zig callbacks.
zig_ctx_id: u32 = 0,
cancel_pool: MemoryPool(ContextCancel),
timeout_pool: MemoryPool(ContextTimeout),
event_callback_pool: MemoryPool(EventCallbackContext),
const Self = @This();
pub const Completion = IO.Completion;
pub const ConnectError = IO.ConnectError;
pub const RecvError = IO.RecvError;
pub const SendError = IO.SendError;
pub fn init(alloc: std.mem.Allocator) !Self {
return Self{
.alloc = alloc,
.io = try IO.init(32, 0),
.js_events_nb = 0,
.zig_events_nb = 0,
.cancel_pool = MemoryPool(ContextCancel).init(alloc),
.timeout_pool = MemoryPool(ContextTimeout).init(alloc),
.event_callback_pool = MemoryPool(EventCallbackContext).init(alloc),
};
}
pub fn deinit(self: *Self) void {
// run tail events. We do run the tail events to ensure all the
// contexts are correcly free.
while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) {
self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| {
log.err("deinit run tail events: {any}", .{err});
break;
};
}
self.cancelAll();
self.io.deinit();
self.cancel_pool.deinit();
self.timeout_pool.deinit();
self.event_callback_pool.deinit();
}
// Retrieve all registred I/O events completed by OS kernel,
// and execute sequentially their callbacks.
// Stops when there is no more I/O events registered on the loop.
// Note that I/O events callbacks might register more I/O events
// on the go when they are executed (ie. nested I/O events).
pub fn run(self: *Self) !void {
while (self.eventsNb(.js) > 0) {
try self.io.run_for_ns(10 * std.time.ns_per_ms);
// at each iteration we might have new events registred by previous callbacks
}
// TODO: return instead immediatly on the first JS callback error
// and let the caller decide what to do next
// (typically retrieve the exception through the TryCatch and
// continue the execution of callbacks with a new call to loop.run)
if (self.cbk_error) {
return error.JSExecCallback;
}
}
const Event = enum { js, zig };
fn eventsPtr(self: *Self, comptime event: Event) *usize {
return switch (event) {
.zig => &self.zig_events_nb,
.js => &self.js_events_nb,
};
}
// Register events atomically
// - add 1 event and return previous value
fn addEvent(self: *Self, comptime event: Event) void {
_ = @atomicRmw(usize, self.eventsPtr(event), .Add, 1, .acq_rel);
}
// - remove 1 event and return previous value
fn removeEvent(self: *Self, comptime event: Event) void {
_ = @atomicRmw(usize, self.eventsPtr(event), .Sub, 1, .acq_rel);
}
// - get the number of current events
fn eventsNb(self: *Self, comptime event: Event) usize {
return @atomicLoad(usize, self.eventsPtr(event), .seq_cst);
}
fn resetEvents(self: *Self, comptime event: Event) void {
@atomicStore(usize, self.eventsPtr(event), 0, .unordered);
}
// JS callbacks APIs
// -----------------
// Timeout
const ContextTimeout = struct {
loop: *Self,
js_cbk: ?JSCallback,
js_ctx_id: u32,
};
fn timeoutCallback(
ctx: *ContextTimeout,
completion: *IO.Completion,
result: IO.TimeoutError!void,
) void {
const loop = ctx.loop;
defer {
loop.removeEvent(.js);
loop.timeout_pool.destroy(ctx);
loop.alloc.destroy(completion);
}
// If the loop's context id has changed, don't call the js callback
// function. The callback's memory has already be cleaned and the
// events nb reset.
if (ctx.js_ctx_id != loop.js_ctx_id) return;
// TODO: return the error to the callback
result catch |err| {
switch (err) {
error.Canceled => {},
else => log.err("timeout callback: {any}", .{err}),
}
return;
};
// js callback
if (ctx.js_cbk) |*js_cbk| {
js_cbk.call(null) catch {
ctx.loop.cbk_error = true;
};
}
}
pub fn timeout(self: *Self, nanoseconds: u63, js_cbk: ?JSCallback) !usize {
const completion = try self.alloc.create(Completion);
errdefer self.alloc.destroy(completion);
completion.* = undefined;
const ctx = try self.timeout_pool.create();
errdefer self.timeout_pool.destroy(ctx);
ctx.* = ContextTimeout{
.loop = self,
.js_cbk = js_cbk,
.js_ctx_id = self.js_ctx_id,
};
self.addEvent(.js);
self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds);
return @intFromPtr(completion);
}
const ContextCancel = struct {
loop: *Self,
js_cbk: ?JSCallback,
js_ctx_id: u32,
};
fn cancelCallback(
ctx: *ContextCancel,
completion: *IO.Completion,
result: IO.CancelOneError!void,
) void {
const loop = ctx.loop;
defer {
loop.removeEvent(.js);
loop.cancel_pool.destroy(ctx);
loop.alloc.destroy(completion);
}
// If the loop's context id has changed, don't call the js callback
// function. The callback's memory has already be cleaned and the
// events nb reset.
if (ctx.js_ctx_id != loop.js_ctx_id) return;
// TODO: return the error to the callback
result catch |err| {
switch (err) {
error.NotFound => log.debug("cancel callback: {any}", .{err}),
else => log.err("cancel callback: {any}", .{err}),
}
return;
};
// js callback
if (ctx.js_cbk) |*js_cbk| {
js_cbk.call(null) catch {
ctx.loop.cbk_error = true;
};
}
}
pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void {
if (IO.supports_cancel == false) {
return;
}
const comp_cancel: *IO.Completion = @ptrFromInt(id);
const completion = try self.alloc.create(Completion);
errdefer self.alloc.destroy(completion);
completion.* = undefined;
const ctx = self.alloc.create(ContextCancel) catch unreachable;
ctx.* = ContextCancel{
.loop = self,
.js_cbk = js_cbk,
.js_ctx_id = self.js_ctx_id,
};
self.addEvent(.js);
self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel);
}
fn cancelAll(self: *Self) void {
self.resetEvents(.js);
self.resetEvents(.zig);
self.io.cancel_all();
}
// Reset all existing JS callbacks.
pub fn resetJS(self: *Self) void {
self.js_ctx_id += 1;
}
// Reset all existing Zig callbacks.
pub fn resetZig(self: *Self) void {
self.zig_ctx_id += 1;
}
// IO callbacks APIs
// -----------------
// Connect
pub fn connect(
self: *Self,
comptime Ctx: type,
ctx: *Ctx,
completion: *Completion,
comptime cbk: fn (ctx: *Ctx, _: *Completion, res: ConnectError!void) void,
socket: std.posix.socket_t,
address: std.net.Address,
) !void {
const onConnect = struct {
fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void {
defer callback.loop.event_callback_pool.destroy(callback);
callback.loop.removeEvent(.js);
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
}
}.onConnect;
const callback = try self.event_callback_pool.create();
errdefer self.event_callback_pool.destroy(callback);
callback.* = .{ .loop = self, .ctx = ctx };
self.addEvent(.js);
self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address);
}
// Send
pub fn send(
self: *Self,
comptime Ctx: type,
ctx: *Ctx,
completion: *Completion,
comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: SendError!usize) void,
socket: std.posix.socket_t,
buf: []const u8,
) !void {
const onSend = struct {
fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void {
defer callback.loop.event_callback_pool.destroy(callback);
callback.loop.removeEvent(.js);
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
}
}.onSend;
const callback = try self.event_callback_pool.create();
errdefer self.event_callback_pool.destroy(callback);
callback.* = .{ .loop = self, .ctx = ctx };
self.addEvent(.js);
self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf);
}
// Recv
pub fn recv(
self: *Self,
comptime Ctx: type,
ctx: *Ctx,
completion: *Completion,
comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: RecvError!usize) void,
socket: std.posix.socket_t,
buf: []u8,
) !void {
const onRecv = struct {
fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void {
defer callback.loop.event_callback_pool.destroy(callback);
callback.loop.removeEvent(.js);
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
}
}.onRecv;
const callback = try self.event_callback_pool.create();
errdefer self.event_callback_pool.destroy(callback);
callback.* = .{ .loop = self, .ctx = ctx };
self.addEvent(.js);
self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf);
}
// Zig timeout
const ContextZigTimeout = struct {
loop: *Self,
zig_ctx_id: u32,
context: *anyopaque,
callback: *const fn (
context: ?*anyopaque,
) void,
};
fn zigTimeoutCallback(
ctx: *ContextZigTimeout,
completion: *IO.Completion,
result: IO.TimeoutError!void,
) void {
const loop = ctx.loop;
defer {
loop.removeEvent(.zig);
loop.alloc.destroy(ctx);
loop.alloc.destroy(completion);
}
// If the loop's context id has changed, don't call the js callback
// function. The callback's memory has already be cleaned and the
// events nb reset.
if (ctx.zig_ctx_id != loop.zig_ctx_id) return;
result catch |err| {
switch (err) {
error.Canceled => {},
else => log.err("zig timeout callback: {any}", .{err}),
}
return;
};
// callback
ctx.callback(ctx.context);
}
// zigTimeout performs a timeout but the callback is a zig function.
pub fn zigTimeout(
self: *Self,
nanoseconds: u63,
comptime Context: type,
context: Context,
comptime callback: fn (context: Context) void,
) void {
const completion = self.alloc.create(IO.Completion) catch unreachable;
completion.* = undefined;
const ctxtimeout = self.alloc.create(ContextZigTimeout) catch unreachable;
ctxtimeout.* = ContextZigTimeout{
.loop = self,
.zig_ctx_id = self.zig_ctx_id,
.context = context,
.callback = struct {
fn wrapper(ctx: ?*anyopaque) void {
callback(@ptrCast(@alignCast(ctx)));
}
}.wrapper,
};
self.addEvent(.zig);
self.io.timeout(*ContextZigTimeout, ctxtimeout, zigTimeoutCallback, completion, nanoseconds);
}
};
const EventCallbackContext = struct {
ctx: *anyopaque,
loop: *Loop,
};