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).
This commit is contained in:
Karl Seguin
2026-03-31 16:13:55 +08:00
parent 47afdc003a
commit c3c465347d
2 changed files with 48 additions and 3 deletions

View File

@@ -245,15 +245,37 @@ pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator); return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
} }
// Currently does not support host objects (Blob, File, etc.) or transferables // Throws a DataCloneError for host objects (Blob, File, etc.) that cannot be serialized.
// which require delegate callbacks to be implemented. // Does not support transferables which require additional delegate callbacks.
pub fn structuredClone(self: Value) !Value { pub fn structuredClone(self: Value) !Value {
const local = self.local; const local = self.local;
const v8_context = local.handle; const v8_context = local.handle;
const v8_isolate = local.isolate.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 size, const data = blk: {
const serializer = v8.v8__ValueSerializer__New(v8_isolate, null) orelse return error.JsException; 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); defer v8.v8__ValueSerializer__DELETE(serializer);
var write_result: v8.MaybeBool = undefined; var write_result: v8.MaybeBool = undefined;

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<script src="../testing.js"></script> <script src="../testing.js"></script>
<body></body>
<script id=window> <script id=window>
testing.expectEqual(window, globalThis); testing.expectEqual(window, globalThis);
@@ -260,6 +261,28 @@
} }
testing.expectEqual(true, threw); testing.expectEqual(true, threw);
} }
// Host objects (DOM elements) cannot be cloned - should throw, not crash
{
let threw = false;
try {
structuredClone(document.body);
} catch (err) {
threw = true;
}
testing.expectEqual(true, threw);
}
// Objects containing host objects cannot be cloned - should throw, not crash
{
let threw = false;
try {
structuredClone({ element: document.body });
} catch (err) {
threw = true;
}
testing.expectEqual(true, threw);
}
</script> </script>
<script id=cached_getter_wrong_this> <script id=cached_getter_wrong_this>