feat(Struct)

This commit is contained in:
2025-08-03 23:18:27 +03:00
parent 72f287a4e4
commit 38f0d922aa
15 changed files with 410 additions and 150 deletions

47
src/ConstArray.js Normal file
View File

@ -0,0 +1,47 @@
import { limits, parse, serialize, sizeofHead, Type } from "."
export function ConstArray(size) {
const obj = { _size: size }
Object.setPrototypeOf(obj, ConstArray.prototype)
obj.new(ConstArray, arguments)
return obj
}
ConstArray.prototype.serialize = function(dv, src, ...inner_types) {
const item_size = sizeofHead(src[0])
let size = this._size
if (dv.byteLength < size * item_size) {
throw new Error('too small buffer')
}
if (size > limits.u32.MAX_VALUE) {
throw new Error('array is too long')
}
dv.setUint32(0, size)
for (let i = 0; i < size; i++) {
const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i)
serialize(item_frame, src[i], ...inner_types)
}
return
}
ConstArray.prototype.parse = function(dv, ...inner_types) {
const size = this._size
const item_size = sizeofHead(...inner_types)
const array = Array(size)
for (let i = 0; i < size; i++) {
const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i)
array[i] = parse(item_frame, ...inner_types)
}
return array
}
ConstArray.prototype.isHeadless = function() {
return false
}
ConstArray.prototype.sizeof = function(...inner_types) {
return sizeofHead(...inner_types) * this._size
}
Object.setPrototypeOf(ConstArray.prototype, Type.prototype)
Object.freeze(ConstArray.prototype)

33
src/ConstString.js Normal file
View File

@ -0,0 +1,33 @@
import { memcpy } from "./mem"
import { Type } from "./Type"
export function ConstString(size) {
const obj = { _size: size }
Object.setPrototypeOf(obj, ConstString.prototype)
obj.new(ConstString, arguments)
return obj
}
ConstString.prototype.serialize = function(dv, src) {
const encoder = new TextEncoder('utf-8')
const encoded = new DataView(encoder.encode(src).buffer, 0, this._size)
if (dv.byteLength < encoded.byteLength) {
throw new Error('too small buffer')
}
memcpy(dv, encoded)
return
}
ConstString.prototype.parse = function(dv) {
const frame = new DataView(dv.buffer, dv.byteOffset, this._size)
const decoder = new TextDecoder('utf-8')
return decoder.decode(frame)
}
ConstString.prototype.isHeadless = function() {
return false
}
ConstString.prototype.sizeof = function() {
return this._size
}
Object.setPrototypeOf(ConstString.prototype, Type.prototype)
Object.freeze(ConstString.prototype)

93
src/Struct.js Normal file
View File

@ -0,0 +1,93 @@
import { isHeadless, parse, serialize, sizeof, sizeofHead, Type } from "."
export function Struct(type_obj) {
const obj = {}
Object.setPrototypeOf(obj, Struct.prototype)
obj._info_by_key = new Map
obj._headless = false
const arg = {}
let offset = 0
for (const [key, value] of Object.entries(type_obj)) {
let types
if (Array.isArray(value)) {
types = value
} else {
types = [value]
}
arg[key] = types.map(type => type.name)
const headless = isHeadless(...types)
obj._info_by_key.set(key, {
offset,
types,
headless,
})
obj._headless |= headless
offset += sizeofHead(...types)
}
obj._size = offset
obj.new(Struct, [arg])
return obj
}
Struct.prototype.serialize = function(dv, src) {
let data_offset = this._size
for (const [key, value] of Object.entries(src)) {
const info = this._info_by_key.get(key)
if (info.headless) {
dv.setUint32(info.offset, data_offset)
const frame = new DataView(dv.buffer, dv.byteOffset + data_offset)
serialize(frame, value, ...info.types)
data_offset += sizeof(value, ...info.types)
} else {
const frame = new DataView(dv.buffer, dv.byteOffset + info.offset)
serialize(frame, value, ...info.types)
}
}
}
Struct.prototype.parse = function(dv) {
const res = {}
for (const [key, info] of this._info_by_key.entries()) {
if (info.headless) {
const data_offset = dv.getUint32(info.offset)
const frame = new DataView(dv.buffer, dv.byteOffset + data_offset)
res[key] = parse(frame, ...info.types)
} else {
const frame = new DataView(dv.buffer, dv.byteOffset + info.offset)
res[key] = parse(frame, ...info.types)
}
}
return res
}
Struct.prototype.isHeadless = function() {
return this._headless
}
Struct.prototype.sizeof = function(arg) {
if (this._headless) {
if (arg === undefined) {
throw new Error('unknown size of ' + this)
}
let size = this._size
for (const [key, info] of this._info_by_key.entries()) {
if (info.headless) {
size += sizeof(arg[key], ...info.types)
}
}
return size
} else {
return this._size
}
}
Object.setPrototypeOf(Struct.prototype, Type.prototype)
Object.freeze(Struct.prototype)

