@3m1/binary-object 中文文档教程
Binary Object
使用严格类型化的 JavaScript 面向对象编程管理二进制数据。
Summary
- Install
- Usage
- First: polyfill if needed
- Second: decorators or not
- API
- Examples
- Memory owner
- Use cases
- WebAssembly
- Disable Garbage Collector (GC)
- Workers API
- Saving/restoring states
- Accessing binary data files
- Accessing binary APIs
- Develop backend DB APIs
- See also
Install
使用 npm
:
npm install binary-object
使用 yarn
:
yarn add binary-object
Usage
First: polyfill if needed
该库使用 TextEncoder
和 TextDecoder
将文本与二进制数据相互转换。 这些是 JavaScript 原生函数,但 Node 缺少它们。 你需要先填充它们:
if(!('TextEncoder' in global)) {
import('util').then(nodeUtil => {;
global.TextEncoder = nodeUtil.TextEncoder;
global.TextDecoder = nodeUtil.TextDecoder;
});
}
为了在 Node >= 14 上工作,你需要先安装 util
:
npm install --save util
Second: decorators or not
这个库鼓励使用类成员装饰器,在 Typescript 中可用,但在 < href="https://github.com/tc39/proposals#stage-2">第二阶段提案。 要将它添加到你的 Babel
配置中,你需要类似的东西:
"plugins": [
"@babel/plugin-transform-runtime",
[ "@babel/plugin-proposal-decorators", { "legacy": true } ],
[ "@babel/plugin-proposal-class-properties", { "loose": true } ]
]
如果你不想使用装饰器,你需要像这样使用它(未测试):
class MyBinaryClass extends Binary { /* Functions/logic/statics */ }
Object.defineProperty(MyBinaryClass.prototype, "someMember", binary(Types.Float32));
API
参见自动生成的 jsdoc< /code> 此存储库的 GitHub 页面 中的 API 文档。
Examples
import {Binary, binary, Types} from 'binary-object';
// Declare object shape
class BinaryTest extends Binary {
@binary(Types.Uint32)
id = 0;
@binary(Types.Float64)
testFloat = 0;
// Array of 10 low precision floating point numbers
@binary(Types.Array(Types.Float32, 10))
testFloatArray;
showId = () => console.log(`My id is ${this.id}`);
}
// Allocate memory
const binTest = new ArrayBuffer( BinaryTest.binarySize );
// Instantiate memory parser
const proxyObject = new BinaryTest(binTest);
// Use it
proxyObject.id = 12345;
proxyObject.testFloat = 7.8;
expect(proxyObject.id).toBe(12345);
expect(proxyObject.testFloat).toBe(7.8);
访问和修改数组和数组元素也有效:
const testArray = proxyObject.testFloatArray;
// Iteratibility
expect([...testArray]).toEqual(new Array(10).fill(0));
// Elements access and modification
expect(testArray[0]).toBe(0);
testArray[0] = 1;
expect(testArray[0]).toBe(1);
expect(testArray[1]).toBe(0);
testArray[1] = 2;
expect(testArray[0]).toBe(1);
expect(testArray[1]).toBe(2);
// Modify full array
const newArr = [1,0,0,0,0,0,0,0,0,1];
proxyObject.testFloatArray = newArr;
expect([...testArray]).toEqual(newArr);
您可以定义填充数组以获得更好的性能,并且可能由 API 强制执行:
const type = Types.Uint32;
const length = 5;
class BinaryPadTest extends Binary {
@binary(Types.Uint8)
someMemberAtTheBegining;
// Here, `true` reffers to padding
@binary(Types.Array(type, length, true))
testArray;
@binary(Types.Uint8)
someMemberAtTheEnd;
}
const binTest2 = new ArrayBuffer(BinaryPadTest.binarySize);
const testObj = new BinaryPadTest(binTest2);
// The first byte of someMemberAtTheBegining forces to consume
// a full testArray element before it (for padding)
const expectedSize = ((length + 1) * type.bytes) + 1;
expect(BinaryPadTest.binarySize).toBe(expectedSize);
expect(testObj.testArray).toBeInstanceOf(type.extractor); // Uint32Array
也允许
class BinaryArrayOfNestedTest extends Binary {
// Array of 3 BinaryTest objects
@binary( Types.Array( Types.Struct(BinaryTest), 3))
testNested;
get id() { return this.testNested[0].id }
set id(value) {
this.testNested[0].id = value;
return true
}
@binary(Types.Uint32)
someNumber = 0;
showId = this.testNested[0].showId;
}
const binTest2 = new ArrayBuffer( BinaryArrayOfNestedTest.binarySize );
const proxyNested = new BinaryArrayOfNestedTest( binTest2 );
对象组合:也允许来自不同内存源的对象组合:
class Position extends Binary {
@binary(Types.Uint32)
x;
@binary(Types.Uint32)
y;
static testCollision(pos1, pos2) {
...
}
collision = (pos2) => this.constructor.testCollision(this, pos2);
}
class Player extends Binary {
@binary( Types.Struct(Position) )
position;
@binary(Types.Float64)
life;
// Non managed binary data (like pointers)
bullets = [];
}
class Bullet extends Binary {
@binary( Types.Struct(Position) )
position;
@binary(Types.Float64)
direction;
}
class Game {
constructor() {
// malloc for players
this.binPlayers = new ArrayBuffer( Player.binarySize * 2 );
this.player1 = new Player( this.binPlayers );
this.player2 = new Player( this.binPlayers, Player.binarySize );
// malloc for bullets
this.maxBullets = 100;
this.binBullets = new ArrayBuffer( Bullet.binarySize * this.maxBullets );
// Optimize bullets by using a unique DataView for all of them
this.dvBullets = new DataView(this.binBullets);
// Half of the bullets for each player
Bullets.arrayFactory(
this.dvBullets,
maxBullets / 2,
0,
player1.bullets);
Bullets.arrayFactory(
this.dvBullets,
maxBullets / 2,
maxBullets / 2,
player2.bullets);
}
// You can move Bullets using a parallel worker or a WASM code block
// Sometimes, check if a bullet touched a victim
testTouched(attacker, victim) {
const {position: {collision}} = victim;
const touched = attacker.bullets.some( ({position}) => collision(position) );
}
}
将二进制数据转换为 JSON 字符串:
console.log( JSON.stringify(proxyNested) );
从 JS 对象分配数据:
proxyNested.testNested = {testFloatArray: [0,0,0,0,0,0,0,0,0,1]};
expect( [...proxyNested.testNested.testFloatArray] ).toEqual([0,0,0,0,0,0,0,0,0,1]);
将大内存块分配并解析为对象数组:
const iterations = 5e5;
const binTest3 = new ArrayBuffer( BinaryTest.binarySize * iterations );
const dv3 = new DataView(binTest3);
const bObjList = new Array(iterations);
for(let i = 0; i < iterations; i++) {
bObjList.push(new BinaryTest(dv3, BinaryTest.binarySize * i));
}
bObjList.forEach( (obj, index) => obj.id = `i${index}`);
const ids = bObjList.map( ({id}) => id);
Memory owner
该库的目的不是拥有内存,而是允许以不同的方式使用它。 内存可以从并行 worker、WebAssembly 代码或其他库或 API 中分配,这些库或 API 可以 使用 ArrayBuffer
或 SharedBuffer
使用。 建议将此库与管理内存的库一起包装。
注意:选择管理您自己的内存(如在 C 中)需要您了解您在低级别上所做的事情。 如果您不这样做,诸如动态数组大小或文本操作之类的事情可能会很痛苦 了解基础知识(更不用说字节顺序或内存填充)。 有几种标准和已知的方法来管理内存。 该库旨在使其更容易解析(读取和写入), 但您仍然需要知道它是如何工作的。 具体来说,这个库并不旨在拥有内存块:您需要围绕实例化 JS 二进制对象来创建/分配/移动/复制/重新分配/释放它们。
Use cases
需要直接内存管理的环境在 JS 中是最少的。 尽管如此,在某些边缘情况下,这样做可能是有益的或强制性的。
WebAssembly
WebAssembly 允许使用一些低级语言(如 C、C++ 或 Rust)进行编码,并编译成一些可以由 JS 解释器执行的二进制代码(当然,使用 WebAssembly API)。
编译成 WebAssembly 的代码可以使用专门的库与 JS 代码通信。 它允许访问 DOM(因此,window
对象),但也共享内存 使用 WebAssembly.Memory
。 使用它,您可以编写复杂但非常快速的代码来在编译代码中进行大量计算,并使用当前的触发某种 JS 重新渲染 某个时间或帧速率的计算值。
Disable Garbage Collector (GC)
由于应用程序状态主要由 ArrayBuffer
管理(一些 JS 内部缓存除外),您可以最大限度地减少 GC 工作,这将非常顺利地运行您的游戏(应用程序),而不会出现意外的后台 GC 任务。
Workers API
与 WebAssembly 用例类似,有些人可以创建一个 SharedBuffer
并从不同的 Worker 使用它,从而允许您的应用程序使用多个 CPU。 例如在游戏中:你可以 主要工作人员使用缓冲区中的数据更新 DOM
(或 canvas
或 OpenGL),同时有几个工作人员计算 3D 碰撞。
Saving/restoring states
由于所有数据都在一个内存块中,您可以轻松地将其保存到某个地方并稍后加载以恢复状态。 这可能对游戏以及 AI、3D 渲染器、科学 计算、密码破解等。
Accessing binary data files
动态二进制读取、处理和写入(到服务器或直接到用户),如图像、音频、视频、医疗数据 (DICOM) 等。
Accessing binary APIs
您可以充分利用浏览器 USB 和/或蓝牙API,但您也可以轻松地与二进制 API 服务器或物联网进行通信。 虽然,因为它们通常是只读的或 基于流,您会更喜欢使用 DataStreams.js
代替。
Develop backend DB APIs
一些库允许在后端应用程序和数据库之间维护共享内存。 这个库可以帮助开发 Node 数据库中间件。
See also
有几个 JS 项目旨在处理二进制数据:
- buffer-backed-object: creates objects that are backed by an ArrayBuffer
- @bnaya/objectbuffer (source code): JavaScript Object like api, backed by an arraybuffer
- DataStream.js: library for reading data from ArrayBuffers
- Restructure
- Restructure (next)
- buffercodec
- Buffer Plus
- Binary Protocol
- Binary-parser
- @yotamshacham/schema
- Structron
- cppmsg
- bin-protocol
- byte-data
- binobject
- Binarcular
- Uttori Data Tools
- @avro/types
- Structurae
- @thi.ng/unionstruct
- Typed Array Buffer Schema
- @sighmir/bstruct
- binary-parser-encoder
- binary-encoder
- c-struct
- binary-transfer
- Superbuffer
- @binary-files/structjs
- js Binary Schema Parser
- jDataView/jBinary
Binary Object
Manage binary data with strictly typed JavaScript Object-oriented programming.
Summary
- Install
- Usage
- First: polyfill if needed
- Second: decorators or not
- API
- Examples
- Memory owner
- Use cases
- WebAssembly
- Disable Garbage Collector (GC)
- Workers API
- Saving/restoring states
- Accessing binary data files
- Accessing binary APIs
- Develop backend DB APIs
- See also
Install
With npm
:
npm install binary-object
With yarn
:
yarn add binary-object
Usage
First: polyfill if needed
This library uses TextEncoder
and TextDecoder
to transform text to and from binary data. These are JavaScript native functions, but Node lacks them. You need to polyfill them first:
if(!('TextEncoder' in global)) {
import('util').then(nodeUtil => {;
global.TextEncoder = nodeUtil.TextEncoder;
global.TextDecoder = nodeUtil.TextDecoder;
});
}
For this to work on Node >= 14, you need to install util
first:
npm install --save util
Second: decorators or not
This library encourages the use of class member decorators, available in Typescript, but at a stage 2 proposal. To add it into your Babel
configuration, you will need something like:
"plugins": [
"@babel/plugin-transform-runtime",
[ "@babel/plugin-proposal-decorators", { "legacy": true } ],
[ "@babel/plugin-proposal-class-properties", { "loose": true } ]
]
If you don't want to use decorators, you will need to use it like (not tested):
class MyBinaryClass extends Binary { /* Functions/logic/statics */ }
Object.defineProperty(MyBinaryClass.prototype, "someMember", binary(Types.Float32));
API
See autogenerated jsdoc
API documentation at this repo's GitHub Page.
Examples
import {Binary, binary, Types} from 'binary-object';
// Declare object shape
class BinaryTest extends Binary {
@binary(Types.Uint32)
id = 0;
@binary(Types.Float64)
testFloat = 0;
// Array of 10 low precision floating point numbers
@binary(Types.Array(Types.Float32, 10))
testFloatArray;
showId = () => console.log(`My id is ${this.id}`);
}
// Allocate memory
const binTest = new ArrayBuffer( BinaryTest.binarySize );
// Instantiate memory parser
const proxyObject = new BinaryTest(binTest);
// Use it
proxyObject.id = 12345;
proxyObject.testFloat = 7.8;
expect(proxyObject.id).toBe(12345);
expect(proxyObject.testFloat).toBe(7.8);
Accessing and modifying arrays and array elements also work:
const testArray = proxyObject.testFloatArray;
// Iteratibility
expect([...testArray]).toEqual(new Array(10).fill(0));
// Elements access and modification
expect(testArray[0]).toBe(0);
testArray[0] = 1;
expect(testArray[0]).toBe(1);
expect(testArray[1]).toBe(0);
testArray[1] = 2;
expect(testArray[0]).toBe(1);
expect(testArray[1]).toBe(2);
// Modify full array
const newArr = [1,0,0,0,0,0,0,0,0,1];
proxyObject.testFloatArray = newArr;
expect([...testArray]).toEqual(newArr);
You can define padded arrays for better performance and, maybe, enforced by API:
const type = Types.Uint32;
const length = 5;
class BinaryPadTest extends Binary {
@binary(Types.Uint8)
someMemberAtTheBegining;
// Here, `true` reffers to padding
@binary(Types.Array(type, length, true))
testArray;
@binary(Types.Uint8)
someMemberAtTheEnd;
}
const binTest2 = new ArrayBuffer(BinaryPadTest.binarySize);
const testObj = new BinaryPadTest(binTest2);
// The first byte of someMemberAtTheBegining forces to consume
// a full testArray element before it (for padding)
const expectedSize = ((length + 1) * type.bytes) + 1;
expect(BinaryPadTest.binarySize).toBe(expectedSize);
expect(testObj.testArray).toBeInstanceOf(type.extractor); // Uint32Array
Object composition is also allowed:
class BinaryArrayOfNestedTest extends Binary {
// Array of 3 BinaryTest objects
@binary( Types.Array( Types.Struct(BinaryTest), 3))
testNested;
get id() { return this.testNested[0].id }
set id(value) {
this.testNested[0].id = value;
return true
}
@binary(Types.Uint32)
someNumber = 0;
showId = this.testNested[0].showId;
}
const binTest2 = new ArrayBuffer( BinaryArrayOfNestedTest.binarySize );
const proxyNested = new BinaryArrayOfNestedTest( binTest2 );
Object composition from different memory sources is also allowed:
class Position extends Binary {
@binary(Types.Uint32)
x;
@binary(Types.Uint32)
y;
static testCollision(pos1, pos2) {
...
}
collision = (pos2) => this.constructor.testCollision(this, pos2);
}
class Player extends Binary {
@binary( Types.Struct(Position) )
position;
@binary(Types.Float64)
life;
// Non managed binary data (like pointers)
bullets = [];
}
class Bullet extends Binary {
@binary( Types.Struct(Position) )
position;
@binary(Types.Float64)
direction;
}
class Game {
constructor() {
// malloc for players
this.binPlayers = new ArrayBuffer( Player.binarySize * 2 );
this.player1 = new Player( this.binPlayers );
this.player2 = new Player( this.binPlayers, Player.binarySize );
// malloc for bullets
this.maxBullets = 100;
this.binBullets = new ArrayBuffer( Bullet.binarySize * this.maxBullets );
// Optimize bullets by using a unique DataView for all of them
this.dvBullets = new DataView(this.binBullets);
// Half of the bullets for each player
Bullets.arrayFactory(
this.dvBullets,
maxBullets / 2,
0,
player1.bullets);
Bullets.arrayFactory(
this.dvBullets,
maxBullets / 2,
maxBullets / 2,
player2.bullets);
}
// You can move Bullets using a parallel worker or a WASM code block
// Sometimes, check if a bullet touched a victim
testTouched(attacker, victim) {
const {position: {collision}} = victim;
const touched = attacker.bullets.some( ({position}) => collision(position) );
}
}
Transform your binary data into a JSON string:
console.log( JSON.stringify(proxyNested) );
Assign data from JS objects:
proxyNested.testNested = {testFloatArray: [0,0,0,0,0,0,0,0,0,1]};
expect( [...proxyNested.testNested.testFloatArray] ).toEqual([0,0,0,0,0,0,0,0,0,1]);
Allocate and parse a big memory chunk as array of objects:
const iterations = 5e5;
const binTest3 = new ArrayBuffer( BinaryTest.binarySize * iterations );
const dv3 = new DataView(binTest3);
const bObjList = new Array(iterations);
for(let i = 0; i < iterations; i++) {
bObjList.push(new BinaryTest(dv3, BinaryTest.binarySize * i));
}
bObjList.forEach( (obj, index) => obj.id = `i${index}`);
const ids = bObjList.map( ({id}) => id);
Memory owner
This library does not aims to own the memory, allowing to use it in different ways. The memory can be allocated from a parallel worker, from the WebAssembly code or from another library or API which can be consumed using ArrayBuffer
or SharedBuffer
. It's recommended to wrap this library with the one managing the memory.
Note: Opting in into managing your own memory (as in C) requires you to understand what you are doing at a low level. Things like dynamic array sizes or text manipulation can be a pain if you don't understand the basics (not to say about endianness or memory padding). There are several standards and known ways to manage the memroy. This library aims to make it much easier to parse it (read and write), but you'll still need to know how it works. Specifically, this libs does not aims to own the memory pieces: you'll need to create/allocate/move/copy/reallocate/free them around instantiating the JS binary objects.
Use cases
The environments where direct memory management would be desirable are minimal in JS. Still, there are some edge cases where it could be benefficial or mandatory to do so.
WebAssembly
WebAssembly allows coding in some low level language (like C, C++ or Rust) and compiling to some binary code which can be executed by a JS interpreter (with WebAssembly API, of course).
The code compiled into WebAssembly can use dedicated libraries to communicate with the JS code. It allows accessing the DOM (thus, the window
object), but also sharing pieces of memory using WebAssembly.Memory
. Using that, you could write complex but very fast code to do heavy calculations in your compiled code, and trigger some kind of JS re-render using the currently calculated values at some time or frame-rate.
Disable Garbage Collector (GC)
As app states is mostly managed by the ArrayBuffer
(except some JS internal caches), you can minimize GC works, which will run your game (app) much smoothly without unexpected background GC tasks.
Workers API
Similar to the WebAssembly use case, some one could create a SharedBuffer
and use it from different Workers, allowing your app to use more than one CPU. For example in a game: you could have the main worker which updates the DOM
(or a canvas
or a OpenGL) using the data from the buffer, while having several workers calculating 3D collisions.
Saving/restoring states
As all the data is in a single memory piece, you can easily save it to somewhere and load it later to restore the state. This could be benefficial for games, as well as AIs, 3D renderers, scientific calculations, password crackers, etc.
Accessing binary data files
Dynamic binary reading, processing and writing (to server or directly to user), like images, audio, video, medical data (DICOM), etc.
Accessing binary APIs
You could take full advantage of browsers USB and/or Bluetooth APIs, but also you could easily communicate against binary API servers or IoT. Though, as those are usually read-only or stream based, you would preffer using DataStreams.js
instead.
Develop backend DB APIs
Some libraries allows maintaining shared pieces of memory between the backend app and the database. This lib could help developing Node database middlewares.
See also
There are several JS projects aiming to handle binary data:
- buffer-backed-object: creates objects that are backed by an ArrayBuffer
- @bnaya/objectbuffer (source code): JavaScript Object like api, backed by an arraybuffer
- DataStream.js: library for reading data from ArrayBuffers
- Restructure
- Restructure (next)
- buffercodec
- Buffer Plus
- Binary Protocol
- Binary-parser
- @yotamshacham/schema
- Structron
- cppmsg
- bin-protocol
- byte-data
- binobject
- Binarcular
- Uttori Data Tools
- @avro/types
- Structurae
- @thi.ng/unionstruct
- Typed Array Buffer Schema
- @sighmir/bstruct
- binary-parser-encoder
- binary-encoder
- c-struct
- binary-transfer
- Superbuffer
- @binary-files/structjs
- js Binary Schema Parser
- jDataView/jBinary