quickwin 2.0.0 → 2.1.1

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 (103) hide show
  1. package/README.en.md +21 -26
  2. package/README.md +3 -6
  3. package/examples/exec_server.js +139 -31
  4. package/examples/exec_server.tsx +7 -8
  5. package/examples/exec_server_worker.js +1 -1
  6. package/examples/exec_server_worker.ts +2 -2
  7. package/examples/pdf_preview2.js +1 -1
  8. package/examples/pdf_preview2.ts +1 -1
  9. package/examples/pdf_viewer.js +132 -28
  10. package/examples/pdf_viewer.tsx +1 -1
  11. package/examples/test_gallery.js +131 -27
  12. package/examples/test_react_lazy.js +131 -27
  13. package/examples/test_react_listbox.js +131 -27
  14. package/examples/test_root_window.js +131 -27
  15. package/examples/test_tab.js +131 -27
  16. package/lib/certs.js +13 -8
  17. package/lib/certs.ts +13 -8
  18. package/lib/fetch.js +51 -3
  19. package/lib/fetch.ts +38 -3
  20. package/lib/ffi/bind.js +94 -5
  21. package/lib/ffi/bind.ts +107 -10
  22. package/lib/ffi/ctype.js +33 -7
  23. package/lib/ffi/ctype.ts +48 -10
  24. package/lib/ffi/struct.js +92 -14
  25. package/lib/ffi/struct.ts +108 -19
  26. package/lib/react-qw/components/ListView.js +1 -0
  27. package/lib/react-qw/components/PathPicker.js +2 -2
  28. package/lib/react-qw/index.js +1 -1
  29. package/lib/react-qw/index.ts +4 -1
  30. package/lib/text-measure.js +3 -1
  31. package/lib/text-measure.ts +4 -3
  32. package/lib/websocket.js +32 -2
  33. package/lib/websocket.ts +25 -2
  34. package/lib/windows/advapi32.js +53 -0
  35. package/lib/windows/advapi32.ts +64 -0
  36. package/lib/windows/comctl32.js +15 -15
  37. package/lib/windows/comctl32.ts +15 -16
  38. package/lib/windows/comdlg32.js +3 -2
  39. package/lib/windows/comdlg32.ts +3 -2
  40. package/lib/windows/gdi32.js +328 -26
  41. package/lib/windows/gdi32.ts +349 -26
  42. package/lib/windows/kernel32.js +206 -10
  43. package/lib/windows/kernel32.ts +225 -10
  44. package/lib/windows/msimg32.js +38 -0
  45. package/lib/windows/msimg32.ts +45 -0
  46. package/lib/windows/ntdll.js +3 -2
  47. package/lib/windows/ntdll.ts +3 -2
  48. package/lib/windows/ole32.js +1 -1
  49. package/lib/windows/ole32.ts +1 -1
  50. package/lib/windows/psapi.js +32 -0
  51. package/lib/windows/psapi.ts +37 -0
  52. package/lib/windows/shell32.js +5 -3
  53. package/lib/windows/shell32.ts +5 -3
  54. package/lib/windows/structs.js +135 -0
  55. package/lib/windows/structs.ts +144 -0
  56. package/lib/windows/user32.js +202 -99
  57. package/lib/windows/user32.ts +204 -99
  58. package/lib/windows/version.js +29 -0
  59. package/lib/windows/version.ts +35 -0
  60. package/lib/windows/winspool.js +71 -0
  61. package/lib/windows/winspool.ts +80 -0
  62. package/package.json +1 -1
  63. package/quickwin.d.ts +11 -5
  64. package/quickwin_const.d.ts +136 -0
  65. package/qwin-nowasm-x86.exe +0 -0
  66. package/qwin-nowasm.exe +0 -0
  67. package/qwin-x86.exe +0 -0
  68. package/qwin.exe +0 -0
  69. package/test/run.js +7 -0
  70. package/test/run.ts +7 -0
  71. package/test/test_basic_fetch.js +21 -0
  72. package/test/test_basic_fetch.ts +17 -0
  73. package/test/test_ffi.js +6 -6
  74. package/test/test_ffi.ts +6 -6
  75. package/test/test_ffi_abi.js +55 -0
  76. package/test/test_ffi_abi.ts +56 -0
  77. package/test/test_ffi_array.js +116 -0
  78. package/test/test_ffi_array.ts +141 -0
  79. package/test/test_ffi_bind.js +82 -0
  80. package/test/test_ffi_bind.ts +71 -0
  81. package/test/test_files.js +102 -0
  82. package/test/test_files.ts +121 -0
  83. package/test/test_gdi_draw.js +681 -0
  84. package/test/test_gdi_draw.ts +725 -0
  85. package/test/test_net_fetch.js +4 -3
  86. package/test/test_net_fetch.ts +4 -3
  87. package/test/test_printer.js +137 -0
  88. package/test/test_printer.ts +148 -0
  89. package/test/test_proc.js +141 -0
  90. package/test/test_proc.ts +163 -0
  91. package/test/test_react_complex.js +131 -27
  92. package/test/test_react_counter.js +131 -27
  93. package/test/test_react_flex.js +131 -27
  94. package/test/test_react_hidden.js +131 -27
  95. package/test/test_react_listview_link.js +131 -27
  96. package/test/test_react_notify.js +131 -27
  97. package/test/test_react_render.js +131 -27
  98. package/test/test_registry.js +74 -0
  99. package/test/test_registry.ts +95 -0
  100. package/test/test_sysinfo.js +122 -0
  101. package/test/test_sysinfo.ts +141 -0
  102. package/test/test_wolfssl_read.js +45 -44
  103. package/test/test_wolfssl_read.ts +42 -42
