quickwin 1.3.0 → 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 (143) hide show
  1. package/README.en.md +1 -2
  2. package/README.md +19 -23
  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 +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1318 -1178
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1313 -1074
  16. package/examples/test_react_lazy.js +1284 -1056
  17. package/examples/test_react_listbox.js +1283 -1055
  18. package/examples/test_root_window.js +1279 -1051
  19. package/examples/test_tab.js +1284 -1056
  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 +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +49 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  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 +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  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 +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  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 -23
  59. package/lib/text-measure.ts +20 -25
  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 -37
  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 +12 -4
  96. package/test/run.ts +13 -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 +25 -43
  100. package/test/test_ffi.ts +33 -52
  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 +538 -8
  104. package/test/test_ffi_bind.ts +559 -8
  105. package/test/test_ffi_struct.js +632 -88
  106. package/test/test_ffi_struct.ts +666 -91
  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 +1294 -1066
  126. package/test/test_react_counter.js +1284 -1056
  127. package/test/test_react_flex.js +1304 -1069
  128. package/test/test_react_hidden.js +1287 -1059
  129. package/test/test_react_listview_link.js +1284 -1056
  130. package/test/test_react_notify.js +18534 -0
  131. package/test/test_react_render.js +1284 -1056
  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
  138. package/lib/ffi-bind.js +0 -85
  139. package/lib/ffi-bind.ts +0 -170
  140. package/lib/ffi-struct.js +0 -224
  141. package/lib/ffi-struct.ts +0 -239
  142. package/lib/win-common-structs.js +0 -39
  143. package/lib/win-common-structs.ts +0 -41
package/lib/fetch.js CHANGED
@@ -26,6 +26,27 @@ function parseHeaders(data) {
26
26
  }
27
27
  return { status, statusText, headers };
28
28
  }
