Cleanup js -> string

Converting a JS value to a string is a bit messy right now. There's duplication
between string helpers in js.Local, and what js.String and js.Value provide.

Now, all stringifying functions are in js.String, with some helpers in js.Value.

Also tried to streamline the APIs around most common-cases (e.g. js.String ->
[]u8 using call_arena). js.String now also implements format, so it can be
used as-is in some cases.
This commit is contained in:
Karl Seguin
2026-01-29 14:21:24 +08:00
parent 232e7a1759
commit c84106570f
18 changed files with 173 additions and 205 deletions

View File

@@ -276,7 +276,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._arena_pool_leak_track.clearRetainingCapacity(); self._arena_pool_leak_track.clearRetainingCapacity();
} }
// We force a garbage collection between page navigations to keep v8 // We force a garbage collection between page navigations to keep v8
// memory usage as low as possible. // memory usage as low as possible.
self._session.browser.env.memoryPressureNotification(.moderate); self._session.browser.env.memoryPressureNotification(.moderate);

View File

@@ -314,14 +314,14 @@ fn isInErrorSet(err: anyerror, comptime T: type) bool {
} }
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T { fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
const v8_string = @as(*const v8.String, @ptrCast(name)); const handle = @as(*const v8.String, @ptrCast(name));
if (T == string.String) { if (T == string.String) {
return self.local.jsStringToStringSSO(v8_string, .{}); return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
} }
if (T == string.Global) { if (T == string.Global) {
return self.local.jsStringToStringSSO(v8_string, .{ .allocator = self.local.ctx.allocator }); return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
} }
return try self.local.valueHandleToString(v8_string, .{}); return try js.String.toSlice(.{ .local = &self.local, .handle = handle });
} }
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void { fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {

View File

@@ -357,9 +357,13 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
// Some module-loading errors aren't handled by TryCatch. We need to // Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself. // get the error from the module itself.
const message = blk: {
const e = mod.getException().toString() catch break :blk "???";
break :blk e.toSlice() catch "???";
};
log.warn(.js, "evaluate module", .{ log.warn(.js, "evaluate module", .{
.message = message,
.specifier = owned_url, .specifier = owned_url,
.message = mod.getException().toString(.{}) catch "???",
}); });
return error.EvaluationError; return error.EvaluationError;
}; };
@@ -457,11 +461,11 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *
const request_len = requests.len(); const request_len = requests.len();
const script_manager = self.script_manager.?; const script_manager = self.script_manager.?;
for (0..request_len) |i| { for (0..request_len) |i| {
const specifier = try local.jsStringToZigZ(requests.get(i).specifier(), .{}); const specifier = requests.get(i).specifier(local);
const normalized_specifier = try script_manager.resolveSpecifier( const normalized_specifier = try script_manager.resolveSpecifier(
self.call_arena, self.call_arena,
url, url,
specifier, try specifier.toSliceZ(),
); );
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier); const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!nested_gop.found_existing) { if (!nested_gop.found_existing) {
@@ -494,14 +498,14 @@ fn resolveModuleCallback(
_ = import_attributes; _ = import_attributes;
const self = fromC(c_context.?); const self = fromC(c_context.?);
var local = js.Local{ const local = js.Local{
.ctx = self, .ctx = self,
.handle = c_context.?, .handle = c_context.?,
.isolate = self.isolate, .isolate = self.isolate,
.call_arena = self.call_arena, .call_arena = self.call_arena,
}; };
const specifier = local.jsStringToZigZ(c_specifier.?, .{}) catch |err| { const specifier = js.String.toSliceZ(.{ .local = &local, .handle = c_specifier.? }) catch |err| {
log.err(.js, "resolve module", .{ .err = err }); log.err(.js, "resolve module", .{ .err = err });
return null; return null;
}; };
@@ -527,19 +531,19 @@ pub fn dynamicModuleCallback(
_ = import_attrs; _ = import_attrs;
const self = fromC(c_context.?); const self = fromC(c_context.?);
var local = js.Local{ const local = js.Local{
.ctx = self, .ctx = self,
.handle = c_context.?, .handle = c_context.?,
.call_arena = self.call_arena, .call_arena = self.call_arena,
.isolate = self.isolate, .isolate = self.isolate,
}; };
const resource = local.jsStringToZigZ(resource_name.?, .{}) catch |err| { const resource = js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
return @constCast((local.rejectPromise("Out of memory") catch return null).handle); return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
}; };
const specifier = local.jsStringToZigZ(v8_specifier.?, .{}) catch |err| { const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" }); log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
return @constCast((local.rejectPromise("Out of memory") catch return null).handle); return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
}; };

View File

@@ -254,8 +254,7 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
const value = const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value| if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
// @HandleScope - no reason to create a js.Context here js.Value.toStringSlice(.{ .local = &local, .handle = v8_value }) catch |err| @errorName(err)
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
else else
"no value"; "no value";

View File

@@ -473,10 +473,10 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
if (ptr.child == u8) { if (ptr.child == u8) {
if (ptr.sentinel()) |s| { if (ptr.sentinel()) |s| {
if (comptime s == 0) { if (comptime s == 0) {
return self.valueToStringZ(js_val, .{}); return try js_val.toStringSliceZ();
} }
} else { } else {
return self.valueToString(js_val, .{}); return try js_val.toStringSlice();
} }
} }
@@ -549,10 +549,8 @@ pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
}, },
.@"enum" => |e| { .@"enum" => |e| {
if (@hasDecl(T, "js_enum_from_string")) { if (@hasDecl(T, "js_enum_from_string")) {
if (!js_val.isString()) { const js_str = js_val.isString() orelse return error.InvalidArgument;
return error.InvalidArgument; return std.meta.stringToEnum(T, try js_str.toSlice()) orelse return error.InvalidArgument;
}
return std.meta.stringToEnum(T, try self.valueToString(js_val, .{})) orelse return error.InvalidArgument;
} }
switch (@typeInfo(e.tag_type)) { switch (@typeInfo(e.tag_type)) {
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)), .int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)),
@@ -625,17 +623,12 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
return try promise.persist(); return try promise.persist();
}, },
string.String => { string.String => {
if (!js_val.isString()) { const js_str = js_val.isString() orelse return null;
return null; return try js_str.toSSO(false);
}
return try self.valueToStringSSO(js_val, .{ .allocator = self.ctx.call_arena });
}, },
string.Global => { string.Global => {
if (!js_val.isString()) { const js_str = js_val.isString() orelse return null;
return null; return try js_str.toSSO(true);
}
// Use arena for persistent strings
return .{ .str = try self.valueToStringSSO(js_val, .{ .allocator = self.ctx.arena }) };
}, },
else => { else => {
if (!js_val.isObject()) { if (!js_val.isObject()) {
@@ -883,7 +876,7 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
} }
if (ptr.child == u8) { if (ptr.child == u8) {
if (js_val.isString()) { if (v8.v8__Value__IsString(js_val.handle)) {
return .{ .ok = {} }; return .{ .ok = {} };
} }
// anything can be coerced into a string // anything can be coerced into a string
@@ -931,12 +924,13 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
if (js_arr.len() == arr.len) { if (js_arr.len() == arr.len) {
return .{ .ok = {} }; return .{ .ok = {} };
} }
} else if (js_val.isString() and arr.child == u8) { } else if (arr.child == u8) {
const str = try js_val.toString(self.local); if (js_val.isString()) |js_str| {
if (str.lenUtf8(self.isolate) == arr.len) { if (js_str.lenUtf8(self.isolate) == arr.len) {
return .{ .ok = {} }; return .{ .ok = {} };
} }
} }
}
return .{ .invalid = {} }; return .{ .invalid = {} };
}, },
.compatible => return .{ .compatible = {} }, .compatible => return .{ .compatible = {} },
@@ -947,7 +941,7 @@ fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !Pr
.@"struct" => { .@"struct" => {
// Handle string.String and string.Global specially // Handle string.String and string.Global specially
if (T == string.String or T == string.Global) { if (T == string.String or T == string.Global) {
if (js_val.isString()) { if (v8.v8__Value__IsString(js_val.handle)) {
return .{ .ok = {} }; return .{ .ok = {} };
} }
// Anything can be coerced to a string // Anything can be coerced to a string
@@ -1080,14 +1074,15 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle); const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle);
if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| { if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| {
try writer.print("{s}<{s}>", .{ separator, try self.jsStringToZig(script, .{}) }); const stack = js.String{ .local = self, .handle = script };
try writer.print("{s}<{f}>", .{ separator, stack });
} }
for (0..@intCast(frame_count)) |i| { for (0..@intCast(frame_count)) |i| {
const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?; const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?;
if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| { if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| {
const script = try self.jsStringToZig(name, .{}); const script = js.String{ .local = self, .handle = name };
try writer.print("{s}{s}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) }); try writer.print("{s}{f}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) });
} else { } else {
try writer.print("{s}<anonymous>:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) }); try writer.print("{s}<anonymous>:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) });
} }
@@ -1095,100 +1090,6 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
return buf.items; return buf.items;
} }
// == Stringifiers ==
const ToStringOpts = struct {
allocator: ?Allocator = null,
};
pub fn valueToString(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![]u8 {
return self.valueHandleToString(js_val.handle, opts);
}
pub fn valueToStringZ(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![:0]u8 {
return self.valueHandleToStringZ(js_val.handle, opts);
}
pub fn valueHandleToString(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![]u8 {
return self._valueToString(false, js_val, opts);
}
pub fn valueHandleToStringZ(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![:0]u8 {
return self._valueToString(true, js_val, opts);
}
fn _valueToString(self: *const Local, comptime null_terminate: bool, value_handle: *const v8.Value, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) {
var resolved_value_handle = value_handle;
if (v8.v8__Value__IsSymbol(value_handle)) {
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(value_handle), self.isolate.handle).?;
resolved_value_handle = @ptrCast(symbol_handle);
}
const string_handle = v8.v8__Value__ToString(resolved_value_handle, self.handle) orelse {
return error.JsException;
};
return self._jsStringToZig(null_terminate, string_handle, opts);
}
pub fn jsStringToZig(self: *const Local, str: anytype, opts: ToStringOpts) ![]u8 {
return self._jsStringToZig(false, str, opts);
}
pub fn jsStringToZigZ(self: *const Local, str: anytype, opts: ToStringOpts) ![:0]u8 {
return self._jsStringToZig(true, str, opts);
}
fn _jsStringToZig(self: *const Local, comptime null_terminate: bool, str: anytype, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) {
const handle = if (@TypeOf(str) == js.String) str.handle else str;
const len = v8.v8__String__Utf8Length(handle, self.isolate.handle);
const allocator = opts.allocator orelse self.call_arena;
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
std.debug.assert(n == len);
return buf;
}
// Convert JS string to string.String with SSO
pub fn valueToStringSSO(self: *const Local, js_val: js.Value, opts: ToStringOpts) !string.String {
const string_handle = v8.v8__Value__ToString(js_val.handle, self.handle) orelse {
return error.JsException;
};
return self.jsStringToStringSSO(string_handle, opts);
}
pub fn jsStringToStringSSO(self: *const Local, str: anytype, opts: ToStringOpts) !string.String {
const handle = if (@TypeOf(str) == js.String) str.handle else str;
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, self.isolate.handle));
if (len <= 12) {
var content: [12]u8 = undefined;
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
if (comptime IS_DEBUG) {
std.debug.assert(n == len);
}
// Weird that we do this _after_, but we have to..I've seen weird issues
// in ReleaseMode where v8 won't write to content if it starts off zero
// initiated
@memset(content[len..], 0);
return .{ .len = @intCast(len), .payload = .{ .content = content } };
}
const allocator = opts.allocator orelse self.call_arena;
const buf = try allocator.alloc(u8, len);
const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
if (comptime IS_DEBUG) {
std.debug.assert(n == len);
}
var prefix: [4]u8 = @splat(0);
@memcpy(&prefix, buf[0..4]);
return .{
.len = @intCast(len),
.payload = .{ .heap = .{
.prefix = prefix,
.ptr = buf.ptr,
} },
};
}
// == Promise Helpers == // == Promise Helpers ==
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise { pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
var resolver = js.PromiseResolver.init(self); var resolver = js.PromiseResolver.init(self);
@@ -1233,18 +1134,16 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
if (js_val.isSymbol()) { if (js_val.isSymbol()) {
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?; const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
const js_sym_str = try self.valueToString(.{ .local = self, .handle = symbol_handle }, .{}); return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
return writer.print("{s} (symbol)", .{js_sym_str});
} }
const js_type = try self.jsStringToZig(js_val.typeOf(), .{}); const js_val_str = try js_val.toStringSlice();
const js_val_str = try self.valueToString(js_val, .{});
if (js_val_str.len > 2000) { if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]); try writer.writeAll(js_val_str[0..2000]);
try writer.writeAll(" ... (truncated)"); try writer.writeAll(" ... (truncated)");
} else { } else {
try writer.writeAll(js_val_str); try writer.writeAll(js_val_str);
} }
return writer.print(" ({s})", .{js_type}); return writer.print(" ({f})", .{js_val.typeOf()});
} }
const js_obj = js_val.toObject(); const js_obj = js_val.toObject();
@@ -1266,7 +1165,7 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
} }
const own_len = js_obj.getOwnPropertyNames().len(); const own_len = js_obj.getOwnPropertyNames().len();
if (own_len == 0) { if (own_len == 0) {
const js_val_str = try self.valueToString(js_val, .{}); const js_val_str = try js_val.toStringSlice();
if (js_val_str.len > 2000) { if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]); try writer.writeAll(js_val_str[0..2000]);
return writer.writeAll(" ... (truncated)"); return writer.writeAll(" ... (truncated)");
@@ -1281,10 +1180,11 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
try writer.writeByte('\n'); try writer.writeByte('\n');
} }
const field_name = try names_arr.get(@intCast(i)); const field_name = try names_arr.get(@intCast(i));
const name = try self.valueToString(field_name, .{}); const name = try field_name.toStringSlice();
try writer.splatByteAll(' ', depth); try writer.splatByteAll(' ', depth);
try writer.writeAll(name); try writer.writeAll(name);
try writer.writeAll(": "); try writer.writeAll(": ");
const field_val = try js_obj.get(name); const field_val = try js_obj.get(name);
try self._debugValue(field_val, seen, depth + 1, writer); try self._debugValue(field_val, seen, depth + 1, writer);
if (i != len - 1) { if (i != len - 1) {

View File

@@ -131,7 +131,7 @@ const Requests = struct {
const Request = struct { const Request = struct {
handle: *const v8.ModuleRequest, handle: *const v8.ModuleRequest,
pub fn specifier(self: Request) *const v8.String { pub fn specifier(self: Request, local: *const js.Local) js.String {
return v8.v8__ModuleRequest__GetSpecifier(self.handle).?; return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };
} }
}; };

View File

@@ -201,8 +201,8 @@ pub const NameIterator = struct {
} }
self.idx += 1; self.idx += 1;
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.local.handle, idx) orelse return error.JsException; const local = self.local;
const js_val = js.Value{ .local = self.local, .handle = js_val_handle }; const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
return try self.local.valueToString(js_val, .{}); return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
} }
}; };

