quickwin 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +21 -24
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20054 -1423
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +31 -73
  10. package/examples/pdf_preview2.ts +33 -76
  11. package/examples/pdf_viewer.js +1496 -616
  12. package/examples/pdf_viewer.tsx +23 -64
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1491 -525
  16. package/examples/test_react_lazy.js +1455 -500
  17. package/examples/test_react_listbox.js +1454 -499
  18. package/examples/test_root_window.js +1450 -495
  19. package/examples/test_tab.js +1455 -500
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +214 -0
  23. package/lib/certs.ts +224 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +37 -4
  27. package/lib/fetch.ts +35 -4
  28. package/lib/ffi/bind.js +352 -0
  29. package/lib/ffi/bind.ts +442 -0
  30. package/lib/ffi/ctype.js +168 -0
  31. package/lib/ffi/ctype.ts +331 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +412 -0
  35. package/lib/ffi/struct.ts +664 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +20 -33
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +7 -22
  43. package/lib/react-qw/components/ListView.js +80 -141
  44. package/lib/react-qw/components/PathPicker.js +36 -80
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +15 -20
  48. package/lib/react-qw/components/Tooltip.js +21 -20
  49. package/lib/react-qw/components/TreeView.js +14 -47
  50. package/lib/react-qw/index.js +4 -2
  51. package/lib/react-qw/index.ts +4 -2
  52. package/lib/react-qw/nmhdr.js +5 -0
  53. package/lib/react-qw/nmhdr.ts +7 -0
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +18 -35
  59. package/lib/text-measure.ts +20 -37
  60. package/lib/websocket.js +12 -2
  61. package/lib/websocket.ts +15 -2
  62. package/lib/windows/comctl32.js +18 -0
  63. package/lib/windows/comctl32.ts +20 -0
  64. package/lib/windows/comdlg32.js +7 -0
  65. package/lib/windows/comdlg32.ts +9 -0
  66. package/lib/windows/gdi32.js +90 -0
  67. package/lib/windows/gdi32.ts +97 -0
  68. package/lib/windows/kernel32.js +66 -0
  69. package/lib/windows/kernel32.ts +76 -0
  70. package/lib/windows/ntdll.js +8 -0
  71. package/lib/windows/ntdll.ts +10 -0
  72. package/lib/windows/ole32.js +7 -0
  73. package/lib/windows/ole32.ts +9 -0
  74. package/lib/windows/shell32.js +11 -0
  75. package/lib/windows/shell32.ts +14 -0
  76. package/lib/windows/structs.js +384 -0
  77. package/lib/windows/structs.ts +422 -0
  78. package/lib/windows/user32.js +469 -0
  79. package/lib/windows/user32.ts +502 -0
  80. package/package.json +1 -1
  81. package/quickwin.d.ts +22 -33
  82. package/quickwin_const.d.ts +1 -0
  83. package/qwin-nowasm-x86.exe +0 -0
  84. package/qwin-nowasm.exe +0 -0
  85. package/qwin-x86.exe +0 -0
  86. package/qwin.exe +0 -0
  87. package/test/bench_ffi_callraw.js +236 -0
  88. package/test/bench_ffi_callraw.ts +236 -0
  89. package/test/bench_ffi_overhead.js +139 -0
  90. package/test/bench_ffi_overhead.ts +141 -0
  91. package/test/bench_sendmessage.js +104 -0
  92. package/test/bench_sendmessage.ts +100 -0
  93. package/test/bench_sendmessage_nonnull.js +187 -0
  94. package/test/bench_sendmessage_nonnull.ts +188 -0
  95. package/test/run.js +14 -4
  96. package/test/run.ts +15 -5
  97. package/test/test_basic_fetch.js +8 -0
  98. package/test/test_basic_fetch.ts +9 -0
  99. package/test/test_ffi.js +33 -47
  100. package/test/test_ffi.ts +41 -60
  101. package/test/test_ffi_abi.js +235 -0
  102. package/test/test_ffi_abi.ts +218 -0
  103. package/test/test_ffi_bind.js +572 -0
  104. package/test/test_ffi_bind.ts +597 -0
  105. package/test/test_ffi_struct.js +745 -0
  106. package/test/test_ffi_struct.ts +798 -0
  107. package/test/test_ffi_struct_layout.js +495 -0
  108. package/test/test_ffi_struct_layout.ts +543 -0
  109. package/test/test_ffi_user32.js +103 -0
  110. package/test/test_ffi_user32.ts +113 -0
  111. package/test/test_ffi_windows.js +101 -0
  112. package/test/test_ffi_windows.ts +113 -0
  113. package/test/test_helper.js +8 -1
  114. package/test/test_helper.ts +9 -1
  115. package/test/test_http_import.js +18 -0
  116. package/test/test_http_import.ts +19 -0
  117. package/test/test_http_server.js +73 -1
  118. package/test/test_http_server.ts +75 -1
  119. package/test/test_ipv6.js +2 -2
  120. package/test/test_ipv6.ts +2 -2
  121. package/test/test_net_fetch.js +70 -8
  122. package/test/test_net_fetch.ts +69 -7
  123. package/test/test_net_websocket.js +1 -1
  124. package/test/test_net_websocket.ts +1 -1
  125. package/test/test_react_complex.js +1465 -510
  126. package/test/test_react_counter.js +1455 -500
  127. package/test/test_react_flex.js +1475 -513
  128. package/test/test_react_hidden.js +1458 -503
  129. package/test/test_react_listview_link.js +1455 -500
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1455 -500
  132. package/test/test_wolfssl_read.js +3 -2
  133. package/test/test_wolfssl_read.ts +3 -1
  134. package/test/test_worker_net.js +35 -17
  135. package/test/test_worker_net.ts +27 -15
  136. package/tsconfig.json +1 -1
  137. package/examples/exec_server.ts +0 -80
