refactor(struct): remove all DataView usage

This commit is contained in:
Simon Chan 2024-07-11 20:50:31 +08:00
parent 1d319929ed
commit 75ff6d0ed6
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
15 changed files with 62 additions and 115 deletions

View file

@ -30,7 +30,7 @@ This package is part of [Tango ADB](https://github.com/yume-chan/ya-webadb). Gen
## Documentation
Check the latest documentation at https://tango-adb.github.io/docs/
Check the latest documentation at https://docs.tangoapp.dev/
## Sponsors

View file

@ -1,4 +1,4 @@
import { getUint16 } from "@yume-chan/no-data-view";
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
import type { NumberFieldVariant } from "@yume-chan/struct";
import { NumberFieldDefinition } from "@yume-chan/struct";
@ -22,11 +22,11 @@ export const ScrcpyUnsignedFloatNumberVariant: NumberFieldVariant = {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22
return value === 0xffff ? 1 : value / 0x10000;
},
serialize(dataView, offset, value, littleEndian) {
serialize(array, offset, value, littleEndian) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
value = clamp(value, -1, 1);
value = value === 1 ? 0xffff : value * 0x10000;
dataView.setUint16(offset, value, littleEndian);
setUint16(array, offset, value, littleEndian);
},
};

View file

@ -9,24 +9,24 @@ import {
describe("ScrcpyFloatToInt16NumberType", () => {
it("should serialize", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -1, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
const array = new Uint8Array(2);
ScrcpySignedFloatNumberVariant.serialize(array, 0, -1, true);
expect(new DataView(array.buffer).getInt16(0, true)).toBe(-0x8000);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 0, true);
expect(dataView.getInt16(0, true)).toBe(0);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 0, true);
expect(new DataView(array.buffer).getInt16(0, true)).toBe(0);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 1, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 1, true);
expect(new DataView(array.buffer).getInt16(0, true)).toBe(0x7fff);
});
it("should clamp input values", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, -2, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
const array = new Uint8Array(2);
ScrcpySignedFloatNumberVariant.serialize(array, 0, -2, true);
expect(new DataView(array.buffer).getInt16(0, true)).toBe(-0x8000);
ScrcpySignedFloatNumberVariant.serialize(dataView, 0, 2, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
ScrcpySignedFloatNumberVariant.serialize(array, 0, 2, true);
expect(new DataView(array.buffer).getInt16(0, true)).toBe(0x7fff);
});
it("should deserialize", () => {

View file

@ -1,4 +1,4 @@
import { getInt16 } from "@yume-chan/no-data-view";
import { getInt16, setInt16 } from "@yume-chan/no-data-view";
import type { NumberFieldVariant } from "@yume-chan/struct";
import Struct, { NumberFieldDefinition } from "@yume-chan/struct";
@ -15,11 +15,11 @@ export const ScrcpySignedFloatNumberVariant: NumberFieldVariant = {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
return value === 0x7fff ? 1 : value / 0x8000;
},
serialize(dataView, offset, value, littleEndian) {
serialize(array, offset, value, littleEndian) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L65
value = clamp(value, -1, 1);
value = value === 1 ? 0x7fff : value * 0x8000;
dataView.setInt16(offset, value, littleEndian);
setInt16(array, offset, value, littleEndian);
},
};

View file

@ -93,7 +93,6 @@ Some features can be polyfilled to support older runtimes, but this library does
| [`Promise`][mdn_promise] | 32 | 12 | 29 | No | 8 | 0.12 |
| [`ArrayBuffer`][mdn_arraybuffer] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| [`Uint8Array`][mdn_uint8array] | 7 | 12 | 4 | 10 | 5.1 | 0.10 |
| [`DataView`][mdn_dataview] | 9 | 12 | 15 | 10 | 5.1 | 0.10 |
| _Overall_ | 32 | 12 | 29 | No | 8 | 0.12 |
### [`int64`/`uint64`](#int64uint64-1)
@ -115,9 +114,7 @@ Some features can be polyfilled to support older runtimes, but this library does
[mdn_promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[mdn_arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
[mdn_uint8array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
[mdn_dataview]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
[mdn_bigint]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[mdn_dataview]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
[mdn_textencoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
## API
@ -169,7 +166,7 @@ class Struct<
TFields extends object = {},
TOmitInitKey extends string | number | symbol = never,
TExtra extends object = {},
TPostDeserialized = undefined
TPostDeserialized = undefined,
> {
public constructor(options: Partial<StructOptions> = StructDefaultOptions);
}
@ -640,7 +637,7 @@ struct.field("foo", new NumberFieldDefinition(NumberFieldType.Int8));
abstract class StructFieldDefinition<
TOptions = void,
TValue = unknown,
TOmitInitKey extends PropertyKey = never
TOmitInitKey extends PropertyKey = never,
> {
public readonly options: TOptions;
@ -742,9 +739,9 @@ If one needs to manipulate other states when getting/setting values, they can ov
```ts
abstract serialize(
dataView: DataView,
array: Uint8Array,
offset: number
): void;
```
Derived classes must implement this method to serialize current value into `dataView`, from `offset`. It must not write more bytes than what its `getSize` returned.
Derived classes must implement this method to serialize current value into `array`, from `offset`. It must not write more bytes than what its `getSize` returned.

View file

@ -13,12 +13,7 @@ describe("StructFieldValue", () => {
describe(".constructor", () => {
it("should save parameters", () => {
class MockStructFieldValue extends StructFieldValue<never> {
override serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void {
void dataView;
override serialize(array: Uint8Array, offset: number): void {
void array;
void offset;
throw new Error("Method not implemented.");
@ -83,12 +78,7 @@ describe("StructFieldValue", () => {
}
class MockStructFieldValue extends StructFieldValue<any> {
override serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void {
void dataView;
override serialize(array: Uint8Array, offset: number): void {
void array;
void offset;
throw new Error("Method not implemented.");
@ -109,12 +99,7 @@ describe("StructFieldValue", () => {
describe("#set", () => {
it("should update its internal value", () => {
class MockStructFieldValue extends StructFieldValue<any> {
override serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void {
void dataView;
override serialize(array: Uint8Array, offset: number): void {
void array;
void offset;
throw new Error("Method not implemented.");

View file

@ -65,11 +65,7 @@ export abstract class StructFieldValue<
}
/**
* When implemented in derived classes, serializes this field into `dataView` at `offset`
* When implemented in derived classes, serializes this field into `array` at `offset`
*/
abstract serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void;
abstract serialize(array: Uint8Array, offset: number): void;
}

View file

@ -690,14 +690,9 @@ export class Struct<
throw new TypeError("Output buffer is too small");
}
const dataView = new DataView(
output.buffer,
output.byteOffset,
output.byteLength,
);
let offset = 0;
for (const { fieldValue, size } of fieldsInfo) {
fieldValue.serialize(dataView, output, offset);
fieldValue.serialize(output, offset);
offset += size;
}

View file

@ -109,7 +109,7 @@ export class BigIntFieldDefinition<
export class BigIntFieldValue<
TDefinition extends BigIntFieldDefinition<BigIntFieldVariant, unknown>,
> extends StructFieldValue<TDefinition> {
override serialize(_: DataView, array: Uint8Array, offset: number): void {
override serialize(array: Uint8Array, offset: number): void {
this.definition.variant.serialize(
array,
offset,

View file

@ -190,8 +190,7 @@ describe("Types", () => {
);
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, targetArray, 0);
fieldValue.serialize(targetArray, 0);
expect(targetArray).toEqual(sourceArray);
});
@ -220,8 +219,7 @@ describe("Types", () => {
fieldValue.set(sourceArray);
const targetArray = new Uint8Array(size);
const targetView = new DataView(targetArray.buffer);
fieldValue.serialize(targetView, targetArray, 0);
fieldValue.serialize(targetArray, 0);
expect(targetArray).toEqual(sourceArray);
});

View file

@ -188,7 +188,7 @@ export class BufferLikeFieldValue<
this.array = undefined;
}
override serialize(_: DataView, array: Uint8Array, offset: number): void {
override serialize(array: Uint8Array, offset: number): void {
this.array ??= this.definition.converter.toBuffer(this.value);
array.set(this.array, offset);
}

View file

@ -39,12 +39,10 @@ class MockLengthFieldValue extends StructFieldValue<any> {
void value;
});
serialize = jest.fn(
(dataView: DataView, _: Uint8Array, offset: number): void => {
void dataView;
serialize = jest.fn((array: Uint8Array, offset: number): void => {
void array;
void offset;
},
);
});
}
describe("Types", () => {
@ -64,12 +62,7 @@ describe("Types", () => {
override getSize = jest.fn(() => this.size);
override serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
): void {
void dataView;
override serialize(array: Uint8Array, offset: number): void {
void array;
void offset;
throw new Error("Method not implemented.");
@ -174,13 +167,12 @@ describe("Types", () => {
mockBufferFieldValue,
);
const dataView = {} as any;
const array = {} as any;
const offset = {} as any;
mockOriginalFieldValue.value = 10;
mockBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.get).toHaveReturnedWith(10);
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
@ -189,7 +181,6 @@ describe("Types", () => {
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);
@ -197,14 +188,13 @@ describe("Types", () => {
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.set).toHaveBeenCalledWith(100);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledTimes(
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);
@ -219,13 +209,12 @@ describe("Types", () => {
mockBufferFieldValue,
);
const dataView = {} as any;
const array = {} as any;
const offset = {} as any;
mockOriginalFieldValue.value = "10";
mockBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.get).toHaveReturnedWith("10");
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
@ -234,7 +223,6 @@ describe("Types", () => {
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);
@ -242,14 +230,13 @@ describe("Types", () => {
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.set).toHaveBeenCalledWith("100");
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledTimes(
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);
@ -268,13 +255,12 @@ describe("Types", () => {
mockBufferFieldValue.definition.options.lengthFieldRadix =
radix;
const dataView = {} as any;
const array = {} as any;
const offset = {} as any;
mockOriginalFieldValue.value = "10";
mockBufferFieldValue.size = 0;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.get).toHaveReturnedWith("10");
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
@ -283,7 +269,6 @@ describe("Types", () => {
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);
@ -291,7 +276,7 @@ describe("Types", () => {
mockOriginalFieldValue.set.mockClear();
mockOriginalFieldValue.serialize.mockClear();
mockBufferFieldValue.size = 100;
lengthFieldValue.serialize(dataView, array, offset);
lengthFieldValue.serialize(array, offset);
expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1);
expect(mockOriginalFieldValue.set).toHaveBeenCalledWith(
(100).toString(radix),
@ -300,7 +285,6 @@ describe("Types", () => {
1,
);
expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith(
dataView,
array,
offset,
);

View file

@ -192,8 +192,8 @@ export class VariableLengthBufferLikeFieldLengthValue extends StructFieldValue<
// It will always be in sync with the buffer size
}
serialize(dataView: DataView, array: Uint8Array, offset: number) {
serialize(array: Uint8Array, offset: number) {
this.originalValue.set(this.get());
this.originalValue.serialize(dataView, array, offset);
this.originalValue.serialize(array, offset);
}
}

View file

@ -315,8 +315,7 @@ describe("Types", () => {
);
const array = new Uint8Array(10);
const dataView = new DataView(array.buffer);
value.serialize(dataView, array, 2);
value.serialize(array, 2);
expect(Array.from(array)).toEqual([
0, 0, 42, 0, 0, 0, 0, 0, 0, 0,

View file

@ -4,6 +4,10 @@ import {
getInt8,
getUint16,
getUint32,
setInt16,
setInt32,
setUint16,
setUint32,
} from "@yume-chan/no-data-view";
import type {
@ -21,7 +25,7 @@ export interface NumberFieldVariant {
size: number;
deserialize(array: Uint8Array, littleEndian: boolean): number;
serialize(
dataView: DataView,
array: Uint8Array,
offset: number,
value: number,
littleEndian: boolean,
@ -35,8 +39,8 @@ export namespace NumberFieldVariant {
deserialize(array) {
return array[0]!;
},
serialize(dataView, offset, value) {
dataView.setUint8(offset, value);
serialize(array, offset, value) {
array[offset] = value;
},
};
@ -46,8 +50,8 @@ export namespace NumberFieldVariant {
deserialize(array) {
return getInt8(array, 0);
},
serialize(dataView, offset, value) {
dataView.setInt8(offset, value);
serialize(array, offset, value) {
array[offset] = value;
},
};
@ -55,14 +59,9 @@ export namespace NumberFieldVariant {
signed: false,
size: 2,
deserialize(array, littleEndian) {
// PERF: Creating many `DataView`s over small buffers is 90% slower
// than this. Even if the `DataView` is cached, `DataView#getUint16`
// is still 1% slower than this.
return getUint16(array, 0, littleEndian);
},
serialize(dataView, offset, value, littleEndian) {
dataView.setUint16(offset, value, littleEndian);
},
serialize: setUint16,
};
export const Int16: NumberFieldVariant = {
@ -71,9 +70,7 @@ export namespace NumberFieldVariant {
deserialize(array, littleEndian) {
return getInt16(array, 0, littleEndian);
},
serialize(dataView, offset, value, littleEndian) {
dataView.setInt16(offset, value, littleEndian);
},
serialize: setInt16,
};
export const Uint32: NumberFieldVariant = {
@ -82,9 +79,7 @@ export namespace NumberFieldVariant {
deserialize(array, littleEndian) {
return getUint32(array, 0, littleEndian);
},
serialize(dataView, offset, value, littleEndian) {
dataView.setUint32(offset, value, littleEndian);
},
serialize: setUint32,
};
export const Int32: NumberFieldVariant = {
@ -93,9 +88,7 @@ export namespace NumberFieldVariant {
deserialize(array, littleEndian) {
return getInt32(array, 0, littleEndian);
},
serialize(dataView, offset, value, littleEndian) {
dataView.setInt32(offset, value, littleEndian);
},
serialize: setInt32,
};
}
@ -155,9 +148,9 @@ export class NumberFieldDefinition<
export class NumberFieldValue<
TDefinition extends NumberFieldDefinition<NumberFieldVariant, unknown>,
> extends StructFieldValue<TDefinition> {
serialize(dataView: DataView, _: Uint8Array, offset: number): void {
serialize(array: Uint8Array, offset: number): void {
this.definition.variant.serialize(
dataView,
array,
offset,
this.value as never,
this.options.littleEndian,