View File

@@ -18,8 +18,10 @@
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const SSO = @import("../../string.zig").String;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
@@ -28,26 +30,82 @@ const String = @This();
local: *const js.Local, local: *const js.Local,
handle: *const v8.String, handle: *const v8.String,
pub const ToZigOpts = struct { pub fn toSlice(self: String) ![]u8 {
allocator: ?Allocator = null, return self._toSlice(false, self.local.call_arena);
};
pub fn toZig(self: String, opts: ToZigOpts) ![]u8 {
return self._toZig(false, opts);
} }
pub fn toSliceZ(self: String) ![:0]u8 {
pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 { return self._toSlice(true, self.local.call_arena);
return self._toZig(true, opts);
} }
pub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {
return self._toSlice(false, allocator);
}
fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {
const local = self.local;
const handle = self.handle;
const isolate = local.isolate.handle;
fn _toZig(self: String, comptime null_terminate: bool, opts: ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) { const len = v8.v8__String__Utf8Length(handle, isolate);
const isolate = self.local.isolate.handle; const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
const allocator = opts.allocator orelse self.local.ctx.call_arena; const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate)); if (comptime IS_DEBUG) {
const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len);
const options = v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8;
const n = v8.v8__String__WriteUtf8(self.handle, isolate, buf.ptr, buf.len, options);
std.debug.assert(n == len); std.debug.assert(n == len);
}
return buf; return buf;
} }
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
if (comptime global) {
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.arena) };
}
return self.toSSOWithAlloc(self.local.call_arena);
}
pub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {
const handle = self.handle;
const isolate = self.local.isolate.handle;
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));
if (len <= 12) {
var content: [12]u8 = undefined;
const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
if (comptime IS_DEBUG) {
std.debug.assert(n == len);
}
// Weird that we do this _after_, but we have to..I've seen weird issues
// in ReleaseMode where v8 won't write to content if it starts off zero
// initiated
@memset(content[len..], 0);
return .{ .len = @intCast(len), .payload = .{ .content = content } };
}
const buf = try allocator.alloc(u8, len);
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
if (comptime IS_DEBUG) {
std.debug.assert(n == len);
}
var prefix: [4]u8 = @splat(0);
@memcpy(&prefix, buf[0..4]);
return .{
.len = @intCast(len),
.payload = .{ .heap = .{
.prefix = prefix,
.ptr = buf.ptr,
} },
};
}
pub fn format(self: String, writer: *std.Io.Writer) !void {
const local = self.local;
const handle = self.handle;
const isolate = local.isolate.handle;
var small: [1024]u8 = undefined;
const len = v8.v8__String__Utf8Length(handle, isolate);
var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
return writer.writeAll(buf[0..n]);
}

