Files
browser/src/browser/js/Value.zig
Karl Seguin c3c465347d Provide a failing callback to ValueSerializer for host objects
V8 needs our help when serializing host (e.g. a Zig dom instance) objects. We
don't currently have this implemented, so this provides the callback that throws
an error. Our wrapper returns Nothing when no callback is provided, which v8
doesn't allow (via assertion).
2026-03-31 16:13:55 +08:00

413 lines
12 KiB
Zig

// Copyright (C) 2023-2025 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 js = @import("js.zig");
const SSO = @import("../../string.zig").String;
const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
const Value = @This();
local: *const js.Local,
handle: *const v8.Value,
pub fn isObject(self: Value) bool {
return v8.v8__Value__IsObject(self.handle);
}
pub fn isString(self: Value) ?js.String {
const handle = self.handle;
if (!v8.v8__Value__IsString(handle)) {
return null;
}
return .{ .local = self.local, .handle = @ptrCast(handle) };
}
pub fn isArray(self: Value) bool {
return v8.v8__Value__IsArray(self.handle);
}
pub fn isSymbol(self: Value) bool {
return v8.v8__Value__IsSymbol(self.handle);
}
pub fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
pub fn isNull(self: Value) bool {
return v8.v8__Value__IsNull(self.handle);
}
pub fn isUndefined(self: Value) bool {
return v8.v8__Value__IsUndefined(self.handle);
}
pub fn isNullOrUndefined(self: Value) bool {
return v8.v8__Value__IsNullOrUndefined(self.handle);
}
pub fn isNumber(self: Value) bool {
return v8.v8__Value__IsNumber(self.handle);
}
pub fn isNumberObject(self: Value) bool {
return v8.v8__Value__IsNumberObject(self.handle);
}
pub fn isInt32(self: Value) bool {
return v8.v8__Value__IsInt32(self.handle);
}
pub fn isUint32(self: Value) bool {
return v8.v8__Value__IsUint32(self.handle);
}
pub fn isBigInt(self: Value) bool {
return v8.v8__Value__IsBigInt(self.handle);
}
pub fn isBigIntObject(self: Value) bool {
return v8.v8__Value__IsBigIntObject(self.handle);
}
pub fn isBoolean(self: Value) bool {
return v8.v8__Value__IsBoolean(self.handle);
}
pub fn isBooleanObject(self: Value) bool {
return v8.v8__Value__IsBooleanObject(self.handle);
}
pub fn isTrue(self: Value) bool {
return v8.v8__Value__IsTrue(self.handle);
}
pub fn isFalse(self: Value) bool {
return v8.v8__Value__IsFalse(self.handle);
}
pub fn isTypedArray(self: Value) bool {
return v8.v8__Value__IsTypedArray(self.handle);
}
pub fn isArrayBufferView(self: Value) bool {
return v8.v8__Value__IsArrayBufferView(self.handle);
}
pub fn isArrayBuffer(self: Value) bool {
return v8.v8__Value__IsArrayBuffer(self.handle);
}
pub fn isUint8Array(self: Value) bool {
return v8.v8__Value__IsUint8Array(self.handle);
}
pub fn isUint8ClampedArray(self: Value) bool {
return v8.v8__Value__IsUint8ClampedArray(self.handle);
}
pub fn isInt8Array(self: Value) bool {
return v8.v8__Value__IsInt8Array(self.handle);
}
pub fn isUint16Array(self: Value) bool {
return v8.v8__Value__IsUint16Array(self.handle);
}
pub fn isInt16Array(self: Value) bool {
return v8.v8__Value__IsInt16Array(self.handle);
}
pub fn isUint32Array(self: Value) bool {
return v8.v8__Value__IsUint32Array(self.handle);
}
pub fn isInt32Array(self: Value) bool {
return v8.v8__Value__IsInt32Array(self.handle);
}
pub fn isBigUint64Array(self: Value) bool {
return v8.v8__Value__IsBigUint64Array(self.handle);
}
pub fn isBigInt64Array(self: Value) bool {
return v8.v8__Value__IsBigInt64Array(self.handle);
}
pub fn isPromise(self: Value) bool {
return v8.v8__Value__IsPromise(self.handle);
}
pub fn toBool(self: Value) bool {
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
}
pub fn typeOf(self: Value) js.String {
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
return js.String{ .local = self.local, .handle = str_handle };
}
pub fn toF32(self: Value) !f32 {
return @floatCast(try self.toF64());
}
pub fn toF64(self: Value) !f64 {
var maybe: v8.MaybeF64 = undefined;
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toI32(self: Value) !i32 {
var maybe: v8.MaybeI32 = undefined;
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toU32(self: Value) !u32 {
var maybe: v8.MaybeU32 = undefined;
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toPromise(self: Value) js.Promise {
if (comptime IS_DEBUG) {
std.debug.assert(self.isPromise());
}
return .{
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toString(self: Value) !js.String {
const l = self.local;
const value_handle: *const v8.Value = blk: {
if (self.isSymbol()) {
break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);
}
break :blk self.handle;
};
const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;
return .{ .local = self.local, .handle = str_handle };
}
pub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {
return (try self.toString()).toSSO(global);
}
pub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {
return (try self.toString()).toSSOWithAlloc(allocator);
}
pub fn toStringSlice(self: Value) ![]u8 {
return (try self.toString()).toSlice();
}
pub fn toStringSliceZ(self: Value) ![:0]u8 {
return (try self.toString()).toSliceZ();
}
pub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {
return (try self.toString()).toSliceWithAlloc(allocator);
}
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
const local = self.local;
const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
}
// Throws a DataCloneError for host objects (Blob, File, etc.) that cannot be serialized.
// Does not support transferables which require additional delegate callbacks.
pub fn structuredClone(self: Value) !Value {
const local = self.local;
const v8_context = local.handle;
const v8_isolate = local.isolate.handle;
const SerializerDelegate = struct {
// Called when V8 encounters a host object it doesn't know how to serialize.
// Returns false to indicate the object cannot be cloned, and throws a DataCloneError.
// V8 asserts has_exception() after this returns false, so we must throw here.
fn writeHostObject(_: ?*anyopaque, isolate: ?*v8.Isolate, _: ?*const v8.Object) callconv(.c) v8.MaybeBool {
const iso = isolate orelse return .{ .has_value = true, .value = false };
const message = v8.v8__String__NewFromUtf8(iso, "The object cannot be cloned.", v8.kNormal, -1);
const error_value = v8.v8__Exception__Error(message) orelse return .{ .has_value = true, .value = false };
_ = v8.v8__Isolate__ThrowException(iso, error_value);
return .{ .has_value = true, .value = false };
}
// Called by V8 to report serialization errors. The exception should already be thrown.
fn throwDataCloneError(_: ?*anyopaque, _: ?*const v8.String) callconv(.c) void {}
};
const size, const data = blk: {
const serializer = v8.v8__ValueSerializer__New(v8_isolate, &.{
.data = null,
.get_shared_array_buffer_id = null,
.write_host_object = SerializerDelegate.writeHostObject,
.throw_data_clone_error = SerializerDelegate.throwDataCloneError,
}) orelse return error.JsException;
defer v8.v8__ValueSerializer__DELETE(serializer);
var write_result: v8.MaybeBool = undefined;
v8.v8__ValueSerializer__WriteHeader(serializer);
v8.v8__ValueSerializer__WriteValue(serializer, v8_context, self.handle, &write_result);
if (!write_result.has_value or !write_result.value) {
return error.JsException;
}
var size: usize = undefined;
const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;
break :blk .{ size, data };
};
defer v8.v8__ValueSerializer__FreeBuffer(data);
const cloned_handle = blk: {
const deserializer = v8.v8__ValueDeserializer__New(v8_isolate, data, size, null) orelse return error.JsException;
defer v8.v8__ValueDeserializer__DELETE(deserializer);
var read_header_result: v8.MaybeBool = undefined;
v8.v8__ValueDeserializer__ReadHeader(deserializer, v8_context, &read_header_result);
if (!read_header_result.has_value or !read_header_result.value) {
return error.JsException;
}
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, v8_context) orelse return error.JsException;
};
return .{ .local = local, .handle = cloned_handle };
}
pub fn persist(self: Value) !Global {
return self._persist(true);
}
pub fn temp(self: Value) !Temp {
return self._persist(false);
}
fn _persist(self: *const Value, 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.session.temps };
}
pub fn toZig(self: Value, comptime T: type) !T {
return self.local.jsValueToZig(T, self);
}
pub fn toObject(self: Value) js.Object {
if (comptime IS_DEBUG) {
std.debug.assert(self.isObject());
}
return .{
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toArray(self: Value) js.Array {
if (comptime IS_DEBUG) {
std.debug.assert(self.isArray());
}
return .{
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toBigInt(self: Value) js.BigInt {
if (comptime IS_DEBUG) {
std.debug.assert(self.isBigInt());
}
return .{
.handle = @ptrCast(self.handle),
};
}
pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.local.debugValue(self, writer);
}
const js_str = self.toString() catch return error.WriteFailed;
return js_str.format(writer);
}
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) Value {
return .{
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
pub fn isEqual(self: *const Self, other: Value) 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);
}
}
};
}