cstruct-buffer
Version:
Binary serialization framework for C-style structs
505 lines (504 loc) • 22.6 kB
JavaScript
;
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;