Skip to content

Commit 2ecf901

Browse files
committed
Better support for Uint8Array in ReadableStream
There's always going to be ambiguity between a string and a Uint8Array. We already had TypedArray(u8) as a discriminator when _returning_ values. But now the type is also used by mapping JS values to Zig. To support this efficiently when probing the union, the typed array mapping logic was extracted into its own function (so that it can be used by the probe).
1 parent 20cb6cd commit 2ecf901

File tree

7 files changed

+156
-96
lines changed

7 files changed

+156
-96
lines changed

src/browser/fetch/Request.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
181181
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
182182
if (self.body) |body| {
183183
const stream = try ReadableStream.constructor(null, null, page);
184-
try stream.queue.append(page.arena, body);
184+
try stream.queue.append(page.arena, .{ .string = body });
185185
return stream;
186186
} else return null;
187187
}

src/browser/fetch/Response.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
109109
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
110110
const stream = try ReadableStream.constructor(null, null, page);
111111
if (self.body) |body| {
112-
try stream.queue.append(page.arena, body);
112+
try stream.queue.append(page.arena, .{ .string = body });
113113
}
114114
return stream;
115115
}

src/browser/streams/ReadableStream.zig

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
const std = @import("std");
2020
const log = @import("../../log.zig");
2121

22-
const Page = @import("../page.zig").Page;
22+
const Allocator = std.mem.Allocator;
2323
const Env = @import("../env.zig").Env;
24+
const Page = @import("../page.zig").Page;
2425

2526
const ReadableStream = @This();
2627
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
@@ -45,16 +46,42 @@ cancel_fn: ?Env.Function = null,
4546
pull_fn: ?Env.Function = null,
4647

4748
strategy: QueueingStrategy,
48-
queue: std.ArrayListUnmanaged([]const u8) = .empty,
49+
queue: std.ArrayListUnmanaged(Chunk) = .empty,
50+
51+
pub const Chunk = union(enum) {
52+
// the order matters, sorry.
53+
uint8array: Env.TypedArray(u8),
54+
string: []const u8,
55+
56+
pub fn dupe(self: Chunk, allocator: Allocator) !Chunk {
57+
return switch (self) {
58+
.string => |str| .{ .string = try allocator.dupe(u8, str) },
59+
.uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) },
60+
};
61+
}
62+
};
4963

5064
pub const ReadableStreamReadResult = struct {
51-
const ValueUnion =
52-
union(enum) { data: []const u8, empty: void };
53-
54-
value: ValueUnion,
5565
done: bool,
66+
value: Value = .empty,
67+
68+
const Value = union(enum) {
69+
empty,
70+
data: Chunk,
71+
};
72+
73+
pub fn init(chunk: Chunk, done: bool) ReadableStreamReadResult {
74+
if (done) {
75+
return .{ .done = true, .value = .empty };
76+
}
77+
78+
return .{
79+
.done = false,
80+
.value = .{ .data = chunk },
81+
};
82+
}
5683

57-
pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
84+
pub fn get_value(self: *const ReadableStreamReadResult) Value {
5885
return self.value;
5986
}
6087

src/browser/streams/ReadableStreamDefaultController.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,17 @@ pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page
5151
// to discard, must use cancel.
5252
}
5353

