@rotu/structview 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deno.yml +32 -0
- package/.github/workflows/publish.yml +20 -0
- package/.vscode/settings.json +4 -0
- package/README.md +86 -0
- package/_dist/bigendian.d.ts +32 -0
- package/_dist/bigendian.d.ts.map +1 -0
- package/_dist/mod.d.ts +135 -0
- package/_dist/mod.d.ts.map +1 -0
- package/bigendian.js +122 -0
- package/bigendian.js.map +1 -0
- package/bigendian.ts +136 -0
- package/bigendian_test.ts +70 -0
- package/deno.json +18 -0
- package/deno.lock +23 -0
- package/mod.js +336 -0
- package/mod.js.map +1 -0
- package/mod.ts +487 -0
- package/mod_bench.ts +98 -0
- package/mod_test.ts +319 -0
- package/package.json +18 -0
package/mod.ts
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strongly typed classes for accessing binary data in a structured way
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const dataViewSymbol = Symbol.for("Struct.dataview")
|
|
7
|
+
type AnyStruct = {
|
|
8
|
+
get [dataViewSymbol](): DataView
|
|
9
|
+
}
|
|
10
|
+
// deno-lint-ignore no-explicit-any
|
|
11
|
+
type Constructor<T> = { new (...args: any[]): T }
|
|
12
|
+
// deno-lint-ignore no-explicit-any
|
|
13
|
+
type AnyConstructor = Constructor<any>
|
|
14
|
+
|
|
15
|
+
type SubclassWithProperties<
|
|
16
|
+
Ctor extends Constructor<object>,
|
|
17
|
+
Mixin,
|
|
18
|
+
> =
|
|
19
|
+
& { [K in keyof Ctor]: Ctor[K] }
|
|
20
|
+
& {
|
|
21
|
+
new (
|
|
22
|
+
...args: ConstructorParameters<Ctor>
|
|
23
|
+
): InstanceType<Ctor> & { -readonly [K in keyof Mixin]: Mixin[K] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type TPropertyDescriptor<T> = {
|
|
27
|
+
enumerable?: boolean
|
|
28
|
+
configurable?: boolean
|
|
29
|
+
get?(): T
|
|
30
|
+
set?(t: T): undefined
|
|
31
|
+
value?: T
|
|
32
|
+
writable?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type MixinFromProps<Props extends object> = {
|
|
36
|
+
[K in keyof Props]: Props[K] extends TPropertyDescriptor<infer V> ? V
|
|
37
|
+
: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the underlying DataView of a struct
|
|
42
|
+
* @param struct
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
export function structDataView(struct: AnyStruct): DataView {
|
|
46
|
+
const result = struct[dataViewSymbol]
|
|
47
|
+
if (!(result instanceof DataView)) {
|
|
48
|
+
throw new TypeError("not a struct")
|
|
49
|
+
}
|
|
50
|
+
return struct[dataViewSymbol]
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Helper method to create a view of a contiguous subregion of a Struct's memory
|
|
54
|
+
* @param struct
|
|
55
|
+
* @param start byte offset to start
|
|
56
|
+
* @param end byte offset of the end of the subrange
|
|
57
|
+
* @returns region of the given struct.
|
|
58
|
+
*/
|
|
59
|
+
function structBytes(struct: AnyStruct, start?: number, end?: number) {
|
|
60
|
+
const dv = structDataView(struct)
|
|
61
|
+
start ??= 0
|
|
62
|
+
end ??= dv.byteLength
|
|
63
|
+
console.assert(start <= end)
|
|
64
|
+
console.assert(end <= dv.byteLength)
|
|
65
|
+
return new Uint8Array(dv.buffer, dv.byteOffset + start, end - start)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type of a property descriptor for a struct
|
|
70
|
+
*/
|
|
71
|
+
export type StructPropertyDescriptor<T> =
|
|
72
|
+
& ThisType<AnyStruct>
|
|
73
|
+
& TPropertyDescriptor<T>
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Define a descriptor based on a dataview of the struct
|
|
77
|
+
* @param fieldGetter function which, given a dataview, returns
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
export function fromDataView<Fn extends (dv: DataView) => unknown>(
|
|
81
|
+
fieldGetter: Fn,
|
|
82
|
+
): StructPropertyDescriptor<ReturnType<Fn>> {
|
|
83
|
+
return {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
get() {
|
|
86
|
+
const dv = this[dataViewSymbol]
|
|
87
|
+
return fieldGetter(dv)
|
|
88
|
+
},
|
|
89
|
+
} as ThisType<Struct>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Field for a 8-bit unsigned integer
|
|
94
|
+
*/
|
|
95
|
+
export function u8(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
96
|
+
return {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
get() {
|
|
99
|
+
return structDataView(this).getUint8(fieldOffset)
|
|
100
|
+
},
|
|
101
|
+
set(value) {
|
|
102
|
+
structDataView(this).setUint8(fieldOffset, value)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Field for a little-endian 16-bit unsigned integer
|
|
108
|
+
*/
|
|
109
|
+
export function u16(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
110
|
+
return {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
get() {
|
|
113
|
+
return structDataView(this).getUint16(fieldOffset, true)
|
|
114
|
+
},
|
|
115
|
+
set(value) {
|
|
116
|
+
structDataView(this).setUint16(fieldOffset, value, true)
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Field for a little-endian 32-bit unsigned integer
|
|
122
|
+
*/
|
|
123
|
+
export function u32(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
124
|
+
return {
|
|
125
|
+
enumerable: true,
|
|
126
|
+
get() {
|
|
127
|
+
return structDataView(this).getUint32(fieldOffset, true)
|
|
128
|
+
},
|
|
129
|
+
set(value) {
|
|
130
|
+
structDataView(this).setUint32(fieldOffset, value, true)
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Field for a little-endian 64-bit unsigned integer
|
|
136
|
+
*/
|
|
137
|
+
export function u64(fieldOffset: number): StructPropertyDescriptor<bigint> {
|
|
138
|
+
return {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
get() {
|
|
141
|
+
return structDataView(this).getBigUint64(fieldOffset, true)
|
|
142
|
+
},
|
|
143
|
+
set(value) {
|
|
144
|
+
structDataView(this).setBigUint64(fieldOffset, value, true)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Field for a little-endian 8-bit signed integer
|
|
150
|
+
*/
|
|
151
|
+
export function i8(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
152
|
+
return {
|
|
153
|
+
enumerable: true,
|
|
154
|
+
get() {
|
|
155
|
+
return structDataView(this).getInt8(fieldOffset)
|
|
156
|
+
},
|
|
157
|
+
set(value) {
|
|
158
|
+
structDataView(this).setInt8(fieldOffset, value)
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Field for a little-endian 16-bit signed integer
|
|
164
|
+
*/
|
|
165
|
+
export function i16(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
166
|
+
return {
|
|
167
|
+
enumerable: true,
|
|
168
|
+
get() {
|
|
169
|
+
return structDataView(this).getInt16(fieldOffset, true)
|
|
170
|
+
},
|
|
171
|
+
set(value) {
|
|
172
|
+
structDataView(this).setInt16(fieldOffset, value, true)
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Field for a little-endian 32-bit signed integer
|
|
178
|
+
*/
|
|
179
|
+
export function i32(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
180
|
+
return {
|
|
181
|
+
enumerable: true,
|
|
182
|
+
get() {
|
|
183
|
+
return structDataView(this).getInt32(fieldOffset, true)
|
|
184
|
+
},
|
|
185
|
+
set(value) {
|
|
186
|
+
structDataView(this).setInt32(fieldOffset, value, true)
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Field for a little-endian 64-bit signed integer
|
|
192
|
+
*/
|
|
193
|
+
export function i64(fieldOffset: number): StructPropertyDescriptor<bigint> {
|
|
194
|
+
return {
|
|
195
|
+
enumerable: true,
|
|
196
|
+
get() {
|
|
197
|
+
return structDataView(this).getBigInt64(fieldOffset, true)
|
|
198
|
+
},
|
|
199
|
+
set(value) {
|
|
200
|
+
structDataView(this).setBigInt64(fieldOffset, value, true)
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Field for a little-endian 16-bit binary float (float16_t)
|
|
207
|
+
*/
|
|
208
|
+
export function f16(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
209
|
+
return {
|
|
210
|
+
enumerable: true,
|
|
211
|
+
get() {
|
|
212
|
+
return structDataView(this).getFloat16(fieldOffset, true)
|
|
213
|
+
},
|
|
214
|
+
set(value) {
|
|
215
|
+
structDataView(this).setFloat16(fieldOffset, value, true)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Field for a little-endian 32-bit binary float (float32_t)
|
|
222
|
+
*/
|
|
223
|
+
export function f32(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
224
|
+
return {
|
|
225
|
+
enumerable: true,
|
|
226
|
+
get() {
|
|
227
|
+
return structDataView(this).getFloat32(fieldOffset, true)
|
|
228
|
+
},
|
|
229
|
+
set(value) {
|
|
230
|
+
structDataView(this).setFloat32(fieldOffset, value, true)
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Field for a little-endian 64-bit binary float (float64_t)
|
|
237
|
+
*/
|
|
238
|
+
export function f64(fieldOffset: number): StructPropertyDescriptor<number> {
|
|
239
|
+
return {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
get() {
|
|
242
|
+
return structDataView(this).getFloat64(fieldOffset, true)
|
|
243
|
+
},
|
|
244
|
+
set(value) {
|
|
245
|
+
structDataView(this).setFloat64(fieldOffset, value, true)
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Field for a UTF-8 fixed-length string
|
|
252
|
+
*/
|
|
253
|
+
export function string(
|
|
254
|
+
fieldOffset: number,
|
|
255
|
+
byteLength: number,
|
|
256
|
+
): StructPropertyDescriptor<string> {
|
|
257
|
+
const TEXT_DECODER = new TextDecoder()
|
|
258
|
+
const TEXT_ENCODER = new TextEncoder()
|
|
259
|
+
return {
|
|
260
|
+
enumerable: true,
|
|
261
|
+
get() {
|
|
262
|
+
const str = TEXT_DECODER.decode(
|
|
263
|
+
structBytes(this, fieldOffset, fieldOffset + byteLength),
|
|
264
|
+
)
|
|
265
|
+
// trim all trailing null characters
|
|
266
|
+
return str.replace(/\0+$/, "")
|
|
267
|
+
},
|
|
268
|
+
set(value) {
|
|
269
|
+
const bytes = structBytes(
|
|
270
|
+
this,
|
|
271
|
+
fieldOffset,
|
|
272
|
+
fieldOffset + byteLength,
|
|
273
|
+
)
|
|
274
|
+
bytes.fill(0)
|
|
275
|
+
TEXT_ENCODER.encodeInto(value, bytes)
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Field for a boolean stored in a byte (0 = false, nonzero = true)
|
|
282
|
+
* True will be stored as 1
|
|
283
|
+
*/
|
|
284
|
+
export function bool(fieldOffset: number): StructPropertyDescriptor<boolean> {
|
|
285
|
+
return {
|
|
286
|
+
enumerable: true,
|
|
287
|
+
get() {
|
|
288
|
+
return Boolean(structDataView(this).getUint8(fieldOffset))
|
|
289
|
+
},
|
|
290
|
+
set(value) {
|
|
291
|
+
structDataView(this).setUint8(fieldOffset, value ? 1 : 0)
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type StructConstructor<T extends object> = {
|
|
297
|
+
new (arg: {
|
|
298
|
+
buffer: ArrayBufferLike
|
|
299
|
+
byteOffset?: number
|
|
300
|
+
byteLength?: number
|
|
301
|
+
}): T
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Field for an embedded struct
|
|
306
|
+
* @param ctor constructor for the inner struct
|
|
307
|
+
* @param byteOffset where the inner struct starts relative to the outer struct
|
|
308
|
+
* @param bytelength the length in bytes of the inner struct
|
|
309
|
+
* @returns property descriptor for a struct
|
|
310
|
+
*/
|
|
311
|
+
export function substruct<
|
|
312
|
+
T extends object,
|
|
313
|
+
>(
|
|
314
|
+
ctor: StructConstructor<T>,
|
|
315
|
+
byteOffset?: number,
|
|
316
|
+
bytelength?: number,
|
|
317
|
+
): StructPropertyDescriptor<T> {
|
|
318
|
+
return fromDataView(
|
|
319
|
+
function (dv) {
|
|
320
|
+
const offset2 = dv.byteOffset + (byteOffset ?? 0)
|
|
321
|
+
const bytelength2 = bytelength ?? (dv.byteLength - (byteOffset ?? 0))
|
|
322
|
+
return Reflect.construct(ctor, [{
|
|
323
|
+
buffer: dv.buffer,
|
|
324
|
+
byteOffset: offset2,
|
|
325
|
+
byteLength: bytelength2,
|
|
326
|
+
}])
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Base class for a structured binary object
|
|
333
|
+
* Note there are no predeclared string-keyed properties - all property names are reserved for user-defined fields
|
|
334
|
+
*/
|
|
335
|
+
export class Struct {
|
|
336
|
+
[dataViewSymbol]: DataView
|
|
337
|
+
get [Symbol.toStringTag](): string {
|
|
338
|
+
return Struct.name
|
|
339
|
+
}
|
|
340
|
+
static toDataView(o: Struct): DataView {
|
|
341
|
+
return o[dataViewSymbol]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create a new Struct
|
|
346
|
+
* @param arg options for creating the struct.
|
|
347
|
+
* If options has a `.buffer` property, we will use that as the backing memory (e.g. any TypedArray or DataView).
|
|
348
|
+
* If options has no `.buffer` property but has a `.byteLength`, we will allocate a new buffer for the object.
|
|
349
|
+
*/
|
|
350
|
+
constructor(
|
|
351
|
+
arg:
|
|
352
|
+
| {
|
|
353
|
+
buffer: ArrayBufferLike
|
|
354
|
+
byteOffset?: number
|
|
355
|
+
byteLength?: number
|
|
356
|
+
}
|
|
357
|
+
| {
|
|
358
|
+
buffer?: undefined
|
|
359
|
+
byteOffset?: number
|
|
360
|
+
byteLength: number
|
|
361
|
+
},
|
|
362
|
+
) {
|
|
363
|
+
if (typeof arg !== "object" || arg === null) {
|
|
364
|
+
throw new TypeError("Expected argument to be an object")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
Object.preventExtensions(this)
|
|
368
|
+
if (arg.buffer) {
|
|
369
|
+
this[dataViewSymbol] = new DataView(
|
|
370
|
+
arg.buffer,
|
|
371
|
+
arg.byteOffset,
|
|
372
|
+
arg.byteLength,
|
|
373
|
+
)
|
|
374
|
+
} else if (typeof arg.byteLength === "number") {
|
|
375
|
+
this[dataViewSymbol] = new DataView(
|
|
376
|
+
new ArrayBuffer(arg.byteLength + (arg.byteOffset ?? 0)),
|
|
377
|
+
arg.byteOffset,
|
|
378
|
+
arg.byteLength,
|
|
379
|
+
)
|
|
380
|
+
} else {
|
|
381
|
+
throw new TypeError(
|
|
382
|
+
"Must provide either {buffer} or {byteLength}",
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Subclass a type by adding the given property descriptors
|
|
390
|
+
* @param ctor constructor for the base class
|
|
391
|
+
* @param propertyDescriptors properties to add to subclass instances
|
|
392
|
+
* @returns A new class, inheriting from the base class, with the new property descriptors added
|
|
393
|
+
*/
|
|
394
|
+
function subclassWithProperties<
|
|
395
|
+
const Ctor extends AnyConstructor,
|
|
396
|
+
const Props extends PropertyDescriptorMap,
|
|
397
|
+
>(
|
|
398
|
+
ctor: Ctor,
|
|
399
|
+
propertyDescriptors: Props,
|
|
400
|
+
): SubclassWithProperties<Ctor, MixinFromProps<Props>> {
|
|
401
|
+
return (class extends ctor {
|
|
402
|
+
static {
|
|
403
|
+
Object.defineProperties(this.prototype, propertyDescriptors)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Subclass struct by adding the given property descriptors
|
|
410
|
+
* @param propertyDescriptors properties to add to subclass instances
|
|
411
|
+
* @returns A new class, inheriting from `Struct`, with the new property descriptors added
|
|
412
|
+
*/
|
|
413
|
+
export function defineStruct<const Props extends PropertyDescriptorMap>(
|
|
414
|
+
propertyDescriptors: Props,
|
|
415
|
+
): SubclassWithProperties<typeof Struct, MixinFromProps<Props>> {
|
|
416
|
+
return subclassWithProperties(Struct, propertyDescriptors)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create a new struct subclass for an array of structs
|
|
421
|
+
* @param arrayOptions
|
|
422
|
+
* @returns A new class, inheriting from `Struct` whose elements are statically typed structs
|
|
423
|
+
*/
|
|
424
|
+
export function defineArray<Ctor extends StructConstructor<object>>(
|
|
425
|
+
arrayOptions: {
|
|
426
|
+
/** Constructor for an object view of each element*/
|
|
427
|
+
struct: Ctor
|
|
428
|
+
/** Number of bytes between the start of consecutive elements */
|
|
429
|
+
byteStride: number
|
|
430
|
+
/** Total number of elements in the array (not bytes). If omitted, the array length will depend on the size of its underlying buffer */
|
|
431
|
+
length?: number
|
|
432
|
+
},
|
|
433
|
+
): StructConstructor<
|
|
434
|
+
{
|
|
435
|
+
get length(): number
|
|
436
|
+
element(i: number): InstanceType<Ctor>
|
|
437
|
+
[i: number]: InstanceType<Ctor>
|
|
438
|
+
} & Iterable<InstanceType<Ctor>>
|
|
439
|
+
> {
|
|
440
|
+
const { struct, byteStride, length } = arrayOptions
|
|
441
|
+
|
|
442
|
+
// Define a subclass of Struct which translates array-like index access into element access
|
|
443
|
+
// x[2] -> x.element(2)
|
|
444
|
+
const ProxiedStruct = function (
|
|
445
|
+
...args: ConstructorParameters<typeof Struct>
|
|
446
|
+
) {
|
|
447
|
+
return Reflect.construct(Struct, args, new.target)
|
|
448
|
+
} as unknown as StructConstructor<Struct & { [i: number]: Ctor }>
|
|
449
|
+
|
|
450
|
+
ProxiedStruct.prototype = new Proxy(Struct.prototype, {
|
|
451
|
+
get(target, p, receiver) {
|
|
452
|
+
if (typeof p === "string") {
|
|
453
|
+
const i = parseInt(p)
|
|
454
|
+
if (p === String(i)) {
|
|
455
|
+
return receiver.element(i)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return Reflect.get(target, p, receiver)
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
class StructArray extends ProxiedStruct {
|
|
463
|
+
#struct = struct
|
|
464
|
+
#length = length
|
|
465
|
+
#byteStride = byteStride
|
|
466
|
+
|
|
467
|
+
get length() {
|
|
468
|
+
if (typeof this.#length === "number") {
|
|
469
|
+
return this.#length
|
|
470
|
+
}
|
|
471
|
+
return structDataView(this).byteLength / this.#byteStride
|
|
472
|
+
}
|
|
473
|
+
element(i: number) {
|
|
474
|
+
const ctor = this.#struct
|
|
475
|
+
return new ctor(
|
|
476
|
+
structBytes(this, this.#byteStride * i, this.#byteStride * (i + 1)),
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
*[Symbol.iterator]() {
|
|
480
|
+
for (let i = 0; i < this.length; ++i) {
|
|
481
|
+
yield this.element(i)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// deno-lint-ignore no-explicit-any
|
|
486
|
+
return StructArray as any
|
|
487
|
+
}
|
package/mod_bench.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark tests for struct packing and unpacking.
|
|
3
|
+
* We don't care about being blazing fast - this is to make sure we're not doing anything *eggregiously slow*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { assertEquals } from "jsr:/@std/assert@1/equals"
|
|
8
|
+
import { defineStruct, f32, f64, string, u16, u32, u8 } from "./mod.ts"
|
|
9
|
+
|
|
10
|
+
const StructClass = defineStruct({
|
|
11
|
+
x1: f32(0),
|
|
12
|
+
x2: f64(4),
|
|
13
|
+
x3: u32(12),
|
|
14
|
+
x4: u16(16),
|
|
15
|
+
x5: u8(18),
|
|
16
|
+
x6: string(19, 11),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const testObject = {
|
|
20
|
+
x1: Math.fround(Math.SQRT2),
|
|
21
|
+
x2: Math.PI,
|
|
22
|
+
x3: 0x01234567,
|
|
23
|
+
x4: 0x0123,
|
|
24
|
+
x5: 0x01,
|
|
25
|
+
x6: "Hello world",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const testBytes = Uint8Array.fromHex(
|
|
29
|
+
"f304b53f182d4454fb2109406745230123010148656c6c6f20776f726c64",
|
|
30
|
+
)
|
|
31
|
+
const BYTE_LENGTH = 30
|
|
32
|
+
|
|
33
|
+
function manualUnpack(ab: ArrayBuffer) {
|
|
34
|
+
const dv = new DataView(ab)
|
|
35
|
+
return {
|
|
36
|
+
x1: dv.getFloat32(0, true),
|
|
37
|
+
x2: dv.getFloat64(4, true),
|
|
38
|
+
x3: dv.getUint32(12, true),
|
|
39
|
+
x4: dv.getUint16(16, true),
|
|
40
|
+
x5: dv.getUint8(18),
|
|
41
|
+
x6: new TextDecoder().decode(new Uint8Array(ab, 19, 11)),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function manualPack(
|
|
46
|
+
x: typeof testObject,
|
|
47
|
+
) {
|
|
48
|
+
const ab = new ArrayBuffer(BYTE_LENGTH)
|
|
49
|
+
const dv = new DataView(ab)
|
|
50
|
+
dv.setFloat32(0, x.x1, true)
|
|
51
|
+
dv.setFloat64(4, x.x2, true)
|
|
52
|
+
dv.setUint32(12, x.x3, true)
|
|
53
|
+
dv.setUint16(16, x.x4, true)
|
|
54
|
+
dv.setUint8(18, x.x5)
|
|
55
|
+
new TextEncoder().encodeInto(x.x6, new Uint8Array(ab, 19, 11))
|
|
56
|
+
return ab
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Deno.bench("pack with Struct", { group: "pack", baseline: true }, () => {
|
|
60
|
+
const result = new Uint8Array(BYTE_LENGTH)
|
|
61
|
+
const s = new StructClass(result)
|
|
62
|
+
s.x1 = testObject.x1
|
|
63
|
+
s.x2 = testObject.x2
|
|
64
|
+
s.x3 = testObject.x3
|
|
65
|
+
s.x4 = testObject.x4
|
|
66
|
+
s.x5 = testObject.x5
|
|
67
|
+
s.x6 = testObject.x6
|
|
68
|
+
assertEquals(result, testBytes)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
Deno.bench("pack manually", { group: "pack" }, () => {
|
|
72
|
+
const result = manualPack(testObject)
|
|
73
|
+
assertEquals(new Uint8Array(result), testBytes)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Surprisingly, this turns out to be *faster* than the DataView version! I think it's because of V8 optimizations for a class instance vs an object literal.
|
|
77
|
+
Deno.bench("unpack with Struct", { group: "unpack", baseline: true }, () => {
|
|
78
|
+
const result = new StructClass({
|
|
79
|
+
buffer: testBytes.buffer,
|
|
80
|
+
byteLength: BYTE_LENGTH,
|
|
81
|
+
})
|
|
82
|
+
assertEquals(result.x1, testObject.x1)
|
|
83
|
+
assertEquals(result.x2, testObject.x2)
|
|
84
|
+
assertEquals(result.x3, testObject.x3)
|
|
85
|
+
assertEquals(result.x4, testObject.x4)
|
|
86
|
+
assertEquals(result.x5, testObject.x5)
|
|
87
|
+
assertEquals(result.x6, testObject.x6)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
Deno.bench("unpack manually", { group: "unpack" }, () => {
|
|
91
|
+
const result = manualUnpack(testBytes.buffer)
|
|
92
|
+
assertEquals(result.x1, testObject.x1)
|
|
93
|
+
assertEquals(result.x2, testObject.x2)
|
|
94
|
+
assertEquals(result.x3, testObject.x3)
|
|
95
|
+
assertEquals(result.x4, testObject.x4)
|
|
96
|
+
assertEquals(result.x5, testObject.x5)
|
|
97
|
+
assertEquals(result.x6, testObject.x6)
|
|
98
|
+
})
|