quickwin 1.3.0 → 2.1.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 (167) hide show
  1. package/README.en.md +18 -24
  2. package/README.md +17 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20155 -1420
  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 +1424 -1180
  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 +1418 -1075
  16. package/examples/test_react_lazy.js +1391 -1059
  17. package/examples/test_react_listbox.js +1390 -1058
  18. package/examples/test_root_window.js +1384 -1052
  19. package/examples/test_tab.js +1391 -1059
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +219 -0
  23. package/lib/certs.ts +229 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +88 -7
  27. package/lib/fetch.ts +73 -7
  28. package/lib/ffi/bind.js +441 -0
  29. package/lib/ffi/bind.ts +539 -0
  30. package/lib/ffi/ctype.js +194 -0
  31. package/lib/ffi/ctype.ts +369 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +490 -0
  35. package/lib/ffi/struct.ts +753 -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 +50 -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 +5 -3
  51. package/lib/react-qw/index.ts +8 -3
  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 +20 -23
  59. package/lib/text-measure.ts +22 -26
  60. package/lib/websocket.js +44 -4
  61. package/lib/websocket.ts +40 -4
  62. package/lib/windows/advapi32.js +53 -0
  63. package/lib/windows/advapi32.ts +64 -0
  64. package/lib/windows/comctl32.js +18 -0
  65. package/lib/windows/comctl32.ts +19 -0
  66. package/lib/windows/comdlg32.js +8 -0
  67. package/lib/windows/comdlg32.ts +10 -0
  68. package/lib/windows/gdi32.js +392 -0
  69. package/lib/windows/gdi32.ts +420 -0
  70. package/lib/windows/kernel32.js +262 -0
  71. package/lib/windows/kernel32.ts +291 -0
  72. package/lib/windows/msimg32.js +38 -0
  73. package/lib/windows/msimg32.ts +45 -0
  74. package/lib/windows/ntdll.js +9 -0
  75. package/lib/windows/ntdll.ts +11 -0
  76. package/lib/windows/ole32.js +7 -0
  77. package/lib/windows/ole32.ts +9 -0
  78. package/lib/windows/psapi.js +32 -0
  79. package/lib/windows/psapi.ts +37 -0
  80. package/lib/windows/shell32.js +13 -0
  81. package/lib/windows/shell32.ts +16 -0
  82. package/lib/windows/structs.js +519 -0
  83. package/lib/windows/structs.ts +566 -0
  84. package/lib/windows/user32.js +572 -0
  85. package/lib/windows/user32.ts +607 -0
  86. package/lib/windows/version.js +29 -0
  87. package/lib/windows/version.ts +35 -0
  88. package/lib/windows/winspool.js +71 -0
  89. package/lib/windows/winspool.ts +80 -0
  90. package/package.json +1 -1
  91. package/quickwin.d.ts +31 -40
  92. package/quickwin_const.d.ts +137 -0
  93. package/qwin-nowasm-x86.exe +0 -0
  94. package/qwin-nowasm.exe +0 -0
  95. package/qwin-x86.exe +0 -0
  96. package/qwin.exe +0 -0
  97. package/test/bench_ffi_callraw.js +236 -0
  98. package/test/bench_ffi_callraw.ts +236 -0
  99. package/test/bench_ffi_overhead.js +139 -0
  100. package/test/bench_ffi_overhead.ts +141 -0
  101. package/test/bench_sendmessage.js +104 -0
  102. package/test/bench_sendmessage.ts +100 -0
  103. package/test/bench_sendmessage_nonnull.js +187 -0
  104. package/test/bench_sendmessage_nonnull.ts +188 -0
  105. package/test/run.js +19 -4
  106. package/test/run.ts +20 -5
  107. package/test/test_basic_fetch.js +29 -0
  108. package/test/test_basic_fetch.ts +26 -0
  109. package/test/test_ffi.js +27 -45
  110. package/test/test_ffi.ts +35 -54
  111. package/test/test_ffi_abi.js +290 -0
  112. package/test/test_ffi_abi.ts +274 -0
  113. package/test/test_ffi_array.js +116 -0
  114. package/test/test_ffi_array.ts +141 -0
  115. package/test/test_ffi_bind.js +620 -8
  116. package/test/test_ffi_bind.ts +630 -8
  117. package/test/test_ffi_struct.js +632 -88
  118. package/test/test_ffi_struct.ts +666 -91
  119. package/test/test_ffi_struct_layout.js +495 -0
  120. package/test/test_ffi_struct_layout.ts +543 -0
  121. package/test/test_ffi_user32.js +103 -0
  122. package/test/test_ffi_user32.ts +113 -0
  123. package/test/test_ffi_windows.js +101 -0
  124. package/test/test_ffi_windows.ts +113 -0
  125. package/test/test_files.js +102 -0
  126. package/test/test_files.ts +121 -0
  127. package/test/test_gdi_draw.js +681 -0
  128. package/test/test_gdi_draw.ts +725 -0
  129. package/test/test_helper.js +8 -1
  130. package/test/test_helper.ts +9 -1
  131. package/test/test_http_import.js +18 -0
  132. package/test/test_http_import.ts +19 -0
  133. package/test/test_http_server.js +73 -1
  134. package/test/test_http_server.ts +75 -1
  135. package/test/test_ipv6.js +2 -2
  136. package/test/test_ipv6.ts +2 -2
  137. package/test/test_net_fetch.js +74 -11
  138. package/test/test_net_fetch.ts +73 -10
  139. package/test/test_net_websocket.js +1 -1
  140. package/test/test_net_websocket.ts +1 -1
  141. package/test/test_printer.js +137 -0
  142. package/test/test_printer.ts +148 -0
  143. package/test/test_proc.js +141 -0
  144. package/test/test_proc.ts +163 -0
  145. package/test/test_react_complex.js +1401 -1069
  146. package/test/test_react_counter.js +1391 -1059
  147. package/test/test_react_flex.js +1411 -1072
  148. package/test/test_react_hidden.js +1394 -1062
  149. package/test/test_react_listview_link.js +1391 -1059
  150. package/test/test_react_notify.js +18638 -0
  151. package/test/test_react_render.js +1391 -1059
  152. package/test/test_registry.js +74 -0
  153. package/test/test_registry.ts +95 -0
  154. package/test/test_sysinfo.js +122 -0
  155. package/test/test_sysinfo.ts +141 -0
  156. package/test/test_wolfssl_read.js +45 -43
  157. package/test/test_wolfssl_read.ts +42 -40
  158. package/test/test_worker_net.js +35 -17
  159. package/test/test_worker_net.ts +27 -15
  160. package/tsconfig.json +1 -1
  161. package/examples/exec_server.ts +0 -80
  162. package/lib/ffi-bind.js +0 -85
  163. package/lib/ffi-bind.ts +0 -170
  164. package/lib/ffi-struct.js +0 -224
  165. package/lib/ffi-struct.ts +0 -239
  166. package/lib/win-common-structs.js +0 -39
  167. package/lib/win-common-structs.ts +0 -41
