// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // 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 . const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; const log = @import("../../log.zig"); const Function = @This(); local: *const js.Local, this: ?*const v8.Object = null, handle: *const v8.Function, pub const Result = struct { stack: ?[]const u8, exception: []const u8, }; pub fn withThis(self: *const Function, value: anytype) !Function { const local = self.local; const this_obj = if (@TypeOf(value) == js.Object) value.handle else (try local.zigValueToJs(value, .{})).handle; return .{ .local = local, .this = this_obj, .handle = self.handle, }; } pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object { const local = self.local; var try_catch: js.TryCatch = undefined; try_catch.init(local); defer try_catch.deinit(); // This creates a new instance using this Function as a constructor. // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{})); const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse { caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown); return error.JsConstructorFailed; }; return .{ .local = local, .handle = handle, }; } pub fn call(self: *const Function, comptime T: type, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| { log.warn(.js, "call caught", .{ .err = err, .caught = caught }); return err; }; } pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| { log.warn(.js, "call caught", .{ .err = err, .caught = caught }); return err; }; } pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| { log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught }); return err; }; } pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T { return self._tryCallWithThis(T, self.getThis(), args, caught, .{}); } pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { return self._tryCallWithThis(T, this, args, caught, .{}); } const CallOpts = struct { rethrow: bool = false, }; fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T { caught.* = .{}; const local = self.local; // When we're calling a function from within JavaScript itself, this isn't // necessary. We're within a Caller instantiation, which will already have // incremented the call_depth and it won't decrement it until the Caller is // done. // But some JS functions are initiated from Zig code, and not v8. For // example, Observers, some event and window callbacks. In those cases, we // need to increase the call_depth so that the call_arena remains valid for // the duration of the function call. If we don't do this, the call_arena // will be reset after each statement of the function which executes Zig code. const ctx = local.ctx; const call_depth = ctx.call_depth; ctx.call_depth = call_depth + 1; defer ctx.call_depth = call_depth; const js_this = blk: { if (@TypeOf(this) == js.Object) { break :blk this; } break :blk try local.zigValueToJs(this, .{}); }; const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) { .@"struct" => |s| blk: { const fields = s.fields; var js_args: [fields.len]*const v8.Value = undefined; inline for (fields, 0..) |f, i| { js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle; } const cargs: [fields.len]*const v8.Value = js_args; break :blk &cargs; }, .pointer => blk: { var values = try local.call_arena.alloc(*const v8.Value, args.len); for (args, 0..) |a, i| { values[i] = (try local.zigValueToJs(a, .{})).handle; } break :blk values; }, else => @compileError("JS Function called with invalid paremter type"), }; const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr)); var try_catch: js.TryCatch = undefined; try_catch.init(local); defer try_catch.deinit(); const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse { if ((comptime opts.rethrow) and try_catch.hasCaught()) { try_catch.rethrow(); return error.TryCatchRethrow; } caught.* = try_catch.caughtOrError(local.call_arena, error.JsException); return error.JsException; }; if (@typeInfo(T) == .void) { return {}; } return local.jsValueToZig(T, .{ .local = local, .handle = handle }); } fn getThis(self: *const Function) js.Object { const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?; return .{ .local = self.local, .handle = handle, }; } pub fn src(self: *const Function) ![]const u8 { return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{}); } pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value { const local = self.local; const key = local.isolate.initStringHandle(name); const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse { return error.JsException; }; return .{ .local = local, .handle = handle, }; } pub fn persist(self: *const Function) !Global { return self._persist(true); } pub fn temp(self: *const Function) !Temp { return self._persist(false); } fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) { var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { try ctx.trackGlobal(global); return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); return .{ .handle = global, .temps = &ctx.identity.temps }; } pub fn tempWithThis(self: *const Function, value: anytype) !Temp { const with_this = try self.withThis(value); return with_this.temp(); } pub fn persistWithThis(self: *const Function, value: anytype) !Global { const with_this = try self.withThis(value); return with_this.persist(); } pub const Temp = G(.temp); pub const Global = G(.global); const GlobalType = enum(u8) { temp, global, }; fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void, const Self = @This(); pub fn deinit(self: *Self) void { v8.v8__Global__Reset(&self.handle); } pub fn local(self: *const Self, l: *const js.Local) Function { return .{ .local = l, .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } pub fn isEqual(self: *const Self, other: Function) bool { return v8.v8__Global__IsEqual(&self.handle, other.handle); } pub fn release(self: *const Self) void { if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| { var g = kv.value; v8.v8__Global__Reset(&g); } } }; }