View File

@@ -46,12 +46,12 @@ pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
const exception: ?[]const u8 = blk: { const exception: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null; const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err); break :blk js.String.toSliceWithAlloc(.{ .local = l, .handle = @ptrCast(handle) }, allocator) catch |err| @errorName(err);
}; };
const stack: ?[]const u8 = blk: { const stack: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null; const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err); break :blk js.String.toSliceWithAlloc(.{ .local = l, .handle = @ptrCast(handle) }, allocator) catch |err| @errorName(err);
}; };
return .{ return .{

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const SSO = @import("../../string.zig").String;
const v8 = js.v8; const v8 = js.v8;
@@ -34,8 +35,12 @@ pub fn isObject(self: Value) bool {
return v8.v8__Value__IsObject(self.handle); return v8.v8__Value__IsObject(self.handle);
} }
pub fn isString(self: Value) bool { pub fn isString(self: Value) ?js.String {
return v8.v8__Value__IsString(self.handle); 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 { pub fn isArray(self: Value) bool {
@@ -204,35 +209,40 @@ pub fn toPromise(self: Value) js.Promise {
}; };
} }
pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 { pub fn toString(self: Value) !js.String {
return self._toString(false, opts); 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).?);
} }
pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 { break :blk self.handle;
return self._toString(true, opts); };
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 { pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException; const local = self.local;
return self.local.jsStringToZig(json_str_handle, .{ .allocator = allocator }); 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);
fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
const l = self.local;
if (self.isSymbol()) {
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .local = l }, null_terminate, opts);
}
const str_handle = v8.v8__Value__ToString(self.handle, l.handle) orelse {
return error.JsException;
};
const str = js.String{ .local = l, .handle = str_handle };
if (comptime null_terminate) {
return js.String.toZigZ(str, opts);
}
return js.String.toZig(str, opts);
} }
pub fn persist(self: Value) !Global { pub fn persist(self: Value) !Global {
@@ -296,8 +306,8 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
return self.local.debugValue(self, writer); return self.local.debugValue(self, writer);
} }
const str = self.toString(.{}) catch return error.WriteFailed; const js_str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str); return js_str.format(writer);
} }
pub const Temp = G(0); pub const Temp = G(0);