@@ -0,0 +1,188 @@
1
+ // 一次性性能 bench:FFI 签名 '!' 标注的调用时 0 检验成本(同函数多臂对照 + A/A 噪声底)
2
+ // 不入 CI。用法:make js 后 POST /exec {"cmd":"qwin.exe test/bench_sendmessage_nonnull.js"}
3
+ // 臂(同一 proc 各自 bind,唯一变量是 '!'):
4
+ // A1 base '<HWND>ptr u32 <>ptr <>ptr -> <>ptr' 基线
5
+ // A2 ctrl '<HWND>ptr u32 <>ptr <>ptr -> <>ptr' 与 A1 完全同签名——两臂差值 = 机器噪声底
6
+ // B arg! '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr' + 参数位 arg#1 0 检查
7
+ // C ret! '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr!' + 返回位 0 检查(仅非零返回场景)
8
+ // 方法学(前两轮教训):批量缩到 50k/样本 → 臂序每 ~650ms 轮转一次,抵消 QEMU 调度抖动;
9
+ // S1/S3 各臂 30 样本取 median(对 GC/调度离群稳健);A2-A1 是效应检验的参照带——
10
+ // B/A 落在 A2/A 带内即「低于噪声底,不可测」。'!' 边际 = 每标注位 1 次整数 === 比较
11
+ // (endsWith 无条件跑、所有臂共模),理论 ≪ 轮噪声。
12
+ // C 臂标了返回 '!',撞真实 0 即 throw —— 先用 A1 探各场景原始返回值,非零才让 C 进。
13
+ // 全部消息发自有窗口(直调 wndproc)。结束必须 DestroyWindow + std.exit。
14
+ import * as std from 'std'
15
+ import * as gui from 'gui'
16
+ import { bind, WCHAR } from '../lib/ffi/bind.js'
17
+ import { PtrArrayBuffer } from '../lib/ffi/ctype.js'
18
+
19
+ const BATCH = 50_000 // 每样本批量(~650ms)——小批量快轮转,去相关调度抖动
20
+ const ROUNDS = 30 // S1/S3 每臂样本数(30 × 3-4 臂 ≈ 60-80s/suite)
21
+ const N_S2 = 50_000
22
+ const ROUNDS_S2 = 7
23
+ const WM_NULL = 0x0000
24
+ const WM_SETTEXT = 0x000c
25
+ const WM_GETTEXT = 0x000d
26
+
27
+ // median(离群稳健)供 micro 与 suite 共用
28
+ const med = (t: number[]): number => {
29
+ const s = [...t].sort((x, y) => x - y)
30
+ const m = s.length >> 1
31
+ return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2
32
+ }
33
+
34
+ // S0:'!' 边际操作本身(1 次整数 === 比较 + 分支)的解释器成本——FFI 层噪声
35
+ // ±400~900ns/call 盖住了 12.5μs 调用上的 ns 级效应,这里绕开 ffiCall 直接量单检查。
36
+ // flag 必须是模块级可变量:QuickJS 编译期会常量折叠字面量,折叠掉就测不到了。
37
+ let microFlag = true
38
+ const MICRO_N = 100_000_000
39
+ function microLoop(): number {
40
+ const t0 = Date.now()
41
+ let hits = 0
42
+ for (let i = 0; i < MICRO_N; i++)
43
+ if (microFlag && i === 0) hits++
44
+ const dt = Date.now() - t0
45
+ if (hits !== (microFlag ? 1 : 0)) throw new Error('micro sanity failed: hits=' + hits)
46
+ return dt
47
+ }
48
+ function microSuite(): void {
49
+ step('suite: S0 micro 0-check interpreter cost (n=' + String(MICRO_N) + ')')
50
+ microFlag = true; microLoop()
51
+ microFlag = false; microLoop()
52
+ const ts: number[] = []
53
+ const fs: number[] = []
54
+ for (let r = 0; r < 5; r++) {
55
+ // 奇偶交叉起跑顺序(同原 bench 方法)
56
+ if (r % 2 === 0) {
57
+ microFlag = true; ts.push(microLoop())
58
+ microFlag = false; fs.push(microLoop())
59
+ } else {
60
+ microFlag = false; fs.push(microLoop())
61
+ microFlag = true; ts.push(microLoop())
62
+ }
63
+ step(' round ' + String(r) + ': check=' + String(ts[ts.length - 1]) +
64
+ 'ms base=' + String(fs[fs.length - 1]) + 'ms')
65
+ }
66
+ const ns = (ms: number) => (ms * 1e6 / MICRO_N).toFixed(2)
67
+ const tmed = med(ts), fmed = med(fs)
68
+ step(' ** median: check=' + ns(tmed) + 'ns/op base=' + ns(fmed) +
69
+ 'ns/op ==> per-check cost=' + ((tmed - fmed) * 1e6 / MICRO_N).toFixed(2) + 'ns')
70
+ }
71
+
72
+ // 文件日志(cwd=Z:\quickwin,宿主机 _build/bench_nonnull_log.txt 可实时读)——exec 只在
73
+ // 进程退出后回包,挂了靠它定位卡点
74
+ const log = std.open('bench_nonnull_log.txt', 'w')
75
+ function step(msg: string): void {
76
+ std.printf('%s\n', msg)
77
+ if (log) { log.puts(msg + '\n'); log.flush() }
78
+ }
79
+
80
+ step('stage: imports ok')
81
+
82
+ const winRaw = gui.CreateWindow('STATIC', 'bench-title', 0, 0, 0, 20, 20, null, null)
83
+ if (!winRaw) throw new Error('CreateWindow(STATIC) failed')
84
+ const win: gui.HWND = winRaw
85
+ step('stage: window created')
86
+
87
+ const SIG = '<HWND>ptr u32 <>ptr <>ptr -> <>ptr'
88
+ const fA1 = bind('user32.dll', 'SendMessageW', SIG)
89
+ const fA2 = bind('user32.dll', 'SendMessageW', SIG) // A/A 对照:同签名二绑 = 噪声底
90
+ const fB = bind('user32.dll', 'SendMessageW', '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr')
91
+ const fC = bind('user32.dll', 'SendMessageW', '<HWND>ptr! u32 <>ptr <>ptr -> <>ptr!')
92
+
93
+ // 各 BindFn 结构等价 → 收敛到一个可传参的函数型(循环内统一走局部参数,开销对齐)
94
+ type SM = (h: gui.HWND, msg: number, w: number, l: number) => number
95
+
96
+ const TEXT = 'bench'
97
+ const buf = new PtrArrayBuffer<'WCHAR'>(64)
98
+
99
+ // C 臂可行性探针:A1 读各场景原始返回值,撞 0 的场景不进 C('!' 会 throw)。
100
+ // S1 的 WM_NULL 走 DefWindowProc 预期返 0 → C 不进 S1 是预期而非缺测。
101
+ const retS1 = fA1(win, WM_NULL, 0, 0x1234)
102
+ const retS2 = fA1(win, WM_SETTEXT, 0, WCHAR.encode(TEXT).ptr)
103
+ const retS3 = fA1(win, WM_GETTEXT, 64, buf.ptr)
104
+ step('probe ret: S1=' + String(retS1) + ' S2=' + String(retS2) + ' S3=' + String(retS3) +
105
+ ' (C arm only enters nonzero scenes)')
106
+
107
+ function run1(f: SM): number {
108
+ const t0 = Date.now()
109
+ for (let i = 0; i < BATCH; i++) f(win, WM_NULL, 0, 0x1234)
110
+ return Date.now() - t0
111
+ }
112
+ function run2(f: SM): number {
113
+ const t0 = Date.now()
114
+ for (let i = 0; i < N_S2; i++) {
115
+ const w = WCHAR.encode(TEXT)
116
+ f(win, WM_SETTEXT, 0, w.ptr)
117
+ }
118
+ return Date.now() - t0
119
+ }
120
+ function run3(f: SM): number {
121
+ const t0 = Date.now()
122
+ for (let i = 0; i < BATCH; i++) f(win, WM_GETTEXT, 64, buf.ptr)
123
+ return Date.now() - t0
124
+ }
125
+
126
+ type Arm = { label: string; fn: () => number }
127
+
128
+ function suite(name: string, n: number, rounds: number, arms: Arm[]): void {
129
+ step('suite: ' + name + ' warmup')
130
+ for (const a of arms) a.fn() // 预热各 1 轮
131
+ const times: number[][] = arms.map(() => [])
132
+ for (let r = 0; r < rounds; r++) {
133
+ // 臂序按 r 轮转起跑,抵消顺序性偏置(温度/调度漂移)
134
+ for (let k = 0; k < arms.length; k++) {
135
+ const i = (r + k) % arms.length
136
+ times[i]!.push(arms[i]!.fn())
137
+ }
138
+ }
139
+ // 每轮原始数据只 dump 前 3 轮(30 轮全打没意义,log 只留诊断线索)
140
+ for (let r = 0; r < Math.min(rounds, 3); r++) {
141
+ step(' round ' + String(r) + ': ' +
142
+ arms.map((a, i) => a.label + '=' + String(times[i]![r]) + 'ms').join(' '))
143
+ }
144
+ const ns = (ms: number) => (ms * 1e6 / n).toFixed(1)
145
+ // median 是主指标(离群稳健);min 留作与前两轮/原 bench 的可比项
146
+ const meds = times.map(med)
147
+ const mins = times.map(t => Math.min(...t))
148
+ step(' ** median: ' + arms.map((a, i) =>
149
+ a.label + '=' + ns(meds[i]!) + 'ns').join(' | '))
150
+ step(' min: ' + arms.map((a, i) =>
151
+ a.label + '=' + ns(mins[i]!) + 'ns').join(' | '))
152
+ for (let i = 1; i < arms.length; i++)
153
+ step(' vs A1: ' + arms[i]!.label + ' delta=' +
154
+ ((meds[i]! - meds[0]!) * 1e6 / n).toFixed(1) + 'ns/call (median), ' +
155
+ ((mins[i]! - mins[0]!) * 1e6 / n).toFixed(1) + 'ns/call (min)' +
156
+ (i === 1 ? ' <-- A2-A1 = NOISE FLOOR' : ''))
157
+ }
158
+
159
+ const s1Arms: Arm[] = [
160
+ { label: 'A1', fn: () => run1(fA1) },
161
+ { label: 'A2', fn: () => run1(fA2) },
162
+ { label: 'B ', fn: () => run1(fB) },
163
+ ]
164
+ if (retS1 !== 0) s1Arms.push({ label: 'C ', fn: () => run1(fC) })
165
+ const s2Arms: Arm[] = [
166
+ { label: 'A1', fn: () => run2(fA1) },
167
+ { label: 'A2', fn: () => run2(fA2) },
168
+ { label: 'B ', fn: () => run2(fB) },
169
+ ]
170
+ if (retS2 !== 0) s2Arms.push({ label: 'C ', fn: () => run2(fC) })
171
+ const s3Arms: Arm[] = [
172
+ { label: 'A1', fn: () => run3(fA1) },
173
+ { label: 'A2', fn: () => run3(fA2) },
174
+ { label: 'B ', fn: () => run3(fB) },
175
+ ]
176
+ if (retS3 !== 0) s3Arms.push({ label: 'C ', fn: () => run3(fC) })
177
+
178
+ step('bench_sendmessage_nonnull: S1/S3 batch=' + String(BATCH) + '×' + String(ROUNDS) +
179
+ 'r, S2 n=' + String(N_S2) + '×' + String(ROUNDS_S2) + 'r (median 主指标)')
180
+ microSuite()
181
+ suite('S1 WM_NULL 纯数字参数 (最灵敏测点)', BATCH, ROUNDS, s1Arms)
182
+ suite('S2 WM_SETTEXT 字符串 lParam (FFI 侧含 WCHAR.encode)', N_S2, ROUNDS_S2, s2Arms)
183
+ suite('S3 WM_GETTEXT 缓冲出参往返', BATCH, ROUNDS, s3Arms)
184
+
185
+ gui.DestroyWindow(win)
186
+ step('DONE')
187
+ if (log) log.close()
188
+ std.exit(0)
package/test/run.js CHANGED
@@ -1,3 +1,4 @@
1
+ import '../lib/certs.js';
1
2
  import * as std from 'std';