54-
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
54+
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: ReadableStream.Chunk, page: *Page) !void {
5555
const stream = self.stream;
5656

5757
if (stream.state != .readable) {
5858
return error.TypeError;
5959
}
6060

61-
const duped_chunk = try page.arena.dupe(u8, chunk);
61+
const duped_chunk = try chunk.dupe(page.arena);
6262

6363
if (self.stream.reader_resolver) |*rr| {
64-
try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
64+
try rr.resolve(ReadableStreamReadResult.init(duped_chunk, false));
6565
self.stream.reader_resolver = null;
6666
}
6767

src/browser/streams/ReadableStreamDefaultReader.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise
4949
const data = self.stream.queue.orderedRemove(0);
5050
const resolver = page.main_context.createPromiseResolver();
5151

52-
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
52+
try resolver.resolve(ReadableStreamReadResult.init(data, false));
5353
try self.stream.pullIf();
5454
return resolver.promise();
5555
} else {
@@ -67,9 +67,9 @@ pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise
6767

6868
if (stream.queue.items.len > 0) {
6969
const data = self.stream.queue.orderedRemove(0);
70-
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
70+
try resolver.resolve(ReadableStreamReadResult.init(data, false));
7171
} else {
72-
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
72+
try resolver.resolve(ReadableStreamReadResult{ .done = true });
7373
}
7474

7575
return resolver.promise();

src/runtime/js.zig

Lines changed: 97 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,88 +1094,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
10941094
}
10951095
},
10961096
.slice => {
1097-
var force_u8 = false;
1098-
var array_buffer: ?v8.ArrayBuffer = null;
1099-
if (js_value.isTypedArray()) {
1100-
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1101-
array_buffer = buffer_view.getBuffer();
1102-
} else if (js_value.isArrayBufferView()) {
1103-
force_u8 = true;
1104-
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1105-
array_buffer = buffer_view.getBuffer();
1106-
} else if (js_value.isArrayBuffer()) {
1107-
force_u8 = true;
1108-
array_buffer = js_value.castTo(v8.ArrayBuffer);
1109-
}
1110-
1111-
if (array_buffer) |buffer| {
1112-
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
1113-
const data = backing_store.getData();
1114-
const byte_len = backing_store.getByteLength();
1115-
1116-
switch (ptr.child) {
1117-
u8 => {
1118-
// need this sentinel check to keep the compiler happy
1119-
if (ptr.sentinel() == null) {
1120-
if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
1121-
if (byte_len == 0) return &[_]u8{};
1122-
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
1123-
return arr_ptr[0..byte_len];
1124-
}
1125-
}
1126-
},
1127-
i8 => {
1128-
if (js_value.isInt8Array()) {
1129-
if (byte_len == 0) return &[_]i8{};
1130-
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
1131-
return arr_ptr[0..byte_len];
1132-
}
1133-
},
1134-
u16 => {
1135-
if (js_value.isUint16Array()) {
1136-
if (byte_len == 0) return &[_]u16{};
1137-
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
1138-
return arr_ptr[0 .. byte_len / 2];
1139-
}
1140-
},
1141-
i16 => {
1142-
if (js_value.isInt16Array()) {
1143-
if (byte_len == 0) return &[_]i16{};
1144-
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
1145-
return arr_ptr[0 .. byte_len / 2];
1146-
}
1147-
},
1148-
u32 => {
1149-
if (js_value.isUint32Array()) {
1150-
if (byte_len == 0) return &[_]u32{};
1151-
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
1152-
return arr_ptr[0 .. byte_len / 4];
1153-
}
1154-
},
1155-
i32 => {
1156-
if (js_value.isInt32Array()) {
1157-
if (byte_len == 0) return &[_]i32{};
1158-
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
1159-
return arr_ptr[0 .. byte_len / 4];
1160-
}
1161-
},
1162-
u64 => {
1163-
if (js_value.isBigUint64Array()) {
1164-
if (byte_len == 0) return &[_]u64{};
1165-
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
1166-
return arr_ptr[0 .. byte_len / 8];
1167-
}
1168-
},
1169-
i64 => {
1170-
if (js_value.isBigInt64Array()) {
1171-
if (byte_len == 0) return &[_]i64{};
1172-
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
1173-
return arr_ptr[0 .. byte_len / 8];
1174-
}
1175-
},
1176-
else => {},
1097+
if (ptr.sentinel() == null) {
1098+
if (try self.jsValueToTypedArray(ptr.child, js_value)) |value| {
1099+
return value;
11771100
}
1178-
return error.InvalidArgument;
11791101
}
11801102

11811103
if (ptr.child == u8) {
@@ -1282,6 +1204,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
12821204
return try self.createFunction(js_value);
12831205
}
12841206

1207+
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
1208+
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
1209+
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
1210+
return .{ .values = arr };
1211+
}
1212+
12851213
if (T == String) {
12861214
return .{ .string = try valueToString(self.context_arena, js_value, self.isolate, self.v8_context) };
12871215
}
@@ -1320,6 +1248,90 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
13201248
return value;
13211249
}
13221250