View File

@@ -422,7 +422,7 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.Prope
hs.init(local.isolate); hs.init(local.isolate);
defer hs.deinit(); defer hs.deinit();
const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch { const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
return 0; return 0;
}; };

View File

@@ -116,7 +116,7 @@ pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
if (self._aborted) { if (self._aborted) {
const exception = switch (self._reason) { const exception = switch (self._reason) {
.string => |str| local.throw(str), .string => |str| local.throw(str),
.js_val => |js_val| local.throw(try local.toLocal(js_val).toString(.{ .allocator = page.call_arena })), .js_val => |js_val| local.throw(try local.toLocal(js_val).toStringSlice()),
.undefined => local.throw("AbortError"), .undefined => local.throw("AbortError"),
}; };
return .{ .exception = exception }; return .{ .exception = exception };

View File

@@ -146,7 +146,7 @@ const ValueWriter = struct {
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
for (self.values, 0..) |value, i| { for (self.values, 0..) |value, i| {
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i}); const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i});
try writer.write(name, try value.toString(.{})); try writer.write(name, value);
} }
} }

View File

@@ -73,9 +73,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
var js_arr = observed_attrs.toArray(); var js_arr = observed_attrs.toArray();
for (0..js_arr.len()) |i| { for (0..js_arr.len()) |i| {
const attr_val = js_arr.get(@intCast(i)) catch continue; const attr_val = js_arr.get(@intCast(i)) catch continue;
const attr_name = attr_val.toString(.{ .allocator = page.arena }) catch continue; const attr_name = attr_val.toStringSliceWithAlloc(page.arena) catch continue;
const owned_attr = page.dupeString(attr_name) catch continue; definition.observed_attributes.put(page.arena, attr_name, {}) catch continue;
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;
} }
} }
} }

