UNPKG

cstruct-buffer

Version:

Binary serialization framework for C-style structs

505 lines (504 loc) 22.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CStruct = void 0; const typeByteSizeMap = new Map([ ['u8', { size: 1, isBigInt: false, getter: [DataView.prototype.getUint8, Uint8Array], setter: DataView.prototype.setUint8 }], ['u16', { size: 2, isBigInt: false, getter: [DataView.prototype.getUint16, Uint16Array], setter: DataView.prototype.setUint16 }], ['u32', { size: 4, isBigInt: false, getter: [DataView.prototype.getUint32, Uint32Array], setter: DataView.prototype.setUint32 }], ['u64', { size: 8, isBigInt: true, getter: [DataView.prototype.getBigUint64, BigUint64Array], setter: DataView.prototype.setBigUint64 }], ['i8', { size: 1, isBigInt: false, getter: [DataView.prototype.getInt8, Int8Array], setter: DataView.prototype.setInt8 }], ['i16', { size: 2, isBigInt: false, getter: [DataView.prototype.getInt16, Int16Array], setter: DataView.prototype.setInt16 }], ['i32', { size: 4, isBigInt: false, getter: [DataView.prototype.getInt32, Int32Array], setter: DataView.prototype.setInt32 }], ['i64', { size: 8, isBigInt: true, getter: [DataView.prototype.getBigInt64, BigInt64Array], setter: DataView.prototype.setBigInt64 }], ['f32', { size: 4, isBigInt: false, getter: [DataView.prototype.getFloat32, Float32Array], setter: DataView.prototype.setFloat32 }], ['f64', { size: 8, isBigInt: false, getter: [DataView.prototype.getFloat64, Float64Array], setter: DataView.prototype.setFloat64 }], // Single-byte storage (ASCII only). // For UTF-8: Characters will be converted to bytes with // truncation prevention (multi-byte sequences preserved) ['str', { size: 1, isBigInt: false }], ]); class CStruct { static { this.fieldHierarchy = Symbol("CStruct.fieldHierarchy"); } // 增加命名空间前缀 static { this.metadataFields = Symbol("CStruct.metadataFields"); } static { this.PlatformEndian = (() => { const buffer = new ArrayBuffer(2); new DataView(buffer).setUint16(0, 256, true); return new Uint8Array(buffer)[0] === 0 ? 'little' : 'big'; })(); } static { this.VALID_ALIGN = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256]; } static Layout(attr = {}) { const { pack = 0, align = 0 } = attr; if (!CStruct.VALID_ALIGN.includes(pack)) { throw new Error(`Invalid pack value: ${pack}. Must be one of ${this.VALID_ALIGN.join(', ')}`); } if (!CStruct.VALID_ALIGN.includes(align)) { throw new Error(`Invalid align value: ${align}. Must be one of ${this.VALID_ALIGN.join(', ')}`); } return function (target) { const proto = target.prototype; if (!Object.hasOwn(proto, CStruct.metadataFields)) { throw new Error(`[CStruct] Invalid class "${target.name}": Empty structure detected. ` + `Must contain at least one field.\n` + `Usage example:\n` + `@CStruct.Layout()\n` + `class MyStruct extends CStruct {\n` + ` @CStruct.U8() myField: number = 0;\n` + `}`); } // 递归合并继承链上的字段 let hierarchy = []; let currentProto = proto; while (currentProto && currentProto !== CStruct.prototype) { if (Object.hasOwn(currentProto, CStruct.metadataFields)) { hierarchy.unshift(currentProto[CStruct.metadataFields]); } currentProto = Object.getPrototypeOf(currentProto); } proto[CStruct.fieldHierarchy] = hierarchy; proto.pack = pack; proto.align = align; CStruct.calculateLayout(proto); }; } static field(ctype, options = {}) { const { arrLength = 1, align = 0, isLength = false, includeSelf = true } = options; if (!typeByteSizeMap.has(ctype) && typeof ctype !== 'function') { throw new Error(`[CStruct] Undefined type: ${ctype}`); } // 新增嵌套结构体验证(基于layout.test.ts的测试用例) if (typeof ctype === 'function') { const proto = ctype.prototype; if (!(proto instanceof CStruct)) { throw new Error(`[CStruct] Invalid nested struct type: ${ctype.name}` + ` (Must inherit from CStruct)`); } // 如果嵌套结构体本身是可变长度(包含动态字段) if (proto.closed && arrLength !== 1) { throw new Error(`[CStruct] Variable-length struct ${ctype.name} cannot be array`); } if (proto.effectiveAlignment === undefined) { CStruct.Layout()(ctype); } } if ((align & (align - 1)) !== 0) { throw new Error(`[CStruct] Invalid alignment ${align}, must be power of two`); } if (!Number.isSafeInteger(arrLength) || arrLength < 0) { throw new Error(`[CStruct] Invalid array length: ${arrLength}. The length must be a safe integer and ≥ 0`); } if (arrLength !== 1 && isLength) { throw new Error(`[CStruct] autoSize cannot be used with array fields (length=${length})`); } return (function (target, propertyKey) { if (typeof ctype === 'function' && ctype.prototype === target) { throw new Error(`[CStruct] Struct cannot directly nest itself: ${propertyKey}`); } if (target.closed) { throw new Error(`Cannot add field after zero-length array: ${propertyKey}`); } const { size: elementSize, getter, setter, isBigInt } = typeByteSizeMap.get(ctype) || {}; const fieldMetadata = { ctype, name: propertyKey, align: align, elementSize, arrLength, isBigInt, getter, setter, isLength, includeSelf }; target.getOrInitFields().push(fieldMetadata); if (arrLength == 0 || (typeof ctype == "function" && ctype.prototype.closed)) { target.closed = true; } }); } static calculateLayout(target) { const allFields = target[CStruct.fieldHierarchy].flat(); let offsetTracker = 0; let maxAlignment = 0; const seenFields = new Set(); // 新增字段名检测集合 target.Fields = allFields.map((field, index) => { // 字段名冲突检测(新增) if (seenFields.has(field.name)) { throw new Error(`[CStruct] Duplicate field name: "${field.name}" ` + `(Violation of C language structure naming rules)`); } seenFields.add(field.name); if (field.arrLength === 0 && index !== allFields.length - 1) { throw new Error(`[CStruct] Variable-length field '${field.name}' must be last member` + ` (Found at position ${index + 1}/${allFields.length})`); } let tmpAlign = 0; // 处理嵌套结构体 if (typeof field.ctype === 'function') { const nestedProto = field.ctype.prototype; field.elementSize = nestedProto.size; // 从原型获取已计算的 length const effectiveNestedAlign = target.pack > 0 ? Math.min(nestedProto.effectiveAlignment, target.pack) : nestedProto.effectiveAlignment; tmpAlign = Math.max(field.align, effectiveNestedAlign); } else { tmpAlign = Math.max(field.align, field.elementSize); } // C 标准对齐规则 const fieldAlignment = target.pack == 0 ? tmpAlign : Math.min(tmpAlign, target.pack); const fieldAlignmentMask = fieldAlignment - 1; maxAlignment = Math.max(maxAlignment, fieldAlignment); // 对齐当前偏移 offsetTracker = (offsetTracker + fieldAlignmentMask) & (~fieldAlignmentMask); // 计算字段总大小(包含数组) const fieldSize = field.elementSize * field.arrLength; const calculated = { name: field.name, align: fieldAlignment, offset: offsetTracker, size: fieldSize, metadata: field, }; offsetTracker += fieldSize; return calculated; }); // C 标准总大小对齐(取结构体对齐设置和自然对齐的最大值) const structAlignment = Math.max(target.align, maxAlignment); const structAlignmentMask = structAlignment - 1; target.effectiveAlignment = structAlignment; offsetTracker = (offsetTracker + structAlignmentMask) & (~structAlignmentMask); target.size = offsetTracker; } constructor() { // 若未使用 Layout 装饰器,设置默认布局参数并计算布局 if (this.pack === undefined) { const ctor = this.constructor; CStruct.Layout()(ctor); } } getOrInitFields() { if (!Object.hasOwn(this, CStruct.metadataFields)) { this[CStruct.metadataFields] = []; } return this[CStruct.metadataFields]; } serialize(endian = 'little') { let buffer; if (this.closed) { const { v, offset, size, isStr } = this.calculateDynaSize(endian); buffer = new Uint8Array(offset + size); buffer.set(v, offset); if (isStr) { buffer[offset + size - 1] = 0; } } else { buffer = new Uint8Array(this.size); } this.writeFixedSizeField(buffer, 0, endian); return buffer; } writeFixedSizeField(buffer, baseOffset, endian) { const view = new DataView(buffer.buffer); const isLittleEndian = endian === 'little'; for (const field of this.Fields) { const { arrLength, ctype, setter, elementSize, isBigInt, isLength, includeSelf } = field.metadata; let offset = baseOffset + field.offset; //因为是可变长度字段已经处理,且该字段应该是最后一个 //所以这里不需要继续处理,直接返回 if (arrLength === 0) return; // 处理长度字段的特殊逻辑 if (isLength && arrLength === 1) { const endOffset = buffer.length; const length = endOffset - offset; const finalLength = includeSelf ? length : length - field.size; const val = isBigInt ? BigInt(finalLength) : finalLength; setter.call(view, offset, val, isLittleEndian); continue; } const value = this[field.name]; //嵌套结构体的处理 if (typeof ctype === 'function') { // 处理结构体数组 if (arrLength > 1) { for (let i = 0; i < arrLength; i++) { const elemOffset = offset + (i * elementSize); value[i].writeFixedSizeField(buffer, elemOffset, endian); } } else { // 处理单个结构体 value.writeFixedSizeField(buffer, offset, endian); } continue; } //字符串的处理 if (ctype === 'str') { const buf = buffer.subarray(offset, offset + field.size); new TextEncoder().encodeInto(value, buf); continue; } //长度为1时,为基本数据类型,所以这里是对基本数据类型的处理 if (arrLength === 1) { const val = isBigInt ? BigInt(value) : value; setter.call(view, offset, val, isLittleEndian); continue; } //对于数组的处理 const isAligned = (offset & (elementSize - 1)) === 0; const TypedArray = field.metadata.getter[1]; if (isAligned && endian === CStruct.PlatformEndian && !isBigInt) { new TypedArray(buffer.buffer, offset, arrLength).set(value); } else { //这里使用两个分支来处理,主要目的是减少在循环内的判断。 //因为在循环内的判断会影响性能。 if (isBigInt) { for (let i = 0; i < arrLength; i++) { setter.call(view, offset, BigInt(value[i]), isLittleEndian); offset += elementSize; } } else { for (let i = 0; i < arrLength; i++) { setter.call(view, offset, value[i], isLittleEndian); offset += elementSize; } } } } } //处理动态长度字段。 //返回值v为序列化后的数据, // offset为在整体缓冲区中的偏移量, // size为序列化后的数据长度, // isStr为是否为字符串。 calculateDynaSize(endian) { const field = this.Fields[this.Fields.length - 1]; const { ctype, getter, arrLength, setter, elementSize, isBigInt } = field.metadata; const value = this[field.name]; if (typeof ctype === 'function') { if (arrLength == 0) { const structArray = value; const elementCount = structArray.length; const totalSize = elementCount * elementSize; // 直接创建缓冲区写入 const buffer = new Uint8Array(totalSize); let offset = 0; for (const elem of structArray) { elem.writeFixedSizeField(buffer, offset, endian); offset += elementSize; } return { v: buffer, offset: field.offset, size: totalSize, isStr: false }; } else { const nestedStruct = this[field.name]; const nestedResult = nestedStruct.calculateDynaSize(endian); return { v: nestedResult.v, offset: field.offset + nestedResult.offset, size: nestedResult.size, isStr: nestedResult.isStr }; } } if (ctype === 'str') { const strValue = this[field.name]; const encoded = new TextEncoder().encode(strValue); return { v: encoded, offset: field.offset, size: encoded.length + 1, // 保留终止符空间 isStr: true }; } const numericArray = this[field.name]; const TypedArrayCtor = field.metadata.getter[1]; const needsEndianSwap = endian !== CStruct.PlatformEndian; const arr = isBigInt || needsEndianSwap //如果是BigInt类型,这里先创建一个空数据,在随后的操作中填充这个数组 //这么做的目的是不使用map来减少内存分配的次数。 ? new ArrayBuffer(numericArray.length * elementSize) : new TypedArrayCtor(numericArray).buffer; if (needsEndianSwap || isBigInt) { const view = new DataView(arr); const isLittleEndian = endian === 'little'; let byteOffset = 0; if (!isBigInt) { for (let i = 0; i < numericArray.length; i++) { setter.call(view, byteOffset, numericArray[i], isLittleEndian); byteOffset += elementSize; } } else { for (let i = 0; i < numericArray.length; i++) { setter.call(view, byteOffset, BigInt(numericArray[i]), isLittleEndian); byteOffset += elementSize; } } } const v = new Uint8Array(arr); return { v, offset: field.offset, size: v.length }; } static deserialize(buffer, endian = 'little') { const instance = new this(); const view = new DataView(buffer.buffer); instance.readFromBuffer(view, buffer.byteOffset, endian); instance.size = buffer.length; return instance; } readFromBuffer(view, baseOffset, endian) { const isLittleEndian = endian === 'little'; for (const field of this.Fields) { const { arrLength, ctype, getter, elementSize, isBigInt } = field.metadata; let offset = baseOffset + field.offset; // 处理可变长度字段(提前返回机制) if (arrLength === 0) { this.handleVariableLengthField(view, field, offset, endian); return; // 提前返回避免后续循环 } // 预计算常用值提升性能 const isAligned = (offset & (elementSize - 1)) === 0; // 结构体处理优化 if (typeof ctype === 'function') { const arr = arrLength > 1 ? new Array(arrLength) : undefined; if (arr) { let elemOffset = offset; for (let i = 0; i < arrLength; i++) { const nested = new ctype(); nested.readFromBuffer(view, elemOffset, endian); elemOffset += elementSize; arr[i] = nested; } this[field.name] = arr; } else { const nested = new ctype(); nested.readFromBuffer(view, offset, endian); this[field.name] = nested; } continue; } // 字符串处理优化 if (ctype === 'str') { const strBytes = new Uint8Array(view.buffer, offset, field.size); let nullIndex = -1; // 手动循环比 indexOf 更快 for (let i = 0; i < strBytes.length; i++) { if (strBytes[i] === 0) { nullIndex = i; break; } } this[field.name] = new TextDecoder().decode(strBytes.subarray(0, nullIndex === -1 ? field.size : nullIndex)); continue; } // 基本类型数组优化 if (arrLength > 1) { const TypedArray = getter[1]; if (isAligned && endian === CStruct.PlatformEndian && !isBigInt) { this[field.name] = Array.from(new TypedArray(view.buffer, offset, arrLength)); } else { const values = new Array(arrLength); for (let i = 0; i < arrLength; i++) { values[i] = getter[0].call(view, offset, isLittleEndian); offset += elementSize; } this[field.name] = values; } } else { this[field.name] = getter[0].call(view, offset, isLittleEndian); } } } handleVariableLengthField(view, field, offset, endian) { const { ctype, getter, elementSize } = field.metadata; const maxBytes = view.byteLength - offset; if (typeof ctype === 'function') { // 处理嵌套结构体数组(动态长度) if (field.metadata.arrLength === 0) { const count = Math.floor(maxBytes / elementSize); const arr = new Array(count); for (let i = 0; i < count; i++) { const nested = new ctype(); nested.readFromBuffer(view, offset + i * elementSize, endian); arr[i] = nested; } this[field.name] = arr; } else { // 处理单个嵌套结构体 const dynamicStruct = new ctype(); dynamicStruct.readFromBuffer(view, offset, endian); this[field.name] = dynamicStruct; } } else if (ctype === 'str') { // 直接内联字符串终止符查找逻辑 const u8 = new Uint8Array(view.buffer, offset, maxBytes); let terminatorPos = maxBytes; for (let i = 0; i < u8.length; i++) { if (u8[i] === 0) { terminatorPos = i; break; } } this[field.name] = new TextDecoder().decode(u8.subarray(0, terminatorPos)); } else { const count = Math.floor(maxBytes / elementSize); const values = new Array(count); for (let i = 0; i < count; i++) { values[i] = getter[0].call(view, offset + i * elementSize, endian === 'little'); } this[field.name] = values; } } static U8(options = {}) { return this.field('u8', options); } static U16(options = {}) { return CStruct.field('u16', options); } static U32(options = {}) { return CStruct.field('u32', options); } static U64(options = {}) { return CStruct.field('u64', options); } static I8(options = {}) { return CStruct.field('i8', options); } static I16(options = {}) { return CStruct.field('i16', options); } static I32(options = {}) { return CStruct.field('i32', options); } static I64(options = {}) { return CStruct.field('i64', options); } static F32(options = {}) { return CStruct.field('f32', options); } static F64(options = {}) { return CStruct.field('f64', options); } static Str(maxBytes = 0, align = 0) { return CStruct.field('str', { arrLength: maxBytes, align: align }); } static Struct(constructor, options = {}) { return CStruct.field(constructor, options); } } exports.CStruct = CStruct;