1251+
fn jsValueToTypedArray(_: *JsContext, comptime T: type, js_value: v8.Value) !?[]T {
1252+
var force_u8 = false;
1253+
var array_buffer: ?v8.ArrayBuffer = null;
1254+
if (js_value.isTypedArray()) {
1255+
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1256+
array_buffer = buffer_view.getBuffer();
1257+
} else if (js_value.isArrayBufferView()) {
1258+
force_u8 = true;
1259+
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1260+
array_buffer = buffer_view.getBuffer();
1261+
} else if (js_value.isArrayBuffer()) {
1262+
force_u8 = true;
1263+
array_buffer = js_value.castTo(v8.ArrayBuffer);
1264+
}
1265+
1266+
const buffer = array_buffer orelse return null;
1267+
1268+
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
1269+
const data = backing_store.getData();
1270+
const byte_len = backing_store.getByteLength();
1271+
1272+
switch (T) {
1273+
u8 => {
1274+
// need this sentinel check to keep the compiler happy
1275+
if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
1276+
if (byte_len == 0) return &[_]u8{};
1277+
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
1278+
return arr_ptr[0..byte_len];
1279+
}
1280+
},
1281+
i8 => {
1282+
if (js_value.isInt8Array()) {
1283+
if (byte_len == 0) return &[_]i8{};
1284+
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
1285+
return arr_ptr[0..byte_len];
1286+
}
1287+
},
1288+
u16 => {
1289+
if (js_value.isUint16Array()) {
1290+
if (byte_len == 0) return &[_]u16{};
1291+
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
1292+
return arr_ptr[0 .. byte_len / 2];
1293+
}
1294+
},
1295+
i16 => {
1296+
if (js_value.isInt16Array()) {
1297+
if (byte_len == 0) return &[_]i16{};
1298+
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
1299+
return arr_ptr[0 .. byte_len / 2];
1300+
}
1301+
},
1302+
u32 => {
1303+
if (js_value.isUint32Array()) {
1304+
if (byte_len == 0) return &[_]u32{};
1305+
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
1306+
return arr_ptr[0 .. byte_len / 4];
1307+
}
1308+
},
1309+
i32 => {
1310+
if (js_value.isInt32Array()) {
1311+
if (byte_len == 0) return &[_]i32{};
1312+
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
1313+
return arr_ptr[0 .. byte_len / 4];
1314+
}
1315+
},
1316+
u64 => {
1317+
if (js_value.isBigUint64Array()) {
1318+
if (byte_len == 0) return &[_]u64{};
1319+
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
1320+
return arr_ptr[0 .. byte_len / 8];
1321+
}
1322+
},
1323+
i64 => {
1324+
if (js_value.isBigInt64Array()) {
1325+
if (byte_len == 0) return &[_]i64{};
1326+
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
1327+
return arr_ptr[0 .. byte_len / 8];
1328+
}
1329+
},
1330+
else => {},
1331+
}
1332+
return error.InvalidArgument;
1333+
}
1334+
13231335
fn createFunction(self: *JsContext, js_value: v8.Value) !Function {
13241336
// caller should have made sure this was a function
13251337
std.debug.assert(js_value.isFunction());
@@ -2387,6 +2399,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
23872399
const _TYPED_ARRAY_ID_KLUDGE = true;
23882400

23892401
values: []const T,
2402+
2403+
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
2404+
return .{ .values = try allocator.dupe(T, self.values) };
2405+
}
23902406
};
23912407
}
23922408

src/tests/streams/readable_stream.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!DOCTYPE html>
12
<script src="../testing.js"></script>
23

34
<script id=readable_stream>
@@ -17,6 +18,22 @@
1718
});
1819
</script>
1920

21+
<script id=readable_stream_binary>
22+
const input = new TextEncoder().encode('over 9000!');
23+
const binStream = new ReadableStream({
24+
start(controller) {
25+
controller.enqueue(input);
26+
controller.enqueue("world");
27+
controller.close();
28+
}
29+
});
30+
31+
testing.async(binStream.getReader().read(), (data) => {
32+
testing.expectEqual(input, data.value);
33+
testing.expectEqual(false, data.done);
34+
});
35+
</script>
36+
2037
<script id=readable_stream_close>
2138
var closeResult;
2239

0 commit comments

Comments
 (0)