View File

@@ -68,12 +68,11 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
while (try it.next()) |name| { while (try it.next()) |name| {
const js_value = try js_obj.get(name); const js_value = try js_obj.get(name);
const value = try js_value.toString(.{});
const normalized = if (comptime normalizer) |n| n(name, page) else name; const normalized = if (comptime normalizer) |n| n(name, page) else name;
list._entries.appendAssumeCapacity(.{ list._entries.appendAssumeCapacity(.{
.name = try String.init(arena, normalized, .{}), .name = try String.init(arena, normalized, .{}),
.value = try String.init(arena, value, .{}), .value = try js_value.toSSOWithAlloc(arena),
}); });
} }

View File

@@ -277,7 +277,7 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void { pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted("error", .{ const error_event = try ErrorEvent.initTrusted("error", .{
.@"error" = try err.persist(), .@"error" = try err.persist(),
.message = err.toString(.{}) catch "Unknown error", .message = err.toStringSlice() catch "Unknown error",
.bubbles = false, .bubbles = false,
.cancelable = true, .cancelable = true,
}, page); }, page);

View File

@@ -49,8 +49,8 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
if (js_val.isObject()) { if (js_val.isObject()) {
break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page); break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page);
} }
if (js_val.isString()) { if (js_val.isString()) |js_str| {
break :blk try paramsFromString(arena, try js_val.toString(.{ .allocator = arena }), &page.buf); break :blk try paramsFromString(arena, try js_str.toSliceWithAlloc(arena), &page.buf);
} }
return error.InvalidArgument; return error.InvalidArgument;
}, },

View File

@@ -140,7 +140,7 @@ fn run(
return err; return err;
}; };
return value.toString(.{ .allocator = arena }); return value.toStringSliceWithAlloc(arena);
} }
const Writer = struct { const Writer = struct {