Skip to content

Commit 713801e

Browse files
Add event loop and handle JS async code
*Async JS* For now only callback style is handled (Promises is planned later). We use persistent handle on v8 JS callback call after retrieving the event from the kernel, has the parent JS function is finished and therefore local handles are already garbage collected by v8. * Event Loop* We do not use the event loop provided in Zig stdlib but instead Tigerbeetle IO (https://github.com/tigerbeetledb/tigerbeetle/tree/main/src/io). The main reason is to have a strictly single-threaded event loop, see ziglang/zig#1908. In addition the desing of Tigerbeetle IO based on io_uring (for Linux, with wrapper around kqueue for MacOS), seems to be the right direction for IO. Our loop provides callback style native APIs. Async/await style native API are not planned until zig self-hosted compiler (stage2) support concurrent features (see ziglang/zig#6025). Signed-off-by: Francis Bouvier <[email protected]>
1 parent 57d5f69 commit 713801e

14 files changed

+687
-131
lines changed

build.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub fn build(b: *std.build.Builder) !void {
1010
exe.setBuildMode(mode);
1111
exe.single_threaded = true;
1212
try linkV8(exe, mode);
13+
addDeps(exe);
1314
if (mode == .ReleaseSafe) {
1415
// remove debug info
1516
// TODO: check if mandatory in release-safe
@@ -33,11 +34,22 @@ pub fn build(b: *std.build.Builder) !void {
3334
test_exe.setBuildMode(mode);
3435
test_exe.single_threaded = true;
3536
try linkV8(test_exe, mode);
37+
addDeps(test_exe);
3638

3739
const test_step = b.step("test", "Run unit tests");
3840
test_step.dependOn(&test_exe.step);
3941
}
4042

43+
fn addDeps(step: *std.build.LibExeObjStep) void {
44+
// tigerbeetle IO loop
45+
// incompatible stage1 and stage2 versions
46+
if (step.builder.use_stage1 != null and step.builder.use_stage1.?) {
47+
step.addPackagePath("tigerbeetle-io", "deps/tigerbeetle-io/io_stage1.zig");
48+
} else {
49+
step.addPackagePath("tigerbeetle-io", "deps/tigerbeetle-io/io.zig");
50+
}
51+
}
52+
4153
fn linkV8(step: *std.build.LibExeObjStep, mode: std.builtin.Mode) !void {
4254
const mode_str: []const u8 = if (mode == .Debug) "debug" else "release";
4355
// step.linkLibC(); // TODO: do we need to link libc?

src/callback.zig

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
const std = @import("std");
2+
3+
const v8 = @import("v8"); // TODO: remove
4+
5+
const refl = @import("reflect.zig");
6+
const utils = @import("utils.zig");
7+
const Loop = @import("loop.zig").SingleThreaded;
8+
9+
// TODO: Make this JS engine agnostic
10+
// by providing a common interface
11+
12+
pub const Arg = struct {
13+
// TODO: it's required to have a non-empty struct
14+
// otherwise LLVM emits a warning
15+
// "stack frame size (x) exceeds limit (y)"
16+
foo: bool = false,
17+
};
18+
19+
// TODO: set the correct "this" on Func object
20+
// see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#the_this_problem
21+
// should we use:
22+
// - the context globals?
23+
// - null?
24+
// - the calling function (info.getThis)?
25+
26+
pub const FuncSync = struct {
27+
js_func: v8.Function,
28+
js_args: []v8.Value,
29+
isolate: v8.Isolate,
30+
31+
pub fn init(
32+
alloc: std.mem.Allocator,
33+
comptime func: refl.Func,
34+
info: v8.FunctionCallbackInfo,
35+
isolate: v8.Isolate,
36+
) !FuncSync {
37+
38+
// retrieve callback arguments indexes
39+
// TODO: Should we do that at reflection?
40+
comptime var js_args_indexes: [func.args_callback_nb]usize = undefined;
41+
comptime var x: usize = 0;
42+
inline for (func.args) |arg, i| {
43+
if (arg.T == Arg) {
44+
js_args_indexes[x] = i;
45+
x += 1;
46+
}
47+
}
48+
49+
// retrieve callback arguments
50+
// var js_args: [func.args_callback_nb]v8.Value = undefined;
51+
var js_args = try alloc.alloc(v8.Value, func.args_callback_nb);
52+
for (js_args_indexes) |index, i| {
53+
js_args[i] = info.getArg(@intCast(u32, index - func.index_offset));
54+
}
55+
56+
// retrieve callback function
57+
const js_func_index = func.callback_index.? - func.index_offset - 1; // -1 because of self
58+
const js_func_val = info.getArg(js_func_index);
59+
if (!js_func_val.isFunction()) {
60+
return error.JSWrongType;
61+
}
62+
const js_func = js_func_val.castTo(v8.Function);
63+
64+
return FuncSync{
65+
.js_func = js_func,
66+
.js_args = js_args,
67+
.isolate = isolate,
68+
};
69+
}
70+
71+
pub fn call(self: FuncSync, alloc: std.mem.Allocator) void {
72+
73+
// retrieve context
74+
// NOTE: match the Func.call implementation
75+
const ctx = self.isolate.getCurrentContext();
76+
77+
// retrieve JS this from persistent handle
78+
// TODO: see correct "this" comment above
79+
const this = ctx.getGlobal();
80+
81+
// execute function
82+
_ = self.js_func.call(ctx, this, self.js_args);
83+
84+
// free heap
85+
alloc.free(self.js_args);
86+
}
87+
};
88+
89+
const PersistentFunction = v8.Persistent(v8.Function);
90+
const PersistentValue = v8.Persistent(v8.Value);
91+
92+
pub const Func = struct {
93+
94+
// NOTE: we use persistent handles here
95+
// to ensure the references are not garbage collected
96+
// at the end of the JS calling function execution.
97+
js_func_pers: *PersistentFunction,
98+
js_args_pers: []PersistentValue,
99+
100+
isolate: v8.Isolate,
101+
102+
pub fn init(
103+
// NOTE: we need to store the JS callback arguments on the heap
104+
// as the call method will be executed in another stack frame,
105+
// once the asynchronous operation will be fetched back from the kernel.
106+
alloc: std.mem.Allocator,
107+
comptime func: refl.Func,
108+
info: v8.FunctionCallbackInfo,
109+
isolate: v8.Isolate,
110+
) !Func {
111+
112+
// retrieve callback arguments indexes
113+
// TODO: Should we do that at reflection?
114+
comptime var js_args_indexes: [func.args_callback_nb]usize = undefined;
115+
comptime var x: usize = 0;
116+
inline for (func.args) |arg, i| {
117+
if (arg.T == Arg) {
118+
js_args_indexes[x] = i;
119+
x += 1;
120+
}
121+
}
122+
123+
// retrieve callback arguments
124+
var js_args_pers = try alloc.alloc(PersistentValue, func.args_callback_nb);
125+
for (js_args_indexes) |index, i| {
126+
const js_arg = info.getArg(@intCast(u32, index - func.index_offset));
127+
const js_arg_pers = PersistentValue.init(isolate, js_arg);
128+
js_args_pers[i] = js_arg_pers;
129+
}
130+
131+
// retrieve callback function
132+
const js_func_index = func.callback_index.? - func.index_offset - 1; // -1 because of self
133+
const js_func_val = info.getArg(js_func_index);
134+
if (!js_func_val.isFunction()) {
135+
return error.JSWrongType;
136+
}
137+
const js_func = js_func_val.castTo(v8.Function);
138+
139+
// const js_func_pers = PersistentFunction.init(isolate, js_func);
140+
var js_func_pers = try alloc.create(PersistentFunction);
141+
js_func_pers.* = PersistentFunction.init(isolate, js_func);
142+
143+
return Func{
144+
.js_func_pers = js_func_pers,
145+
.js_args_pers = js_args_pers,
146+
.isolate = isolate,
147+
};
148+
}
149+
150+
fn deinit(self: Func, alloc: std.mem.Allocator) void {
151+
// cleanup persistent references in v8
152+
var js_func_pers = self.js_func_pers; // TODO: why do we need var here?
153+
js_func_pers.deinit();
154+
155+
for (self.js_args_pers) |arg| {
156+
var arg_pers = arg; // TODO: why do we need var here?
157+
arg_pers.deinit();
158+
}
159+
160+
// free heap
161+
alloc.free(self.js_args_pers);
162+
alloc.destroy(self.js_func_pers);
163+
}
164+
165+
pub fn call(self: Func, alloc: std.mem.Allocator) !void {
166+
defer self.deinit(alloc);
167+
168+
// retrieve context
169+
// TODO: should we instead store the original context in the Func object?
170+
// in this case we need to have a permanent handle (Global ?) on it.
171+
const ctx = self.isolate.getCurrentContext();
172+
173+
// retrieve JS function from persistent handle
174+
const js_func = self.js_func_pers.castToFunction();
175+
176+
// retrieve JS arguments from persistent handle
177+
const js_args = try alloc.alloc(v8.Value, self.js_args_pers.len);
178+
defer alloc.free(js_args);
179+
for (self.js_args_pers) |arg, i| {
180+
js_args[i] = arg.toValue();
181+
}
182+
183+
// retrieve JS "this" from persistent handle
184+
// TODO: see correct "this" comment above
185+
const this = ctx.getGlobal();
186+
187+
// execute function
188+
const result = js_func.call(ctx, this, js_args);
189+
if (result == null) {
190+
return error.JSCallback;
191+
}
192+
}
193+
};

src/cbk_test.zig

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,55 @@ const v8 = @import("v8");
44
const utils = @import("utils.zig");
55
const gen = @import("generate.zig");
66
const eng = @import("engine.zig");
7+
const Loop = @import("loop.zig").SingleThreaded;
8+
9+
const u64Num = @import("types.zig").u64Num;
10+
const cbk = @import("callback.zig");
11+
712
const tests = @import("test_utils.zig");
813

14+
const Console = @import("console.zig").Console;
15+
916
const Window = struct {
10-
// value: u32,
17+
pub fn constructor() Window {
18+
return Window{};
19+
}
1120

12-
const Callback = @import("types.zig").Callback;
13-
const CallbackArg = @import("types.zig").CallbackArg;
21+
pub fn cbkSyncWithoutArg(_: Window, _: cbk.FuncSync) void {
22+
tests.sleep(1 * std.time.ns_per_ms);
23+
}
1424

15-
pub fn constructor(_: u32) Window {
16-
return Window{};
25+
pub fn cbkSyncWithArg(_: Window, _: cbk.FuncSync, _: cbk.Arg) void {
26+
tests.sleep(1 * std.time.ns_per_ms);
1727
}
1828

19-
pub fn cbkSyncWithoutArg(_: Window, _: Callback) void {
20-
// TODO: handle async
21-
std.time.sleep(1 * std.time.ns_per_ms);
29+
pub fn cbkAsync(_: Window, loop: *Loop, callback: cbk.Func, milliseconds: u32) void {
30+
const n = @intCast(u63, milliseconds);
31+
// TODO: check this value can be holded in u63
32+
loop.timeout(n * std.time.ns_per_ms, callback);
2233
}
2334

24-
pub fn cbkSyncWithArg(_: Window, _: Callback, _: CallbackArg) void {
25-
// TODO: handle async
26-
std.time.sleep(1 * std.time.ns_per_ms);
35+
pub fn cbkAsyncWithArg(
36+
_: Window,
37+
loop: *Loop,
38+
callback: cbk.Func,
39+
milliseconds: u32,
40+
_: cbk.Arg,
41+
) void {
42+
const n = @intCast(u63, milliseconds);
43+
// TODO: check this value can be holded in u63
44+
loop.timeout(n * std.time.ns_per_ms, callback);
2745
}
2846
};
2947

3048
// generate API, comptime
3149
pub fn generate() []gen.API {
32-
return gen.compile(.{Window});
50+
return gen.compile(.{ Console, Window });
3351
}
3452

3553
// exec tests
3654
pub fn exec(
55+
loop: *Loop,
3756
isolate: v8.Isolate,
3857
globals: v8.ObjectTemplate,
3958
tpls: []gen.ProtoTpl,
@@ -45,11 +64,21 @@ pub fn exec(
4564
context.enter();
4665
defer context.exit();
4766

67+
// console
68+
_ = try eng.createV8Object(
69+
utils.allocator,
70+
isolate,
71+
context,
72+
context.getGlobal(),
73+
tpls[0].tpl,
74+
apis[0].T_refl,
75+
);
76+
4877
// constructor
4978
const case_cstr = [_]tests.Case{
50-
.{ .src = "let window = new Window(0);", .ex = "undefined" },
79+
.{ .src = "let window = new Window();", .ex = "undefined" },
5180
};
52-
try tests.checkCases(utils.allocator, isolate, context, case_cstr.len, case_cstr);
81+
try tests.checkCases(loop, utils.allocator, isolate, context, case_cstr.len, case_cstr);
5382

5483
// cbkSyncWithoutArg
5584
const cases_cbk_sync_without_arg = [_]tests.Case{
@@ -73,9 +102,9 @@ pub fn exec(
73102
},
74103
.{ .src = "m;", .ex = "2" },
75104
};
76-
try tests.checkCases(utils.allocator, isolate, context, cases_cbk_sync_without_arg.len, cases_cbk_sync_without_arg);
105+
try tests.checkCases(loop, utils.allocator, isolate, context, cases_cbk_sync_without_arg.len, cases_cbk_sync_without_arg);
77106

78-
// cbkSyncWithoutArg
107+
// cbkSyncWithArg
79108
const cases_cbk_sync_with_arg = [_]tests.Case{
80109
// traditional anonymous function
81110
.{
@@ -97,7 +126,63 @@ pub fn exec(
97126
},
98127
.{ .src = "y;", .ex = "3" },
99128
};
100-
try tests.checkCases(utils.allocator, isolate, context, cases_cbk_sync_with_arg.len, cases_cbk_sync_with_arg);
129+
try tests.checkCases(loop, utils.allocator, isolate, context, cases_cbk_sync_with_arg.len, cases_cbk_sync_with_arg);
130+
131+
// cbkAsync
132+
const cases_cbk_async = [_]tests.Case{
133+
// traditional anonymous function
134+
.{
135+
.src =
136+
\\let o = 1;
137+
\\function f() {
138+
\\o++;
139+
\\if (o != 2) {throw Error('cases_cbk_async error: o is not equal to 2');}
140+
\\};
141+
\\window.cbkAsync(f, 300); // 0.3 second
142+
,
143+
.ex = "undefined",
144+
},
145+
// arrow functional
146+
.{
147+
.src =
148+
\\let p = 1;
149+
\\window.cbkAsync(() => {
150+
\\p++;
151+
\\if (p != 2) {throw Error('cases_cbk_async error: p is not equal to 2');}
152+
\\}, 300); // 0.3 second
153+
,
154+
.ex = "undefined",
155+
},
156+
};
157+
try tests.checkCases(loop, utils.allocator, isolate, context, cases_cbk_async.len, cases_cbk_async);
158+
159+
// cbkAsyncWithArg
160+
const cases_cbk_async_with_arg = [_]tests.Case{
161+
// traditional anonymous function
162+
.{
163+
.src =
164+
\\let i = 1;
165+
\\function f(a) {
166+
\\i = i + a;
167+
\\if (i != 3) {throw Error('i is not equal to 3');}
168+
\\};
169+
\\window.cbkAsyncWithArg(f, 300, 2); // 0.3 second
170+
,
171+
.ex = "undefined",
172+
},
173+
// arrow functional
174+
.{
175+
.src =
176+
\\let j = 1;
177+
\\window.cbkAsyncWithArg((a) => {
178+
\\j = j + a;
179+
\\if (j != 3) {throw Error('j is not equal to 3');}
180+
\\}, 300, 2); // 0.3 second
181+
,
182+
.ex = "undefined",
183+
},
184+
};
185+
try tests.checkCases(loop, utils.allocator, isolate, context, cases_cbk_async_with_arg.len, cases_cbk_async_with_arg);
101186

102187
return eng.ExecOK;
103188
}

0 commit comments

Comments
 (0)