29
+ /** True when the chunked body is complete. With a declared `Trailer`,
30
+ * the terminating block is followed by 0..n header lines and a final
31
+ * empty line (`\r\n0\r\n<trailer...>\r\n\r\n`) instead of the bare
32
+ * `\r\n0\r\n\r\n` form. */
33
+ function chunkedEndReached(data, hasTrailer) {
34
+ if (!hasTrailer)
35
+ return _hasChunkedEnd(data);
36
+ const tail = '\r\n0\r\n';
37
+ for (let i = data.length - 1; i >= 0; i--) {
38
+ let j = 0;
39
+ while (j < tail.length && i + j < data.length && data[i + j] === tail.charCodeAt(j))
40
+ j++;
41
+ if (j === tail.length) {
42
+ const after = i + tail.length;
43
+ if (data.length >= after + 2 &&
44
+ data[data.length - 2] === 0x0D && data[data.length - 1] === 0x0A)
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
29
50
  // ── State machine constants ──
30
51
  const ST_CONNECTING = 0;
31
52
  const ST_HANDSHAKE = 1;
@@ -76,6 +97,7 @@ async function fetchRequest(req) {
76
97
  let _controller = null;
77
98
  let headerRaw = new Uint8Array(0);
78
99
  let isChunked = false;
100
+ let hasTrailer = false;
79
101
  let contentLength = 0;
80
102
  let chunkedParts = [];
81
103
  let receivedBytes = 0;
@@ -168,15 +190,25 @@ async function fetchRequest(req) {
168
190
  doReject(new Error('SSL_CTX_new failed'));
169
191
  return;
170
192
  }
171
- wolfssl.wolfSSL_CTX_set_verify(ctx, 0 /* wolfssl.VerifyMode.SSL_VERIFY_NONE */);
193
+ if (req.rejectUnauthorized === false) {
194
+ wolfssl.wolfSSL_CTX_set_verify(ctx, 0 /* wolfssl.VerifyMode.SSL_VERIFY_NONE */);
195
+ }
196
+ else {
197
+ wolfssl.wolfSSL_CTX_set_verify(ctx, 1 /* wolfssl.VerifyMode.SSL_VERIFY_PEER */);
198
+ wolfssl.loadTrustedCerts(ctx);
199
+ }
172
200
  ssl = wolfssl.wolfSSL_new(ctx);
173
201
  if (!ssl) {
174
202
  doReject(new Error('SSL_new failed'));
175
203
  return;
176
204
  }
177
205
  wolfssl.wolfSSL_set_fd(ssl, sock.get_fd(fd));
178
- if (parsedUrl.hostname)
206
+ if (parsedUrl.hostname) {
179
207
  wolfssl.wolfSSL_UseSNI(ssl, 0 /* wolfssl.SniType.WOLFSSL_SNI_HOST_NAME */, parsedUrl.hostname);
208
+ if (req.rejectUnauthorized !== false) {
209
+ wolfssl.wolfSSL_check_domain_name(ssl, parsedUrl.hostname);
210
+ }
211
+ }
180
212
  state = ST_HANDSHAKE;
181
213
  }
182
214
  else {
@@ -237,6 +269,7 @@ async function fetchRequest(req) {
237
269
  }
238
270
  const trailingBodyBytes = headerRaw.subarray(headerEnd + 4);
239
271
  isChunked = (parsed.headers.get('transfer-encoding') || '').toLowerCase().includes('chunked');
272
+ hasTrailer = parsed.headers.get('trailer') != null;
240
273
  contentLength = isChunked ? 0 : parseInt(parsed.headers.get('content-length') || '0', 10);
241
274
  _controller = null;
242
275
  stream = new ReadableStream({
@@ -246,7 +279,7 @@ async function fetchRequest(req) {
246
279
  if (trailingBodyBytes.length > 0) {
247
280
  if (isChunked) {
248
281
  chunkedParts = [trailingBodyBytes];
249
- if (_hasChunkedEnd(trailingBodyBytes)) {
282
+ if (chunkedEndReached(trailingBodyBytes, hasTrailer)) {
250
283
  const decoded = decodeChunked(trailingBodyBytes);
251
284
  _controller.enqueue(decoded);
252
285
  _controller.close();
@@ -294,7 +327,7 @@ async function fetchRequest(req) {
294
327
  if (isChunked) {
295
328
  chunkedParts.push(new Uint8Array(data));
296
329
  const combined = _concat(chunkedParts);
297
- if (_hasChunkedEnd(combined)) {
330
+ if (chunkedEndReached(combined, hasTrailer)) {
298
331
  const decoded = decodeChunked(combined);
299
332
  _controller.enqueue(decoded);
300
333
  _controller.close();
package/lib/fetch.ts CHANGED
@@ -47,6 +47,25 @@ function parseHeaders(data: string): ParsedResponse | null {
47
47
  return { status, statusText, headers }
48
48
  }
49
49
 
50
+ /** True when the chunked body is complete. With a declared `Trailer`,
51
+ * the terminating block is followed by 0..n header lines and a final
52
+ * empty line (`\r\n0\r\n<trailer...>\r\n\r\n`) instead of the bare
53
+ * `\r\n0\r\n\r\n` form. */
54
+ function chunkedEndReached(data: Uint8Array, hasTrailer: boolean): boolean {
55
+ if (!hasTrailer) return _hasChunkedEnd(data)
56
+ const tail = '\r\n0\r\n'
57
+ for (let i = data.length - 1; i >= 0; i--) {
58
+ let j = 0
59
+ while (j < tail.length && i + j < data.length && data[i + j] === tail.charCodeAt(j)) j++
60
+ if (j === tail.length) {
61
+ const after = i + tail.length
62
+ if (data.length >= after + 2 &&
63
+ data[data.length - 2] === 0x0D && data[data.length - 1] === 0x0A) return true
64
+ }
65
+ }
66
+ return false
67
+ }
68
+
50
69
  // ── State machine constants ──
51
70
 
52
71
  const ST_CONNECTING = 0
@@ -101,6 +120,7 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
101
120
  let _controller: ReadableStreamDefaultController<Uint8Array> | null = null
102
121
  let headerRaw: Uint8Array = new Uint8Array(0)
103
122
  let isChunked = false
123
+ let hasTrailer = false
104
124
  let contentLength = 0
105
125
  let chunkedParts: Uint8Array[] = []
106
126
  let receivedBytes = 0
@@ -163,11 +183,21 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
163
183
  const tlsMethod = wolfssl.wolfTLSv1_2_client_method()
164
184
  ctx = wolfssl.wolfSSL_CTX_new(tlsMethod)
165
185
  if (!ctx) { doReject(new Error('SSL_CTX_new failed')); return }
166
- wolfssl.wolfSSL_CTX_set_verify(ctx, wolfssl.VerifyMode.SSL_VERIFY_NONE)
186
+ if (req.rejectUnauthorized === false) {
187
+ wolfssl.wolfSSL_CTX_set_verify(ctx, wolfssl.VerifyMode.SSL_VERIFY_NONE)
188
+ } else {
189
+ wolfssl.wolfSSL_CTX_set_verify(ctx, wolfssl.VerifyMode.SSL_VERIFY_PEER)
190
+ wolfssl.loadTrustedCerts(ctx)
191
+ }
167
192
  ssl = wolfssl.wolfSSL_new(ctx)
168
193
  if (!ssl) { doReject(new Error('SSL_new failed')); return }
169
194
  wolfssl.wolfSSL_set_fd(ssl, sock.get_fd(fd))
170
- if (parsedUrl.hostname) wolfssl.wolfSSL_UseSNI(ssl, wolfssl.SniType.WOLFSSL_SNI_HOST_NAME, parsedUrl.hostname)
195
+ if (parsedUrl.hostname) {
196
+ wolfssl.wolfSSL_UseSNI(ssl, wolfssl.SniType.WOLFSSL_SNI_HOST_NAME, parsedUrl.hostname)
197
+ if (req.rejectUnauthorized !== false) {
198
+ wolfssl.wolfSSL_check_domain_name(ssl, parsedUrl.hostname)
199
+ }
200
+ }
171
201
  state = ST_HANDSHAKE
172
202
  } else {
173
203
  sock.send(fd, httpRequest)
@@ -217,6 +247,7 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
217
247
  const trailingBodyBytes = headerRaw.subarray(headerEnd + 4)
218
248
 
219
249
  isChunked = (parsed.headers.get('transfer-encoding') || '').toLowerCase().includes('chunked')
250
+ hasTrailer = parsed.headers.get('trailer') != null
220
251
  contentLength = isChunked ? 0 : parseInt(
221
252
  parsed.headers.get('content-length') || '0', 10
222
253
  )
@@ -228,7 +259,7 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
228
259
  if (trailingBodyBytes.length > 0) {
229
260
  if (isChunked) {
230
261
  chunkedParts = [trailingBodyBytes]
231
- if (_hasChunkedEnd(trailingBodyBytes)) {
262
+ if (chunkedEndReached(trailingBodyBytes, hasTrailer)) {
232
263
  const decoded = decodeChunked(trailingBodyBytes)
233
264
  _controller!.enqueue(decoded)
234
265
  _controller!.close()
@@ -272,7 +303,7 @@ async function fetchRequest(req: RequestImpl): Promise<ResponseImpl> {
272
303
  if (isChunked) {
273
304
  chunkedParts.push(new Uint8Array(data))
274
305
  const combined = _concat(chunkedParts)
275
- if (_hasChunkedEnd(combined)) {
306
+ if (chunkedEndReached(combined, hasTrailer)) {
276
307
  const decoded = decodeChunked(combined)
277
308
  _controller!.enqueue(decoded)
278
309
  _controller!.close()
@@ -0,0 +1,352 @@
1
+ import * as ffi from 'ffi';
2
+ import * as os from 'os';
3
+ import * as std from 'std';
4
+ import * as win from 'win';
5
+ import '../text-codec.js';
6
+ import { cTokenToType, isCPtrToken, normToken, PTR_SIZE, ptrName, readScalar, writeSlot, PtrArrayBuffer } from './ctype.js';
7
+ const _dllCache = new Map();
8
+ function loadDll(dll) {
9
+ const cached = _dllCache.get(dll);
10
+ if (cached !== undefined)
11
+ return cached;
12
+ const loaded = win.LoadLibrary(dll);
13
+ if (!loaded)
14
+ throw new Error(`ffi-bind: LoadLibrary("${dll}") failed`);
15
+ _dllCache.set(dll, loaded);
16
+ return loaded;
17
+ }
18
+ function parseSig(sig) {
19
+ const parts = sig.split(' -> ');
20
+ if (parts.length !== 2) {
21
+ throw new Error(`ffi-bind: invalid signature "${sig}" (expected "arg1 arg2 -> ret")`);
22
+ }
23
+ // 零参数签名是 ' -> u32',此时 parts[0]==='',split(' ') 会产出 [''];
24
+ // 空串必须先滤掉,否则 normToken('') 抛 Unknown token 整个 suite 崩。
25
+ const rawArgs = parts[0].split(' ').filter(t => t !== '');
26
+ const argTokens = [];
27
+ for (const raw of rawArgs) {
28
+ // '@Name' 枚举标注由 normToken 剥(槽宽/ABI 只看底档),名字合法性由类型层拦
29
+ // (未知枚举塌 never);结构侧的 '@'(键 alignas / 值 encoding)不走本路径——
30
+ // bind 签名是 token 修饰的唯一入口。
31
+ const tok = normToken(raw);
32
+ if (tok === 'void') {
33
+ throw new Error("Void can not in args");
34
+ }
35
+ argTokens.push(tok);
36
+ }
37
+ // 裸 'ptr'/非法 token 由类型层拦截(C_BasicType_Token 已 Exclude 'ptr'、拼错 token 塌成 never);
38
+ // 运行时再比较只会得到 TS2367「两类型无重叠」——类型已表达的约束不重复校验。
39
+ return {
40
+ argTokens,
41
+ retToken: normToken(parts[1]),
42
+ sig,
43
+ };
44
+ }
45
+ function makeFn(proc, sig, encoders, decoders) {
46
+ const ps = parseSig(sig);
47
+ return (...a) => callPacked(proc, ps, a, encoders, decoders);
48
+ }
49
+ // 每个参数在 argFrame 里占的字节数。与 quickjs-ffi-type.h 的 qwin_ffi_arg_size[]
50
+ // 保持一致(ia32:≤4B 类型进 4 字节、更大的进 8 字节;x64:恒 8 字节槽)。
51
+ // 注意这是「槽宽」,不是类型的 sizeof——同一定义也用于返回槽。指针布局恒指针宽。
52
+ const IA32_ARG_SIZE = {
53
+ u8: 4, i8: 4, u16: 4, i16: 4, u32: 4, i32: 4,
54
+ u64: 8, i64: 8, f32: 4, f64: 8,
55
+ ptr: PTR_SIZE,
56
+ };
57
+ // x64 桩无条件双读 slots[0..3],故 argFrame 至少 4 个槽(32 字节)
58
+ const X64_MIN_SLOTS = 4;
59
+ function callPacked(proc, { argTokens, retToken, sig }, args, encoders, decoders) {
60
+ const is64 = os.arch === 'x64';
61
+ const getWidth = (t, is64) => {
62
+ if (is64)
63
+ return 8;
64
+ if (isCPtrToken(t))
65
+ return PTR_SIZE;
66
+ return IA32_ARG_SIZE[t];
67
+ };
68
+ let total = 0;
69
+ for (const t of argTokens)
70
+ total += getWidth(t, is64);
71
+ if (is64 && argTokens.length < X64_MIN_SLOTS)
72
+ total = X64_MIN_SLOTS * 8;
73
+ const argFrame = new ArrayBuffer(total);
74
+ const dv = new DataView(argFrame);
75
+ const held = []; // 指针布局编码缓冲需存活到调用结束
76
+ let off = 0;
77
+ for (let i = 0; i < argTokens.length; i++) {
78
+ const argToken = argTokens[i];
79
+ const nonnull = argToken.endsWith('!'); // '!' 留在 token 尾缀上,此处按尾缀执法
80
+ const arg = args[i];
81
+ if (isCPtrToken(argToken)) {
82
+ const name = ptrName(argToken); // '' = <>ptr 裸地址档
83
+ if (typeof arg === 'number') {
84
+ // 裸地址 / NULL / 品牌指针(都是 number)透传,不重编码。
85
+ // 合法值形态由类型层约束(MaybePtr | codec 形,如 <BYTE>ptr 另收 ArrayBuffer)。
86
+ if (nonnull && arg === 0)
87
+ throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! got 0 ` +
88
+ `(null pointer forbidden here; the token marks this position non-null)`);
89
+ writeSlot(dv, off, { k: 'ptr', v: arg });
90
+ }
91
+ else {
92
+ if (arg === null || arg === undefined) {
93
+ // JS null/undefined 不是指针词汇(空位只写 NULL)——fail-loud,不静默当 0。
94
+ throw new Error(`ffi-bind: <${name}>ptr got ${String(arg)}; ` +
95
+ `pass the imported NULL constant for an empty pointer`);
96
+ }
97
+ const codec = encoders[name];
98
+ if (!codec) {
99
+ // 未知布局落到这:只收 NULL / 有效地址 / codec 值,不静默当 0。
100
+ throw new Error(name === ''
101
+ ? `ffi-bind: <>ptr expects number (NULL | Ptr), got ${typeof arg}; use <BYTE>ptr with a PtrArrayBuffer (new PtrArrayBuffer(n)), or ffi.bufferPtr(buf) for an address`
102
+ : `ffi-bind: unknown layout "<${name}>ptr" (available: ${Object.keys(encoders).join(', ') || 'none'})`);
103
+ }
104
+ if (!codec.encode) {
105
+ // codec 存在但只声明了 decode/alloc(只读出),入参位无序列化可用。
106
+ throw new Error(`ffi-bind: <${name}>ptr has no encoder (out-only codec)`);
107
+ }
108
+ const buf = codec.encode(arg);
109
+ held.push(buf);
110
+ // 类型层已保证 encode 返回 PtrArrayBuffer;这里再 fail-loud 兜一层
111
+ // (动态传入的无类型 codec 仍可能违反),然后直接取 .ptr——不再重算。
112
+ const p = buf.ptr;
113
+ if (typeof p !== 'number')
114
+ throw new Error('ffi-bind: codec.encode must return a PtrArrayBuffer, got plain ArrayBuffer');
115
+ if (nonnull && p === 0)
116
+ throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! encode produced 0 ` +
117
+ `(null pointer forbidden here)`);
118
+ writeSlot(dv, off, { k: 'ptr', v: p });
119
+ }
120
+ }
121
+ else {
122
+ writeSlot(dv, off, { k: argToken, v: arg });
123
+ }
124
+ off += getWidth(argToken, is64);
125
+ }
126
+ const retBuf = new ArrayBuffer(8);
127
+ const retIsFp = retToken === 'f32' || retToken === 'f64';
128
+ ffi.ffiCall(proc, argFrame, retBuf, retIsFp ? 1 : 0);
129
+ const raw = readRet(cTokenToType(retToken), retBuf);
130
+ // 返回位 '!' = 「C 保证非零」的作者担保,标错由这里当场 throw 兜底(显式崩溃而非静默流窜)。
131
+ if (retToken.endsWith('!') && raw === 0)
132
+ throw new Error(`ffi-bind: ${sig}: return ${retToken} got 0 ` +
133
+ `(the token promises C never returns null here)`);
134
+ // 返回位自动解码:品牌指针且该键声明了 decode → 交还 JS 值;
135
+ // 空指针 0 恒原样返回(无可读内容,不进 decode),裸 <>ptr / 无 decode 的键原样返回。
136
+ if (isCPtrToken(retToken) && raw !== 0) {
137
+ const name = ptrName(retToken);
138
+ if (name !== '') {
139
+ const codec = decoders[name];
140
+ if (codec?.decode)
141
+ return codec.decode(raw);
142
+ }
143
+ }
144
+ return raw;
145
+ }
146
+ // 把标量参数写进 argFrame 槽位。ptr 为裸地址直通;引用型参数见 writePtrSlot。
147
+ function readRet(ret, retBuf) {
148
+ if (ret === 'void')
149
+ return undefined;
150
+ return readScalar(new DataView(retBuf), 0, ret);
151
+ }
152
+ // 内建 codec:类型校验放在这里,与「用户布局 codec」同一分派点。
153
+ // BYTE/WCHAR 拒绝裸 number(无句柄可 pin),提示改用 <>ptr。
154
+ /** 内建 UTF-16 编解码:encode 入参序列化(UTF-16LE + NUL);decode 双态单参——
155
+ * 品牌指针逐字节读到 NUL(野指针无终止 4MiB 上限 fail-fast)、ArrayBuffer 直读到
156
+ * NUL 或末尾(out 参数未写 NUL 时按读满处理);alloc 预分配返回带 .ptr 的
157
+ * PtrArrayBuffer(a = 字符数,缺省 256)。类型取必需形态——
158
+ * 内建三件齐备,调用无需空断言。 */
159
+ export const WCHAR = {
160
+ encode: (v) => {
161
+ // TextEncoder 没有 encodeInto:先编码再拷入带 .ptr 的 buffer(单次 memcpy,
162
+ // 编码逻辑仍全局唯一——这行是全部 UTF-16 字符串编码的唯一实现)。
163
+ const u8 = new TextEncoder('utf-16le').encode(v + '\0');
164
+ const out = new PtrArrayBuffer(u8.byteLength);
165
+ new Uint8Array(out).set(u8);
166
+ return out;
167
+ },
168
+ decode: (p) => {
169
+ // 两分支同构(UTF-16LE code unit、拼接到 NUL 止),唯一差异是读法。
170
+ if (typeof p === 'number') {
171
+ let s = '';
172
+ let addr = p;
173
+ const start = addr;
174
+ for (;;) {
175
+ const lo = ffi.readByte(addr);
176
+ const hi = ffi.readByte(addr + 1);
177
+ if (lo === 0 && hi === 0)
178
+ return s;
179
+ s += String.fromCharCode(lo | (hi << 8));
180
+ addr += 2;
181
+ if (addr - start > 1 << 21) {
182
+ throw new Error('ffi-bind: WCHAR decode exceeded 4MiB without NUL');
183
+ }
184
+ }
185
+ }
186
+ const u8 = new Uint8Array(p);
187
+ const limit = u8.length & ~1; // 奇数末尾的残半字节丢弃
188
+ let s = '';
189
+ for (let i = 0; i < limit; i += 2) {
190
+ const c = u8[i] | (u8[i + 1] << 8);
191
+ if (c === 0)
192
+ return s;
193
+ s += String.fromCharCode(c);
194
+ }
195
+ return s;
196
+ },
197
+ alloc: (size = 256) => new PtrArrayBuffer(size * 2),
198
+ };
199
+ /** 内建字节缓冲:encode identity 直通(收 PtrArrayBuffer、原地存活,native 写回同一
200
+ * buffer);alloc 预分配返回带 .ptr 的 PtrArrayBuffer(a = 字节数)。无 decode——
201
+ * 地址不携带长度(无终止符),读回直接在 buffer 上 DataView / TypedArray 取。 */
202
+ export const BYTE = {
203
+ encode: (v) => {
204
+ // identity 原地直通(native 写回同一 buffer),不能拷贝——只收自带 .ptr 的
205
+ // PtrArrayBuffer;普通 ArrayBuffer 过来 fail-loud,先包一层 new PtrArrayBuffer(n)。
206
+ if (!(v instanceof PtrArrayBuffer))
207
+ throw new Error('ffi-bind: <BYTE>ptr wants a PtrArrayBuffer (new PtrArrayBuffer(n) / BYTE.alloc / struct.encode), not a plain ArrayBuffer');
208
+ return v;
209
+ },
210
+ alloc: (byteLen) => new PtrArrayBuffer(byteLen),
211
+ };
212
+ // 运行时两张表是同一对象:参数位只调 .encode、返回位只调 .decode,互不干扰。
213
+ const builtinCodecs = { WCHAR, BYTE };
214
+ /** 绑定单个函数。encoders/decoders 是两张可选布局表(结构上是 Codec):
215
+ * encoders 喂入参位(encode 值域 → 形参类型),decoders 喂返回位(decode 值域
216
+ * → 返回 V | null,替代 Ptr | null)。decoders 缺省时回退到 encoders——双向 codec
217
+ * 只需传一次(与运行时 `decoders ?? encoders` 同构:都只在实参缺省时生效,
218
+ * 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。 */
219
+ export function bind(dll, name, sig, encoders, decoders) {
220
+ const proc = win.GetProcAddress(loadDll(dll), name);
221
+ if (!proc)
222
+ throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`);
223
+ const effDec = decoders ?? encoders;
224
+ return makeFn(proc, sig, { ...builtinCodecs, ...encoders }, { ...builtinCodecs, ...(effDec ?? {}) });
225
+ }
226
+ /** 批量绑定(map = 函数名 → 签名串)。encoders/decoders 语义同 bind()。 */
227
+ export function bindLib(dll, map, encoders, decoders) {
228
+ const out = {};
229
+ const h = loadDll(dll);
230
+ const effDec = decoders ?? encoders;
231
+ const encTable = { ...builtinCodecs, ...encoders };
232
+ const decTable = { ...builtinCodecs, ...(effDec ?? {}) };
233
+ for (const [name, sig] of Object.entries(map)) {
234
+ const proc = win.GetProcAddress(h, name);
235
+ if (!proc)
236
+ throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`);
237
+ out[name] = makeFn(proc, sig, encTable, decTable);
238
+ }
239
+ return out;
240
+ }
241
+ /* ---- 闭包(回调):JS 函数 → 可传给 Win32 API 的函数指针 ---- */
242
+ // 泄漏警告:target = dispose 本体 —— 警告条件字面对齐「释放通道死亡」:
243
+ // `const {dispose} = closure(...)` 解构出的就是 target 本身,持有期间绝不触发;
244
+ // dispose 体内 unregister(dispose)(自引用 token),释放即撤销,之后丢弃不误报。
245
+ // 回调只打印、绝不自动 free:ptr 会被复制进任意 native API,JS 对象的可达性
246
+ // 推不出 native 是否还在用它,自动释放会把「泄漏」升级成「野指针崩溃」
247
+ // (wrapper 被 C 侧强引用,不 dispose 的代价只是滞留到进程结束)。
248
+ // heldValue 纯数据、不引用 target —— 若反向强持,target 永不可达,警告永不触发。
249
+ // 注意:触发时机依赖 QuickJS 的 GC→job 队列链路,未做强时序断言(best-effort
250
+ // 诊断 —— 即使不触发也只是退回静默泄漏,无任何负面后果)。
251
+ const leakRegistry = new FinalizationRegistry((held) => {
252
+ std.printf('[ffi] closure leaked (GCed without dispose): sig="%s" ptr=0x%x\n', held.sig, held.ptr);
253
+ });
254
+ // 共享解码/编码助手:由每个闭包的 wrapper 调用(wrapper 闭包捕获 args/ret/fn)。
255
+ // args/ret 为「底档 + '!' 绑定」的记录(与 ParsedSig 同形态——修饰随 token 走,
256
+ // 消费者要么整体拿到、要么整体拿不到)。'!' 违约(C 传入 0 / fn 返回 0)打印诊断并
257
+ // 中止本次分发(fn 不被调用 / 结果槽保持 0),与 fn-throw 同风格:此处不 throw,
258
+ // 异常通道只留给用户回调(C 桩捕获 wrapper 异常并清零,quickjs-ffi-closure.c)。
259
+ function dispatchClosure(args, ret, fn, frameBuf, retBuf, sig) {
260
+ const dv = new DataView(retBuf);
261
+ const is64 = os.arch === 'x64';
262
+ const fv = new DataView(frameBuf);
263
+ const a = [];
264
+ let off = 0;
265
+ for (let i = 0; i < args.length; i++) {
266
+ const spec = args[i];
267
+ const k = spec.tok;
268
+ let v;
269
+ if (is64) {
270
+ const fp = k === 'f32' || k === 'f64';
271
+ const base = fp
272
+ ? (i < 4 ? 128 + i * 8 : 32 + (i - 4) * 8) // xmm 区(前 4)/ 溢出区
273
+ : (i < 4 ? i * 8 : 32 + (i - 4) * 8); // 整数寄存器区 / 溢出区
274
+ v = readScalar(fv, base, k);
275
+ }
276
+ else {
277
+ v = readScalar(fv, off, k);
278
+ off += IA32_ARG_SIZE[k];
279
+ }
280
+ // 参数位 '!' = 「C 保证非空」的作者担保,C 传 0 即标错 → 跳过回调。
281
+ if (spec.nonnull && v === 0) {
282
+ std.printf('[ffi] closure %s: arg#%d got 0 (! promise violated by caller), skipping callback\n', sig, i + 1);
283
+ return;
284
+ }
285
+ a.push(v);
286
+ }
287
+ let r;
288
+ try {
289
+ r = fn(...a);
290
+ }
291
+ catch (err) {
292
+ std.printf('[ffi] closure callback threw: %s\n', String(err));
293
+ return; // 结果槽保持 0
294
+ }
295
+ // 返回位 '!' = 「JS 保证非零」的作者担保,fn 返回 0 即标错 → 不写结果槽。
296
+ if (ret.nonnull && r === 0) {
297
+ std.printf('[ffi] closure %s: return got 0 (! promise violated by callback), leaving result slot 0\n', sig);
298
+ return;
299
+ }
300
+ if (ret.tok !== 'void') {
301
+ writeSlot(dv, 0, { k: ret.tok, v: r });
302
+ }
303
+ }
304
+ /** 把 JS 函数变成可传给 Win32 API 的函数指针(同步同线程回调)。
305
+ * sig 为回调签名:实参只许 BasicKind(ptr 槽 0 保真读回 NULL | Ptr),
306
+ * 返回只许 void|u8..u64|i8..i64|f32|f64|ptr(u64/i64 走 EDX:EAX,x64 走 RAX)。
307
+ * '!' 标记位运行时校验(与 bind 调用位同语义,见 dispatchClosure):参数位 C 传入 0
308
+ * → 打印诊断并跳过回调;返回位 fn 返回 0 → 保持结果槽 0。
309
+ * opts.stdcall 仅 ia32 有效(默认 true,Win32 回调标准 CALLBACK);msvcrt 等
310
+ * cdecl 库回调传 { stdcall: false }。x64 恒由调用方清栈,无需指定。
311
+ * 参数上限:x64 至多 16 个、ia32 栈区至多 64 字节(C 桩定长捕获窗),超限抛错。
312
+ * 返回 { ptr, dispose }:ptr 即函数指针(传给 API 的 ptr 参数);dispose 注销
313
+ * 并释放回调函数,幂等;闭包期间回调被强引用,不会被 GC 回收。
314
+ * 注意:dispose 后 ptr 不得再被任何 native 方引用。
315
+ * 从未 dispose 就丢弃通道(整个返回对象或解构出来的 dispose 均算)
316
+ * → 打印泄漏警告(只警告、不自动释放,见 leakRegistry)。 */
317
+ export function closure(sig, fn, opts) {
318
+ const { argTokens, retToken } = parseSig(sig);
319
+ const argSpecs = argTokens.map(t => ({ tok: cTokenToType(t), nonnull: t.endsWith('!') }));
320
+ const retSpec = { tok: cTokenToType(retToken), nonnull: retToken.endsWith('!') };
321
+ const is64 = os.arch === 'x64';
322
+ const stackCapture = argSpecs.reduce((s, x) => s + IA32_ARG_SIZE[x.tok], 0);
323
+ // C 桩捕获窗定长(quickjs-ffi-closure.c:x64 栈区 96B=12 槽,第 5 参起在栈上,
324
+ // 即至多 16 参;ia32 16×4B)——超限 dispatch 会读到捕获缓冲之外的垃圾,或
325
+ // RangeError 被吞成返回 0,故创建期 fail-fast。扩容改 C 侧 buf 尺寸并同步放宽。
326
+ if (is64 ? argSpecs.length > 16 : stackCapture > 64) {
327
+ throw new Error(`ffi-bind: closure signature exceeds capture window ` +
328
+ `(${is64 ? 'max 16 params' : 'max 64 stack bytes'}): ${sig}`);
329
+ }
330
+ const argBytes = (opts?.stdcall === false || is64) ? 0 : stackCapture;
331
+ const retKind = retSpec.tok === 'f32' ? 1
332
+ : retSpec.tok === 'f64' ? 2
333
+ : (retSpec.tok === 'u64' || retSpec.tok === 'i64') ? 3
334
+ : 0;
335
+ // per-closure wrapper:闭包捕获 argSpecs/retSpec/fn/sig,C 侧只存它 + ctx;回调
336
+ // 永远回到创建它的 context(无跨 context 全局、无 registry)。
337
+ const wrapper = (frameBuf, retBuf) => {
338
+ dispatchClosure(argSpecs, retSpec, fn, frameBuf, retBuf, sig);
339
+ };
340
+ const ptr = ffi.closureNew(argBytes, retKind, wrapper);
341
+ let done = false;
342
+ const dispose = () => {
343
+ if (done)
344
+ return;
345
+ done = true;
346
+ leakRegistry.unregister(dispose); // 释放即撤销:之后通道死亡不再警告
347
+ ffi.closureFree(ptr);
348
+ };
349
+ const ret = { ptr, dispose };
350
+ leakRegistry.register(dispose, { sig, ptr }); // target = 通道本体(非 ret)
351
+ return ret;
352
+ }