22
src/Type.js Normal file
View File

@ -0,0 +1,22 @@
export function Type() {
const obj = {}
Object.setPrototypeOf(obj, Type.prototype)
return obj
}
Type.prototype.new = function(func, args) {
this._name = func.name
if (args !== undefined) {
const str_args = Array.from(args).map(arg => JSON.stringify(arg))
this._name += '(' + str_args.join(', ') + ')'
}
}
Type.prototype.toString = function() {
return this._name
}
Type.prototype.serialize = function() {
throw new Error('should be overloaded')
}
Type.prototype.parse = Type.prototype.serialize
Type.prototype.isHeadless = Type.prototype.serialize
Type.prototype.sizeof = Type.prototype.serialize
Object.freeze(Type.prototype)

View File

@ -1,8 +1,11 @@
import { limits } from "./limits"
import { memcpy } from "./mem"
import { ConstArray, ConstString } from "./type"
import { Type } from "./Type"
import { ConstString } from './ConstString'
import { ConstArray } from "./ConstArray"
import { Struct } from './Struct'
export { limits, memcpy, ConstString, ConstArray}
export { limits, memcpy, Type, ConstString, ConstArray, Struct }
export function serialize(dv, src, ...types) {
const [type, ...inner_types] = types
@ -14,19 +17,12 @@ export function serialize(dv, src, ...types) {
dv.setFloat64(0, src)
return
}
if ((type == String || type._func == ConstString) && typeof src == 'string') {
if (type == String && typeof src == 'string') {
const encoder = new TextEncoder('utf-8')
let encoded
if (type == String) {
encoded = new DataView(encoder.encode(src).buffer)
if (dv.byteLength < 4 + encoded.byteLength) {
throw new Error('too small buffer')
}
} else {
encoded = new DataView(encoder.encode(src).buffer, 0, type._size)
if (dv.byteLength < encoded.byteLength) {
throw new Error('too small buffer')
}
let encoded = new DataView(encoder.encode(src).buffer)
if (dv.byteLength < 4 + encoded.byteLength) {
throw new Error('too small buffer')
}
if (encoded.byteLength > limits.u32.MAX_VALUE) {
throw new Error('string is too long')
@ -38,21 +34,13 @@ export function serialize(dv, src, ...types) {
memcpy(frame, encoded)
return
}
if ((type == Array || type._func == ConstArray) && Array.isArray(src)) {
if (type == Array && Array.isArray(src)) {
const item_size = sizeofHead(src[0])
let size
const size = src.length
if (type == Array) {
size = src.length
if (dv.byteLength < 4 + size * item_size) {
throw new Error('too small buffer')
}
} else {
size = type._size
if (dv.byteLength < size * item_size) {
throw new Error('too small buffer')
}
if (dv.byteLength < 4 + size * item_size) {
throw new Error('too small buffer')
}
if (size > limits.u32.MAX_VALUE) {
throw new Error('array is too long')
@ -61,11 +49,15 @@ export function serialize(dv, src, ...types) {
dv.setUint32(0, size)
for (let i = 0; i < size; i++) {
const item_frame = new DataView(dv.buffer, dv.byteOffset + item_size * i)
const item_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_size * i)
serialize(item_frame, src[i], ...inner_types)
}
return
}
if (type instanceof Type) {
type.serialize(dv, src, ...inner_types)
return
}
}
export function parse(dv, ...types) {
@ -74,46 +66,38 @@ export function parse(dv, ...types) {
if (type == Number) {
return dv.getFloat64(0)
}
if (type == String || type._func == ConstString) {
let size
let frame
if (type == String) {
size = dv.getUint32(0)
frame = new DataView(dv.buffer, 4 + dv.byteOffset, size)
} else {
size = type._size
frame = new DataView(dv.buffer, dv.byteOffset, size)
}
if (type == String) {
const size = dv.getUint32(0)
const frame = new DataView(dv.buffer, 4 + dv.byteOffset, size)
const decoder = new TextDecoder('utf-8')
return decoder.decode(frame)
}
if (type == Array || type._func == ConstArray) {
let size
let offset = 0
if (type == Array) {
size = dv.getUint32(0)
offset = 4
} else {
size = type._size
}
if (type == Array) {
const size = dv.getUint32(0)
const item_size = sizeofHead(inner_types[0])
const array = Array(size)
for (let i = 0; i < size; i++) {
const item_frame = new DataView(dv.buffer, dv.byteOffset + offset + item_size * i)
const item_frame = new DataView(dv.buffer, dv.byteOffset + 4 + item_size * i)
array[i] = parse(item_frame, ...inner_types)
}
return array
}
if (type instanceof Type) {
return type.parse(dv, ...inner_types)
}
}
export function isHeadless(...args) {
const [first_arg] = args
return first_arg == Array ||
first_arg == String ||
Array.isArray(first_arg) ||
typeof first_arg == 'string'
const [arg, ...inner_args] = args
if (arg instanceof Type) {
return arg.isHeadless(...inner_args)
}
return arg == Array ||
arg == String ||
Array.isArray(arg) ||
typeof arg == 'string'
}
export function sizeofHead(...args) {
@ -125,23 +109,24 @@ export function sizeofHead(...args) {
}
export function sizeof(...args) {
const [first_arg, ...remain_args] = args
const [arg, ...inner_args] = args
if (first_arg == Number || typeof first_arg == 'number') {
if (arg == Number || typeof arg == 'number') {
return 8
}
if (first_arg._func == ConstArray) {
return sizeofHead(...remain_args) * first_arg._size
}
if (first_arg._func == ConstString) {
return first_arg._size
}
if (typeof first_arg == 'string') {
if (typeof arg == 'string') {
const encoder = new TextEncoder('utf-8')
return 4 + encoder.encode(first_arg).byteLength
return 4 + encoder.encode(arg).byteLength
}
if (Array.isArray(first_arg)) {
return 4 + sizeofHead(first_arg[0]) * first_arg.length
if (Array.isArray(arg)) {
return 4 + sizeofHead(arg[0]) * arg.length
}
throw new Error('unknown size of ' + args)
if (arg instanceof Type) {
return arg.sizeof(...inner_args)
}
const [arg2, ...inner_args2] = inner_args
if (arg2 instanceof Type) {
return arg2.sizeof(arg, ...inner_args2)
}
throw new Error('unknown size of ' + arg)
}

View File

@ -1,18 +0,0 @@
export class Type {
constructor (func, obj) {
this._func = func
Object.assign(this, obj)
}
toString() {
return this._func.name
}
}
export function ConstString(size) {
return new Type(ConstString, { _size: size })
}
export function ConstArray(size) {
return new Type(ConstArray, { _size: size })
}