also looking at this
fix(native): interaction, audio, document and checkpoint gaps stranded after #401
#415
19 files
+987
-50
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -16,28 +16,6 @@ pub const available = false; | ||
| 16 | 16 | /// Matches the zig-js backend's error set, so `js_runtime` handles one shape. |
| 17 | 17 | pub const Error = error{ OutOfMemory, Unavailable, Throw, Parse }; |
| 18 | 18 | |
| 19 | pub const Argument = union(enum) { | |
| 20 | number: f64, | |
| 21 | integer: i64, | |
| 22 | boolean: bool, | |
| 23 | text: []const u8, | |
| 24 | buffer: *const Persistent, | |
| 25 | }; | |
| 26 | ||
| 27 | pub const Result = union(enum) { | |
| 28 | undefined, | |
| 29 | null, | |
| 30 | boolean: bool, | |
| 31 | number: f64, | |
| 32 | text: []const u8, | |
| 33 | }; | |
| 34 | ||
| 35 | pub const Persistent = struct { | |
| 36 | pub fn deinit(self: *Persistent) void { | |
| 37 | self.* = .{}; | |
| 38 | } | |
| 39 | }; | |
| 40 | ||
| 41 | 19 | pub const Context = struct { |
| 42 | 20 | pub fn create(allocator: std.mem.Allocator) Error!*Context { |
| 43 | 21 | _ = allocator; |
| @@ -54,47 +32,4 @@ pub const Context = struct { | ||
| 54 | 32 | _ = source; |
| 55 | 33 | return Error.Unavailable; |
| 56 | 34 | } |
| 57 | ||
| 58 | pub fn persistFunction(self: *Context, source: []const u8) Error!Persistent { | |
| 59 | _ = self; | |
| 60 | _ = source; | |
| 61 | return Error.Unavailable; | |
| 62 | } | |
| 63 | ||
| 64 | pub fn createUint8Buffer(self: *Context, bytes: []const u8) Error!Persistent { | |
| 65 | _ = self; | |
| 66 | _ = bytes; | |
| 67 | return Error.Unavailable; | |
| 68 | } | |
| 69 | ||
| 70 | pub fn readUint8Buffer(self: *Context, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 { | |
| 71 | _ = self; | |
| 72 | _ = buffer; | |
| 73 | _ = arena; | |
| 74 | return Error.Unavailable; | |
| 75 | } | |
| 76 | ||
| 77 | pub fn writeUint8Buffer(self: *Context, buffer: *const Persistent, bytes: []const u8) Error!void { | |
| 78 | _ = self; | |
| 79 | _ = buffer; | |
| 80 | _ = bytes; | |
| 81 | return Error.Unavailable; | |
| 82 | } | |
| 83 | ||
| 84 | pub fn call(self: *Context, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result { | |
| 85 | _ = self; | |
| 86 | _ = callback; | |
| 87 | _ = arena; | |
| 88 | _ = args; | |
| 89 | return Error.Unavailable; | |
| 90 | } | |
| 91 | ||
| 92 | pub fn compact(self: *Context) void { | |
| 93 | _ = self; | |
| 94 | } | |
| 95 | ||
| 96 | pub fn lastError(self: *const Context) []const u8 { | |
| 97 | _ = self; | |
| 98 | return "JavaScript runtime unavailable"; | |
| 99 | } | |
| 100 | 35 | }; |
| @@ -15,89 +15,16 @@ const js = ; | ||
| 15 | 15 | |
| 16 | 16 | pub const available = true; |
| 17 | 17 | |
| 18 | pub const Error = error{ | |
| 19 | OutOfMemory, | |
| 20 | Unavailable, | |
| 21 | Throw, | |
| 22 | Parse, | |
| 23 | InvalidHandle, | |
| 24 | WrongContext, | |
| 25 | TypeMismatch, | |
| 26 | BufferSizeMismatch, | |
| 27 | BufferTooLarge, | |
| 28 | TooManyArguments, | |
| 29 | UnsupportedResult, | |
| 30 | }; | |
| 31 | ||
| 32 | const max_buffer_bytes: usize = 64 * 1024 * 1024; | |
| 33 | const max_call_arguments: usize = 64; | |
| 34 | ||
| 35 | pub const Argument = union(enum) { | |
| 36 | number: f64, | |
| 37 | integer: i64, | |
| 38 | boolean: bool, | |
| 39 | text: []const u8, | |
| 40 | buffer: *const Persistent, | |
| 41 | }; | |
| 42 | ||
| 43 | pub const Result = union(enum) { | |
| 44 | undefined, | |
| 45 | null, | |
| 46 | boolean: bool, | |
| 47 | number: f64, | |
| 48 | text: []const u8, | |
| 49 | }; | |
| 50 | ||
| 51 | const PersistentKind = enum { function, uint8_buffer }; | |
| 52 | ||
| 53 | const PersistentHandle = struct { | |
| 54 | owner: ?*Context, | |
| 55 | root: ?*js.Context.ProtectedValue, | |
| 56 | allocator: std.mem.Allocator, | |
| 57 | kind: PersistentKind, | |
| 58 | ||
| 59 | fn dispose(self: *PersistentHandle) void { | |
| 60 | const allocator = self.allocator; | |
| 61 | if (self.owner) |owner| { | |
| 62 | if (self.root) |root| _ = owner.inner.unprotectValue(root); | |
| 63 | for (owner.handles.items, 0..) |candidate, index| { | |
| 64 | if (candidate == self) { | |
| 65 | _ = owner.handles.swapRemove(index); | |
| 66 | break; | |
| 67 | } | |
| 68 | } | |
| 69 | } | |
| 70 | self.owner = null; | |
| 71 | self.root = null; | |
| 72 | allocator.destroy(self); | |
| 73 | } | |
| 74 | }; | |
| 75 | ||
| 76 | pub const Persistent = struct { | |
| 77 | handle: ?*PersistentHandle = null, | |
| 78 | ||
| 79 | pub fn deinit(self: *Persistent) void { | |
| 80 | const handle = self.handle orelse return; | |
| 81 | self.handle = null; | |
| 82 | handle.dispose(); | |
| 83 | } | |
| 84 | }; | |
| 18 | pub const Error = error{ OutOfMemory, Unavailable, Throw, Parse }; | |
| 85 | 19 | |
| 86 | 20 | pub const Context = struct { |
| 87 | 21 | inner: *js.Context, |
| 88 | 22 | // Kept so `destroy` can free the wrapper it allocated. Without it the |
| 89 | 23 | // wrapper leaked on every evaluation. |
| 90 | 24 | allocator: std.mem.Allocator, |
| 91 | handles: std.ArrayListUnmanaged(*PersistentHandle) = .empty, | |
| 92 | last_error: [256]u8 = undefined, | |
| 93 | last_error_len: usize = 0, | |
| 94 | 25 | |
| 95 | 26 | pub fn create(allocator: std.mem.Allocator) Error!*Context { |
| 96 | return createWithOptions(allocator, false); | |
| 97 | } | |
| 98 | ||
| 99 | pub fn createWithOptions(allocator: std.mem.Allocator, enable_gc: bool) Error!*Context { | |
| 100 | const inner = js.Context.createWith(allocator, .{ .enable_gc = enable_gc }) catch return Error.OutOfMemory; | |
| 27 | const inner = js.Context.create(allocator) catch return Error.OutOfMemory; | |
| 101 | 28 | errdefer inner.destroy(); |
| 102 | 29 | |
| 103 | 30 | const self = allocator.create(Context) catch return Error.OutOfMemory; |
| @@ -106,155 +33,24 @@ pub const Context = struct { | ||
| 106 | 33 | } |
| 107 | 34 | |
| 108 | 35 | pub fn destroy(self: *Context) void { |
| 109 | for (self.handles.items) |handle| { | |
| 110 | handle.owner = null; | |
| 111 | handle.root = null; | |
| 112 | } | |
| 113 | self.handles.deinit(self.allocator); | |
| 114 | 36 | const allocator = self.allocator; |
| 115 | 37 | self.inner.destroy(); |
| 116 | 38 | allocator.destroy(self); |
| 117 | 39 | } |
| 118 | 40 | |
| 119 | fn rememberError(self: *Context, message: []const u8) void { | |
| 120 | const count = @min(message.len, self.last_error.len); | |
| 121 | @memcpy(self.last_error[0..count], message[0..count]); | |
| 122 | self.last_error_len = count; | |
| 123 | } | |
| 124 | ||
| 125 | fn fail(self: *Context, comptime err: Error, message: []const u8) Error { | |
| 126 | self.rememberError(message); | |
| 127 | return err; | |
| 128 | } | |
| 129 | ||
| 130 | pub fn lastError(self: *const Context) []const u8 { | |
| 131 | return self.last_error[0..self.last_error_len]; | |
| 132 | } | |
| 133 | ||
| 134 | 41 | /// Evaluate and render the result as text. |
| 135 | 42 | /// |
| 136 | 43 | /// `arena` rather than a general allocator: zig-js's `toString` allocates |
| 137 | 44 | /// internally and the slice it returns is not a single freeable |
| 138 | 45 | /// allocation, so freeing it individually aborts on invalid free. |
| 139 | 46 | pub fn evalToString(self: *Context, arena: std.mem.Allocator, source: []const u8) Error![]const u8 { |
| 140 | self.last_error_len = 0; | |
| 141 | const evaluated = self.inner.evaluate(source) catch |err| return switch (err) { | |
| 142 | error.OutOfMemory => self.fail(Error.OutOfMemory, "JavaScript runtime ran out of memory"), | |
| 47 | const value = self.inner.evaluate(source) catch |err| return switch (err) { | |
| 48 | error.OutOfMemory => Error.OutOfMemory, | |
| 143 | 49 | // A JS-level throw and a parse failure are different problems for |
| 144 | 50 | // the caller: one is the script's logic, the other is its syntax. |
| 145 | error.Throw => self.fail(Error.Throw, "JavaScript evaluation threw"), | |
| 146 | else => self.fail(Error.Parse, "JavaScript source failed to parse"), | |
| 147 | }; | |
| 148 | return evaluated.toString(arena) catch self.fail(Error.OutOfMemory, "JavaScript result could not be rendered"); | |
| 149 | } | |
| 150 | ||
| 151 | fn mapEvalError(self: *Context, err: anyerror) Error { | |
| 152 | return switch (err) { | |
| 153 | error.OutOfMemory => self.fail(Error.OutOfMemory, "JavaScript runtime ran out of memory"), | |
| 154 | error.Throw => self.fail(Error.Throw, "JavaScript callback threw"), | |
| 155 | else => self.fail(Error.Throw, "JavaScript callback failed"), | |
| 156 | }; | |
| 157 | } | |
| 158 | ||
| 159 | fn retain(self: *Context, retained: js.Value, kind: PersistentKind) Error!Persistent { | |
| 160 | const root = self.inner.protectValue(retained) catch return self.fail(Error.OutOfMemory, "could not retain JavaScript value"); | |
| 161 | errdefer _ = self.inner.unprotectValue(root); | |
| 162 | const handle = self.allocator.create(PersistentHandle) catch return self.fail(Error.OutOfMemory, "could not allocate persistent handle"); | |
| 163 | errdefer self.allocator.destroy(handle); | |
| 164 | handle.* = .{ .owner = self, .root = root, .allocator = self.allocator, .kind = kind }; | |
| 165 | self.handles.append(self.allocator, handle) catch return self.fail(Error.OutOfMemory, "could not register persistent handle"); | |
| 166 | return .{ .handle = handle }; | |
| 167 | } | |
| 168 | ||
| 169 | fn getValue(self: *Context, handle: *const Persistent, expected: PersistentKind) Error!js.Value { | |
| 170 | const record = handle.handle orelse return self.fail(Error.InvalidHandle, "persistent handle has been disposed"); | |
| 171 | if (record.owner == null or record.root == null) return self.fail(Error.InvalidHandle, "persistent handle belongs to a disposed runtime"); | |
| 172 | if (record.owner.? != self) return self.fail(Error.WrongContext, "persistent handle belongs to another runtime"); | |
| 173 | if (record.kind != expected) return self.fail(Error.TypeMismatch, "persistent handle has the wrong type"); | |
| 174 | return record.root.?.get(); | |
| 175 | } | |
| 176 | ||
| 177 | pub fn persistFunction(self: *Context, source: []const u8) Error!Persistent { | |
| 178 | self.last_error_len = 0; | |
| 179 | const function = self.inner.evaluate(source) catch |err| return self.mapEvalError(err); | |
| 180 | if (!function.isCallable()) return self.fail(Error.TypeMismatch, "persistent source did not evaluate to a function"); | |
| 181 | return self.retain(function, .function); | |
| 182 | } | |
| 183 | ||
| 184 | pub fn createUint8Buffer(self: *Context, bytes: []const u8) Error!Persistent { | |
| 185 | self.last_error_len = 0; | |
| 186 | if (bytes.len > max_buffer_bytes) return self.fail(Error.BufferTooLarge, "typed buffer exceeds the 64 MiB host limit"); | |
| 187 | ||
| 188 | const gc_saved = js.gc.setActiveContext(self.inner); | |
| 189 | defer js.gc.restoreActiveContext(gc_saved); | |
| 190 | var machine = self.inner.interpreter(); | |
| 191 | self.inner.pushActiveInterpreter(&machine) catch return self.fail(Error.OutOfMemory, "could not enter JavaScript runtime"); | |
| 192 | defer self.inner.popActiveInterpreter(&machine); | |
| 193 | const created = machine.makeUninitializedTypedArray(.u8, bytes.len) catch |err| return self.mapEvalError(err); | |
| 194 | const typed = created.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "runtime did not create a Uint8Array"); | |
| 195 | const backing = typed.buffer.arrayBuffer() orelse return self.fail(Error.TypeMismatch, "Uint8Array has no ArrayBuffer backing"); | |
| 196 | @memcpy(backing.bytes()[typed.byte_offset..][0..bytes.len], bytes); | |
| 197 | return self.retain(created, .uint8_buffer); | |
| 198 | } | |
| 199 | ||
| 200 | pub fn readUint8Buffer(self: *Context, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 { | |
| 201 | const buffer_value = try self.getValue(buffer, .uint8_buffer); | |
| 202 | const typed = buffer_value.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "persistent value is not a typed buffer"); | |
| 203 | const length = typed.currentLength() orelse return self.fail(Error.InvalidHandle, "typed buffer is detached or out of bounds"); | |
| 204 | const bytes = typed.buffer.arrayBuffer().?.bytes(); | |
| 205 | if (typed.byte_offset > bytes.len or length > bytes.len - typed.byte_offset) | |
| 206 | return self.fail(Error.InvalidHandle, "typed buffer range is invalid"); | |
| 207 | return arena.dupe(u8, bytes[typed.byte_offset..][0..length]) catch self.fail(Error.OutOfMemory, "could not copy typed buffer"); | |
| 208 | } | |
| 209 | ||
| 210 | pub fn writeUint8Buffer(self: *Context, buffer: *const Persistent, bytes: []const u8) Error!void { | |
| 211 | const buffer_value = try self.getValue(buffer, .uint8_buffer); | |
| 212 | const typed = buffer_value.asObj().typedArray() orelse return self.fail(Error.TypeMismatch, "persistent value is not a typed buffer"); | |
| 213 | const length = typed.currentLength() orelse return self.fail(Error.InvalidHandle, "typed buffer is detached or out of bounds"); | |
| 214 | if (bytes.len != length) return self.fail(Error.BufferSizeMismatch, "typed buffer writes must match the view length"); | |
| 215 | const backing = typed.buffer.arrayBuffer().?; | |
| 216 | const destination = backing.bytes(); | |
| 217 | if (typed.byte_offset > destination.len or length > destination.len - typed.byte_offset) | |
| 218 | return self.fail(Error.InvalidHandle, "typed buffer range is invalid"); | |
| 219 | @memcpy(destination[typed.byte_offset..][0..length], bytes); | |
| 220 | } | |
| 221 | ||
| 222 | pub fn call(self: *Context, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result { | |
| 223 | self.last_error_len = 0; | |
| 224 | if (args.len > max_call_arguments) return self.fail(Error.TooManyArguments, "persistent calls accept at most 64 arguments"); | |
| 225 | const function = try self.getValue(callback, .function); | |
| 226 | ||
| 227 | var values: std.ArrayListUnmanaged(js.Value) = .empty; | |
| 228 | try values.ensureTotalCapacity(self.inner.arena(), args.len); | |
| 229 | for (args) |arg| { | |
| 230 | const converted = switch (arg) { | |
| 231 | .number => |n| js.Value.num(n), | |
| 232 | .integer => |n| js.Value.num(@floatFromInt(n)), | |
| 233 | .boolean => |b| js.Value.boolVal(b), | |
| 234 | .text => |text| js.Value.strAlloc(self.inner.arena(), text) catch return self.fail(Error.OutOfMemory, "could not allocate callback text argument"), | |
| 235 | .buffer => |buffer| try self.getValue(buffer, .uint8_buffer), | |
| 236 | }; | |
| 237 | values.appendAssumeCapacity(converted); | |
| 238 | } | |
| 239 | ||
| 240 | const gc_saved = js.gc.setActiveContext(self.inner); | |
| 241 | defer js.gc.restoreActiveContext(gc_saved); | |
| 242 | var machine = self.inner.interpreter(); | |
| 243 | self.inner.pushActiveInterpreter(&machine) catch return self.fail(Error.OutOfMemory, "could not enter JavaScript runtime"); | |
| 244 | defer self.inner.popActiveInterpreter(&machine); | |
| 245 | const result = machine.callValue(function, values.items) catch |err| return self.mapEvalError(err); | |
| 246 | return switch (result.kind()) { | |
| 247 | .undefined => .undefined, | |
| 248 | .null => .null, | |
| 249 | .boolean => .{ .boolean = result.asBool() }, | |
| 250 | .number => .{ .number = result.asNum() }, | |
| 251 | .string => .{ .text = arena.dupe(u8, result.asWtf8(arena) catch return self.fail(Error.OutOfMemory, "could not copy callback result")) catch return self.fail(Error.OutOfMemory, "could not copy callback result") }, | |
| 252 | .object => self.fail(Error.UnsupportedResult, "persistent calls return only primitive values"), | |
| 51 | error.Throw => Error.Throw, | |
| 52 | else => Error.Parse, | |
| 253 | 53 | }; |
| 254 | } | |
| 255 | ||
| 256 | pub fn compact(self: *Context) void { | |
| 257 | if (!self.inner.requestGarbageCompaction()) return; | |
| 258 | _ = self.inner.compactGarbage(); | |
| 54 | return value.toString(arena) catch Error.OutOfMemory; | |
| 259 | 55 | } |
| 260 | 56 | }; |
| @@ -31,15 +31,6 @@ const backend = ; | ||
| 31 | 31 | pub const available = backend.available; |
| 32 | 32 | |
| 33 | 33 | pub const Error = backend.Error; |
| 34 | pub const Argument = backend.Argument; | |
| 35 | pub const Result = backend.Result; | |
| 36 | pub const Persistent = backend.Persistent; | |
| 37 | ||
| 38 | pub const Options = struct { | |
| 39 | /// Enable zig-js's precise moving collector. Keep this on for runtimes | |
| 40 | /// that retain callbacks or typed buffers across host calls. | |
| 41 | enable_gc: bool = false, | |
| 42 | }; | |
| 43 | 34 | |
| 44 | 35 | /// Told to the user rather than logged: an unavailable runtime is a build |
| 45 | 36 | /// choice, and the fix is a flag they can pass. |
| @@ -58,10 +49,6 @@ pub const Runtime = struct { | ||
| 58 | 49 | return .{ .ctx = try backend.Context.create(allocator), .allocator = allocator }; |
| 59 | 50 | } |
| 60 | 51 | |
| 61 | pub fn initWithOptions(allocator: std.mem.Allocator, options: Options) Error!Runtime { | |
| 62 | return .{ .ctx = try backend.Context.createWithOptions(allocator, options.enable_gc), .allocator = allocator }; | |
| 63 | } | |
| 64 | ||
| 65 | 52 | pub fn deinit(self: *Runtime) void { |
| 66 | 53 | self.ctx.destroy(); |
| 67 | 54 | } |
| @@ -74,41 +61,6 @@ pub const Runtime = struct { | ||
| 74 | 61 | pub fn evalToString(self: *Runtime, arena: std.mem.Allocator, source: []const u8) Error![]const u8 { |
| 75 | 62 | return self.ctx.evalToString(arena, source); |
| 76 | 63 | } |
| 77 | ||
| 78 | /// Evaluate a function expression once and retain its identity in the | |
| 79 | /// realm. Calls use the engine's callable path; they do not generate a | |
| 80 | /// fresh source string for every frame. | |
| 81 | pub fn persistFunction(self: *Runtime, source: []const u8) Error!Persistent { | |
| 82 | return self.ctx.persistFunction(source); | |
| 83 | } | |
| 84 | ||
| 85 | /// Create a rooted Uint8Array whose bytes remain owned by the JavaScript | |
| 86 | /// realm until the returned handle is disposed. | |
| 87 | pub fn createUint8Buffer(self: *Runtime, bytes: []const u8) Error!Persistent { | |
| 88 | return self.ctx.createUint8Buffer(bytes); | |
| 89 | } | |
| 90 | ||
| 91 | pub fn readUint8Buffer(self: *Runtime, buffer: *const Persistent, arena: std.mem.Allocator) Error![]const u8 { | |
| 92 | return self.ctx.readUint8Buffer(buffer, arena); | |
| 93 | } | |
| 94 | ||
| 95 | pub fn writeUint8Buffer(self: *Runtime, buffer: *const Persistent, bytes: []const u8) Error!void { | |
| 96 | return self.ctx.writeUint8Buffer(buffer, bytes); | |
| 97 | } | |
| 98 | ||
| 99 | pub fn call(self: *Runtime, callback: *const Persistent, arena: std.mem.Allocator, args: []const Argument) Error!Result { | |
| 100 | return self.ctx.call(callback, arena, args); | |
| 101 | } | |
| 102 | ||
| 103 | /// Request one moving collection and run it at a quiescent host boundary. | |
| 104 | /// Persistent handles are the only values that may safely cross it. | |
| 105 | pub fn compact(self: *Runtime) void { | |
| 106 | self.ctx.compact(); | |
| 107 | } | |
| 108 | ||
| 109 | pub fn lastError(self: *const Runtime) []const u8 { | |
| 110 | return self.ctx.lastError(); | |
| 111 | } | |
| 112 | 64 | }; |
| 113 | 65 | |
| 114 | 66 | /// Evaluate one script and return its result — the whole lifecycle, for a |
| @@ -246,53 +198,3 @@ test "runs without any browser globals" { | ||
| 246 | 198 | const kind = try evalOnce(testing.allocator, arena.allocator(), "typeof window"); |
| 247 | 199 | try testing.expectEqualStrings("undefined", kind); |
| 248 | 200 | } |
| 249 | ||
| 250 | test "persistent callbacks retain typed buffers across compaction" { | |
| 251 | if (!available) return error.SkipZigTest; | |
| 252 | ||
| 253 | var arena = std.heap.ArenaAllocator.init(testing.allocator); | |
| 254 | defer arena.deinit(); | |
| 255 | ||
| 256 | var runtime = try Runtime.initWithOptions(testing.allocator, .{ .enable_gc = true }); | |
| 257 | defer runtime.deinit(); | |
| 258 | var callback = try runtime.persistFunction("function (base, bytes) { return base + bytes[0] + bytes[1]; }"); | |
| 259 | defer callback.deinit(); | |
| 260 | var buffer = try runtime.createUint8Buffer(&.{ 2, 3 }); | |
| 261 | defer buffer.deinit(); | |
| 262 | ||
| 263 | runtime.compact(); | |
| 264 | const first = try runtime.call(&callback, arena.allocator(), &.{ | |
| 265 | .{ .integer = 37 }, | |
| 266 | .{ .buffer = &buffer }, | |
| 267 | }); | |
| 268 | try testing.expectEqual(@as(f64, 42), first.number); | |
| 269 | ||
| 270 | try runtime.writeUint8Buffer(&buffer, &.{ 4, 5 }); | |
| 271 | runtime.compact(); | |
| 272 | const second = try runtime.call(&callback, arena.allocator(), &.{ | |
| 273 | .{ .number = 33 }, | |
| 274 | .{ .buffer = &buffer }, | |
| 275 | }); | |
| 276 | try testing.expectEqual(@as(f64, 42), second.number); | |
| 277 | try testing.expectEqualSlices(u8, &.{ 4, 5 }, try runtime.readUint8Buffer(&buffer, arena.allocator())); | |
| 278 | } | |
| 279 | ||
| 280 | test "persistent handles reject disposal and cross-runtime use" { | |
| 281 | if (!available) return error.SkipZigTest; | |
| 282 | ||
| 283 | var first = try Runtime.init(testing.allocator); | |
| 284 | defer first.deinit(); | |
| 285 | var second = try Runtime.init(testing.allocator); | |
| 286 | defer second.deinit(); | |
| 287 | ||
| 288 | var callback = try first.persistFunction("function () { return 1; }"); | |
| 289 | try std.testing.expectError( | |
| 290 | Error.WrongContext, | |
| 291 | second.call(&callback, testing.allocator, &.{}), | |
| 292 | ); | |
| 293 | callback.deinit(); | |
| 294 | try std.testing.expectError( | |
| 295 | Error.InvalidHandle, | |
| 296 | first.call(&callback, testing.allocator, &.{}), | |
| 297 | ); | |
| 298 | } | |