@@ -0,0 +1,412 @@
1
+ import * as ffi from 'ffi';
2
+ import '../text-codec.js';
3
+ import { isCPtrToken, normToken, PTR_SIZE, PtrArrayBuffer, readScalar, SizeAlign, writeScalar, } from './ctype.js';
4
+ // ============================================================
5
+ // Layout IR —— computeStructLayout/lower 把 CType AST IR lower 到此。
6
+ // 自包含:子布局在 lowering 时内嵌、offset 相对本层起点,运行期 read/write 不回查 AST IR。
7
+ // ============================================================
8
+ /** 向上对齐(a 必须是 2 的幂)。 */
9
+ function alignUp(v, a) {
10
+ return (v + a - 1) & ~(a - 1);
11
+ }
12
+ // 归一后 token 的档位检查:'void'/指针 token 查表为 undefined(SizeAlign 只收 C_Number)。
13
+ const isCNumberToken = (t) => SizeAlign[t] !== undefined;
14
+ // 值 → { size, align, FieldType }:字符串先试字符串糖/数组糖/位域糖,再按 token 归一;
15
+ // 对象值一律按 '#' 分派 —— 无「裸嵌套 map 默认 struct」,漏写 '#' 直接报错。
16
+ // 字符串无 '#' 声明形式(词汇只有糖,见 ctype.ts §4):旧写法落到末尾 unknown '#' 报错。
17
+ function lower(v) {
18
+ if (typeof v === 'string') {
19
+ const st = /^(u8|u16)\[(\d+)\]@(utf-8|utf-16le)$/.exec(v); // 'u16[128]@utf-16le' → 字符串糖
20
+ if (st) {
21
+ const length = Number(st[2]);
22
+ if (!Number.isInteger(length) || length < 1)
23
+ throw new Error(`ffi-struct: string length must be an integer >= 1, got ${length}`);
24
+ // 布局 = unit × length(与 encoding 无关),encoding 只是读写时的解释 ——
25
+ // 'u8[128]@utf-16le'(128 字节按 utf-16le 解出 64 字符)之类的错配语义自洽。
26
+ const s = st[1] === 'u8' ? 1 : 2;
27
+ return { size: s * length, align: s, type: { tag: 'string', encoding: st[3] } };
28
+ }
29
+ const a = /^([^\[\]]+)\[(\d+)\]$/.exec(v); // 'u32[4]' / 'i3z[4]' → 数组糖(元素递归归一)
30
+ if (a) {
31
+ const length = Number(a[2]);
32
+ if (!Number.isInteger(length) || length < 1)
33
+ throw new Error(`ffi-struct: array length must be an integer >= 1, got ${length}`);
34
+ const el = lower(a[1]);
35
+ if (el.type.tag === 'bitfield')
36
+ throw new Error('ffi-struct: bitfields cannot be array elements');
37
+ return {
38
+ size: el.size * length,
39
+ align: el.align,
40
+ type: { tag: 'array', elementType: el.type, elementSize: el.size },
41
+ };
42
+ }
43
+ const b = /^([A-Za-z_]\w*):(\d+)$/.exec(v); // 'u32:3' → 位域糖('f32:3' 归一后非整数档被拒)
44
+ if (b) {
45
+ const rawUnit = b[1];
46
+ const unit = normToken(rawUnit);
47
+ // 'void'/'<>ptr' 查表为 undefined、'float' 等别名 normToken 直接抛 —— 都不是合法位域单元。
48
+ if (!isCNumberToken(unit) || unit === 'f32' || unit === 'f64')
49
+ throw new Error(`ffi-struct: bitfield unit must be an integer type, got "${rawUnit}"`);
50
+ const width = Number(b[2]);
51
+ const s = SizeAlign[unit];
52
+ const cap = s * 8;
53
+ if (!Number.isInteger(width) || width < 1)
54
+ throw new Error(`ffi-struct: bitfield width must be an integer >= 1, got ${width}`);
55
+ if (width > cap)
56
+ throw new Error(`ffi-struct: bitfield width ${width} exceeds unit width ${cap}`);
57
+ // bit 是单元内偏移,由 computeStructLayout 的位域状态机填写;这里占位 0。
58
+ return { size: s, align: s, type: { tag: 'bitfield', unit, bit: 0, width } };
59
+ }
60
+ // 规范 token / 指针 token(别名机制已移除:'DWORD' 等 normToken 直接抛)。
61
+ // 归一后必须先判指针形,否则落 unknown kind;'void' 归一后无大小、
62
+ // 裸 'ptr'/'i3z' 未知 —— 都在这里抛(原先由 SizeAlign 查表兜住,现归一并入)。
63
+ const k = normToken(v);
64
+ if (isCPtrToken(k))
65
+ return { size: PTR_SIZE, align: PTR_SIZE, type: { tag: 'basic', kind: 'ptr' } };
66
+ if (!isCNumberToken(k))
67
+ throw new Error(`ffi-struct: unknown kind "${v}"`);
68
+ const s = SizeAlign[k];
69
+ return { size: s, align: s, type: { tag: 'basic', kind: k } };
70
+ }
71
+ if (v && typeof v === 'object') {
72
+ const o = v;
73
+ const tag = o['#'];
74
+ if (tag === undefined)
75
+ throw new Error(`ffi-struct: nested object value requires a '#' key ('struct' | 'union' | 'array')`);
76
+ if (tag === 'array' && '#pack' in o)
77
+ throw new Error(`ffi-struct: '#pack' is only allowed on struct/union, got '${tag}'`);
78
+ if (tag === 'array') {
79
+ const length = o['length'];
80
+ if (!Number.isInteger(length) || length < 1)
81
+ throw new Error(`ffi-struct: array length must be an integer >= 1, got ${String(length)}`);
82
+ const el = lower(o['element']);
83
+ if (el.type.tag === 'bitfield')
84
+ throw new Error('ffi-struct: bitfields cannot be array elements');
85
+ return {
86
+ size: el.size * length,
87
+ align: el.align,
88
+ type: { tag: 'array', elementType: el.type, elementSize: el.size },
89
+ };
90
+ }
91
+ if (tag === 'struct' || tag === 'union') {
92
+ const l = computeStructLayout(v);
93
+ return { size: l.size, align: l.maxEffectiveAlign, type: { tag, fields: l.fields } };
94
+ }
95
+ throw new Error(`ffi-struct: unknown '#' declaration ${JSON.stringify(String(tag))}`);
96
+ }
97
+ throw new Error(`ffi-struct: invalid member value: ${String(v)}`);
98
+ }
99
+ export function computeArray(t) {
100
+ const { size, align } = lower(t);
101
+ return { align, size };
102
+ }
103
+ export function computeStructLayout(t) {
104
+ const isStruct = t['#'] === 'struct';
105
+ const pack = t['#pack'] ?? 8;
106
+ let maxEffectiveAlign = 1;
107
+ let maxMemberSize = 0;
108
+ const fields = [];
109
+ let cursor = 0;
110
+ // 位域组状态:unitBase 是当前存储单元的字节基址(-1 = 不在位域组内),
111
+ // unitBits 是单元内已用位数,unitSize 是单元宽(字节)。
112
+ // MSVC 规则(mingw 交叉实测 DCB/COMSTAT 验证):
113
+ // - 连续同宽位域共单元,LSB→MSB(签别不参与分组)
114
+ // - 放不下的位域开新单元,单元剩余位废弃
115
+ // - 非位域成员打断位域组,游标跳到完整单元边界(不是按字节截断)
116
+ // 联合体位域不分组:每位域成员独立从 offset 0/bit 0 起算——完全别名
117
+ // (mingw 16 x64+ia32 实测:a=1→b=1、溢出成员回 offset 0、sizeof=单元宽)。
118
+ let unitBase = -1;
119
+ let unitBits = 0;
120
+ let unitSize = 0;
121
+ // 落定当前位域组:游标 = 单元起点 + 已用字节数,向上取整到单元宽。
122
+ const flushUnit = () => {
123
+ if (unitBase < 0)
124
+ return;
125
+ cursor = alignUp(unitBase + Math.ceil(unitBits / 8), unitSize);
126
+ unitBase = -1;
127
+ };
128
+ for (const [rawName, v] of Object.entries(t)) {
129
+ if (rawName.startsWith('#'))
130
+ continue; // '#' 指令键('#'/'#pack')不是字段
131
+ const m = /^([^@]+)@(\d+)$/.exec(rawName); // 键尾 '@N' = alignas(字段名不含 '@')
132
+ const name = m ? m[1] : rawName;
133
+ const alignas = m ? Number(m[2]) : 0;
134
+ const { size, align: natural, type } = lower(v);
135
+ // '!' 非空担保标记(原文 `<X>ptr!`):lower 里 normToken 已拒掉 'u32!'/'HANDLE!' 等
136
+ // 非法尾缀,能走到这里的 '!' 尾必是指针 token —— 记到 Field 上供 decode 的 0 断言。
137
+ const nonnull = typeof v === 'string' && v.endsWith('!');
138
+ let align = natural;
139
+ if (pack > 0)
140
+ align = Math.min(pack, align);
141
+ if (alignas)
142
+ align = Math.max(align, alignas);
143
+ if (type.tag === 'bitfield') {
144
+ if (name.startsWith('$'))
145
+ throw new Error('ffi-struct: bitfield members must be named');
146
+ // union:成员同址(C 标准),每位域成员独立从 offset 0/bit 0 起算。
147
+ // 不顺序分组、不开新单元、不动游标(flushUnit 因 unitBase 恒 -1 而
148
+ // no-op,普通/匿名成员的 offset 也因此保持 0)——曾因游标推进致
149
+ // 溢出单元/后续成员落在 union 声明 size 之外(越界读写)。
150
+ if (!isStruct) {
151
+ fields.push({ name: name, offset: 0, size, type: { ...type, bit: 0 } });
152
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align);
153
+ maxMemberSize = Math.max(maxMemberSize, size);
154
+ continue;
155
+ }
156
+ const width = type.width;
157
+ // 新单元:无活动单元 / 单元宽不同 / 放不下。
158
+ if (unitBase < 0 || size !== unitSize || unitBits + width > unitSize * 8) {
159
+ // 单元宽变化或溢出时先把旧单元落定,新单元从落定点对齐。
160
+ if (unitBase >= 0)
161
+ flushUnit();
162
+ const offset = alignUp(cursor, align);
163
+ unitBase = offset;
164
+ unitBits = 0;
165
+ unitSize = size;
166
+ }
167
+ fields.push({
168
+ name: name,
169
+ offset: unitBase, // 单元基址(不是位域自己的地址)
170
+ size,
171
+ type: { ...type, bit: unitBits },
172
+ });
173
+ unitBits += width;
174
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align);
175
+ maxMemberSize = Math.max(maxMemberSize, size);
176
+ continue;
177
+ }
178
+ flushUnit();
179
+ const offset = alignUp(cursor, align);
180
+ if (!name.startsWith('$')) {
181
+ fields.push({ name: name, offset, size, type, ...(nonnull ? { nonnull: true } : {}) });
182
+ }
183
+ else {
184
+ // 匿名聚合:内嵌 fields 已 lower 好,offset 平移到本成员起点。
185
+ if (type.tag !== 'struct' && type.tag !== 'union')
186
+ throw new Error(`ffi-struct: anonymous member "${name}" must be a struct/union`);
187
+ for (const cf of type.fields)
188
+ fields.push({ ...cf, offset: cf.offset + offset });
189
+ }
190
+ if (isStruct)
191
+ cursor = offset + size;
192
+ maxEffectiveAlign = Math.max(maxEffectiveAlign, align);
193
+ maxMemberSize = Math.max(maxMemberSize, size);
194
+ }
195
+ flushUnit();
196
+ return {
197
+ size: isStruct ? alignUp(cursor, maxEffectiveAlign) : alignUp(maxMemberSize, maxEffectiveAlign),
198
+ maxEffectiveAlign,
199
+ fields,
200
+ };
201
+ }
202
+ function readUnitRaw(dv, off, unit) {
203
+ const s = SizeAlign[unit];
204
+ if (s === 1)
205
+ return BigInt(dv.getUint8(off));
206
+ if (s === 2)
207
+ return BigInt(dv.getUint16(off, true));
208
+ if (s === 4)
209
+ return BigInt(dv.getUint32(off, true));
210
+ return dv.getBigUint64(off, true);
211
+ }
212
+ function writeUnitRaw(dv, off, unit, v) {
213
+ const s = SizeAlign[unit];
214
+ if (s === 1) {
215
+ dv.setUint8(off, Number(v & 0xffn));
216
+ return;
217
+ }
218
+ if (s === 2) {
219
+ dv.setUint16(off, Number(v & 0xffffn), true);
220
+ return;
221
+ }
222
+ if (s === 4) {
223
+ dv.setUint32(off, Number(v & 0xffffffffn), true);
224
+ return;
225
+ }
226
+ dv.setBigUint64(off, v & 0xffffffffffffffffn, true);
227
+ }
228
+ function readBitfield(dv, off, t) {
229
+ const w = BigInt(t.width);
230
+ let v = (readUnitRaw(dv, off, t.unit) >> BigInt(t.bit)) & ((1n << w) - 1n);
231
+ if (t.unit[0] === 'i' && (v & (1n << (w - 1n)))) // 有符号单元且符号位为 1 → 先符号扩展
232
+ v -= 1n << w;
233
+ if (t.width > 53)
234
+ return v; // 值域装不进安全整数 → bigint(与类型层 BitShape 同界)
235
+ return Number(v);
236
+ }
237
+ function writeBitfield(dv, off, t, val) {
238
+ const w = BigInt(t.width);
239
+ const bit = BigInt(t.bit);
240
+ const widthMask = (1n << w) - 1n;
241
+ const posMask = widthMask << bit;
242
+ const raw = readUnitRaw(dv, off, t.unit);
243
+ const bits = (typeof val === 'bigint' ? val : BigInt(Math.trunc(val))) & widthMask;
244
+ writeUnitRaw(dv, off, t.unit, (raw & ~posMask) | (bits << bit));
245
+ }
246
+ // 字符串读写统一走 TextEncoder/TextDecoder(polyfill 见 lib/text-codec.js)。
247
+ const stringEncoders = {
248
+ 'utf-8': new TextEncoder('utf-8'),
249
+ 'utf-16le': new TextEncoder('utf-16le'),
250
+ };
251
+ const stringDecoders = {
252
+ 'utf-8': new TextDecoder('utf-8', { ignoreBOM: true }),
253
+ 'utf-16le': new TextDecoder('utf-16le', { ignoreBOM: true }),
254
+ };
255
+ function readString(dv, off, size, enc) {
256
+ const bytes = new Uint8Array(dv.buffer, dv.byteOffset + off, size);
257
+ const unit = enc === 'utf-16le' ? 2 : 1;
258
+ let end = size;
259
+ for (let i = 0; i + unit <= size; i += unit) {
260
+ if (bytes[i] === 0 && (unit === 1 || bytes[i + 1] === 0)) {
261
+ end = i;
262
+ break;
263
+ }
264
+ }
265
+ return stringDecoders[enc].decode(bytes.subarray(0, end));
266
+ }
267
+ function writeString(dv, off, size, enc, v) {
268
+ const bytes = new Uint8Array(dv.buffer, dv.byteOffset + off, size);
269
+ bytes.fill(0);
270
+ // 始终给 NUL 终止符留一个存储单元(宽度由 encoding 决定,见 readString):
271
+ // 末尾保持 0,payload 最多 size - unit 字节。这样 C 侧 strlen/wcslen 都不会越界。
272
+ const unit = enc === 'utf-16le' ? 2 : 1;
273
+ const cap = size - unit;
274
+ if (cap <= 0)
275
+ return;
276
+ const src = stringEncoders[enc].encode(String(v ?? ''));
277
+ bytes.set(src.subarray(0, Math.min(src.length, cap)));
278
+ }
279
+ function readArray(dv, off, size, t) {
280
+ const count = t.elementSize > 0 ? size / t.elementSize : 0;
281
+ const out = [];
282
+ for (let i = 0; i < count; i++)
283
+ out.push(readElement(dv, off + i * t.elementSize, t.elementType, t.elementSize));
284
+ return out;
285
+ }
286
+ function writeArray(dv, off, size, t, v) {
287
+ const count = t.elementSize > 0 ? size / t.elementSize : 0;
288
+ const list = (v ?? []);
289
+ for (let i = 0; i < count; i++)
290
+ writeElement(dv, off + i * t.elementSize, t.elementType, t.elementSize, list[i]);
291
+ }
292
+ function readElement(dv, off, t, size) {
293
+ switch (t.tag) {
294
+ case 'basic': return readScalar(dv, off, t.kind);
295
+ case 'bitfield': return readBitfield(dv, off, t);
296
+ case 'string': return readString(dv, off, size, t.encoding);
297
+ case 'struct':
298
+ case 'union': return doDecode(dv, off, t.fields);
299
+ case 'array': return readArray(dv, off, size, t);
300
+ }
301
+ }
302
+ function writeElement(dv, off, t, size, val) {
303
+ switch (t.tag) {
304
+ // TODO: remove this any
305
+ case 'basic':
306
+ writeScalar(dv, off, { k: t.kind, v: val });
307
+ break;
308
+ // 直传(不 Number()):writeBitfield 的 val 是 number | bigint,
309
+ // 64 位单元走 bigint,先 Number() 会把 >2^53 的位域值截断。
310
+ case 'bitfield':
311
+ writeBitfield(dv, off, t, (val ?? 0));
312
+ break;
313
+ case 'string':
314
+ writeString(dv, off, size, t.encoding, val);
315
+ break;
316
+ case 'struct':
317
+ case 'union':
318
+ doEncode(dv, off, t.fields, (val ?? {}));
319
+ break;
320
+ case 'array':
321
+ writeArray(dv, off, size, t, val);
322
+ break;
323
+ }
324
+ }
325
+ function doDecode(dv, base, fields) {
326
+ const out = {};
327
+ for (const f of fields) {
328
+ const v = readElement(dv, base + f.offset, f.type, f.size);
329
+ // '!' 非空担保的后置兜底:C 没填(忘判成功就 decode)或拿未填充槽(decode(alloc()))
330
+ // 读出 0 —— 当场 throw,不让 0 带 Ptr 品牌流到下游 '! 消费位'。成功路径必非零,不触发。
331
+ if (f.nonnull && v === 0)
332
+ throw new Error(`ffi-struct: decode: '!' field "${f.name}" got 0 ` +
333
+ `(C did not fill it, or decode was called on an unfilled slot)`);
334
+ out[f.name] = v;
335
+ }
336
+ return out;
337
+ }
338
+ function doEncode(dv, base, fields, v) {
339
+ for (const f of fields) {
340
+ // DeepPartial 缺省:不动该字段(新建 buffer 上保持 0、复用 buffer 上保留旧值);
341
+ // 嵌套/数组字段整个缺失也在这里拦下(writeElement 的 struct 分支回到本函数)。
342
+ if (v[f.name] === undefined)
343
+ continue;
344
+ writeElement(dv, base + f.offset, f.type, f.size, v[f.name]);
345
+ }
346
+ }
347
+ // ============================================================
348
+ // 内部构造器
349
+ // ============================================================
350
+ function createStruct(t) {
351
+ const { fields, size, maxEffectiveAlign } = computeStructLayout(t);
352
+ return {
353
+ __struct: t,
354
+ size,
355
+ structAlign: maxEffectiveAlign,
356
+ decode: (p) => {
357
+ // number = native 拥有的品牌指针(Codec 的 decode(p) 形态):按布局 size
358
+ // 逐字节拷进临时 buffer 再解码;ArrayBuffer = 调用方持有的 out buffer 直读。
359
+ // 顶层不收 offset——out 参数恒从起点读,嵌套偏移由字段布局(doDecode 的 base)承担。
360
+ if (typeof p === 'number') {
361
+ const buf = new ArrayBuffer(size);
362
+ const u8 = new Uint8Array(buf);
363
+ for (let i = 0; i < size; i++)
364
+ u8[i] = ffi.readByte(p + i);
365
+ return doDecode(new DataView(buf), 0, fields);
366
+ }
367
+ return doDecode(new DataView(p), 0, fields);
368
+ },
369
+ alloc: (() => new PtrArrayBuffer(size)),
370
+ encode: ((v, buf) => {
371
+ // fail-loud:无参 = 分配语义,走 alloc();这里绝不静默零填充 —— 类型层由
372
+ // EncodeIn 拦 '!' 字段省略,动态/JS 调用方由本 throw 兜住。顶层不收 offset
373
+ // (与 decode 对齐,见 StructDef 注释),恒从 buffer 起点写。
374
+ if (v === undefined || v === null)
375
+ throw new Error(`ffi-struct: encode(v) requires an argument; ` +
376
+ `use alloc() for a zeroed out-param slot`);
377
+ const out = buf ?? new PtrArrayBuffer(size);
378
+ doEncode(new DataView(out), 0, fields, v);
379
+ return out;
380
+ }),
381
+ offsetOf: (name) => {
382
+ for (const f of fields)
383
+ if (f.name === name)
384
+ return f.offset;
385
+ throw new Error(`ffi-struct: no field "${name}"`);
386
+ },
387
+ };
388
+ }
389
+ /** 平铺字段表 → '#' 声明:'#'/`#pack` 由构造器写入(顶层 kind 由函数名表明,字段表本身
390
+ * 不收指令键)。member 里混进的 '#' 前缀键在此剔除 —— 类型层已禁(fresh literal excess
391
+ * check),这里运行时兜底,避免用户键覆盖构造器写入的 '#'/`#pack`。 */
392
+ function buildDecl(kind, member, pack) {
393
+ const decl = { '#': kind };
394
+ if (pack !== undefined)
395
+ decl['#pack'] = pack;
396
+ for (const [k, v] of Object.entries(member))
397
+ if (!k.startsWith('#'))
398
+ decl[k] = v;
399
+ return decl;
400
+ }
401
+ export function struct(a, b, c) {
402
+ const isNamed = typeof a === 'string';
403
+ const raw = (isNamed ? b : a);
404
+ const opts = (isNamed ? c : b);
405
+ return createStruct(buildDecl('struct', raw, opts?.pack));
406
+ }
407
+ export function union(a, b, c) {
408
+ const isNamed = typeof a === 'string';
409
+ const raw = (isNamed ? b : a);
410
+ const opts = (isNamed ? c : b);
411
+ return createStruct(buildDecl('union', raw, opts?.pack));
412
+ }