2
3
  import { Tester } from './test_helper.js';
3
4
  function formatDuration(ms) {
@@ -25,6 +26,17 @@ const suiteDefs = [
25
26
  { name: 'ffi', file: './test_ffi.js', tags: [] },
26
27
  { name: 'ffi-bind', file: './test_ffi_bind.js', tags: [] },
27
28
  { name: 'ffi-struct', file: './test_ffi_struct.js', tags: [] },
29
+ { name: 'ffi-struct-layout', file: './test_ffi_struct_layout.js', tags: [] },
30
+ { name: 'ffi-array', file: './test_ffi_array.js', tags: [] },
31
+ { name: 'ffi-abi', file: './test_ffi_abi.js', tags: [] },
32
+ { name: 'ffi-user32', file: './test_ffi_user32.js', tags: [] },
33
+ { name: 'ffi-windows', file: './test_ffi_windows.js', tags: [] },
34
+ { name: 'gdi-draw', file: './test_gdi_draw.js', tags: [] },
35
+ { name: 'registry', file: './test_registry.js', tags: [] },
36
+ { name: 'files', file: './test_files.js', tags: [] },
37
+ { name: 'printer', file: './test_printer.js', tags: [] },
38
+ { name: 'sysinfo', file: './test_sysinfo.js', tags: [] },
39
+ { name: 'proc', file: './test_proc.js', tags: [] },
28
40
  { name: 'net-fetch', file: './test_net_fetch.js', tags: ['net'] },
29
41
  { name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
30
42
  { name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
@@ -47,7 +59,7 @@ async function main() {
47
59
  if (filter)
48
60
  std.printf('Filter: %s\n', filter);
49
61
  const results = [];
50
- let totalOk = 0, totalFail = 0;
62
+ let totalOk = 0, totalFail = 0, totalSkip = 0;
51
63
  for (const def of suiteDefs) {
52
64
  if (filter) {
53
65
  if (filter.startsWith('-')) {
@@ -81,16 +93,19 @@ async function main() {
81
93
  }
82
94
  const suiteElapsed = Date.now() - suiteStart;
83
95
  t.summary();
84
- results.push({ name: def.name, ok: t.ok, fail: t.fail, elapsed: suiteElapsed });
96
+ results.push({ name: def.name, ok: t.ok, fail: t.fail, skip: t.skip, elapsed: suiteElapsed });
85
97
  totalOk += t.ok;
86
98
  totalFail += t.fail;
99
+ totalSkip += t.skip;
87
100
  }
88
101
  const color = totalFail > 0 ? RED : GREEN;
89
102
  std.printf('\n%s====== Test Results ======%s\n', BOLD, RESET);
90
103
  for (const r of results) {
91
104
  const c = r.fail > 0 ? RED : GREEN;
92
- std.printf(' %s%-18s %s%d/%d passed %s(%s)%s\n', c, r.name, RESET, r.ok, r.ok + r.fail, c, formatDuration(r.elapsed), RESET);
105
+ const skipped = r.skip > 0 ? ` (${r.skip} skipped)` : '';
106
+ std.printf(' %s%-18s %s%d/%d passed%s %s(%s)%s\n', c, r.name, RESET, r.ok, r.ok + r.fail, skipped, c, formatDuration(r.elapsed), RESET);
93
107
  }
94
- std.printf('%s====== Summary: %d/%d passed ======%s\n', color, totalOk, totalOk + totalFail, RESET);
108
+ const skipped = totalSkip > 0 ? ` (${totalSkip} skipped)` : '';
109
+ std.printf('%s====== Summary: %d/%d passed%s ======%s\n', color, totalOk, totalOk + totalFail, skipped, RESET);
95
110
  }
96
111
  main();
package/test/run.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import '../lib/certs.js'
1
2
  import * as std from 'std'
2
3
  import { Tester } from './test_helper.js'
3
4
 
@@ -29,6 +30,17 @@ const suiteDefs = [
29
30
  { name: 'ffi', file: './test_ffi.js', tags: [] },
30
31
  { name: 'ffi-bind', file: './test_ffi_bind.js', tags: [] },
31
32
  { name: 'ffi-struct', file: './test_ffi_struct.js', tags: [] },
33
+ { name: 'ffi-struct-layout', file: './test_ffi_struct_layout.js', tags: [] },
34
+ { name: 'ffi-array', file: './test_ffi_array.js', tags: [] },
35
+ { name: 'ffi-abi', file: './test_ffi_abi.js', tags: [] },
36
+ { name: 'ffi-user32', file: './test_ffi_user32.js', tags: [] },
37
+ { name: 'ffi-windows', file: './test_ffi_windows.js', tags: [] },
38
+ { name: 'gdi-draw', file: './test_gdi_draw.js', tags: [] },
39
+ { name: 'registry', file: './test_registry.js', tags: [] },
40
+ { name: 'files', file: './test_files.js', tags: [] },
41
+ { name: 'printer', file: './test_printer.js', tags: [] },
42
+ { name: 'sysinfo', file: './test_sysinfo.js', tags: [] },
43
+ { name: 'proc', file: './test_proc.js', tags: [] },
32
44
  { name: 'net-fetch', file: './test_net_fetch.js', tags: ['net'] },
33
45
  { name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
34
46
  { name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
@@ -52,8 +64,8 @@ async function main(): Promise<void> {
52
64
  std.printf('%s====== QuickWin Test Runner ======%s\n', BOLD, RESET)
53
65
  if (filter) std.printf('Filter: %s\n', filter)
54
66
 
55
- const results: { name: string, ok: number, fail: number, elapsed: number }[] = []
56
- let totalOk = 0, totalFail = 0
67
+ const results: { name: string, ok: number, fail: number, skip: number, elapsed: number }[] = []
68
+ let totalOk = 0, totalFail = 0, totalSkip = 0
57
69
 
58
70
  for (const def of suiteDefs) {
59
71
  if (filter) {
@@ -84,18 +96,21 @@ async function main(): Promise<void> {
84
96
  }
85
97
  const suiteElapsed = Date.now() - suiteStart
86
98
  t.summary()
87
- results.push({ name: def.name, ok: t.ok, fail: t.fail, elapsed: suiteElapsed })
99
+ results.push({ name: def.name, ok: t.ok, fail: t.fail, skip: t.skip, elapsed: suiteElapsed })
88
100
  totalOk += t.ok
89
101
  totalFail += t.fail
102
+ totalSkip += t.skip
90
103
  }
91
104
 
92
105
  const color = totalFail > 0 ? RED : GREEN
93
106
  std.printf('\n%s====== Test Results ======%s\n', BOLD, RESET)
94
107
  for (const r of results) {
95
108
  const c = r.fail > 0 ? RED : GREEN
96
- std.printf(' %s%-18s %s%d/%d passed %s(%s)%s\n', c, r.name, RESET, r.ok, r.ok + r.fail, c, formatDuration(r.elapsed), RESET)
109
+ const skipped = r.skip > 0 ? ` (${r.skip} skipped)` : ''
110
+ std.printf(' %s%-18s %s%d/%d passed%s %s(%s)%s\n', c, r.name, RESET, r.ok, r.ok + r.fail, skipped, c, formatDuration(r.elapsed), RESET)
97
111
  }
98
- std.printf('%s====== Summary: %d/%d passed ======%s\n', color, totalOk, totalOk + totalFail, RESET)
112
+ const skipped = totalSkip > 0 ? ` (${totalSkip} skipped)` : ''
113
+ std.printf('%s====== Summary: %d/%d passed%s ======%s\n', color, totalOk, totalOk + totalFail, skipped, RESET)
99
114
  }
100
115
 
101
116
  main()
@@ -30,5 +30,34 @@ export const suite = {
30
30
  const body = await r.text();
31
31
  assert('body = hello from test server', body === 'hello from test server');
32
32
  }
33
+ t.section('fetch chunked response with Trailer');
34
+ {
35
+ const r = await fetch(URL127 + 'trailer');
36
+ assert('status 200', r.status === 200);
37
+ assert('trailer declared', (r.headers.get('trailer') || '').toLowerCase() === 'x-checksum');
38
+ const body = await r.text();
39
+ assert('body decoded without trailer footer', body === 'chunkA/chunkB');
40
+ }
41
+ t.section('protocol whitelist (reject non-http(s))');
42
+ {
43
+ const badUrls = [
44
+ 'file:///C:/secret.txt',
45
+ 'data:text/plain,hello',
46
+ 'javascript:alert(1)',
47
+ 'ftp://example.com/resource',
48
+ ];
49
+ for (const u of badUrls) {
50
+ let err = '';
51
+ let rejected = false;
52
+ try {
53
+ await fetch(u);
54
+ }
55
+ catch (e) {
56
+ rejected = true;
57
+ err = e.message;
58
+ }
59
+ assert(`reject ${u} with TypeError`, rejected && err.includes('unsupported protocol'));
60
+ }
61
+ }
33
62
  }
34
63
  };
@@ -29,5 +29,31 @@ export const suite = {
29
29
  const body = await r.text()
30
30
  assert('body = hello from test server', body === 'hello from test server')
31
31
  }
32
+
33
+ t.section('fetch chunked response with Trailer')
34
+ {
35
+ const r = await fetch(URL127 + 'trailer')
36
+ assert('status 200', r.status === 200)
37
+ assert('trailer declared', (r.headers.get('trailer') || '').toLowerCase() === 'x-checksum')
38
+ const body = await r.text()
39
+ assert('body decoded without trailer footer', body === 'chunkA/chunkB')
40
+ }
41
+
42
+ t.section('protocol whitelist (reject non-http(s))')
43
+ {
44
+ const badUrls = [
45
+ 'file:///C:/secret.txt',
46
+ 'data:text/plain,hello',
47
+ 'javascript:alert(1)',
48
+ 'ftp://example.com/resource',
49
+ ]
50
+ for (const u of badUrls) {
51
+ let err = ''
52
+ let rejected = false
53
+ try { await fetch(u) }
54
+ catch (e: unknown) { rejected = true; err = (e as Error).message }
55
+ assert(`reject ${u} with TypeError`, rejected && err.includes('unsupported protocol'))
56
+ }
57
+ }
32
58
  }
33
59
  }
package/test/test_ffi.js CHANGED
@@ -2,6 +2,8 @@ import * as std from 'std';
2
2
  import * as win from 'win';
3
3
  import * as ffi from 'ffi';
4
4
  import * as os from 'os';
5
+ import { bind, bindLib } from '../lib/ffi/bind.js';
6
+ import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js';
5
7
  const MAX_WCHARS = 4096;
6
8
  function decodeWideAtPtr(ptr) {
7
9
  if (!ptr)
@@ -69,7 +71,7 @@ function formatStatus(status) {
69
71
  export const suite = {
70
72
  name: 'ffi',
71
73
  run: (t) => {
72
- t.section('EnumPrintersW');
74
+ t.section('EnumPrinters');
73
75
  const hWinspool = win.LoadLibrary('winspool.drv');
74
76
  t.checkTrue('LoadLibrary("winspool.drv") succeeds', hWinspool !== null);
75
77
  if (!hWinspool)
@@ -78,50 +80,29 @@ export const suite = {
78
80
  t.checkTrue('GetProcAddress("EnumPrintersW") succeeds', enumPrinters !== null);
79
81
  if (!enumPrinters)
80
82
  return;
83
+ const { EnumPrintersW: EnumPrinters } = bindLib('winspool.drv', {
84
+ EnumPrintersW: 'u32 <>ptr u32 <BYTE>ptr u32 <BYTE>ptr <BYTE>ptr -> i32',
85
+ });
81
86
  const flags = 0x06;
82
87
  const level = 2;
83
- const neededBuf = new Uint32Array(new ArrayBuffer(4));
84
- const returnedBuf = new Uint32Array(new ArrayBuffer(4));
85
- const ret1 = ffi.ffiCall(enumPrinters, [
86
- ffi.FFI_TYPE_UINT32,
87
- ffi.FFI_TYPE_POINTER,
88
- ffi.FFI_TYPE_UINT32,
89
- ffi.FFI_TYPE_POINTER,
90
- ffi.FFI_TYPE_UINT32,
91
- ffi.FFI_TYPE_POINTER,
92
- ffi.FFI_TYPE_POINTER,
93
- ], [
94
- flags,
95
- null,
96
- level,
97
- null,
98
- 0,
99
- neededBuf.buffer,
100
- returnedBuf.buffer,
101
- ], ffi.FFI_TYPE_SINT32);
88
+ const neededB = new PtrArrayBuffer(4);
89
+ const neededBuf = new Uint32Array(neededB);
90
+ const returnedB = new PtrArrayBuffer(4);
91
+ const returnedBuf = new Uint32Array(returnedB);
92
+ const ret1 = EnumPrinters(flags, NULL, level, NULL, 0, neededB, returnedB);
102
93
  std.printf(' first call: ret=%d needed=%d returned=%d\n', ret1, neededBuf[0], returnedBuf[0]);
103
- t.checkTrue('pcbNeeded > 0', neededBuf[0] > 0);
104
- if (neededBuf[0] <= 0)
94
+ if (neededBuf[0] > 0) {
95
+ t.checkTrue('pcbNeeded > 0', neededBuf[0] > 0);
96
+ }
97
+ else {
98
+ // 无打印机环境(CI VM)EnumPrinters 的 pcbNeeded 为 0,属环境差异非缺陷,
99
+ // 记 SKIP 而非 FAIL,CI 无需再对 ffi 用例做失败容忍。
100
+ t.skipCase('pcbNeeded > 0 (no printers on this VM)');
105
101
  return;
106
- const printerBuf = new ArrayBuffer(neededBuf[0]);
107
- const ret2 = ffi.ffiCall(enumPrinters, [
108
- ffi.FFI_TYPE_UINT32,
109
- ffi.FFI_TYPE_POINTER,
110
- ffi.FFI_TYPE_UINT32,
111
- ffi.FFI_TYPE_POINTER,
112
- ffi.FFI_TYPE_UINT32,
113
- ffi.FFI_TYPE_POINTER,
114
- ffi.FFI_TYPE_POINTER,
115
- ], [
116
- flags,
117
- null,
118
- level,
119
- printerBuf,
120
- neededBuf[0],
121
- neededBuf.buffer,
122
- returnedBuf.buffer,
123
- ], ffi.FFI_TYPE_SINT32);
124
- t.checkTrue('EnumPrintersW succeeds', ret2 !== 0);
102
+ }
103
+ const printerBuf = new PtrArrayBuffer(neededBuf[0]);
104
+ const ret2 = EnumPrinters(flags, NULL, level, printerBuf, neededBuf[0], neededB, returnedB);
105
+ t.checkTrue('EnumPrinters succeeds', ret2 !== 0);
125
106
  if (ret2 === 0)
126
107
  return;
127
108
  std.printf(' printers found: %d\n', returnedBuf[0]);
@@ -166,10 +147,11 @@ export const suite = {
166
147
  const getDC = win.GetProcAddress(hUser32, 'GetDC');
167
148
  t.checkTrue('GetProcAddress("GetDC") succeeds', getDC !== null);
168
149
  if (getDC) {
169
- const screenDc = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0], ffi.FFI_TYPE_POINTER);
170
- t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0 && screenDc !== null);
171
- const bogus = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0x12345678], ffi.FFI_TYPE_POINTER);
172
- t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0 || bogus === null);
150
+ const GetDC = bind('user32.dll', 'GetDC', '<>ptr -> <>ptr');
151
+ const screenDc = GetDC(0);
152
+ t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0);
153
+ const bogus = GetDC(0x12345678);
154
+ t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0);
173
155
  }
174
156
  }
175
157
  }
package/test/test_ffi.ts CHANGED
@@ -2,6 +2,8 @@ import * as std from 'std'
2
2
  import * as win from 'win'
3
3
  import * as ffi from 'ffi'
4
4
  import * as os from 'os'
5
+ import { bind, bindLib } from '../lib/ffi/bind.js'
6
+ import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js'
5
7
  import { Tester } from './test_helper.js'
6
8
 
7
9
  const MAX_WCHARS = 4096
@@ -72,7 +74,7 @@ function formatStatus(status: number): string {
72
74
  export const suite = {
73
75
  name: 'ffi',
74
76
  run: (t: Tester) => {
75
- t.section('EnumPrintersW')
77
+ t.section('EnumPrinters')
76
78
  const hWinspool = win.LoadLibrary('winspool.drv')
77
79
  t.checkTrue('LoadLibrary("winspool.drv") succeeds', hWinspool !== null)
78
80
  if (!hWinspool) return
@@ -81,61 +83,39 @@ export const suite = {
81
83
  t.checkTrue('GetProcAddress("EnumPrintersW") succeeds', enumPrinters !== null)
82
84
  if (!enumPrinters) return
83
85
 
86
+ const { EnumPrintersW: EnumPrinters } = bindLib('winspool.drv', {
87
+ EnumPrintersW: 'u32 <>ptr u32 <BYTE>ptr u32 <BYTE>ptr <BYTE>ptr -> i32',
88
+ })
89
+
84
90
  const flags = 0x06
85
91
  const level = 2
86
- const neededBuf = new Uint32Array(new ArrayBuffer(4))
87
- const returnedBuf = new Uint32Array(new ArrayBuffer(4))
92
+ const neededB = new PtrArrayBuffer(4)
93
+ const neededBuf = new Uint32Array(neededB)
94
+ const returnedB = new PtrArrayBuffer(4)
95
+ const returnedBuf = new Uint32Array(returnedB)
88
96
 
89
- const ret1 = ffi.ffiCall(
90
- enumPrinters,
91
- [
92
- ffi.FFI_TYPE_UINT32,
93
- ffi.FFI_TYPE_POINTER,
94
- ffi.FFI_TYPE_UINT32,
95
- ffi.FFI_TYPE_POINTER,
96
- ffi.FFI_TYPE_UINT32,
97
- ffi.FFI_TYPE_POINTER,
98
- ffi.FFI_TYPE_POINTER,
99
- ],
100
- [
101
- flags,
102
- null,
103
- level,
104
- null,
105
- 0,
106
- neededBuf.buffer,
107
- returnedBuf.buffer,
108
- ],
109
- ffi.FFI_TYPE_SINT32
110
- )
97
+ const ret1 = EnumPrinters(flags, NULL, level, NULL, 0, neededB, returnedB)
111
98
  std.printf(' first call: ret=%d needed=%d returned=%d\n', ret1, neededBuf[0], returnedBuf[0])
112
- t.checkTrue('pcbNeeded > 0', neededBuf[0]! > 0)
113
- if (neededBuf[0]! <= 0) return
99
+ if (neededBuf[0]! > 0) {
100
+ t.checkTrue('pcbNeeded > 0', neededBuf[0]! > 0)
101
+ } else {
102
+ // 无打印机环境(CI VM)EnumPrinters 的 pcbNeeded 为 0,属环境差异非缺陷,
103
+ // 记 SKIP 而非 FAIL,CI 无需再对 ffi 用例做失败容忍。
104
+ t.skipCase('pcbNeeded > 0 (no printers on this VM)')
105
+ return
106
+ }
114
107
 
115
- const printerBuf = new ArrayBuffer(neededBuf[0]!)
116
- const ret2 = ffi.ffiCall(
117
- enumPrinters,
118
- [
119
- ffi.FFI_TYPE_UINT32,
120
- ffi.FFI_TYPE_POINTER,
121
- ffi.FFI_TYPE_UINT32,
122
- ffi.FFI_TYPE_POINTER,
123
- ffi.FFI_TYPE_UINT32,
124
- ffi.FFI_TYPE_POINTER,
125
- ffi.FFI_TYPE_POINTER,
126
- ],
127
- [
128
- flags,
129
- null,
130
- level,
131
- printerBuf,
132
- neededBuf[0]!,
133
- neededBuf.buffer,
134
- returnedBuf.buffer,
135
- ],
136
- ffi.FFI_TYPE_SINT32
108
+ const printerBuf = new PtrArrayBuffer(neededBuf[0]!)
109
+ const ret2 = EnumPrinters(
110
+ flags,
111
+ NULL,
112
+ level,
113
+ printerBuf,
114
+ neededBuf[0]!,
115
+ neededB,
116
+ returnedB
137
117
  )
138
- t.checkTrue('EnumPrintersW succeeds', ret2 !== 0)
118
+ t.checkTrue('EnumPrinters succeeds', ret2 !== 0)
139
119
  if (ret2 === 0) return
140
120
 
141
121
  std.printf(' printers found: %d\n', returnedBuf[0])
@@ -180,10 +160,11 @@ export const suite = {
180
160
  const getDC = win.GetProcAddress(hUser32, 'GetDC')
181
161
  t.checkTrue('GetProcAddress("GetDC") succeeds', getDC !== null)
182
162
  if (getDC) {
183
- const screenDc = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0], ffi.FFI_TYPE_POINTER)
184
- t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0 && screenDc !== null)
185
- const bogus = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0x12345678], ffi.FFI_TYPE_POINTER)
186
- t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0 || bogus === null)
163
+ const GetDC = bind('user32.dll', 'GetDC', '<>ptr -> <>ptr')
164
+ const screenDc = GetDC(0)
165
+ t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0)
166
+ const bogus = GetDC(0x12345678)
167
+ t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0)
187
168
  }
188
169
  }
189
170
  }