package/lib/ffi/bind.ts CHANGED
@@ -3,7 +3,7 @@ import * as os from 'os'
3
3
  import * as std from 'std'
4
4
  import * as win from 'win'
5
5
  import '../text-codec.js'
6
- import { type C_BasicType, C_BasicType_No_Void, C_BasicType_Token, C_BasicType_Token_No_Void, cTokenToType, isCPtrToken, ArgJsTypeOfToken, RetJsTypeOfToken, normToken, PTR_SIZE, ptrName, type Ptr, readScalar, writeSlot, PtrArrayBuffer } from './ctype.js'
6
+ import { type C_BasicType, C_BasicType_No_Void, C_BasicType_Token, C_BasicType_Token_No_Void, cTokenToType, isCPtrToken, isCValToken, valName, ArgJsTypeOfToken, RetJsTypeOfToken, normToken, PTR_SIZE, ptrName, type Ptr, readScalar, writeSlot, PtrArrayBuffer } from './ctype.js'
7
7
 
8
8
  // 形状刻意是「方法」而非裸函数:ffi-struct 的 struct()/union() 结果(StructDef)
9
9
  // 结构上即满足 encode 重载(无 buf 新建带品牌 .ptr 的 PtrArrayBuffer / 带 buf 写入既有
@@ -18,20 +18,35 @@ import { type C_BasicType, C_BasicType_No_Void, C_BasicType_Token, C_BasicType_T
18
18
  // .ptr 又带品牌直接喂 <N>ptr 形参——不再有 { buf, ptr } 结构包装。
19
19
  type Codec<N extends string = string, V = unknown, T = unknown> = {
20
20
  // encode 顶层不收 offset(与 decode 对齐):PAB 的 .ptr 恒指向起点,写入位置由
21
- // buf 自身表达;嵌套偏移由布局字段承担。
21
+ // buf 自身表达;偏移写入(数组物化原语)由 StructDef.encode(v, buf, offset) 承担。
22
22
  encode?(v: V, buf?: PtrArrayBuffer<any>): PtrArrayBuffer<any>
23
23
  decode?(p: Ptr<N>| ArrayBuffer): V
24
24
  alloc?(a: T): PtrArrayBuffer<N>
25
25
  }
26
26
  // 约束层三件与 StructDef 形状一致,def 可整体透传;提取层按键存在性走(见下)。
27
- // 类型层硬约束:encode/alloc 只返回 PtrArrayBuffer——裸 ArrayBuffer 出不了 codec
28
- // (callPacked 参数位统一取 .ptr,调用方拿到的也是 .ptr,.buf 词汇从 codec 出口消失)。
27
+ // 类型层硬约束:encode/alloc 只返回 PtrArrayBuffer —— 裸 ArrayBuffer 出不了
28
+ // codec(callPacked 参数位统一取 .ptr,调用方拿到的也是 .ptr,.buf 词汇从 codec 出口消失);
29
+ // NULL(0)是引擎层词汇(number 直通世界),不作为 codec 出口形态。
29
30
  export type CodecMap = Record<string, {
30
31
  encode?(v: any, buf?: PtrArrayBuffer<any>): PtrArrayBuffer<any>
31
32
  decode?(p: any): any
32
33
  alloc?(a: any): PtrArrayBuffer<any>
33
34
  }>
34
35
 
36
+ // 按值位('<N>')消费的布局形态:size(决定槽宽与「传值 / 按引用」分派)+
37
+ // encode(v, buf, offset)(写入任意 buffer 的既有偏移——argFrame 槽内写入、
38
+ // 16 对齐临时副本两条路径共用)。StructDef 结构上即满足,内建 WCHAR/BYTE 无 size。
39
+ type StructValCodec = {
40
+ size: number
41
+ encode(v: unknown, buf: ArrayBuffer, offset: number): unknown
42
+ }
43
+
44
+ // CodecMap 的值是结构形(size 只在 StructDef 上、不属 Codec 契约),统一走 unknown
45
+ // 二跳提取;存在性与 size 由 makeFn 绑定期校验。
46
+ function valCodec(name: string, encoders: CodecMap): StructValCodec | undefined {
47
+ return encoders[name] as unknown as StructValCodec | undefined
48
+ }
49
+
35
50
  const _dllCache: Map<string, win.HMODULE> = new Map()
36
51
 
37
52
  function loadDll(dll: string): win.HMODULE {
@@ -75,15 +90,36 @@ function parseSig(sig: string): ParsedSig {
75
90
 
76
91
  // 裸 'ptr'/非法 token 由类型层拦截(C_BasicType_Token 已 Exclude 'ptr'、拼错 token 塌成 never);
77
92
  // 运行时再比较只会得到 TS2367「两类型无重叠」——类型已表达的约束不重复校验。
93
+ const retToken = normToken(parts[1]!)
94
+ // 按值形 '<N>' 仅支持入参位:大结构返回需要隐藏指针/sret 级 ABI(本运行时的
95
+ // retBuf 恒 8 字节,只支持标量/浮点/指针返回),绑定期 throw 而非静默降级。
96
+ // 类型层对返回位的 '<N>' 不设专门分支(fallthrough),本 throw 是唯一防线。
97
+ if (isCValToken(retToken))
98
+ throw new Error(`ffi-bind: "${sig}": by-value struct at the return position ` +
99
+ `(<${valName(retToken)}>) is not supported — argument positions only; ` +
100
+ `pass a struct out through an out-param slot instead (<N>ptr + caller buffer)`)
78
101
  return {
79
102
  argTokens,
80
- retToken: normToken(parts[1]!),
103
+ retToken,
81
104
  sig,
82
105
  }
83
106
  }
84
107
 
85
108
  function makeFn(proc: number, sig: string, encoders: CodecMap, decoders: CodecMap): (...a: unknown[]) => unknown {
86
109
  const ps = parseSig(sig)
110
+ // 按值位的能力校验前移到绑定期(模块加载即炸,不留到首个调用):布局缺位 / 非结构
111
+ // 布局(无 size)/ 退化 size 都是签名层面的错误,报错指向具体位与可用布局清单。
112
+ for (const t of ps.argTokens) {
113
+ if (!isCValToken(t)) continue
114
+ const name = valName(t)
115
+ const codec = valCodec(name, encoders)
116
+ if (!codec || typeof codec.encode !== 'function')
117
+ throw new Error(`ffi-bind: ${sig}: by-value position <${name}> has no registered layout ` +
118
+ `(available: ${Object.keys(encoders).join(', ') || 'none'})`)
119
+ if (typeof codec.size !== 'number' || codec.size < 1)
120
+ throw new Error(`ffi-bind: ${sig}: by-value position <${name}> requires a struct ` +
121
+ `layout with size >= 1 (got ${codec.size}; non-struct codecs expose no size)`)
122
+ }
87
123
  return (...a: unknown[]) => callPacked(proc, ps, a, encoders, decoders)
88
124
  }
89
125
 
@@ -102,13 +138,34 @@ function callPacked(proc: number, { argTokens, retToken, sig }: ParsedSig, args:
102
138
  const is64 = os.arch === 'x64'
103
139
 
104
140
  const getWidth = (t: C_BasicType_Token_No_Void, is64: boolean): number => {
141
+ // 按值形由 widthOf 分派(槽宽依赖布局 size),此处只认标量/指针档;守卫同时
142
+ // 收窄类型(IA32_ARG_SIZE 索引不认 val 形)。
143
+ if (isCValToken(t))
144
+ throw new Error(`ffi-bind: getWidth: by-value token "${t}" (dispatched by widthOf)`)
105
145
  if (is64) return 8
106
146
  if (isCPtrToken(t)) return PTR_SIZE
107
147
  return IA32_ARG_SIZE[t]
108
148
  }
109
149
 
150
+ // 按值位的槽宽:x64 恒 8 字节(尺寸 ∈{1,2,4,8} 按「同尺寸整数」进槽,其余尺寸进
151
+ // 槽的是指向临时副本的指针);ia32 = ALIGN(size, 4) —— 结构字节原样压栈,无按
152
+ // 引用一说(libffi ffi.c 的 ia32 分支同规则,桩只做一次 rep movsb 搬运)。
153
+ const widthOf = (t: C_BasicType_Token_No_Void): number => {
154
+ if (isCValToken(t)) {
155
+ if (is64) return 8
156
+ const layout = valCodec(valName(t), encoders)
157
+ // 绑定期 makeFn 已校验(存在 + size >= 1);此处兜动态/无类型路径。
158
+ if (!layout)
159
+ throw new Error(`ffi-bind: <${valName(t)}> by-value position has no registered layout`)
160
+ const size = layout.size
161
+ return size % 4 === 0 ? size : size + 4 - (size % 4)
162
+ }
163
+ // val 形已在上分支 return,此处恒为标量/指针形(类型层收窄,见 C_BasicType_Token)。
164
+ return getWidth(t, is64)
165
+ }
166
+
110
167
  let total = 0
111
- for (const t of argTokens) total += getWidth(t, is64)
168
+ for (const t of argTokens) total += widthOf(t)
112
169
  if (is64 && argTokens.length < X64_MIN_SLOTS) total = X64_MIN_SLOTS * 8
113
170
 
114
171
  const argFrame = new ArrayBuffer(total)
@@ -160,10 +217,39 @@ function callPacked(proc: number, { argTokens, retToken, sig }: ParsedSig, args:
160
217
  writeSlot(dv, off, { k: 'ptr', v: p })
161
218
  }
162
219
  }
220
+ else if (isCValToken(argToken)) {
221
+ // 按值位:对象直传(值域由注册布局推导),无地址词汇 —— NULL / number
222
+ // 进这里是必然错位(引擎按值语义,指针位请写 <N>ptr),fail-loud。
223
+ const name = valName(argToken)
224
+ const codec = valCodec(name, encoders)
225
+ if (!codec)
226
+ throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}> by-value position ` +
227
+ `has no registered layout`)
228
+ if (arg === null || arg === undefined)
229
+ throw new Error(`ffi-bind: <${name}> value position got ${String(arg)}; ` +
230
+ `pass a struct object (use <${name}>ptr to pass a pointer)`)
231
+ const size = codec.size
232
+ if (!is64 || (size <= 8 && (size & (size - 1)) === 0)) {
233
+ // 字节直接落槽:ia32 结构按值压栈(任意尺寸);x64 尺寸 ∈{1,2,4,8}
234
+ // 按「同尺寸整数」传值——字节在 8 字节槽低位,callee 右对齐读。
235
+ // argFrame 构造时即零填充,槽高位为 0(callee 忽略高位,无歧义)。
236
+ codec.encode(arg, argFrame, off)
237
+ } else {
238
+ // x64 其余尺寸(>8 或非 2 的幂)按引用:槽放指针,指向 caller 分配的
239
+ // 16 字节对齐临时副本(MS x64 官方规则:单参数永不跨多个寄存器;对齐
240
+ // 偏移自算,不依赖分配器的对齐保证)。held 保持引用直到调用返回。
241
+ const buf = new ArrayBuffer(size + 15)
242
+ const base = ffi.bufferPtr(buf)
243
+ const pad = (16 - (base % 16)) % 16
244
+ codec.encode(arg, buf, pad)
245
+ held.push(buf)
246
+ writeSlot(dv, off, { k: 'ptr', v: base + pad })
247
+ }
248
+ }
163
249
  else {
164
250
  writeSlot(dv, off, { k: argToken, v: arg })
165
251
  }
166
- off += getWidth(argToken, is64)
252
+ off += widthOf(argToken)
167
253
  }
168
254
 
169
255
  const retBuf = new ArrayBuffer(8)
@@ -231,8 +317,9 @@ type BindFn<S extends string, E, D> =
231
317
  * 品牌指针逐字节读到 NUL(野指针无终止 4MiB 上限 fail-fast)、ArrayBuffer 直读到
232
318
  * NUL 或末尾(out 参数未写 NUL 时按读满处理);alloc 预分配返回带 .ptr 的
233
319
  * PtrArrayBuffer(a = 字符数,缺省 256)。类型取必需形态——
234
- * 内建三件齐备,调用无需空断言。 */
235
- export const WCHAR: Required<Codec<'WCHAR', string, number>> = {
320
+ * 内建三件齐备,调用无需空断言。刻意 Pick 而非 Required<Codec>:只声明消费的
321
+ * 三件(encode/decode/alloc),新增可选能力位不被拉成必填。 */
322
+ export const WCHAR: Required<Pick<Codec<'WCHAR', string, number>, 'encode' | 'decode' | 'alloc'>> = {
236
323
  encode: (v: string) => {
237
324
  // TextEncoder 没有 encodeInto:先编码再拷入带 .ptr 的 buffer(单次 memcpy,
238
325
  // 编码逻辑仍全局唯一——这行是全部 UTF-16 字符串编码的唯一实现)。
@@ -407,7 +494,17 @@ function dispatchClosure(
407
494
  export function closure<S extends string>(sig: S, fn: BindFn<S, {}, {}>,
408
495
  opts?: { stdcall?: boolean }): { ptr: number; dispose(): void } {
409
496
  const { argTokens, retToken } = parseSig(sig)
410
- const argSpecs = argTokens.map(t => ({ tok: cTokenToType(t), nonnull: t.endsWith('!') }))
497
+ // 按值位仅支持 bind 的入参方向:closure 的 C→JS 方向由 quickjs-ffi-closure-*.S
498
+ // 按固定槽宽读参数(x64 固定 8 字节槽、ia32 IA32_ARG_SIZE 表),不区分「槽内是
499
+ // 结构字节还是指向临时副本的指针」——该分派未实现,创建期 fail-fast。
500
+ for (const t of argTokens)
501
+ if (isCValToken(t))
502
+ throw new Error(`ffi-bind: closure signature cannot take a by-value struct ` +
503
+ `(<${valName(t)}>); by-value is bind-only — the C→JS direction has no ` +
504
+ `struct slot decoding (pass a pointer and decode on the JS side)`)
505
+ // 上方已拒 '<N>' 按值形:此处 tok 恒为标量/指针档(cTokenToType 对 val 形原样返回,
506
+ // 但那条路径不可达——IA32_ARG_SIZE 索引与 dispatchClosure 只认标量档)。
507
+ const argSpecs = argTokens.map(t => ({ tok: cTokenToType(t) as C_BasicType_No_Void, nonnull: t.endsWith('!') }))
411
508
  const retSpec = { tok: cTokenToType(retToken), nonnull: retToken.endsWith('!') }
412
509
  const is64 = os.arch === 'x64'
413
510
  const stackCapture = argSpecs.reduce((s, x) => s + IA32_ARG_SIZE[x.tok], 0)
package/lib/ffi/ctype.js CHANGED
@@ -27,20 +27,39 @@ const C_BASIC_TYPE = [
27
27
  // '@Name' = 枚举值域标注(类型层启用前先由 Norm 归一剥掉)。两者语义正交但作用面不相交
28
28
  // (! 主品牌位、@ 主裸档整数位,@ 枚举不含 0 时已蕴含 !),故每 token 至多一个修饰。
29
29
  // 组 1 恒为名字(归一名),组 2 为修饰;槽宽/ABI 只看归一形式。
30
- const PTR_TOKEN_RE = /^<([^<>\s]*)>ptr(!|@[A-Za-z_]\w*)?$/;
30
+ // 名字形收紧为标识符([A-Za-z_]\w*)∪ 空串(<>ptr 裸地址档),尾部可挂数组字形:
31
+ // '*' = 零或多(<N*>ptr 入参连续数组位,元素形 = 单对象位 codec)。其余非标识符
32
+ // 字符('+'/'-'/'1' 开头等)不进词法 —— 与 <1abc>ptr/<X-y>ptr 同策略:正则不匹配、
33
+ // 绑定时落通用 Unknown token,类型层不设专属禁分支(运行时兜底,无静默通过)。旧版
34
+ // ([^<>\s]*) 通配曾使 <FOO+>ptr 被静默当品牌 'FOO+' 接受——语义字形必须结构可见。
35
+ const PTR_TOKEN_RE = /^<([A-Za-z_]\w*|)>ptr(!|@[A-Za-z_]\w*)?$/;
31
36
  export function isCPtrToken(t) { return PTR_TOKEN_RE.test(t); }
37
+ // '<N>' 裸品牌形 = 按值传参位(<N>ptr 传地址 / <N> 传值)。名字形同指针形(标识符,
38
+ // 空名 <> 仅 ptr 档有),不带任何修饰 —— 按值位无 '!'(值不为 NULL)、无 '@'(值域由
39
+ // codec 推导、不标枚举)、无 '*'(数组走 structArray encoder 的值域):含修饰的形
40
+ // 正则不匹配,落绑定时通用 Unknown token(与 '<X+>ptr' 同策略,运行时兜底)。
41
+ const VAL_TOKEN_RE = /^<([A-Za-z_]\w*)>$/;
42
+ export function isCValToken(t) { return VAL_TOKEN_RE.test(t); }
43
+ export function valName(t) {
44
+ const m = VAL_TOKEN_RE.exec(t);
45
+ return (m?.[1] || "");
46
+ }
32
47
  export function ptrName(t) {
33
48
  const m = PTR_TOKEN_RE.exec(t);
34
49
  return (m?.[1] || "");
35
50
  }
36
51
  /**
37
- * @description `<${string}>ptr` / `<${string}>ptr!` -> 'ptr'
52
+ * @description `<${string}>ptr` / `<${string}>ptr!` -> 'ptr'。按值形 `<${string}>` 无
53
+ * 标量槽等价物 —— 运行时 throw(返回位由 parseSig 拒、closure 入参位由 closure() 拒,
54
+ * 正常路径不可达),返回类型同时 Exclude 掉按值形:消费方(readRet / dispatchClosure
55
+ * 的 IA32_ARG_SIZE 索引)拿不到会误导编译期的标量误判。
38
56
  */
39
57
  export const cTokenToType = (t) => {
40
58
  if (isCPtrToken(t))
41
59
  return 'ptr';
42
- else
43
- return t;
60
+ if (isCValToken(t))
61
+ throw new Error(`ffi: cTokenToType: by-value token "${t}" has no scalar slot type`);
62
+ return t;
44
63
  };
45
64
  export const PTR_SIZE = os.arch === 'x64' ? 8 : 4;
46
65
  export const SizeAlign = {
@@ -56,11 +75,18 @@ export const NULL = 0;
56
75
  export function normToken(t) {
57
76
  if (isCPtrToken(t)) {
58
77
  // 只剥 '@Name'(枚举标注是纯类型层语义,运行时不校验、不理解);'!' 保留在
59
- // token 上——运行时要执法的语义(0 检查)跟着 token 走到 callPacked/closure,
60
- // 无独立 flag 可被解构丢失。槽宽/ABI 判型走 isCPtrToken,带 '!' 不受影响。
78
+ // token 上——运行时要执法的语义(0 检查)跟着 token 走到 callPacked,
79
+ // 无独立 flag 可被解构丢失。槽宽/ABI 判型走 isCPtrToken,带修饰不受影响。
61
80
  const m = PTR_TOKEN_RE.exec(t);
62
- return (m?.[2]?.startsWith('@') ? `<${m[1]}>ptr` : t);
81
+ const name = m?.[1] || '';
82
+ const mod = m?.[2];
83
+ // 剥 '@'(纯类型层语义);'!' 形随 token 走。('*' 等词法外字符不设专属分支:
84
+ // 不匹配正则,落末尾 Unknown token —— 与 '<X+>ptr' 同待遇。)
85
+ return (mod?.startsWith('@') ? `<${name}>ptr` : t);
63
86
  }
87
+ // 按值形 '<N>' 无修饰可剥(见 VAL_TOKEN_RE 注释),原样归一。
88
+ if (isCValToken(t))
89
+ return t;
64
90
  // 整数档尾缀 '@Name'(枚举值域标注,与 ptr 位的 '@' 同源):剥掉归一到原 token,
65
91
  // 槽宽/ABI 只看归一形式。@ 后缀须为纯枚举名形 —— 'u32@WmMsg:3' 这类混进位域/数组
66
92
  // 串的直接 throw,不静默吞成纯标量(布局会错);名 ∈ EnumMap 的语义校验留给类型层
package/lib/ffi/ctype.ts CHANGED
@@ -29,28 +29,53 @@ const C_BASIC_TYPE = [
29
29
  export type C_BasicType = (typeof C_BASIC_TYPE)[number]
30
30
  export type C_BasicType_No_Void = Exclude<C_BasicType, 'void'>
31
31
 
32
- // '!' 形随 token 走(normToken 只剥运行时不理解的 '@',运行时要执法的 '!' 保留——
33
- // token 即契约,callPacked/closure 按尾缀做 0 检查,无独立 flag 可丢)。
34
- export type C_BasicType_Token = Exclude<C_BasicType, 'ptr'> | `<${string}>ptr` | `<${string}>ptr!`
32
+ // '!' / '*' 形随 token 走(normToken 只剥运行时不理解的 '@',运行时要执法的 '!'(0 检查)
33
+ // 与 '*'(数组分派)保留在 token 上——token 即契约,callPacked 按尾缀执法,无独立 flag
34
+ // 可丢)。'<N*>ptr' 数组位无需新词形:品牌段含 '*',整 token 仍在 <${string}>ptr 模板内。
35
+ export type C_BasicType_Token = Exclude<C_BasicType, 'ptr'> | `<${string}>ptr` | `<${string}>ptr!` | `<${string}>`
35
36
  export type C_BasicType_Token_No_Void = Exclude<C_BasicType_Token, 'void'>
36
37
 
37
38
  // 尾缀修饰:'!' = 非空担保(见 Arg/RetJsTypeOfToken 的 ! 分支与 callPacked 的运行时 0 检查),
38
39
  // '@Name' = 枚举值域标注(类型层启用前先由 Norm 归一剥掉)。两者语义正交但作用面不相交
39
40
  // (! 主品牌位、@ 主裸档整数位,@ 枚举不含 0 时已蕴含 !),故每 token 至多一个修饰。
40
41
  // 组 1 恒为名字(归一名),组 2 为修饰;槽宽/ABI 只看归一形式。
41
- const PTR_TOKEN_RE = /^<([^<>\s]*)>ptr(!|@[A-Za-z_]\w*)?$/
42
+ // 名字形收紧为标识符([A-Za-z_]\w*)∪ 空串(<>ptr 裸地址档),尾部可挂数组字形:
43
+ // '*' = 零或多(<N*>ptr 入参连续数组位,元素形 = 单对象位 codec)。其余非标识符
44
+ // 字符('+'/'-'/'1' 开头等)不进词法 —— 与 <1abc>ptr/<X-y>ptr 同策略:正则不匹配、
45
+ // 绑定时落通用 Unknown token,类型层不设专属禁分支(运行时兜底,无静默通过)。旧版
46
+ // ([^<>\s]*) 通配曾使 <FOO+>ptr 被静默当品牌 'FOO+' 接受——语义字形必须结构可见。
47
+ const PTR_TOKEN_RE = /^<([A-Za-z_]\w*|)>ptr(!|@[A-Za-z_]\w*)?$/
42
48
 
43
49
  export function isCPtrToken(t: string): t is `<${string}>ptr` | `${string}ptr!` | `${string}ptr@${string}` { return PTR_TOKEN_RE.test(t) }
50
+
51
+ // '<N>' 裸品牌形 = 按值传参位(<N>ptr 传地址 / <N> 传值)。名字形同指针形(标识符,
52
+ // 空名 <> 仅 ptr 档有),不带任何修饰 —— 按值位无 '!'(值不为 NULL)、无 '@'(值域由
53
+ // codec 推导、不标枚举)、无 '*'(数组走 structArray encoder 的值域):含修饰的形
54
+ // 正则不匹配,落绑定时通用 Unknown token(与 '<X+>ptr' 同策略,运行时兜底)。
55
+ const VAL_TOKEN_RE = /^<([A-Za-z_]\w*)>$/
56
+
57
+ export function isCValToken(t: string): t is `<${string}>` { return VAL_TOKEN_RE.test(t) }
58
+ export function valName<const N extends string>(t: `<${N}>`): N {
59
+ const m = VAL_TOKEN_RE.exec(t)
60
+ return (m?.[1] || "") as N
61
+ }
44
62
  export function ptrName<const N extends string>(t: `<${N}>ptr` | `<${N}>ptr!`): N {
45
63
  const m = PTR_TOKEN_RE.exec(t)
46
64
  return (m?.[1] || "") as N
47
65
  }
48
66
  /**
49
- * @description `<${string}>ptr` / `<${string}>ptr!` -> 'ptr'
67
+ * @description `<${string}>ptr` / `<${string}>ptr!` -> 'ptr'。按值形 `<${string}>` 无
68
+ * 标量槽等价物 —— 运行时 throw(返回位由 parseSig 拒、closure 入参位由 closure() 拒,
69
+ * 正常路径不可达),返回类型同时 Exclude 掉按值形:消费方(readRet / dispatchClosure
70
+ * 的 IA32_ARG_SIZE 索引)拿不到会误导编译期的标量误判。
50
71
  */
51
- export const cTokenToType = <const T extends string>(t: T): Exclude<T, `<${string}>ptr` | `<${string}>ptr!`> | 'ptr' => {
72
+ export const cTokenToType = <const T extends string>(
73
+ t: T,
74
+ ): Exclude<T, `<${string}>ptr` | `<${string}>ptr!` | `<${string}>`> | 'ptr' => {
52
75
  if (isCPtrToken(t)) return 'ptr'
53
- else return t as Exclude<T, `<${string}>ptr` | `<${string}>ptr!`>
76
+ if (isCValToken(t))
77
+ throw new Error(`ffi: cTokenToType: by-value token "${t}" has no scalar slot type`)
78
+ return t as Exclude<T, `<${string}>ptr` | `<${string}>ptr!` | `<${string}>`>
54
79
  }
55
80
 
56
81
  // C 的数字类型档:可作结构体字段 / bind 签名 token。排除项理由见 §1。
@@ -202,9 +227,16 @@ declare global {
202
227
  // 泛型体内)由 `string & keyof map` = 全键联合兜底成非 never —— 否则 BindFn 塌
203
228
  // (never)=>never 使内层强转失去可比性。
204
229
  export type ArgJsTypeOfToken<K extends string, L> =
230
+ // 非标识符名字形('<X+>ptr'/'<X*>ptr'/'<1abc>ptr')不设专属分支:落 '<N>ptr'
231
+ // 通配、运行时 Unknown token 兜底('*' 已非 token 字形 —— 数组物化走
232
+ // structArray(def) encoder 的值域推导,token/引擎/类型层零数组概念)。
205
233
  K extends `<${infer N}>ptr!` ? Ptr<N> | L[N & keyof L] :
206
234
  K extends `<${string}>ptr@${infer E extends keyof EnumMap}` ? EnumMap[E] :
207
235
  K extends `<${infer N}>ptr` ? MaybePtr<N> | L[N & keyof L] :
236
+ // '<N>' 按值位:值域 = 注册布局的 encode 入参形(对象直传),无 MaybePtr —— 按值
237
+ // 位不收 NULL / 地址(引擎按值语义编码,地址进位是必然错位)。未注册布局 →
238
+ // L[never] → 整位塌 never(与拼错 token 同策略)。
239
+ K extends `<${infer N}>` ? L[N & keyof L] :
208
240
  K extends `${C_Number}@${infer E extends keyof EnumMap}` ? EnumMap[E] :
209
241
  TokenArgJsTypeMap[K & keyof TokenArgJsTypeMap] extends never ? never
210
242
  : TokenArgJsTypeMap[K & keyof TokenArgJsTypeMap]
@@ -234,11 +266,17 @@ export type RetJsTypeOfToken<K extends string, D> =
234
266
  export function normToken(t: string): C_BasicType_Token {
235
267
  if (isCPtrToken(t)) {
236
268
  // 只剥 '@Name'(枚举标注是纯类型层语义,运行时不校验、不理解);'!' 保留在
237
- // token 上——运行时要执法的语义(0 检查)跟着 token 走到 callPacked/closure,
238
- // 无独立 flag 可被解构丢失。槽宽/ABI 判型走 isCPtrToken,带 '!' 不受影响。
269
+ // token 上——运行时要执法的语义(0 检查)跟着 token 走到 callPacked,
270
+ // 无独立 flag 可被解构丢失。槽宽/ABI 判型走 isCPtrToken,带修饰不受影响。
239
271
  const m = PTR_TOKEN_RE.exec(t)
240
- return (m?.[2]?.startsWith('@') ? `<${m[1]}>ptr` : t) as C_BasicType_Token
272
+ const name = m?.[1] || ''
273
+ const mod = m?.[2]
274
+ // 剥 '@'(纯类型层语义);'!' 形随 token 走。('*' 等词法外字符不设专属分支:
275
+ // 不匹配正则,落末尾 Unknown token —— 与 '<X+>ptr' 同待遇。)
276
+ return (mod?.startsWith('@') ? `<${name}>ptr` : t) as C_BasicType_Token
241
277
  }
278
+ // 按值形 '<N>' 无修饰可剥(见 VAL_TOKEN_RE 注释),原样归一。
279
+ if (isCValToken(t)) return t
242
280
  // 整数档尾缀 '@Name'(枚举值域标注,与 ptr 位的 '@' 同源):剥掉归一到原 token,
243
281
  // 槽宽/ABI 只看归一形式。@ 后缀须为纯枚举名形 —— 'u32@WmMsg:3' 这类混进位域/数组
244
282
  // 串的直接 throw,不静默吞成纯标量(布局会错);名 ∈ EnumMap 的语义校验留给类型层
package/lib/ffi/struct.js CHANGED
@@ -354,28 +354,31 @@ function createStruct(t) {
354
354
  size,
355
355
  structAlign: maxEffectiveAlign,
356
356
  decode: (p) => {
357
- // number = native 拥有的品牌指针(Codec 的 decode(p) 形态):按布局 size
358
- // 逐字节拷进临时 buffer 再解码;ArrayBuffer = 调用方持有的 out buffer 直读。
357
+ // number = native 拥有的品牌指针(Codec 的 decode(p) 形态):readBytes 一次
358
+ // native memcpy 拷出 size 字节再解码(替代逐字节 readByte 循环,N 次跨语言
359
+ // 调用降为 1 次);ArrayBuffer = 调用方持有的 out buffer 直读。
359
360
  // 顶层不收 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
- }
361
+ if (typeof p === 'number')
362
+ return doDecode(new DataView(ffi.readBytes(p, size)), 0, fields);
367
363
  return doDecode(new DataView(p), 0, fields);
368
364
  },
369
- alloc: (() => new PtrArrayBuffer(size)),
370
- encode: ((v, buf) => {
365
+ alloc: ((n) => {
366
+ // fail-loud:n < 1 / 非整数绝不静默产出 PAB(0)(其 .ptr 是"非 0 的 1 字节
367
+ // 堆指针",会骗过 C 的 NULL 防御)—— 空位语义由调用方显式传 NULL。
368
+ if (n !== undefined && (!Number.isInteger(n) || n < 1))
369
+ throw new Error(`ffi-struct: alloc(n) requires an integer n >= 1 ` +
370
+ `(PAB(0) never escapes — pass NULL for an empty pointer position)`);
371
+ return new PtrArrayBuffer(size * (n ?? 1));
372
+ }),
373
+ encode: ((v, buf, offset) => {
371
374
  // fail-loud:无参 = 分配语义,走 alloc();这里绝不静默零填充 —— 类型层由
372
- // EncodeIn 拦 '!' 字段省略,动态/JS 调用方由本 throw 兜住。顶层不收 offset
373
- // (与 decode 对齐,见 StructDef 注释),恒从 buffer 起点写。
375
+ // EncodeIn 拦 '!' 字段省略,动态/JS 调用方由本 throw 兜住。顶层默认从起点写,
376
+ // offset 显式给写入点(数组物化原语,见接口注释)。
374
377
  if (v === undefined || v === null)
375
378
  throw new Error(`ffi-struct: encode(v) requires an argument; ` +
376
379
  `use alloc() for a zeroed out-param slot`);
377
380
  const out = buf ?? new PtrArrayBuffer(size);
378
- doEncode(new DataView(out), 0, fields, v);
381
+ doEncode(new DataView(out), offset ?? 0, fields, v);
379
382
  return out;
380
383
  }),
381
384
  offsetOf: (name) => {
@@ -398,6 +401,81 @@ function buildDecl(kind, member, pack) {
398
401
  decl[k] = v;
399
402
  return decl;
400
403
  }
404
+ /** 结构/联合体的数组编解码对工厂:返回 `{ encode, decode }`。
405
+ * encode —— 单值 encoder 的数组版:收元素数组 → 按本布局 size 步长紧密排列
406
+ * (stride = size,已含尾部对齐 padding),返回带品牌 .ptr 的 PAB —— 与 C 的
407
+ * `const T apt[]` 同构。值域刻意收普通数组(调用方持有的 `const pts = [...]` 变量
408
+ * 直传,非空元组会拒之);[] / 非数组 / 单对象运行时 throw —— 任何路径都不产出
409
+ * PAB(0)(其 .ptr 是"非 0 的 1 字节堆指针",会骗过 C 的 NULL 防御;空位语义由
410
+ * 调用方显式传 NULL)。注册进 bind 的 encoders 表(`{ POINT: structArray(POINT) }`)
411
+ * 后,普通 `<POINT>ptr` 位的值域即元素数组(值域由 encoder 推导,token/引擎/类型层
412
+ * 零数组概念);直接调用 `.encode(els).ptr` 亦可。
413
+ * decode —— 出参数组读回(与 def.decode 的 ArrayBuffer/地址双态对称):
414
+ * - buffer 形(含 PAB):count? 缺省自算 byteLength/size —— 要求整数倍,% ≠ 0
415
+ * throw(混合缓冲如 EnumPrinters 的"结构 + 尾部字符串"必须显式 count);显式
416
+ * count 校验 count*size ≤ byteLength(写错 count → 越界读的防线,buf 形可校才
417
+ * 校;不验品牌,与 def.decode 的 buffer 形同款)。
418
+ * - 裸地址形:count 必填(地址无长度信息),逐字节拷读(def.decode 的 number 形
419
+ * 同机制);长度不可校 → 信任 C 的 count。
420
+ * - count = 0 → [](读 0 个真空安全,与 encode 禁空的 PAB(0) 理由刻意不对称)。
421
+ * 实现:encode 走纯公开 API(alloc + encode offset 循环);decode 在本文件内复用
422
+ * computeStructLayout/doDecode。 */
423
+ export function structArray(def) {
424
+ const { fields, size } = computeStructLayout(def.__struct);
425
+ const readN = (dv, n) => {
426
+ const out = [];
427
+ for (let i = 0; i < n; i++)
428
+ out.push(doDecode(dv, i * size, fields));
429
+ return out;
430
+ };
431
+ return {
432
+ encode: (els) => {
433
+ if (!Array.isArray(els) || els.length === 0)
434
+ throw new Error(`ffi-struct: structArray(def).encode(els) requires a ` +
435
+ `non-empty element array (pass NULL for an empty pointer position)`);
436
+ const out = def.alloc(els.length);
437
+ for (let i = 0; i < els.length; i++)
438
+ def.encode(els[i], out, i * def.size);
439
+ return out;
440
+ },
441
+ decode: ((p, count) => {
442
+ // fail-loud 校验矩阵(见 JSDoc):显式 count 信调用方但拦越界;缺省只对
443
+ // 有 byteLength 的 buffer 形自算;裸地址必带 count。
444
+ if (count !== undefined && (!Number.isInteger(count) || count < 0))
445
+ throw new Error(`ffi-struct: structArray.decode(p, count) requires a ` +
446
+ `non-negative integer count, got ${count}`);
447
+ if (typeof p === 'number') {
448
+ if (count === undefined)
449
+ throw new Error(`ffi-struct: structArray.decode(address, count) ` +
450
+ `requires an explicit count (a bare address carries no length)`);
451
+ if (count === 0)
452
+ return [];
453
+ // readBytes 一次 native memcpy 拷出整段再解码(def.decode 的 number 形同机制)。
454
+ return readN(new DataView(ffi.readBytes(p, count * size)), count);
455
+ }
456
+ let n;
457
+ if (count !== undefined) {
458
+ n = count;
459
+ // 越界防线:写错 count 是越界读的唯一入口,buf 形有 byteLength 就校死。
460
+ if (n * size > p.byteLength)
461
+ throw new Error(`ffi-struct: structArray.decode: count ${n} × ` +
462
+ `size ${size} = ${n * size} exceeds buffer byteLength ${p.byteLength}`);
463
+ }
464
+ else {
465
+ // 自算形态:byteLength 必须是纯元素序列(size = 0 的退化布局同样落此
466
+ // throw —— 0 % 0 = NaN),混合缓冲显式传 count。
467
+ if (p.byteLength % size !== 0)
468
+ throw new Error(`ffi-struct: structArray.decode: byteLength ` +
469
+ `${p.byteLength} is not a multiple of size ${size} ` +
470
+ `(mixed buffer? pass count explicitly)`);
471
+ n = p.byteLength / size;
472
+ }
473
+ if (n === 0)
474
+ return [];
475
+ return readN(new DataView(p), n);
476
+ }),
477
+ };
478
+ }
401
479
  export function struct(a, b, c) {
402
480
  const isNamed = typeof a === 'string';
403
481
  const raw = (isNamed ? b : a);