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.
- package/README.en.md +1 -2
- package/README.md +19 -23
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20054 -1423
- package/examples/exec_server.tsx +489 -0
- package/examples/exec_server_worker.js +383 -33
- package/examples/exec_server_worker.ts +408 -30
- package/examples/pdf_preview2.js +25 -82
- package/examples/pdf_preview2.ts +27 -84
- package/examples/pdf_viewer.js +1318 -1178
- package/examples/pdf_viewer.tsx +16 -67
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1313 -1074
- package/examples/test_react_lazy.js +1284 -1056
- package/examples/test_react_listbox.js +1283 -1055
- package/examples/test_root_window.js +1279 -1051
- package/examples/test_tab.js +1284 -1056
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +214 -0
- package/lib/certs.ts +224 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +37 -4
- package/lib/fetch.ts +35 -4
- package/lib/ffi/bind.js +352 -0
- package/lib/ffi/bind.ts +442 -0
- package/lib/ffi/ctype.js +168 -0
- package/lib/ffi/ctype.ts +331 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +412 -0
- package/lib/ffi/struct.ts +664 -0
- package/lib/http-common.js +43 -5
- package/lib/http-common.ts +48 -6
- package/lib/http-server.js +187 -29
- package/lib/http-server.ts +177 -26
- package/lib/react-qw/components/DateTimePicker.js +18 -37
- package/lib/react-qw/components/Input.js +3 -10
- package/lib/react-qw/components/Link.js +6 -19
- package/lib/react-qw/components/ListView.js +49 -154
- package/lib/react-qw/components/PathPicker.js +28 -88
- package/lib/react-qw/components/ScrollView.js +15 -8
- package/lib/react-qw/components/Slider.js +2 -1
- package/lib/react-qw/components/Tab.js +6 -22
- package/lib/react-qw/components/Tooltip.js +10 -21
- package/lib/react-qw/components/TreeView.js +8 -31
- package/lib/react-qw/index.js +4 -2
- package/lib/react-qw/index.ts +4 -2
- package/lib/react-qw/nmhdr.js +3 -24
- package/lib/react-qw/nmhdr.ts +5 -30
- package/lib/react-qw/props.js +7 -2
- package/lib/react-qw/props.ts +7 -3
- package/lib/react-qw/reconciler.js +23 -13
- package/lib/react-qw/reconciler.ts +22 -13
- package/lib/text-measure.js +18 -23
- package/lib/text-measure.ts +20 -25
- package/lib/websocket.js +12 -2
- package/lib/websocket.ts +15 -2
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +20 -0
- package/lib/windows/comdlg32.js +7 -0
- package/lib/windows/comdlg32.ts +9 -0
- package/lib/windows/gdi32.js +90 -0
- package/lib/windows/gdi32.ts +97 -0
- package/lib/windows/kernel32.js +66 -0
- package/lib/windows/kernel32.ts +76 -0
- package/lib/windows/ntdll.js +8 -0
- package/lib/windows/ntdll.ts +10 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/shell32.js +11 -0
- package/lib/windows/shell32.ts +14 -0
- package/lib/windows/structs.js +384 -0
- package/lib/windows/structs.ts +422 -0
- package/lib/windows/user32.js +469 -0
- package/lib/windows/user32.ts +502 -0
- package/package.json +1 -1
- package/quickwin.d.ts +22 -37
- package/quickwin_const.d.ts +1 -0
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/bench_ffi_callraw.js +236 -0
- package/test/bench_ffi_callraw.ts +236 -0
- package/test/bench_ffi_overhead.js +139 -0
- package/test/bench_ffi_overhead.ts +141 -0
- package/test/bench_sendmessage.js +104 -0
- package/test/bench_sendmessage.ts +100 -0
- package/test/bench_sendmessage_nonnull.js +187 -0
- package/test/bench_sendmessage_nonnull.ts +188 -0
- package/test/run.js +12 -4
- package/test/run.ts +13 -5
- package/test/test_basic_fetch.js +8 -0
- package/test/test_basic_fetch.ts +9 -0
- package/test/test_ffi.js +25 -43
- package/test/test_ffi.ts +33 -52
- package/test/test_ffi_abi.js +235 -0
- package/test/test_ffi_abi.ts +218 -0
- package/test/test_ffi_bind.js +538 -8
- package/test/test_ffi_bind.ts +559 -8
- package/test/test_ffi_struct.js +632 -88
- package/test/test_ffi_struct.ts +666 -91
- package/test/test_ffi_struct_layout.js +495 -0
- package/test/test_ffi_struct_layout.ts +543 -0
- package/test/test_ffi_user32.js +103 -0
- package/test/test_ffi_user32.ts +113 -0
- package/test/test_ffi_windows.js +101 -0
- package/test/test_ffi_windows.ts +113 -0
- package/test/test_helper.js +8 -1
- package/test/test_helper.ts +9 -1
- package/test/test_http_import.js +18 -0
- package/test/test_http_import.ts +19 -0
- package/test/test_http_server.js +73 -1
- package/test/test_http_server.ts +75 -1
- package/test/test_ipv6.js +2 -2
- package/test/test_ipv6.ts +2 -2
- package/test/test_net_fetch.js +70 -8
- package/test/test_net_fetch.ts +69 -7
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_react_complex.js +1294 -1066
- package/test/test_react_counter.js +1284 -1056
- package/test/test_react_flex.js +1304 -1069
- package/test/test_react_hidden.js +1287 -1059
- package/test/test_react_listview_link.js +1284 -1056
- package/test/test_react_notify.js +18534 -0
- package/test/test_react_render.js +1284 -1056
- package/test/test_wolfssl_read.js +3 -2
- package/test/test_wolfssl_read.ts +3 -1
- package/test/test_worker_net.js +35 -17
- package/test/test_worker_net.ts +27 -15
- package/tsconfig.json +1 -1
- package/examples/exec_server.ts +0 -80
- package/lib/ffi-bind.js +0 -85
- package/lib/ffi-bind.ts +0 -170
- package/lib/ffi-struct.js +0 -224
- package/lib/ffi-struct.ts +0 -239
- package/lib/win-common-structs.js +0 -39
- package/lib/win-common-structs.ts +0 -41
package/test/test_ffi_struct.ts
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import * as os from 'os'
|
|
2
|
+
import * as gui from 'gui'
|
|
2
3
|
import { Tester } from './test_helper.js'
|
|
3
|
-
import { struct } from '../lib/ffi
|
|
4
|
-
import { bind } from '../lib/ffi
|
|
4
|
+
import { struct, union } from '../lib/ffi/struct.js'
|
|
5
|
+
import { bind, } from '../lib/ffi/bind.js'
|
|
6
|
+
import { type MaybePtr, Ptr, PtrArrayBuffer } from '../lib/ffi/ctype.js'
|
|
7
|
+
import type { C_Union } from '../lib/ffi/ctype.js'
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
// 编译期断言工具(仅类型层,运行时无开销)
|
|
10
|
+
type Equal<A, B> = (<G>() => G extends A ? 1 : 2) extends (<G>() => G extends B ? 1 : 2) ? true : false
|
|
11
|
+
function expectType<T extends true>(_value?: T): void { }
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
// Ptr 定义:'' 判右侧(不作 naked 分布)——字面品牌照旧收窄、Ptr<''> 归一 number、
|
|
14
|
+
// never 不再塌缩成 never(旧定义下 Ptr<never> 即 never,decode(… | Ptr<N>) 会在 N=never 时堵死)
|
|
15
|
+
expectType<Equal<Ptr<''>, number>>(true)
|
|
16
|
+
expectType<Equal<Ptr<never>, never> extends false ? true : false>(true)
|
|
17
|
+
|
|
18
|
+
const RECT = /* @__PURE__ */ struct('RECT', {
|
|
19
|
+
left: 'i32',
|
|
20
|
+
top: 'i32',
|
|
21
|
+
right: 'i32',
|
|
22
|
+
bottom: 'i32',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const TVITEM = /* @__PURE__ */ struct('TVITEM', {
|
|
9
26
|
mask: 'u32',
|
|
10
|
-
hItem: 'ptr',
|
|
27
|
+
hItem: '<>ptr',
|
|
11
28
|
state: 'u32',
|
|
12
29
|
stateMask: 'u32',
|
|
13
|
-
pszText: 'ptr',
|
|
30
|
+
pszText: '<>ptr',
|
|
14
31
|
cchTextMax: 'i32',
|
|
15
32
|
iImage: 'i32',
|
|
16
33
|
iSelectedImage: 'i32',
|
|
17
34
|
cChildren: 'i32',
|
|
18
|
-
lParam: 'ptr',
|
|
35
|
+
lParam: '<>ptr',
|
|
36
|
+
})
|
|
37
|
+
const TVINSERTSTRUCT = /* @__PURE__ */ struct('TVINSERTSTRUCT', {
|
|
38
|
+
hParent: '<>ptr',
|
|
39
|
+
hInsertAfter: '<>ptr',
|
|
40
|
+
item: TVITEM.__struct,
|
|
19
41
|
})
|
|
20
|
-
const TVINSERTSTRUCT = struct({ hParent: 'ptr', hInsertAfter: 'ptr', item: TVITEM })
|
|
21
42
|
|
|
22
43
|
export const suite = {
|
|
23
44
|
name: 'ffi-struct',
|
|
@@ -26,8 +47,8 @@ export const suite = {
|
|
|
26
47
|
|
|
27
48
|
t.section('RECT layout & roundtrip')
|
|
28
49
|
t.check('RECT.size == 16', 16, RECT.size)
|
|
29
|
-
const rbuf = RECT.
|
|
30
|
-
const r = RECT.
|
|
50
|
+
const rbuf = RECT.encode({ left: 10, top: 20, right: 30, bottom: 40 })
|
|
51
|
+
const r = RECT.decode(rbuf)
|
|
31
52
|
t.check('left', 10, r.left)
|
|
32
53
|
t.check('top', 20, r.top)
|
|
33
54
|
t.check('right', 30, r.right)
|
|
@@ -35,16 +56,35 @@ export const suite = {
|
|
|
35
56
|
t.check('offsetOf left', 0, RECT.offsetOf('left'))
|
|
36
57
|
t.check('offsetOf bottom', 12, RECT.offsetOf('bottom'))
|
|
37
58
|
|
|
59
|
+
// decode 入参随 N 品牌化:命名 struct 收 ArrayBuffer | Ptr<其名字>,
|
|
60
|
+
// 未命名(N='')才归一 ArrayBuffer | number
|
|
61
|
+
expectType<Equal<Parameters<typeof RECT.decode>[0], ArrayBuffer | Ptr<'RECT'>>>(true)
|
|
62
|
+
expectType<Equal<Parameters<typeof TVITEM.decode>[0], ArrayBuffer | Ptr<'TVITEM'>>>(true)
|
|
63
|
+
if (false) {
|
|
64
|
+
// @ts-expect-error plain number is not assignable to ArrayBuffer | Ptr<'RECT'>
|
|
65
|
+
RECT.decode(123)
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
t.section('GetWindowRect fills RECT buffer')
|
|
39
|
-
const getWindowRect = bind('user32.dll', 'GetWindowRect', 'ptr ptr ->
|
|
40
|
-
const
|
|
69
|
+
const getWindowRect = bind('user32.dll', 'GetWindowRect', '<>ptr <BYTE>ptr -> i32')
|
|
70
|
+
const getWindowRectLayout = bind('user32.dll', 'GetWindowRect', '<>ptr <RECT>ptr -> i32', { RECT })
|
|
71
|
+
const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> <>ptr')
|
|
41
72
|
const hwnd = getDesktopWindow()
|
|
42
|
-
const wrect = new
|
|
73
|
+
const wrect = new PtrArrayBuffer(16)
|
|
43
74
|
const ok = getWindowRect(hwnd, wrect)
|
|
44
75
|
t.checkTrue('GetWindowRect succeeds', ok !== 0)
|
|
45
|
-
const wr = RECT.
|
|
76
|
+
const wr = RECT.decode(wrect)
|
|
46
77
|
t.checkTrue('screen RECT non-empty', wr.right > 0 && wr.bottom > 0)
|
|
47
78
|
|
|
79
|
+
// out 参数:encode() 新建零 buffer 返回 PtrArrayBuffer<'RECT'> → .ptr 直接喂
|
|
80
|
+
// <RECT>ptr,读回走 def.decode(实例)(ArrayBuffer 分支直读)
|
|
81
|
+
t.section('RECT.alloc() out-param → PtrArrayBuffer')
|
|
82
|
+
const out = RECT.alloc()
|
|
83
|
+
t.checkTrue('encode() exposes ArrayBuffer subclass + ptr', out instanceof ArrayBuffer && typeof out.ptr === 'number')
|
|
84
|
+
t.checkTrue('GetWindowRect(hwnd, out.ptr) succeeds', getWindowRectLayout(hwnd, out.ptr) !== 0)
|
|
85
|
+
const or = RECT.decode(out)
|
|
86
|
+
t.checkTrue('RECT.decode(encode()) decodes out-param', or.right > 0 && or.bottom > 0)
|
|
87
|
+
|
|
48
88
|
t.section('ptr layout matches arch')
|
|
49
89
|
t.check(`TVITEM.size (${os.arch})`, is64 ? 56 : 40, TVITEM.size)
|
|
50
90
|
t.check('TVITEM.offsetOf pszText', is64 ? 24 : 16, TVITEM.offsetOf('pszText'))
|
|
@@ -52,7 +92,7 @@ export const suite = {
|
|
|
52
92
|
t.check('TVINSERTSTRUCT.offsetOf item', is64 ? 16 : 8, TVINSERTSTRUCT.offsetOf('item'))
|
|
53
93
|
|
|
54
94
|
t.section('nested struct roundtrip')
|
|
55
|
-
const ins = TVINSERTSTRUCT.
|
|
95
|
+
const ins = TVINSERTSTRUCT.encode({
|
|
56
96
|
hParent: 0x11111111,
|
|
57
97
|
hInsertAfter: 0x22222222,
|
|
58
98
|
item: {
|
|
@@ -61,108 +101,344 @@ export const suite = {
|
|
|
61
101
|
iImage: 0, iSelectedImage: 0, cChildren: 5, lParam: 0x55555555,
|
|
62
102
|
},
|
|
63
103
|
})
|
|
64
|
-
const got = TVINSERTSTRUCT.
|
|
104
|
+
const got = TVINSERTSTRUCT.decode(ins)
|
|
65
105
|
t.check('nested hParent', 0x11111111, got.hParent)
|
|
66
106
|
t.check('nested hInsertAfter', 0x22222222, got.hInsertAfter)
|
|
67
107
|
t.check('nested cChildren', 5, got.item.cChildren)
|
|
68
108
|
t.check('nested ptr pszText', 0x44444444, got.item.pszText)
|
|
69
109
|
|
|
70
|
-
t.section('
|
|
71
|
-
const M = struct(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
t.check(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
t.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
t.check('
|
|
86
|
-
|
|
87
|
-
t.check('
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
t.section('mixed scalar layout (i32/u32/i16/<>ptr;别名机制已移除)')
|
|
111
|
+
const M = struct('M', {
|
|
112
|
+
n: 'i32',
|
|
113
|
+
d: 'u32',
|
|
114
|
+
w: '<>ptr',
|
|
115
|
+
s: 'i16',
|
|
116
|
+
q: '<>ptr',
|
|
117
|
+
})
|
|
118
|
+
t.check(`mixed size (${os.arch})`, is64 ? 32 : 20, M.size)
|
|
119
|
+
const mb = M.encode({ n: -5, d: 0xFFFFFFFF, w: 0xAABBCCDD, s: -7, q: 0x11223344 })
|
|
120
|
+
const m = M.decode(mb)
|
|
121
|
+
t.check('i32 -5', -5, m.n)
|
|
122
|
+
t.check('u32 0xFFFFFFFF', 0xFFFFFFFF, m.d)
|
|
123
|
+
// ptr 位有符号读(对齐 C 版 JS_NewInt64):0xAABBCCDD 在 ia32 按 4 字节
|
|
124
|
+
// 存取 → 符号扩展读成负;x64 8 字节高位 0 → 仍为正
|
|
125
|
+
t.check('<>ptr 符号读', is64 ? 0xAABBCCDD : -1430532899, m.w)
|
|
126
|
+
t.check('i16 -7', -7, m.s)
|
|
127
|
+
t.check('<>ptr 0x11223344', 0x11223344, m.q)
|
|
128
|
+
// 别名机制已移除:typedef 名塌出词汇表 —— struct 字段值只收规范 token
|
|
129
|
+
// (仅类型层断言,包成函数体不执行,避免运行时 fail-loud 崩掉 suite)
|
|
130
|
+
// @ts-expect-error typedef 别名 'DWORD' 已移除
|
|
131
|
+
void (() => struct('XD', { d: 'DWORD' }))
|
|
132
|
+
|
|
133
|
+
t.section('array sugar + trailing bitfield(数组糖与尾随位域单元并存)')
|
|
134
|
+
const AS = struct('AS', {
|
|
135
|
+
v: 'u32[4]',
|
|
136
|
+
f: 'u32:3',
|
|
137
|
+
})
|
|
138
|
+
t.check('size == 20 (16 + 位域单元)', 20, AS.size)
|
|
139
|
+
t.check('f offset == 16', 16, AS.offsetOf('f'))
|
|
140
|
+
const asd = AS.decode(AS.encode({ v: [1, 2, 3, 4], f: 5 }))
|
|
141
|
+
t.check('u32[4][1]', 2, asd.v[1])
|
|
142
|
+
t.check('u32:3', 5, asd.f)
|
|
143
|
+
expectType<Equal<ReturnType<typeof AS.decode>['f'], number>>()
|
|
144
|
+
|
|
145
|
+
t.section('union() entry roundtrip')
|
|
146
|
+
const U = union({ a: 'u8', b: 'u32' })
|
|
147
|
+
t.check('union size == 4', 4, U.size)
|
|
148
|
+
t.check('union offsetOf a', 0, U.offsetOf('a'))
|
|
149
|
+
t.check('union offsetOf b', 0, U.offsetOf('b'))
|
|
150
|
+
const ud = U.decode(U.encode({ a: 0, b: 0x01020304 }))
|
|
151
|
+
t.check('union a 与 b 同偏移(低字节别名)', 0x04, ud.a)
|
|
152
|
+
t.check('union b', 0x01020304, ud.b)
|
|
153
|
+
|
|
154
|
+
t.section('union 位域完全别名 roundtrip(mingw 实测语义)')
|
|
155
|
+
{
|
|
156
|
+
const UB = union({ a: 'u32:1', b: 'u32:1' })
|
|
157
|
+
t.check('union bitfield size == 4', 4, UB.size)
|
|
158
|
+
const ud1 = UB.decode(UB.encode({ a: 1, b: 1 }))
|
|
159
|
+
t.check('a=1,b=1 roundtrip a', 1, ud1.a)
|
|
160
|
+
t.check('a=1,b=1 roundtrip b', 1, ud1.b)
|
|
161
|
+
// 同位别名:字段按声明序写入,后写字段覆盖先写字段
|
|
162
|
+
const ud2 = UB.decode(UB.encode({ a: 1, b: 0 }))
|
|
163
|
+
t.check('别名覆盖:b=0 后写 → a 读 0', 0, ud2.a)
|
|
164
|
+
t.check('别名覆盖:b == 0', 0, ud2.b)
|
|
165
|
+
|
|
166
|
+
// 溢出形状(30+4>32):union 每成员回 offset 0,曾开新单元越出 size
|
|
167
|
+
const OF = union({ a: 'u32:30', b: 'u32:4' })
|
|
168
|
+
t.check('overflow union size == 4', 4, OF.size)
|
|
169
|
+
const od1 = OF.decode(OF.encode({ a: 0x3FFFFFFF, b: 0xF }))
|
|
170
|
+
t.check('overflow roundtrip a', 0x3FFFFFFF, od1.a)
|
|
171
|
+
t.check('overflow roundtrip b', 0xF, od1.b)
|
|
172
|
+
const od2 = OF.decode(OF.encode({ a: 0x3FFFFFFF, b: 0 }))
|
|
173
|
+
t.check('overflow 别名覆盖 → a 低 4 位被清', 0x3FFFFFF0, od2.a)
|
|
174
|
+
|
|
175
|
+
// 位域 + 普通成员混合:b 曾被 flushUnit 推到 offset 4(越出 size 4)
|
|
176
|
+
const MX = union({ a: 'u32:1', b: 'u8' })
|
|
177
|
+
t.check('mixed union size == 4', 4, MX.size)
|
|
178
|
+
t.check('mixed offsetOf b == 0(曾为 4)', 0, MX.offsetOf('b'))
|
|
179
|
+
const md = MX.decode(MX.encode({ a: 1, b: 0x54 }))
|
|
180
|
+
t.check('mixed: b 覆盖 a 所在字节 → a 读 0', 0, md.a)
|
|
181
|
+
t.check('mixed: b == 0x54', 0x54, md.b)
|
|
182
|
+
|
|
183
|
+
// 嵌套回归:struct 内嵌该 union(raw '#' 形式;union() 装饰对象不能嵌套,
|
|
184
|
+
// 嵌套 union 的解码类型推导会 TS2589,沿用旧行例 as any——本条是运行时断言),
|
|
185
|
+
// tail 不被 union 内错位写污染
|
|
186
|
+
const nestedU: C_Union = { '#': 'union', a: 'u32:1', b: 'u8' }
|
|
187
|
+
const NS = (struct as any)({ tag: 'u32', u: nestedU, tail: 'u16' })
|
|
188
|
+
t.check('nested size == 12', 12, NS.size)
|
|
189
|
+
const nd = NS.decode(NS.encode({ tag: 0xAABBCCDD, u: { a: 1, b: 0x54 }, tail: 0x1234 }))
|
|
190
|
+
t.check('nested tag', 0xAABBCCDD, nd.tag)
|
|
191
|
+
t.check('nested u.b == 0x54', 0x54, nd.u.b)
|
|
192
|
+
t.check('nested tail 不被污染', 0x1234, nd.tail)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
t.section('struct() entry: pack 选项 + 键尾 @N(alignas)')
|
|
196
|
+
const P = struct('P', { a: 'u8', b: 'u32', c: 'u8' }, { pack: 1 })
|
|
197
|
+
t.check('pack(1) size == 6', 6, P.size)
|
|
198
|
+
t.check('pack(1) offsetOf b', 1, P.offsetOf('b'))
|
|
199
|
+
const AL = struct('AL', { a: 'u8', 'b@8': 'u32' })
|
|
200
|
+
t.check('@N offsetOf a', 0, AL.offsetOf('a'))
|
|
201
|
+
t.check('@N offsetOf b(字段名已剥离 @N)', 8, AL.offsetOf('b'))
|
|
202
|
+
|
|
203
|
+
t.section('utf-16le string[8] roundtrip & truncation')
|
|
204
|
+
const W = struct('W', {name: 'u16[8]@utf-16le' })
|
|
205
|
+
t.check('utf16[8].size == 16', 16, W.size)
|
|
206
|
+
const wb = W.encode({ name: 'hello' })
|
|
207
|
+
t.check('read back "hello"', 'hello', W.decode(wb).name)
|
|
208
|
+
const lb = W.encode({ name: 'a very long string over' })
|
|
209
|
+
t.check('utf16[8]: payload 7 + NUL', 'a very ', W.decode(lb).name)
|
|
210
|
+
t.check('utf16 terminator[14..15] == 0', 0, new DataView(lb).getUint16(14, true))
|
|
211
|
+
|
|
212
|
+
t.section("utf-8 string[8] roundtrip & truncation")
|
|
213
|
+
const C = struct('C', {name: 'u8[8]@utf-8' })
|
|
214
|
+
t.check('char[8].size == 8', 8, C.size)
|
|
215
|
+
const cb = C.encode({ name: 'hi' })
|
|
216
|
+
t.check('read back "hi"', 'hi', C.decode(cb).name)
|
|
217
|
+
t.check('NUL at [2]', 0, new DataView(cb).getUint8(2))
|
|
218
|
+
const ctrunc = C.encode({ name: '1234567890' })
|
|
219
|
+
t.check('char[8]: payload 7 + NUL', '1234567', C.decode(ctrunc).name)
|
|
220
|
+
t.check('char terminator[7] == 0', 0, new DataView(ctrunc).getUint8(7))
|
|
221
|
+
|
|
222
|
+
t.section('string sugar: 布局与解释正交(错配 roundtrip)')
|
|
223
|
+
const XS = struct('XS', { buf: 'u8[8]@utf-16le' })
|
|
224
|
+
t.check('u8[8]@utf-16le size == 8(encoding 不参与布局)', 8, XS.size)
|
|
225
|
+
const xsd = XS.decode(XS.encode({ buf: 'AB' }))
|
|
226
|
+
t.check('8 字节按 utf-16le 解出 "AB"', 'AB', xsd.buf)
|
|
227
|
+
|
|
228
|
+
t.section('string sugar as array element')
|
|
229
|
+
const SA = struct('SA', { rows: { '#': 'array', element: 'u16[4]@utf-16le', length: 3 } })
|
|
230
|
+
t.check('string[4]×3 size == 24', 24, SA.size)
|
|
231
|
+
const sad = SA.decode(SA.encode({ rows: ['ab', 'cd', 'ef'] }))
|
|
232
|
+
t.check('rows[0]', 'ab', sad.rows[0])
|
|
233
|
+
t.check('rows[1]', 'cd', sad.rows[1])
|
|
234
|
+
expectType<Equal<ReturnType<typeof SA.decode>['rows'], [string, string, string]>>()
|
|
235
|
+
|
|
236
|
+
t.section('键侧 alignas @N 与值侧 encoding @ 并存')
|
|
237
|
+
const KV = struct('KV', { a: 'u8', 's@8': 'u16[4]@utf-8' })
|
|
238
|
+
t.check('offsetOf a', 0, KV.offsetOf('a'))
|
|
239
|
+
t.check('offsetOf s(键尾 @8 生效)', 8, KV.offsetOf('s'))
|
|
240
|
+
t.check('KV size == 16', 16, KV.size)
|
|
241
|
+
|
|
242
|
+
t.section('numeric arrays roundtrip (CArray)')
|
|
243
|
+
const A = struct('A', {
|
|
244
|
+
v: 'u16[4]',
|
|
245
|
+
k: 'i32[2]',
|
|
246
|
+
})
|
|
91
247
|
t.check('size == 16', 16, A.size)
|
|
92
|
-
const ab = A.
|
|
93
|
-
const ad = A.
|
|
248
|
+
const ab = A.encode({ v: [1, 2, 3, 4], k: [-1, 300000] })
|
|
249
|
+
const ad = A.decode(ab)
|
|
94
250
|
t.check('u16[0]', 1, ad.v[0])
|
|
95
251
|
t.check('u16[3]', 4, ad.v[3])
|
|
96
252
|
t.check('i32[0] -1', -1, ad.k[0])
|
|
97
253
|
t.check('i32[1]', 300000, ad.k[1])
|
|
98
254
|
|
|
255
|
+
t.section('T[1] stays an array (no scalar degradation)')
|
|
256
|
+
const ONE = struct('ONE', {v: 'u16[1]'})
|
|
257
|
+
t.check('size == 2', 2, ONE.size)
|
|
258
|
+
const one = ONE.decode(ONE.encode({ v: [42] }))
|
|
259
|
+
t.check('length 1', 1, one.v.length)
|
|
260
|
+
t.check('[0]', 42, one.v[0])
|
|
261
|
+
|
|
262
|
+
t.section('array of struct roundtrip (stride = child size)')
|
|
263
|
+
const PT = struct('PT', {
|
|
264
|
+
x: 'i32',
|
|
265
|
+
y: 'i32',
|
|
266
|
+
})
|
|
267
|
+
const POLY = struct('POLY', {
|
|
268
|
+
count: 'u32',
|
|
269
|
+
pts: { '#': 'array', element: PT.__struct, length: 3 },
|
|
270
|
+
})
|
|
271
|
+
t.check('POLY.size == 28', 28, POLY.size)
|
|
272
|
+
t.check('POLY.offsetOf pts', 4, POLY.offsetOf('pts'))
|
|
273
|
+
const poly = POLY.decode(POLY.encode({ count: 3, pts: [{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }] }))
|
|
274
|
+
t.check('count', 3, poly.count)
|
|
275
|
+
t.check('pts[0].x', 1, poly.pts[0].x)
|
|
276
|
+
t.check('pts[2].y', 6, poly.pts[2].y)
|
|
277
|
+
|
|
278
|
+
// 编译期断言:CType IR 推导 — string / 定长数组元组 / 嵌套 struct 递归
|
|
279
|
+
expectType<Equal<ReturnType<typeof W.decode>['name'], string>>()
|
|
280
|
+
expectType<Equal<ReturnType<typeof A.decode>['v'], [number, number, number, number]>>()
|
|
281
|
+
expectType<Equal<ReturnType<typeof A.decode>['k'], [number, number]>>()
|
|
282
|
+
expectType<Equal<ReturnType<typeof ONE.decode>['v'], [number]>>()
|
|
283
|
+
expectType<Equal<ReturnType<typeof POLY.decode>['pts']['length'], 3>>()
|
|
284
|
+
expectType<Equal<ReturnType<typeof POLY.decode>['pts'][0], ReturnType<typeof PT.decode>>>()
|
|
285
|
+
|
|
286
|
+
let rejected = false
|
|
287
|
+
try { struct({v: 'i32[0]'}) } catch { rejected = true }
|
|
288
|
+
t.check('array length 0 rejected', true, rejected)
|
|
289
|
+
|
|
290
|
+
let rejectedBare = false
|
|
291
|
+
try { (struct as any)({ p: 'ptr' }) } catch { rejectedBare = true }
|
|
292
|
+
t.check('bare "ptr" member rejected', true, rejectedBare)
|
|
293
|
+
|
|
294
|
+
// '#' 必填:字段值漏写 '#'(裸嵌套 map)不再默认 struct,直接 throw
|
|
295
|
+
let rejectedMap = false
|
|
296
|
+
try { (struct as any)({ outer: { inner: 'u32' } }) } catch { rejectedMap = true }
|
|
297
|
+
t.check('bare nested map rejected(漏写 #)', true, rejectedMap)
|
|
298
|
+
|
|
299
|
+
// 顶层 '#' 由构造器写入:字段表混入的 '#' 指令键被剔除(运行时兜底,类型层已禁)
|
|
300
|
+
const H = (struct as any)({ '#': 'union', a: 'u8', b: 'u32' })
|
|
301
|
+
t.check('top-level # stripped(仍按函数名 struct 布局)', 8, H.size)
|
|
302
|
+
|
|
303
|
+
t.section("pointer member '<>ptr' raw vs '<T>ptr' branded")
|
|
304
|
+
const PTR = struct('PTR', {
|
|
305
|
+
raw: '<>ptr',
|
|
306
|
+
r: '<RECT>ptr',
|
|
307
|
+
tag: 'u32',
|
|
308
|
+
})
|
|
309
|
+
t.check('PTR.size', is64 ? 24 : 12, PTR.size)
|
|
310
|
+
t.check('offsetOf raw', 0, PTR.offsetOf('raw'))
|
|
311
|
+
t.check('offsetOf r', is64 ? 8 : 4, PTR.offsetOf('r'))
|
|
312
|
+
t.check('offsetOf tag', is64 ? 16 : 8, PTR.offsetOf('tag'))
|
|
313
|
+
const rawOut = RECT.alloc()
|
|
314
|
+
const pd = PTR.decode(PTR.encode({ raw: rawOut.ptr, r: rawOut.ptr, tag: 7 }))
|
|
315
|
+
t.check('raw roundtrip', rawOut.ptr, pd.raw)
|
|
316
|
+
t.check('r roundtrip', rawOut.ptr, pd.r)
|
|
317
|
+
t.check('tag', 7, pd.tag)
|
|
318
|
+
// 编译期:'<>ptr' → MaybePtr<''>(NULL|number,与 number 双向可赋值,NULL 即 0 保真);
|
|
319
|
+
// '<RECT>ptr' → MaybePtr<'RECT'>(NULL|Ptr,0 保真,传参前收窄)。
|
|
320
|
+
expectType<Equal<ReturnType<typeof PTR.decode>['raw'], MaybePtr<''>>>()
|
|
321
|
+
expectType<[ReturnType<typeof PTR.decode>['raw']] extends [number] ? true : false>()
|
|
322
|
+
expectType<Equal<ReturnType<typeof PTR.decode>['r'], MaybePtr<'RECT'>>>()
|
|
323
|
+
|
|
99
324
|
t.section('f32/f64 layout (MSVC: f@0, double@8)')
|
|
100
|
-
const FL = struct(
|
|
325
|
+
const FL = struct('FL', {
|
|
326
|
+
f: 'f32', // float
|
|
327
|
+
d: 'f64', // double
|
|
328
|
+
})
|
|
101
329
|
t.check('size == 16 (f@0, double aligned @8)', 16, FL.size)
|
|
102
|
-
const fb = FL.
|
|
103
|
-
const fr = FL.
|
|
330
|
+
const fb = FL.encode({ f: 1.5, d: -2.25 })
|
|
331
|
+
const fr = FL.decode(fb)
|
|
104
332
|
t.check('float', 1.5, fr.f)
|
|
105
333
|
t.check('double', -2.25, fr.d)
|
|
106
334
|
|
|
107
335
|
t.section('comctl dialog structs track 32/64-bit ABI')
|
|
108
|
-
const TCITEMW = struct({
|
|
109
|
-
mask: 'u32',
|
|
110
|
-
|
|
336
|
+
const TCITEMW = struct('TCITEMW', {
|
|
337
|
+
mask: 'u32',
|
|
338
|
+
dwState: 'u32',
|
|
339
|
+
dwStateMask: 'u32',
|
|
340
|
+
pszText: '<>ptr',
|
|
341
|
+
cchTextMax: 'i32',
|
|
342
|
+
iImage: 'i32',
|
|
343
|
+
lParam: '<>ptr',
|
|
111
344
|
})
|
|
112
345
|
t.check('TCITEMW.size', is64 ? 40 : 28, TCITEMW.size)
|
|
113
346
|
t.check('TCITEMW.offsetOf pszText', is64 ? 16 : 12, TCITEMW.offsetOf('pszText'))
|
|
114
347
|
t.check('TCITEMW.offsetOf cchTextMax', is64 ? 24 : 16, TCITEMW.offsetOf('cchTextMax'))
|
|
115
348
|
|
|
116
|
-
const TTTOOLINFOW = struct({
|
|
117
|
-
cbSize: 'u32',
|
|
118
|
-
|
|
119
|
-
|
|
349
|
+
const TTTOOLINFOW = struct('TTTOOLINFOW', {
|
|
350
|
+
cbSize: 'u32',
|
|
351
|
+
uFlags: 'u32',
|
|
352
|
+
hwnd: '<>ptr',
|
|
353
|
+
uId: '<>ptr',
|
|
354
|
+
rect: 'i32[4]',
|
|
355
|
+
hinst: '<>ptr',
|
|
356
|
+
lpszText: '<>ptr',
|
|
357
|
+
lParam: '<>ptr',
|
|
358
|
+
lpReserved: '<>ptr', // WinXP+ 追加
|
|
120
359
|
})
|
|
121
360
|
t.check('TTTOOLINFOW.size', is64 ? 72 : 48, TTTOOLINFOW.size)
|
|
122
361
|
t.check('TTTOOLINFOW.offsetOf lpszText', is64 ? 48 : 36, TTTOOLINFOW.offsetOf('lpszText'))
|
|
123
362
|
|
|
124
|
-
const OPENFILENAMEW = struct({
|
|
125
|
-
lStructSize: 'u32',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
363
|
+
const OPENFILENAMEW = struct('OPENFILENAMEW', {
|
|
364
|
+
lStructSize: 'u32',
|
|
365
|
+
hwndOwner: '<>ptr',
|
|
366
|
+
hInstance: '<>ptr',
|
|
367
|
+
lpstrFilter: '<>ptr',
|
|
368
|
+
lpstrCustomFilter: '<>ptr',
|
|
369
|
+
nMaxCustFilter: 'u32',
|
|
370
|
+
nFilterIndex: 'u32',
|
|
371
|
+
lpstrFile: '<>ptr',
|
|
372
|
+
nMaxFile: 'u32',
|
|
373
|
+
lpstrFileTitle: '<>ptr',
|
|
374
|
+
nMaxFileTitle: 'u32',
|
|
375
|
+
lpstrInitialDir: '<>ptr',
|
|
376
|
+
lpstrTitle: '<>ptr',
|
|
377
|
+
Flags: 'u32',
|
|
378
|
+
nFileOffset: 'u16',
|
|
379
|
+
nFileExtension: 'u16',
|
|
380
|
+
lpstrDefExt: '<>ptr',
|
|
381
|
+
lCustData: '<>ptr',
|
|
382
|
+
lpfnHook: '<>ptr',
|
|
383
|
+
lpTemplateName: '<>ptr',
|
|
384
|
+
pvReserved: '<>ptr',
|
|
385
|
+
dwReserved: 'u32',
|
|
386
|
+
FlagsEx: 'u32', // Win2000+ 追加
|
|
132
387
|
})
|
|
133
388
|
t.check('OPENFILENAMEW.size', is64 ? 152 : 88, OPENFILENAMEW.size)
|
|
134
389
|
t.check('OPENFILENAMEW.offsetOf lpstrFile', is64 ? 48 : 28, OPENFILENAMEW.offsetOf('lpstrFile'))
|
|
135
390
|
t.check('OPENFILENAMEW.offsetOf Flags', is64 ? 96 : 52, OPENFILENAMEW.offsetOf('Flags'))
|
|
136
391
|
t.check('OPENFILENAMEW.offsetOf lpstrTitle', is64 ? 88 : 48, OPENFILENAMEW.offsetOf('lpstrTitle'))
|
|
137
392
|
|
|
138
|
-
const BROWSEINFOW = struct({
|
|
139
|
-
hwndOwner: 'ptr',
|
|
140
|
-
|
|
393
|
+
const BROWSEINFOW = struct('BROWSEINFOW', {
|
|
394
|
+
hwndOwner: '<>ptr',
|
|
395
|
+
pidlRoot: '<>ptr',
|
|
396
|
+
pszDisplayName: '<>ptr',
|
|
397
|
+
lpszTitle: '<>ptr',
|
|
398
|
+
ulFlags: 'u32',
|
|
399
|
+
lpfn: '<>ptr',
|
|
400
|
+
lParam: '<>ptr',
|
|
401
|
+
iImage: 'i32',
|
|
141
402
|
})
|
|
142
403
|
t.check('BROWSEINFOW.size', is64 ? 64 : 32, BROWSEINFOW.size)
|
|
143
404
|
t.check('BROWSEINFOW.offsetOf lpszTitle', is64 ? 24 : 12, BROWSEINFOW.offsetOf('lpszTitle'))
|
|
144
405
|
t.check('BROWSEINFOW.offsetOf ulFlags', is64 ? 32 : 16, BROWSEINFOW.offsetOf('ulFlags'))
|
|
145
406
|
|
|
146
407
|
t.section('NMHDR(嵌套时 MSVC 尾 padding 传染)')
|
|
147
|
-
const NMHDR = struct(
|
|
408
|
+
const NMHDR = struct('NMHDR', {
|
|
409
|
+
hwndFrom: '<>ptr',
|
|
410
|
+
idFrom: '<>ptr',
|
|
411
|
+
code: 'i32',
|
|
412
|
+
})
|
|
148
413
|
t.check('NMHDR.size', is64 ? 24 : 12, NMHDR.size)
|
|
149
414
|
t.check('NMHDR.offsetOf code', is64 ? 16 : 8, NMHDR.offsetOf('code'))
|
|
150
415
|
|
|
151
416
|
t.section('NMCUSTOMDRAW / NMLVCUSTOMDRAW (ListView custom draw)')
|
|
152
|
-
const NMCUSTOMDRAW = struct({
|
|
153
|
-
hdr: NMHDR,
|
|
154
|
-
dwDrawStage: 'u32',
|
|
155
|
-
|
|
417
|
+
const NMCUSTOMDRAW = struct('NMCUSTOMDRAW', {
|
|
418
|
+
hdr: NMHDR.__struct,
|
|
419
|
+
dwDrawStage: 'u32',
|
|
420
|
+
hdc: '<>ptr',
|
|
421
|
+
rc: 'i32[4]',
|
|
422
|
+
dwItemSpec: '<>ptr',
|
|
423
|
+
uItemState: 'u32',
|
|
424
|
+
lItemlParam: '<>ptr',
|
|
156
425
|
})
|
|
157
426
|
t.check('NMCUSTOMDRAW.size', is64 ? 80 : 48, NMCUSTOMDRAW.size)
|
|
158
427
|
t.check('NMCUSTOMDRAW.offsetOf dwDrawStage', is64 ? 24 : 12, NMCUSTOMDRAW.offsetOf('dwDrawStage'))
|
|
159
428
|
t.check('NMCUSTOMDRAW.offsetOf hdc', is64 ? 32 : 16, NMCUSTOMDRAW.offsetOf('hdc'))
|
|
160
429
|
t.check('NMCUSTOMDRAW.offsetOf dwItemSpec', is64 ? 56 : 36, NMCUSTOMDRAW.offsetOf('dwItemSpec'))
|
|
161
|
-
const NMLVCUSTOMDRAW = struct({
|
|
162
|
-
hdr: NMHDR,
|
|
163
|
-
dwDrawStage: 'u32',
|
|
164
|
-
|
|
165
|
-
|
|
430
|
+
const NMLVCUSTOMDRAW = struct('NMLVCUSTOMDRAW', {
|
|
431
|
+
hdr: NMHDR.__struct,
|
|
432
|
+
dwDrawStage: 'u32',
|
|
433
|
+
hdc: '<>ptr',
|
|
434
|
+
rc: 'i32[4]',
|
|
435
|
+
dwItemSpec: '<>ptr',
|
|
436
|
+
uItemState: 'u32',
|
|
437
|
+
lItemlParam: '<>ptr',
|
|
438
|
+
clrText: 'u32',
|
|
439
|
+
clrTextBk: 'u32',
|
|
440
|
+
iSubItem: 'i32',
|
|
441
|
+
dwItemType: 'u32',
|
|
166
442
|
})
|
|
167
443
|
t.check('NMLVCUSTOMDRAW.size', is64 ? 96 : 64, NMLVCUSTOMDRAW.size)
|
|
168
444
|
t.check('NMLVCUSTOMDRAW.offsetOf clrText', is64 ? 80 : 48, NMLVCUSTOMDRAW.offsetOf('clrText'))
|
|
@@ -170,10 +446,15 @@ export const suite = {
|
|
|
170
446
|
t.check('NMLVCUSTOMDRAW.offsetOf iSubItem', is64 ? 88 : 56, NMLVCUSTOMDRAW.offsetOf('iSubItem'))
|
|
171
447
|
|
|
172
448
|
t.section('NMLISTVIEW / NMITEMACTIVATE')
|
|
173
|
-
const NMLISTVIEW = struct({
|
|
174
|
-
hdr: NMHDR,
|
|
175
|
-
iItem: 'i32',
|
|
176
|
-
|
|
449
|
+
const NMLISTVIEW = struct('NMLISTVIEW', {
|
|
450
|
+
hdr: NMHDR.__struct,
|
|
451
|
+
iItem: 'i32',
|
|
452
|
+
iSubItem: 'i32',
|
|
453
|
+
uNewState: 'u32',
|
|
454
|
+
uOldState: 'u32',
|
|
455
|
+
uChanged: 'u32',
|
|
456
|
+
ptAction: 'i32[2]',
|
|
457
|
+
lParam: '<>ptr',
|
|
177
458
|
})
|
|
178
459
|
t.check('NMLISTVIEW.size', is64 ? 64 : 44, NMLISTVIEW.size)
|
|
179
460
|
t.check('NMLISTVIEW.offsetOf iItem', is64 ? 24 : 12, NMLISTVIEW.offsetOf('iItem'))
|
|
@@ -182,22 +463,40 @@ export const suite = {
|
|
|
182
463
|
t.check('NMLISTVIEW.offsetOf uOldState', is64 ? 36 : 24, NMLISTVIEW.offsetOf('uOldState'))
|
|
183
464
|
|
|
184
465
|
t.section('LVITEMW / LVCOLUMNW')
|
|
185
|
-
const LVITEMW = struct({
|
|
186
|
-
mask: 'u32',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
466
|
+
const LVITEMW = struct('LVITEMW', {
|
|
467
|
+
mask: 'u32',
|
|
468
|
+
iItem: 'i32',
|
|
469
|
+
iSubItem: 'i32',
|
|
470
|
+
state: 'u32',
|
|
471
|
+
stateMask: 'u32',
|
|
472
|
+
pszText: '<>ptr',
|
|
473
|
+
cchTextMax: 'i32',
|
|
474
|
+
iImage: 'i32',
|
|
475
|
+
lParam: '<>ptr',
|
|
476
|
+
iIndent: 'i32',
|
|
477
|
+
iGroupId: 'i32',
|
|
478
|
+
cColumns: 'u32',
|
|
479
|
+
puColumns: '<>ptr',
|
|
480
|
+
piColFmt: '<>ptr',
|
|
481
|
+
iGroup: 'i32',
|
|
191
482
|
})
|
|
192
483
|
t.check('LVITEMW.size', is64 ? 88 : 60, LVITEMW.size)
|
|
193
484
|
t.check('LVITEMW.offsetOf iItem', 4, LVITEMW.offsetOf('iItem'))
|
|
194
485
|
t.check('LVITEMW.offsetOf pszText', is64 ? 24 : 20, LVITEMW.offsetOf('pszText'))
|
|
195
486
|
t.check('LVITEMW.offsetOf iImage', is64 ? 36 : 28, LVITEMW.offsetOf('iImage'))
|
|
196
487
|
t.check('LVITEMW.offsetOf lParam', is64 ? 40 : 32, LVITEMW.offsetOf('lParam'))
|
|
197
|
-
const LVCOLUMNW = struct({
|
|
198
|
-
mask: 'u32',
|
|
199
|
-
|
|
200
|
-
|
|
488
|
+
const LVCOLUMNW = struct('LVCOLUMNW', {
|
|
489
|
+
mask: 'u32',
|
|
490
|
+
fmt: 'i32',
|
|
491
|
+
cx: 'i32',
|
|
492
|
+
pszText: '<>ptr',
|
|
493
|
+
cchTextMax: 'i32',
|
|
494
|
+
iSubItem: 'i32',
|
|
495
|
+
iImage: 'i32',
|
|
496
|
+
iOrder: 'i32',
|
|
497
|
+
cxMin: 'i32',
|
|
498
|
+
cxDefault: 'i32',
|
|
499
|
+
cxIdeal: 'i32',
|
|
201
500
|
})
|
|
202
501
|
t.check('LVCOLUMNW.size', is64 ? 56 : 44, LVCOLUMNW.size)
|
|
203
502
|
t.check('LVCOLUMNW.offsetOf pszText', is64 ? 16 : 12, LVCOLUMNW.offsetOf('pszText'))
|
|
@@ -205,19 +504,295 @@ export const suite = {
|
|
|
205
504
|
t.check('LVCOLUMNW.offsetOf iOrder', is64 ? 36 : 28, LVCOLUMNW.offsetOf('iOrder'))
|
|
206
505
|
|
|
207
506
|
t.section('NMDATETIMECHANGE')
|
|
208
|
-
const SYSTEMTIME = struct({
|
|
209
|
-
wYear: 'u16',
|
|
210
|
-
|
|
507
|
+
const SYSTEMTIME = struct('SYSTEMTIME', {
|
|
508
|
+
wYear: 'u16',
|
|
509
|
+
wMonth: 'u16',
|
|
510
|
+
wDayOfWeek: 'u16',
|
|
511
|
+
wDay: 'u16',
|
|
512
|
+
wHour: 'u16',
|
|
513
|
+
wMinute: 'u16',
|
|
514
|
+
wSecond: 'u16',
|
|
515
|
+
wMilliseconds: 'u16',
|
|
516
|
+
})
|
|
517
|
+
const NMDATETIMECHANGE = struct('NMDATETIMECHANGE', {
|
|
518
|
+
hdr: NMHDR.__struct,
|
|
519
|
+
dwFlags: 'u32',
|
|
520
|
+
st: SYSTEMTIME.__struct,
|
|
211
521
|
})
|
|
212
|
-
const NMDATETIMECHANGE = struct({ hdr: NMHDR, dwFlags: 'u32', st: SYSTEMTIME })
|
|
213
522
|
t.check('NMDATETIMECHANGE.size', is64 ? 48 : 32, NMDATETIMECHANGE.size)
|
|
214
523
|
t.check('NMDATETIMECHANGE.offsetOf dwFlags', is64 ? 24 : 12, NMDATETIMECHANGE.offsetOf('dwFlags'))
|
|
215
524
|
t.check('NMDATETIMECHANGE.offsetOf st', is64 ? 28 : 16, NMDATETIMECHANGE.offsetOf('st'))
|
|
216
525
|
|
|
217
526
|
t.section('NMLINK szUrl offset')
|
|
218
|
-
const LITEM = struct(
|
|
219
|
-
|
|
527
|
+
const LITEM = struct('LITEM', {
|
|
528
|
+
mask: 'u32',
|
|
529
|
+
iLink: 'i32',
|
|
530
|
+
state: 'u32',
|
|
531
|
+
stateMask: 'u32',
|
|
532
|
+
szID: 'u16[48]@utf-16le',
|
|
533
|
+
})
|
|
534
|
+
const NMLINK = struct('NMLINK', {
|
|
535
|
+
hdr: NMHDR.__struct,
|
|
536
|
+
item: LITEM.__struct,
|
|
537
|
+
szUrl: 'u16[2084]@utf-16le',
|
|
538
|
+
})
|
|
220
539
|
t.check('LITEM.offsetOf szID', 16, LITEM.offsetOf('szID'))
|
|
221
540
|
t.check('NMLINK.offsetOf szUrl', is64 ? 136 : 124, NMLINK.offsetOf('szUrl'))
|
|
541
|
+
|
|
542
|
+
// === 位域读/写往返 ===
|
|
543
|
+
// 布局 ground truth 在 test_ffi_struct_layout.ts(mingw 交叉实测),这里只测读写。
|
|
544
|
+
|
|
545
|
+
t.section('bitfield roundtrip: LSB→MSB 打包 + 读改写隔离')
|
|
546
|
+
{
|
|
547
|
+
// 字段按序写入同一 buffer:写 d 时单元里已有 a/b/c 的位,
|
|
548
|
+
// 若 writeBitfield 不做读改写(直接 val<<bit)会把 a/b/c 清零。
|
|
549
|
+
const BF = struct('BF', {
|
|
550
|
+
a: 'u32:3',
|
|
551
|
+
b: 'u32:5',
|
|
552
|
+
c: 'u32:4',
|
|
553
|
+
d: 'u32:10', // 3+5+4+10=22 <= 32
|
|
554
|
+
tail: 'u32',
|
|
555
|
+
})
|
|
556
|
+
t.check('BF.size == 8', 8, BF.size)
|
|
557
|
+
const bd = BF.decode(BF.encode({ a: 5, b: 31, c: 15, d: 1023, tail: 0xDEAD }))
|
|
558
|
+
t.check('a (LSB)', 5, bd.a)
|
|
559
|
+
t.check('b', 31, bd.b)
|
|
560
|
+
t.check('c', 15, bd.c)
|
|
561
|
+
t.check('d (max 10-bit)', 1023, bd.d)
|
|
562
|
+
t.check('tail survives bitfield unit', 0xDEAD, bd.tail)
|
|
563
|
+
// 位域写不破坏同单元的邻居:a=5 后写 b=31,a 仍为 5
|
|
564
|
+
t.check('a survives b write', 5, bd.a)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
t.section('bitfield roundtrip: signed sign extension')
|
|
568
|
+
{
|
|
569
|
+
const SB = struct('SB', {
|
|
570
|
+
s: 'i32:8',
|
|
571
|
+
u: 'u32:4',
|
|
572
|
+
})
|
|
573
|
+
t.check('SB.size == 4 (同宽不分签别,共单元)', 4, SB.size)
|
|
574
|
+
const sbd = SB.decode(SB.encode({ s: -5, u: 9 }))
|
|
575
|
+
t.check('signed -5', -5, sbd.s)
|
|
576
|
+
t.check('unsigned 9', 9, sbd.u)
|
|
577
|
+
t.check('signed -1', -1, SB.decode(SB.encode({ s: -1, u: 0 })).s)
|
|
578
|
+
t.check('signed 127 (min int8)', 127, SB.decode(SB.encode({ s: 127, u: 0 })).s)
|
|
579
|
+
t.check('signed 128 → -128', -128, SB.decode(SB.encode({ s: 128, u: 0 })).s)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
t.section('bitfield roundtrip: overflow starts a new unit')
|
|
583
|
+
{
|
|
584
|
+
const OV = struct('OV', {
|
|
585
|
+
a: 'u32:30',
|
|
586
|
+
b: 'u32:4', // 30+4=34 > 32 → 新单元,剩余 2 位废弃
|
|
587
|
+
c: 'u32',
|
|
588
|
+
})
|
|
589
|
+
t.check('OV.size == 12', 12, OV.size)
|
|
590
|
+
t.check('OV.offsetOf b (new unit)', 4, OV.offsetOf('b'))
|
|
591
|
+
t.check('OV.offsetOf c', 8, OV.offsetOf('c'))
|
|
592
|
+
const ovd = OV.decode(OV.encode({ a: 0x3FFFFFFF, b: 15, c: 7 }))
|
|
593
|
+
t.check('OV a (max 30-bit)', 0x3FFFFFFF, ovd.a)
|
|
594
|
+
t.check('OV b', 15, ovd.b)
|
|
595
|
+
t.check('OV c', 7, ovd.c)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
t.section('bitfield: DCB roundtrip (Windows serial port config)')
|
|
599
|
+
{
|
|
600
|
+
const DCB = struct('DCB', {
|
|
601
|
+
DCBlength: 'u32',
|
|
602
|
+
BaudRate: 'u32',
|
|
603
|
+
fBinary: 'u32:1',
|
|
604
|
+
fParity: 'u32:1',
|
|
605
|
+
fOutxCtsFlow: 'u32:1',
|
|
606
|
+
fOutxDsrFlow: 'u32:1',
|
|
607
|
+
fDtrControl: 'u32:2',
|
|
608
|
+
fDsrSensitivity: 'u32:1',
|
|
609
|
+
fTXContinueOnXoff: 'u32:1',
|
|
610
|
+
fOutX: 'u32:1',
|
|
611
|
+
fInX: 'u32:1',
|
|
612
|
+
fErrorChar: 'u32:1',
|
|
613
|
+
fNull: 'u32:1',
|
|
614
|
+
fRtsControl: 'u32:2',
|
|
615
|
+
fAbortOnError: 'u32:1',
|
|
616
|
+
fDummy2: 'u32:17',
|
|
617
|
+
wReserved: 'u16',
|
|
618
|
+
XonLim: 'u16',
|
|
619
|
+
XoffLim: 'u16',
|
|
620
|
+
ByteSize: 'u8',
|
|
621
|
+
Parity: 'u8',
|
|
622
|
+
StopBits: 'u8',
|
|
623
|
+
XonChar: 'i8',
|
|
624
|
+
XoffChar: 'i8',
|
|
625
|
+
ErrorChar: 'i8',
|
|
626
|
+
EofChar: 'i8',
|
|
627
|
+
EvtChar: 'i8',
|
|
628
|
+
wReserved1: 'u16',
|
|
629
|
+
})
|
|
630
|
+
t.check('DCB.size == 28', 28, DCB.size)
|
|
631
|
+
const d = DCB.decode(DCB.encode({
|
|
632
|
+
DCBlength: 28, BaudRate: 0x1C200,
|
|
633
|
+
fBinary: 1, fParity: 0, fOutxCtsFlow: 0, fOutxDsrFlow: 0,
|
|
634
|
+
fDtrControl: 2, fDsrSensitivity: 1, fTXContinueOnXoff: 0,
|
|
635
|
+
fOutX: 1, fInX: 0, fErrorChar: 1, fNull: 0,
|
|
636
|
+
fRtsControl: 2, fAbortOnError: 1, fDummy2: 0,
|
|
637
|
+
wReserved: 0, XonLim: 200, XoffLim: 100,
|
|
638
|
+
ByteSize: 8, Parity: 0, StopBits: 0,
|
|
639
|
+
XonChar: 3, XoffChar: 4, ErrorChar: 0xFF,
|
|
640
|
+
EofChar: 3, EvtChar: 0x4, wReserved1: 0,
|
|
641
|
+
}))
|
|
642
|
+
t.check('DCBlength', 28, d.DCBlength)
|
|
643
|
+
t.check('BaudRate', 0x1C200, d.BaudRate)
|
|
644
|
+
t.check('fBinary', 1, d.fBinary)
|
|
645
|
+
t.check('fParity', 0, d.fParity)
|
|
646
|
+
t.check('fDtrControl', 2, d.fDtrControl)
|
|
647
|
+
t.check('fDsrSensitivity', 1, d.fDsrSensitivity)
|
|
648
|
+
t.check('fOutX', 1, d.fOutX)
|
|
649
|
+
t.check('fRtsControl', 2, d.fRtsControl)
|
|
650
|
+
t.check('fAbortOnError', 1, d.fAbortOnError)
|
|
651
|
+
t.check('fDummy2', 0, d.fDummy2)
|
|
652
|
+
t.check('wReserved', 0, d.wReserved)
|
|
653
|
+
t.check('XonLim', 200, d.XonLim)
|
|
654
|
+
t.check('XoffLim', 100, d.XoffLim)
|
|
655
|
+
t.check('ByteSize', 8, d.ByteSize)
|
|
656
|
+
t.check('Parity', 0, d.Parity)
|
|
657
|
+
t.check('StopBits', 0, d.StopBits)
|
|
658
|
+
t.check('XonChar', 3, d.XonChar)
|
|
659
|
+
t.check('XoffChar', 4, d.XoffChar)
|
|
660
|
+
t.check('ErrorChar (i8)', -1, d.ErrorChar)
|
|
661
|
+
t.check('EofChar', 3, d.EofChar)
|
|
662
|
+
t.check('EvtChar', 4, d.EvtChar)
|
|
663
|
+
t.check('wReserved1', 0, d.wReserved1)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 类型层:位域成员值类型是 number
|
|
667
|
+
{
|
|
668
|
+
const BF = struct('BF', {
|
|
669
|
+
a: 'u32:3',
|
|
670
|
+
s: 'i32:8',
|
|
671
|
+
})
|
|
672
|
+
expectType<Equal<ReturnType<typeof BF.decode>['a'], number>>()
|
|
673
|
+
expectType<Equal<ReturnType<typeof BF.decode>['s'], number>>()
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 位域 width>53:值域装不进安全整数 → bigint(与类型层 BitShape 同界)
|
|
677
|
+
t.section('bitfield width>53: bigint roundtrip (u64/i64)')
|
|
678
|
+
{
|
|
679
|
+
// u64:60 + u64:4 同单元混合形态:>53 的字段 bigint、≤53 的字段 number
|
|
680
|
+
const W64 = struct('W64', {
|
|
681
|
+
u: 'u64:60',
|
|
682
|
+
v: 'u64:4',
|
|
683
|
+
})
|
|
684
|
+
t.check('u64:60+u64:4 size == 8(同单元)', 8, W64.size)
|
|
685
|
+
t.check('v 与 u 同单元 offset == 0', 0, W64.offsetOf('v'))
|
|
686
|
+
const big = 0x0FFFFFFFFFFFFFFFn // 60 位内大值 > 2^53
|
|
687
|
+
const d1 = W64.decode(W64.encode({ u: big, v: 15 }))
|
|
688
|
+
t.check('u64:60 大值精确往返', big, d1.u)
|
|
689
|
+
t.check('u64:4 同单元往返', 15, d1.v)
|
|
690
|
+
expectType<Equal<ReturnType<typeof W64.decode>['u'], bigint>>()
|
|
691
|
+
expectType<Equal<ReturnType<typeof W64.decode>['v'], number>>()
|
|
692
|
+
|
|
693
|
+
// i64:60 负值符号扩展 → 精确 bigint(回归:曾被有符号分支的 Number() 截断)
|
|
694
|
+
const I64 = struct('I64', { s: 'i64:60' })
|
|
695
|
+
const neg = -12345678901234567n
|
|
696
|
+
t.check('i64:60 负值精确往返', neg, I64.decode(I64.encode({ s: neg })).s)
|
|
697
|
+
// 符号位为 0 的正值:width>53 → 依然 bigint
|
|
698
|
+
t.check('i64:60 正值 bigint', 100n, I64.decode(I64.encode({ s: 100n })).s)
|
|
699
|
+
expectType<Equal<ReturnType<typeof I64.decode>['s'], bigint>>()
|
|
700
|
+
if (false) {
|
|
701
|
+
// @ts-expect-error width>53 位域收 bigint,number 被类型层拒绝
|
|
702
|
+
I64.encode({ s: 42 })
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 分界:53 → number(2^53-1 精确),54 → bigint(2^54-1 精确)
|
|
706
|
+
const B53 = struct('B53', { a: 'u64:53' })
|
|
707
|
+
expectType<Equal<ReturnType<typeof B53.decode>['a'], number>>()
|
|
708
|
+
t.check('u64:53 边界 number 精确', 9007199254740991,
|
|
709
|
+
B53.decode(B53.encode({ a: 9007199254740991 })).a)
|
|
710
|
+
const B54 = struct('B54', { a: 'u64:54' })
|
|
711
|
+
expectType<Equal<ReturnType<typeof B54.decode>['a'], bigint>>()
|
|
712
|
+
t.check('u64:54 边界 bigint 精确', 18014398509481983n,
|
|
713
|
+
B54.decode(B54.encode({ a: 18014398509481983n })).a)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ============ token @Enum:struct 字段值域 ============
|
|
717
|
+
t.section('struct 字段 @Enum: 词汇定义点拒绝 + 值域收窄 + 必填判定')
|
|
718
|
+
{
|
|
719
|
+
// 定义点拒绝(∉ SimpleValue —— 未知枚举名与拼错 token 同一道闸)。if(false)
|
|
720
|
+
// 纯类型层断言:位域反例运行时 normToken 也会 throw,不执行。
|
|
721
|
+
if (false) {
|
|
722
|
+
// @ts-expect-error 未知枚举名(TypoEnum ∉ keyof EnumMap)
|
|
723
|
+
struct('Bad1', { m: 'u32@TypoEnum' })
|
|
724
|
+
// @ts-expect-error 位域单元不收 @(值域标注对位域无意义,词汇拒)
|
|
725
|
+
struct('Bad2', { m: 'u32@WmMsg:3' })
|
|
726
|
+
// @ts-expect-error ptr 位未知枚举名
|
|
727
|
+
struct('Bad3', { p: '<>ptr@TypoEnum' })
|
|
728
|
+
// @ts-expect-error 数组糖元素同样受词汇守卫
|
|
729
|
+
struct('Bad4', { a: 'u32@TypoEnum[4]' })
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 合法定义:数字位 + ptr 位 @ 标注
|
|
733
|
+
const EV = struct('EV', {
|
|
734
|
+
msg: 'u32@WmMsg', // WM_NULL = 0 ∈ 值域 → encode 可省
|
|
735
|
+
key: 'u32@MouseKeyFlag', // 仅 MK_SHIFT = 4,0 ∉ 值域 → encode 必填
|
|
736
|
+
hp: '<>ptr@SetWindowPosHwnd', // ptr 位(HWND_TOP = 0 ∈ → 可省)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
// decode:值域收窄(与 bind 签名位同构;异种枚举/域外字面量由枚举 nominal 拦)
|
|
740
|
+
const dev = EV.decode(EV.encode({ key: 4 }))
|
|
741
|
+
expectType<Equal<typeof dev.msg, EnumMap['WmMsg']>>()
|
|
742
|
+
expectType<Equal<typeof dev.key, EnumMap['MouseKeyFlag']>>()
|
|
743
|
+
expectType<Equal<typeof dev.hp, EnumMap['SetWindowPosHwnd']>>()
|
|
744
|
+
t.check('数字位 @ 字段归一往返', 4, dev.key)
|
|
745
|
+
|
|
746
|
+
// encode 必填判定:0 ∉ 值域的 @ 字段必填(省略 = 新槽写 0 = 静默违约);
|
|
747
|
+
// 0 ∈ 值域可省。下面 { key } 行能编译即 msg/hp 可省的断言。
|
|
748
|
+
EV.encode({ key: gui.MouseKeyFlag.MK_SHIFT })
|
|
749
|
+
// @ts-expect-error key 必填:省略即编译错(0 ∉ MouseKeyFlag)
|
|
750
|
+
EV.encode({ msg: 0 })
|
|
751
|
+
if (false) {
|
|
752
|
+
// @ts-expect-error 域外字面量(3 不是任何 MK_ 标志)
|
|
753
|
+
EV.encode({ key: 3 })
|
|
754
|
+
// @ts-expect-error 跨枚举误用(WmMsg 成员不是 MouseKeyFlag)
|
|
755
|
+
EV.encode({ key: gui.WmMsg.CREATE })
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 数组糖元素带 @:lower 递归归一(normToken 剥 @)+ 元素值域
|
|
759
|
+
const EA = struct('EA', { msgs: 'u32@WmMsg[4]' })
|
|
760
|
+
const dea = EA.decode(EA.encode({ msgs: [0, 1, 2, 0] }))
|
|
761
|
+
expectType<Equal<(typeof dea.msgs)[number], EnumMap['WmMsg']>>()
|
|
762
|
+
t.check('数组 @ 元素运行时归一往返', 2, dea.msgs[2])
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ============ EncodeIn 后序穿透:嵌套/数组子树含必填 ⇒ 父键必给 ============
|
|
766
|
+
t.section('EncodeIn DFS 后序:子树含必填 ⇒ 键必填;值后序合成(非必填兄弟可省)')
|
|
767
|
+
{
|
|
768
|
+
// 嵌套子结构含 '!'(内层强制)+ 标量/字符串糖(内层可省:''/0 是合法零值)
|
|
769
|
+
const NSUB = struct('NSUB', { hp: '<X>ptr!', rest: 'u32', s: 'u16[4]@utf-8' })
|
|
770
|
+
const NOUT = struct('NOUT', { top: 'u32', sub: NSUB.__struct })
|
|
771
|
+
// decode 造全量对象(hp 槽预填非零,绕 '!' 的 decode 0 断言)
|
|
772
|
+
const nbuf = new PtrArrayBuffer(NOUT.size)
|
|
773
|
+
new DataView(nbuf).setUint32(NOUT.offsetOf('sub') + NSUB.offsetOf('hp'), 0x1234, true)
|
|
774
|
+
const dn = NOUT.decode(nbuf)
|
|
775
|
+
NOUT.encode(dn) // 全量回喂 ✓
|
|
776
|
+
NOUT.encode({ sub: dn.sub }) // 顶层 top 可省 ✓
|
|
777
|
+
// B 形态关键钉:内层非必填兄弟 rest/s 可省(A「全量严格形」形态此行编译错)
|
|
778
|
+
NOUT.encode({ sub: { hp: dn.sub.hp } })
|
|
779
|
+
// @ts-expect-error 嵌套穿透:sub 子树含 '!' ⇒ sub 必填 —— 省略即编译错
|
|
780
|
+
NOUT.encode({ top: 1 })
|
|
781
|
+
// @ts-expect-error 内层 '!' 必填:sub.hp 省略即编译错,不静默写 0
|
|
782
|
+
NOUT.encode({ sub: { rest: 1 } })
|
|
783
|
+
|
|
784
|
+
// 数组穿透:元素 0 ∉ 值域 ⇒ 整组必填(省 = 元素全 0 静默违约)
|
|
785
|
+
const ARMAND = struct('ARMAND', { keys: 'u32@MouseKeyFlag[2]' })
|
|
786
|
+
ARMAND.encode({ keys: [4, 4] })
|
|
787
|
+
// @ts-expect-error 数组含必填元素:整组省略即编译错
|
|
788
|
+
ARMAND.encode({})
|
|
789
|
+
|
|
790
|
+
// weak-type 防线在「无必填」分支保持:裸 number 与全可选形无公共键(TS2559)。
|
|
791
|
+
// if(false) 纯类型层断言(运行时 encode(123) 会炸,不执行)。
|
|
792
|
+
if (false) {
|
|
793
|
+
// @ts-expect-error 无必填 struct 的入参退化纯 DeepPartial,裸 number 被拦
|
|
794
|
+
RECT.encode(123)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
222
797
|
},
|
|
223
|
-
}
|
|
798
|
+
}
|