quickwin 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.en.md +21 -26
  2. package/README.md +3 -6
  3. package/examples/exec_server.js +139 -31
  4. package/examples/exec_server.tsx +7 -8
  5. package/examples/exec_server_worker.js +1 -1
  6. package/examples/exec_server_worker.ts +2 -2
  7. package/examples/pdf_preview2.js +1 -1
  8. package/examples/pdf_preview2.ts +1 -1
  9. package/examples/pdf_viewer.js +132 -28
  10. package/examples/pdf_viewer.tsx +1 -1
  11. package/examples/test_gallery.js +131 -27
  12. package/examples/test_react_lazy.js +131 -27
  13. package/examples/test_react_listbox.js +131 -27
  14. package/examples/test_root_window.js +131 -27
  15. package/examples/test_tab.js +131 -27
  16. package/lib/certs.js +13 -8
  17. package/lib/certs.ts +13 -8
  18. package/lib/fetch.js +51 -3
  19. package/lib/fetch.ts +38 -3
  20. package/lib/ffi/bind.js +94 -5
  21. package/lib/ffi/bind.ts +107 -10
  22. package/lib/ffi/ctype.js +33 -7
  23. package/lib/ffi/ctype.ts +48 -10
  24. package/lib/ffi/struct.js +92 -14
  25. package/lib/ffi/struct.ts +108 -19
  26. package/lib/react-qw/components/ListView.js +1 -0
  27. package/lib/react-qw/components/PathPicker.js +2 -2
  28. package/lib/react-qw/index.js +1 -1
  29. package/lib/react-qw/index.ts +4 -1
  30. package/lib/text-measure.js +3 -1
  31. package/lib/text-measure.ts +4 -3
  32. package/lib/websocket.js +32 -2
  33. package/lib/websocket.ts +25 -2
  34. package/lib/windows/advapi32.js +53 -0
  35. package/lib/windows/advapi32.ts +64 -0
  36. package/lib/windows/comctl32.js +15 -15
  37. package/lib/windows/comctl32.ts +15 -16
  38. package/lib/windows/comdlg32.js +3 -2
  39. package/lib/windows/comdlg32.ts +3 -2
  40. package/lib/windows/gdi32.js +328 -26
  41. package/lib/windows/gdi32.ts +349 -26
  42. package/lib/windows/kernel32.js +206 -10
  43. package/lib/windows/kernel32.ts +225 -10
  44. package/lib/windows/msimg32.js +38 -0
  45. package/lib/windows/msimg32.ts +45 -0
  46. package/lib/windows/ntdll.js +3 -2
  47. package/lib/windows/ntdll.ts +3 -2
  48. package/lib/windows/ole32.js +1 -1
  49. package/lib/windows/ole32.ts +1 -1
  50. package/lib/windows/psapi.js +32 -0
  51. package/lib/windows/psapi.ts +37 -0
  52. package/lib/windows/shell32.js +5 -3
  53. package/lib/windows/shell32.ts +5 -3
  54. package/lib/windows/structs.js +135 -0
  55. package/lib/windows/structs.ts +144 -0
  56. package/lib/windows/user32.js +202 -99
  57. package/lib/windows/user32.ts +204 -99
  58. package/lib/windows/version.js +29 -0
  59. package/lib/windows/version.ts +35 -0
  60. package/lib/windows/winspool.js +71 -0
  61. package/lib/windows/winspool.ts +80 -0
  62. package/package.json +1 -1
  63. package/quickwin.d.ts +11 -5
  64. package/quickwin_const.d.ts +136 -0
  65. package/qwin-nowasm-x86.exe +0 -0
  66. package/qwin-nowasm.exe +0 -0
  67. package/qwin-x86.exe +0 -0
  68. package/qwin.exe +0 -0
  69. package/test/run.js +7 -0
  70. package/test/run.ts +7 -0
  71. package/test/test_basic_fetch.js +21 -0
  72. package/test/test_basic_fetch.ts +17 -0
  73. package/test/test_ffi.js +6 -6
  74. package/test/test_ffi.ts +6 -6
  75. package/test/test_ffi_abi.js +55 -0
  76. package/test/test_ffi_abi.ts +56 -0
  77. package/test/test_ffi_array.js +116 -0
  78. package/test/test_ffi_array.ts +141 -0
  79. package/test/test_ffi_bind.js +82 -0
  80. package/test/test_ffi_bind.ts +71 -0
  81. package/test/test_files.js +102 -0
  82. package/test/test_files.ts +121 -0
  83. package/test/test_gdi_draw.js +681 -0
  84. package/test/test_gdi_draw.ts +725 -0
  85. package/test/test_net_fetch.js +4 -3
  86. package/test/test_net_fetch.ts +4 -3
  87. package/test/test_printer.js +137 -0
  88. package/test/test_printer.ts +148 -0
  89. package/test/test_proc.js +141 -0
  90. package/test/test_proc.ts +163 -0
  91. package/test/test_react_complex.js +131 -27
  92. package/test/test_react_counter.js +131 -27
  93. package/test/test_react_flex.js +131 -27
  94. package/test/test_react_hidden.js +131 -27
  95. package/test/test_react_listview_link.js +131 -27
  96. package/test/test_react_notify.js +131 -27
  97. package/test/test_react_render.js +131 -27
  98. package/test/test_registry.js +74 -0
  99. package/test/test_registry.ts +95 -0
  100. package/test/test_sysinfo.js +122 -0
  101. package/test/test_sysinfo.ts +141 -0
  102. package/test/test_wolfssl_read.js +45 -44
  103. package/test/test_wolfssl_read.ts +42 -42
@@ -1,7 +1,10 @@
1
1
  import * as std from 'std';
2
2
  import * as win from 'win';
3
+ import * as os from 'os';
3
4
  import { bind } from '../lib/ffi/bind.js';
5
+ import { struct } from '../lib/ffi/struct.js';
4
6
  import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js';
7
+ import { POINT, RECT } from '../lib/windows/structs.js';
5
8
  // ABI 边界回归:覆盖 REMOVE_LIBFFI_PLAN.md §4.2 的维度,全部用真实 Win32/CRT
6
9
  // 函数(不新增测试专用导出)。缺函数的平台记 SKIP 而非 FAIL,避免环境差异误报。
7
10
  //
@@ -231,5 +234,57 @@ export const suite = {
231
234
  }
232
235
  else
233
236
  t.skipCase('GetDC/IsWindow missing');
237
+ t.section('by-value struct arg: <POINT> (slot value, not a pointer)');
238
+ if (has(usr, 'PtInRect') && has(usr, 'WindowFromPoint') && has(usr, 'ChildWindowFromPointEx')) {
239
+ // PtInRect(rect, pt):RECT 按指针、POINT 按值(x64 位置寄存器 / ia32 压 8 字节上栈)。
240
+ // 非对称矩形 {0,0,10,100} —— 若 x/y 在打包中交换,(99,5) 与 (5,99) 的判定会互换,
241
+ // 本段即可抓出「槽内是地址而非值」「字段顺序/位置错」两类 ABI 错误。
242
+ const ptInRect = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT> -> i32', { RECT, POINT });
243
+ const rectOut = RECT.alloc();
244
+ const rdv = new DataView(rectOut);
245
+ rdv.setInt32(8, 10, true); // right(left/top = 0,零填充)
246
+ rdv.setInt32(12, 100, true); // bottom
247
+ t.check('PtInRect inside (5,5)', 1, ptInRect(rectOut.ptr, { x: 5, y: 5 }));
248
+ t.check('PtInRect inside (5,99)', 1, ptInRect(rectOut.ptr, { x: 5, y: 99 }));
249
+ t.check('PtInRect outside (99,5) — swap-sensitive', 0, ptInRect(rectOut.ptr, { x: 99, y: 5 }));
250
+ t.check('PtInRect outside x=10 (right edge exclusive)', 0, ptInRect(rectOut.ptr, { x: 10, y: 5 }));
251
+ // 位置 1(首参即结构):WindowFromPoint(POINT) → 屏内必非 NULL
252
+ const getSystemMetrics = bind('user32.dll', 'GetSystemMetrics', 'i32 -> i32');
253
+ const cx = getSystemMetrics(0); // SM_CXSCREEN
254
+ const cy = getSystemMetrics(1); // SM_CYSCREEN
255
+ const windowFromPoint = bind('user32.dll', 'WindowFromPoint', '<POINT> -> <>ptr', { POINT });
256
+ t.checkTrue('WindowFromPoint(screen center) non-null', !!windowFromPoint({ x: cx >> 1, y: cy >> 1 }));
257
+ // 位置 2 + 其后参数推移(POINT 之后还有 UINT flags —— 槽宽算错会把 flags 读错位)
258
+ const childFromPointEx = bind('user32.dll', 'ChildWindowFromPointEx', '<>ptr <POINT> u32 -> <>ptr', { POINT });
259
+ const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> <>ptr');
260
+ const desk = getDesktopWindow();
261
+ t.checkTrue('ChildWindowFromPointEx inside → non-null', !!childFromPointEx(desk, { x: 1, y: 1 }, 0 /* CWP_ALL */));
262
+ t.check('ChildWindowFromPointEx outside → NULL', 0, childFromPointEx(desk, { x: cx + 100, y: cy + 100 }, 0 /* CWP_ALL */));
263
+ }
264
+ else
265
+ t.skipCase('PtInRect/WindowFromPoint/ChildWindowFromPointEx missing');
266
+ t.section('by-value struct >8B / odd size: x64 by-reference (caller temp)');
267
+ if (os.arch === 'x64' && has(crt, 'memcpy')) {
268
+ // MS x64:size ∉ {1,2,4,8} 的聚合体按引用传 —— 槽内是指向 caller 分配的
269
+ // 16 字节对齐临时副本的指针(单参数永不跨寄存器)。memcpy 第 2 参语义即
270
+ // 「源地址」,借它白盒验证整条 by-ref 路径:若槽内不是合法副本地址、或
271
+ // 字节未写进副本,拷回内容必错。ia32 按值恒把字节压栈(callee 读地址会
272
+ // 拿到垃圾值),故本段 x64 限定;ia32 侧由上面真实 API 段覆盖栈上传值。
273
+ const BYVAL12 = struct('BYVAL12', { a: 'u32', b: 'u32', c: 'u32' });
274
+ const BYVAL3 = struct('BYVAL3', { p: 'u8', q: 'u8', r: 'u8' });
275
+ const cp12 = bind('msvcrt.dll', 'memcpy', '<BYTE>ptr <BYVAL12> u32 -> <>ptr', { BYVAL12 });
276
+ const cp3 = bind('msvcrt.dll', 'memcpy', '<BYTE>ptr <BYVAL3> u32 -> <>ptr', { BYVAL3 });
277
+ const d12 = new PtrArrayBuffer(12);
278
+ cp12(d12, { a: 1, b: 0x11223344, c: 0x7fffffff }, 12);
279
+ const dv12 = new DataView(d12);
280
+ t.check('12B (>8B) by-ref word 0', 1, dv12.getUint32(0, true));
281
+ t.check('12B (>8B) by-ref word 1', 0x11223344, dv12.getUint32(4, true));
282
+ t.check('12B (>8B) by-ref word 2', 0x7fffffff, dv12.getUint32(8, true));
283
+ const d3 = new PtrArrayBuffer(3);
284
+ cp3(d3, { p: 0xaa, q: 0xbb, r: 0xcc }, 3);
285
+ t.check('3B (odd size) by-ref bytes', 'AA,BB,CC', Array.from(new Uint8Array(d3)).map(x => x.toString(16).toUpperCase()).join(','));
286
+ }
287
+ else
288
+ t.skipCase('by-ref path: x64-only whitebox (ia32 pushes value bytes verbatim)');
234
289
  },
235
290
  };
@@ -1,7 +1,10 @@
1
1
  import * as std from 'std'
2
2
  import * as win from 'win'
3
+ import * as os from 'os'
3
4
  import { bind } from '../lib/ffi/bind.js'
5
+ import { struct } from '../lib/ffi/struct.js'
4
6
  import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js'
7
+ import { POINT, RECT } from '../lib/windows/structs.js'
5
8
  import { Tester } from './test_helper.js'
6
9
 
7
10
  // ABI 边界回归:覆盖 REMOVE_LIBFFI_PLAN.md §4.2 的维度,全部用真实 Win32/CRT
@@ -214,5 +217,58 @@ export const suite = {
214
217
  t.checkTrue('GetDC(undefined) rejected', errUndef.includes('undefined'))
215
218
  t.check('IsWindow(NULL) = FALSE', 0, isWindow(NULL))
216
219
  } else t.skipCase('GetDC/IsWindow missing')
220
+
221
+ t.section('by-value struct arg: <POINT> (slot value, not a pointer)')
222
+ if (has(usr, 'PtInRect') && has(usr, 'WindowFromPoint') && has(usr, 'ChildWindowFromPointEx')) {
223
+ // PtInRect(rect, pt):RECT 按指针、POINT 按值(x64 位置寄存器 / ia32 压 8 字节上栈)。
224
+ // 非对称矩形 {0,0,10,100} —— 若 x/y 在打包中交换,(99,5) 与 (5,99) 的判定会互换,
225
+ // 本段即可抓出「槽内是地址而非值」「字段顺序/位置错」两类 ABI 错误。
226
+ const ptInRect = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT> -> i32', { RECT, POINT })
227
+ const rectOut = RECT.alloc()
228
+ const rdv = new DataView(rectOut)
229
+ rdv.setInt32(8, 10, true) // right(left/top = 0,零填充)
230
+ rdv.setInt32(12, 100, true) // bottom
231
+ t.check('PtInRect inside (5,5)', 1, ptInRect(rectOut.ptr, { x: 5, y: 5 }))
232
+ t.check('PtInRect inside (5,99)', 1, ptInRect(rectOut.ptr, { x: 5, y: 99 }))
233
+ t.check('PtInRect outside (99,5) — swap-sensitive', 0, ptInRect(rectOut.ptr, { x: 99, y: 5 }))
234
+ t.check('PtInRect outside x=10 (right edge exclusive)', 0, ptInRect(rectOut.ptr, { x: 10, y: 5 }))
235
+
236
+ // 位置 1(首参即结构):WindowFromPoint(POINT) → 屏内必非 NULL
237
+ const getSystemMetrics = bind('user32.dll', 'GetSystemMetrics', 'i32 -> i32')
238
+ const cx = getSystemMetrics(0) // SM_CXSCREEN
239
+ const cy = getSystemMetrics(1) // SM_CYSCREEN
240
+ const windowFromPoint = bind('user32.dll', 'WindowFromPoint', '<POINT> -> <>ptr', { POINT })
241
+ t.checkTrue('WindowFromPoint(screen center) non-null', !!windowFromPoint({ x: cx >> 1, y: cy >> 1 }))
242
+
243
+ // 位置 2 + 其后参数推移(POINT 之后还有 UINT flags —— 槽宽算错会把 flags 读错位)
244
+ const childFromPointEx = bind('user32.dll', 'ChildWindowFromPointEx', '<>ptr <POINT> u32 -> <>ptr', { POINT })
245
+ const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> <>ptr')
246
+ const desk = getDesktopWindow()
247
+ t.checkTrue('ChildWindowFromPointEx inside → non-null', !!childFromPointEx(desk, { x: 1, y: 1 }, 0 /* CWP_ALL */))
248
+ t.check('ChildWindowFromPointEx outside → NULL', 0, childFromPointEx(desk, { x: cx + 100, y: cy + 100 }, 0 /* CWP_ALL */))
249
+ } else t.skipCase('PtInRect/WindowFromPoint/ChildWindowFromPointEx missing')
250
+
251
+ t.section('by-value struct >8B / odd size: x64 by-reference (caller temp)')
252
+ if (os.arch === 'x64' && has(crt, 'memcpy')) {
253
+ // MS x64:size ∉ {1,2,4,8} 的聚合体按引用传 —— 槽内是指向 caller 分配的
254
+ // 16 字节对齐临时副本的指针(单参数永不跨寄存器)。memcpy 第 2 参语义即
255
+ // 「源地址」,借它白盒验证整条 by-ref 路径:若槽内不是合法副本地址、或
256
+ // 字节未写进副本,拷回内容必错。ia32 按值恒把字节压栈(callee 读地址会
257
+ // 拿到垃圾值),故本段 x64 限定;ia32 侧由上面真实 API 段覆盖栈上传值。
258
+ const BYVAL12 = struct('BYVAL12', { a: 'u32', b: 'u32', c: 'u32' })
259
+ const BYVAL3 = struct('BYVAL3', { p: 'u8', q: 'u8', r: 'u8' })
260
+ const cp12 = bind('msvcrt.dll', 'memcpy', '<BYTE>ptr <BYVAL12> u32 -> <>ptr', { BYVAL12 })
261
+ const cp3 = bind('msvcrt.dll', 'memcpy', '<BYTE>ptr <BYVAL3> u32 -> <>ptr', { BYVAL3 })
262
+ const d12 = new PtrArrayBuffer(12)
263
+ cp12(d12, { a: 1, b: 0x11223344, c: 0x7fffffff }, 12)
264
+ const dv12 = new DataView(d12)
265
+ t.check('12B (>8B) by-ref word 0', 1, dv12.getUint32(0, true))
266
+ t.check('12B (>8B) by-ref word 1', 0x11223344, dv12.getUint32(4, true))
267
+ t.check('12B (>8B) by-ref word 2', 0x7fffffff, dv12.getUint32(8, true))
268
+ const d3 = new PtrArrayBuffer(3)
269
+ cp3(d3, { p: 0xaa, q: 0xbb, r: 0xcc }, 3)
270
+ t.check('3B (odd size) by-ref bytes', 'AA,BB,CC',
271
+ Array.from(new Uint8Array(d3)).map(x => x.toString(16).toUpperCase()).join(','))
272
+ } else t.skipCase('by-ref path: x64-only whitebox (ia32 pushes value bytes verbatim)')
217
273
  },
218
274
  }
@@ -0,0 +1,116 @@
1
+ import { bind } from '../lib/ffi/bind.js';
2
+ import { struct, structArray } from '../lib/ffi/struct.js';
3
+ import { NULL, normToken, PtrArrayBuffer } from '../lib/ffi/ctype.js';
4
+ function expectType(_value) { }
5
+ // 运行时 throw 捕获:返回错误文本(无异常 → 空串)
6
+ function throws(fn) {
7
+ try {
8
+ fn();
9
+ return '';
10
+ }
11
+ catch (e) {
12
+ return String(e);
13
+ }
14
+ }
15
+ const POINT = struct('POINT', { x: 'i32', y: 'i32' });
16
+ const RECT = struct('RECT', { l: 'i32', t: 'i32', r: 'i32', b: 'i32' });
17
+ // 数组编解码对:encode 注册进 encoders 表后 <POINT>ptr 位收元素数组;
18
+ // decode 是出参数组读回(双态:buffer 自算/显式 count、裸地址 count 必填)
19
+ const POINTArr = structArray(POINT);
20
+ export const suite = {
21
+ name: 'ffi-array',
22
+ run: (t) => {
23
+ t.section("normToken: '*' is an ordinary out-of-lexicon char (same as '+')");
24
+ t.checkTrue("'<X*>ptr' rejected", throws(() => normToken('<POINT*>ptr')).includes('Unknown token'));
25
+ t.checkTrue("'*'+'!' rejected", throws(() => normToken('<POINT*>ptr!')).includes('Unknown token'));
26
+ t.checkTrue("'*'+'@' rejected", throws(() => normToken('<POINT*>ptr@Foo')).includes('Unknown token'));
27
+ t.checkTrue('bare <*>ptr rejected', throws(() => normToken('<*>ptr')).includes('Unknown token'));
28
+ t.checkTrue("'+' not in lexicon: generic Unknown token", throws(() => normToken('<POINT+>ptr')).includes('Unknown token'));
29
+ t.checkTrue('name charset tightened (non-identifier rejected)', throws(() => normToken('<1abc>ptr')).includes('Unknown token'));
30
+ t.check('plain <X>ptr unchanged', '<X>ptr', normToken('<X>ptr'));
31
+ t.check('!-form unchanged', '<X>ptr!', normToken('<X>ptr!'));
32
+ t.check('@-suffix stripped (enum annotation is type-layer only)', '<X>ptr', normToken('<X>ptr@Foo'));
33
+ t.section('type layer: registered encoder derives the array value domain');
34
+ // 注册数组 encoder 后:<POINT>ptr 位 = 非空元素数组(值域由 encoder 推导)∪ number 直通
35
+ const PolylineT = bind('gdi32.dll', 'Polyline', '<HDC>ptr! <POINT>ptr i32 -> i32', { POINT: POINTArr });
36
+ expectType();
37
+ expectType();
38
+ // 类型专用块(只过 tsc、不执行)
39
+ const typeOnly = () => {
40
+ // @ts-expect-error 单对象不是元素数组(encoder 值域收数组,不收裸 POINT)
41
+ PolylineT(0, { x: 1, y: 2 }, 1);
42
+ // @ts-expect-error 品牌拦错布局直传(RECT PAB 不是 POINT 位的值域——传 .ptr 走 number 直通)
43
+ PolylineT(0, RECT.alloc(), 1);
44
+ // @ts-expect-error decode 的裸地址形 count 必填(地址无长度信息)
45
+ POINTArr.decode(POINT.alloc().ptr);
46
+ };
47
+ t.checkTrue('type-only block wired', typeof typeOnly === 'function');
48
+ t.section('structArray.encode: contiguous branded materialization');
49
+ const pab = POINTArr.encode([{ x: 1, y: 2 }, { x: -3, y: 4 }]);
50
+ t.check('byteLength = n * size', 16, pab.byteLength);
51
+ const pdv = new DataView(pab);
52
+ t.check('[0].x', 1, pdv.getInt32(0, true));
53
+ t.check('[0].y', 2, pdv.getInt32(4, true));
54
+ t.check('[1].x', -3, pdv.getInt32(8, true));
55
+ t.check('[1].y', 4, pdv.getInt32(12, true));
56
+ t.checkTrue('PAB .ptr non-null (branded)', pab.ptr !== 0);
57
+ // fail-loud:值域刻意收普通数组(直传变量),空/非数组/单对象全靠运行时
58
+ // throw —— 任何路径都不产出 PAB(0)(其 .ptr 是"非 0 的 1 字节堆指针",会骗过
59
+ // C 的 NULL 防御)。
60
+ t.checkTrue('[] fail-loud (never produces PAB(0))', throws(() => POINTArr.encode([])).includes('non-empty'));
61
+ t.checkTrue('non-array fail-loud', throws(() => POINTArr.encode(123)).includes('non-empty'));
62
+ t.section('structArray.decode: self-evident / explicit count / address form');
63
+ // 自算形态:纯元素序列 buffer(alloc)byteLength/size 整除自证
64
+ t.check('self-computed n = byteLength/size', 3, POINTArr.decode(POINT.alloc(3)).length);
65
+ const rt = POINTArr.decode(POINTArr.encode([{ x: 1, y: -2 }, { x: 3, y: 4 }]));
66
+ t.check('roundtrip [0].x', 1, rt[0].x);
67
+ t.check('roundtrip [1].y', 4, rt[1].y);
68
+ // 混合缓冲(byteLength 非整数倍,如 EnumPrinters 的结构 + 尾部字符串)自算 fail-loud
69
+ t.checkTrue('non-multiple byteLength fail-loud', throws(() => POINTArr.decode(new PtrArrayBuffer(7))).includes('multiple'));
70
+ // 显式 count 越界防线:count*size 超 byteLength → 拦死(写错 count 的越界读入口)
71
+ t.checkTrue('count beyond byteLength fail-loud', throws(() => POINTArr.decode(new PtrArrayBuffer(12), 2)).includes('exceeds'));
72
+ t.checkTrue('negative count fail-loud', throws(() => POINTArr.decode(new PtrArrayBuffer(16), -1)).includes('non-negative'));
73
+ t.check('count = 0 → []', 0, POINTArr.decode(POINT.alloc(), 0).length);
74
+ // 裸地址形(C 返回的 T* / 预建缓冲 .ptr):count 必填,逐字节拷读
75
+ const addrBack = POINTArr.decode(pab.ptr, 2);
76
+ t.check('address form count', 2, addrBack.length);
77
+ t.check('address form [0].x', 1, addrBack[0].x);
78
+ t.checkTrue('address without count throws at runtime (dynamic path)', throws(() => POINTArr.decode(pab.ptr)).includes('count'));
79
+ t.section('StructDef.alloc(n): n-slot zero-filled (default 1)');
80
+ t.check('alloc() default = 1 slot', POINT.size, POINT.alloc().byteLength);
81
+ t.check('alloc(3) = 3 slots', POINT.size * 3, POINT.alloc(3).byteLength);
82
+ t.checkTrue('alloc slots zero-filled', new Int32Array(POINT.alloc(2))[3] === 0);
83
+ t.checkTrue('alloc(0) fail-loud (PAB(0) never escapes)', throws(() => POINT.alloc(0)).includes('n >= 1'));
84
+ t.checkTrue('alloc(-1) fail-loud', throws(() => POINT.alloc(-1)).includes('n >= 1'));
85
+ t.checkTrue('alloc(1.5) fail-loud', throws(() => POINT.alloc(1.5)).includes('n >= 1'));
86
+ t.section('StructDef.encode(v, buf, offset): positioned write primitive');
87
+ const big = POINT.alloc(2);
88
+ POINT.encode({ x: 7, y: -8 }, big, POINT.size); // 写入第 2 槽
89
+ const bdv = new DataView(big);
90
+ t.check('slot 0 stays zero', 0, bdv.getInt32(0, true));
91
+ t.check('slot 1.x written at offset', 7, bdv.getInt32(POINT.size, true));
92
+ t.check('slot 1.y written at offset', -8, bdv.getInt32(POINT.size + 4, true));
93
+ t.checkTrue('offset form returns the buf itself', POINT.encode({ x: 1, y: 2 }, big, 0) === big);
94
+ t.section('registered encoder dispatch: kernel32!IsBadReadPtr observes the pointer C gets');
95
+ const isBad = bind('kernel32.dll', 'IsBadReadPtr', '<POINT>ptr u64 -> i32', { POINT: POINTArr });
96
+ t.check('3-point array: whole 24-byte region readable', 0, isBad([{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }], 24n));
97
+ t.check('number passthrough: prebuilt PAB .ptr', 0, isBad(pab.ptr, 16n));
98
+ t.check('number passthrough: NULL literal', 1, isBad(NULL, 1n));
99
+ t.checkTrue('bare object rejected at encoder', throws(() => isBad({ x: 1, y: 2 }, 8n)).includes('non-empty'));
100
+ t.checkTrue('[] rejected at encoder (NULL stays explicit)', throws(() => isBad([], 8n)).includes('non-empty'));
101
+ t.checkTrue('null rejected with NULL hint', throws(() => isBad(null, 8n)).includes('NULL'));
102
+ t.section('content fidelity: msvcrt!memcpy reads encoder array source');
103
+ const memcpy = bind('msvcrt.dll', 'memcpy', '<POINT>ptr <POINT>ptr u64 -> <>ptr', { POINT: POINTArr });
104
+ const dst = POINT.alloc();
105
+ memcpy(dst.ptr, [{ x: 7, y: -8 }], 8n);
106
+ const back = POINT.decode(dst.ptr);
107
+ t.check('copied x', 7, back.x);
108
+ t.check('copied y', -8, back.y);
109
+ t.section("decode(alloc()): '!' unfilled field fails (C did not fill)");
110
+ const OutH = struct('OutH', { h: '<HANDLE>ptr!' });
111
+ t.checkTrue("'!' 0 slot throws on decode", throws(() => OutH.decode(OutH.alloc())).includes("'!'"));
112
+ t.section('bind-time lexicon checks (fail at module load)');
113
+ t.checkTrue('<POINT+>ptr rejected at bind (not in lexicon)', throws(() => bind('kernel32.dll', 'IsBadReadPtr', '<POINT+>ptr u64 -> i32', { POINT: POINTArr })).includes('Unknown token'));
114
+ t.checkTrue('<POINT*>ptr rejected at bind (out-of-lexicon)', throws(() => bind('kernel32.dll', 'IsBadReadPtr', '<POINT*>ptr u64 -> i32', { POINT: POINTArr })).includes('Unknown token'));
115
+ }
116
+ };
@@ -0,0 +1,141 @@
1
+ import { Tester } from './test_helper.js'
2
+ import { bind } from '../lib/ffi/bind.js'
3
+ import { struct, structArray } from '../lib/ffi/struct.js'
4
+ import { NULL, normToken, PtrArrayBuffer, type MaybePtr, type Ptr } from '../lib/ffi/ctype.js'
5
+
6
+ // 编译期断言工具(仅类型层,运行时无开销)
7
+ type Equal<A, B> = (<G>() => G extends A ? 1 : 2) extends (<G>() => G extends B ? 1 : 2) ? true : false
8
+ function expectType<T extends true>(_value?: T): void {}
9
+
10
+ // 运行时 throw 捕获:返回错误文本(无异常 → 空串)
11
+ function throws(fn: () => unknown): string {
12
+ try { fn(); return '' } catch (e) { return String(e) }
13
+ }
14
+
15
+ const POINT = struct('POINT', { x: 'i32', y: 'i32' })
16
+ const RECT = struct('RECT', { l: 'i32', t: 'i32', r: 'i32', b: 'i32' })
17
+ // 数组编解码对:encode 注册进 encoders 表后 <POINT>ptr 位收元素数组;
18
+ // decode 是出参数组读回(双态:buffer 自算/显式 count、裸地址 count 必填)
19
+ const POINTArr = structArray(POINT)
20
+
21
+ export const suite = {
22
+ name: 'ffi-array',
23
+ run: (t: Tester) => {
24
+ t.section("normToken: '*' is an ordinary out-of-lexicon char (same as '+')")
25
+ t.checkTrue("'<X*>ptr' rejected", throws(() => normToken('<POINT*>ptr')).includes('Unknown token'))
26
+ t.checkTrue("'*'+'!' rejected", throws(() => normToken('<POINT*>ptr!')).includes('Unknown token'))
27
+ t.checkTrue("'*'+'@' rejected", throws(() => normToken('<POINT*>ptr@Foo')).includes('Unknown token'))
28
+ t.checkTrue('bare <*>ptr rejected', throws(() => normToken('<*>ptr')).includes('Unknown token'))
29
+ t.checkTrue("'+' not in lexicon: generic Unknown token", throws(() => normToken('<POINT+>ptr')).includes('Unknown token'))
30
+ t.checkTrue('name charset tightened (non-identifier rejected)',
31
+ throws(() => normToken('<1abc>ptr')).includes('Unknown token'))
32
+ t.check('plain <X>ptr unchanged', '<X>ptr', normToken('<X>ptr'))
33
+ t.check('!-form unchanged', '<X>ptr!', normToken('<X>ptr!'))
34
+ t.check('@-suffix stripped (enum annotation is type-layer only)', '<X>ptr', normToken('<X>ptr@Foo'))
35
+
36
+ t.section('type layer: registered encoder derives the array value domain')
37
+ // 注册数组 encoder 后:<POINT>ptr 位 = 非空元素数组(值域由 encoder 推导)∪ number 直通
38
+ const PolylineT = bind('gdi32.dll', 'Polyline', '<HDC>ptr! <POINT>ptr i32 -> i32', { POINT: POINTArr })
39
+ expectType<Equal<Parameters<typeof PolylineT>[1],
40
+ readonly { readonly x?: number | undefined; readonly y?: number | undefined }[] | MaybePtr<'POINT'>>>()
41
+ expectType<Equal<Parameters<typeof PolylineT>[0], Ptr<'HDC'>>>()
42
+ // 类型专用块(只过 tsc、不执行)
43
+ const typeOnly: unknown = () => {
44
+ // @ts-expect-error 单对象不是元素数组(encoder 值域收数组,不收裸 POINT)
45
+ PolylineT(0 as Ptr<'HDC'>, { x: 1, y: 2 }, 1)
46
+ // @ts-expect-error 品牌拦错布局直传(RECT PAB 不是 POINT 位的值域——传 .ptr 走 number 直通)
47
+ PolylineT(0 as Ptr<'HDC'>, RECT.alloc(), 1)
48
+ // @ts-expect-error decode 的裸地址形 count 必填(地址无长度信息)
49
+ POINTArr.decode(POINT.alloc().ptr)
50
+ }
51
+ t.checkTrue('type-only block wired', typeof typeOnly === 'function')
52
+
53
+ t.section('structArray.encode: contiguous branded materialization')
54
+ const pab = POINTArr.encode([{ x: 1, y: 2 }, { x: -3, y: 4 }])
55
+ t.check('byteLength = n * size', 16, pab.byteLength)
56
+ const pdv = new DataView(pab)
57
+ t.check('[0].x', 1, pdv.getInt32(0, true))
58
+ t.check('[0].y', 2, pdv.getInt32(4, true))
59
+ t.check('[1].x', -3, pdv.getInt32(8, true))
60
+ t.check('[1].y', 4, pdv.getInt32(12, true))
61
+ t.checkTrue('PAB .ptr non-null (branded)', pab.ptr !== 0)
62
+ // fail-loud:值域刻意收普通数组(直传变量),空/非数组/单对象全靠运行时
63
+ // throw —— 任何路径都不产出 PAB(0)(其 .ptr 是"非 0 的 1 字节堆指针",会骗过
64
+ // C 的 NULL 防御)。
65
+ t.checkTrue('[] fail-loud (never produces PAB(0))',
66
+ throws(() => POINTArr.encode([])).includes('non-empty'))
67
+ t.checkTrue('non-array fail-loud',
68
+ throws(() => POINTArr.encode(123 as never)).includes('non-empty'))
69
+
70
+ t.section('structArray.decode: self-evident / explicit count / address form')
71
+ // 自算形态:纯元素序列 buffer(alloc)byteLength/size 整除自证
72
+ t.check('self-computed n = byteLength/size', 3, POINTArr.decode(POINT.alloc(3)).length)
73
+ const rt = POINTArr.decode(POINTArr.encode([{ x: 1, y: -2 }, { x: 3, y: 4 }]))
74
+ t.check('roundtrip [0].x', 1, rt[0]!.x)
75
+ t.check('roundtrip [1].y', 4, rt[1]!.y)
76
+ // 混合缓冲(byteLength 非整数倍,如 EnumPrinters 的结构 + 尾部字符串)自算 fail-loud
77
+ t.checkTrue('non-multiple byteLength fail-loud',
78
+ throws(() => POINTArr.decode(new PtrArrayBuffer(7))).includes('multiple'))
79
+ // 显式 count 越界防线:count*size 超 byteLength → 拦死(写错 count 的越界读入口)
80
+ t.checkTrue('count beyond byteLength fail-loud',
81
+ throws(() => POINTArr.decode(new PtrArrayBuffer(12), 2)).includes('exceeds'))
82
+ t.checkTrue('negative count fail-loud',
83
+ throws(() => POINTArr.decode(new PtrArrayBuffer(16), -1)).includes('non-negative'))
84
+ t.check('count = 0 → []', 0, POINTArr.decode(POINT.alloc(), 0).length)
85
+ // 裸地址形(C 返回的 T* / 预建缓冲 .ptr):count 必填,逐字节拷读
86
+ const addrBack = POINTArr.decode(pab.ptr, 2)
87
+ t.check('address form count', 2, addrBack.length)
88
+ t.check('address form [0].x', 1, addrBack[0]!.x)
89
+ t.checkTrue('address without count throws at runtime (dynamic path)',
90
+ throws(() => (POINTArr.decode as unknown as (p: number) => unknown[])(pab.ptr)).includes('count'))
91
+
92
+ t.section('StructDef.alloc(n): n-slot zero-filled (default 1)')
93
+ t.check('alloc() default = 1 slot', POINT.size, POINT.alloc().byteLength)
94
+ t.check('alloc(3) = 3 slots', POINT.size * 3, POINT.alloc(3).byteLength)
95
+ t.checkTrue('alloc slots zero-filled', new Int32Array(POINT.alloc(2))[3] === 0)
96
+ t.checkTrue('alloc(0) fail-loud (PAB(0) never escapes)',
97
+ throws(() => POINT.alloc(0)).includes('n >= 1'))
98
+ t.checkTrue('alloc(-1) fail-loud', throws(() => POINT.alloc(-1)).includes('n >= 1'))
99
+ t.checkTrue('alloc(1.5) fail-loud', throws(() => POINT.alloc(1.5)).includes('n >= 1'))
100
+
101
+ t.section('StructDef.encode(v, buf, offset): positioned write primitive')
102
+ const big = POINT.alloc(2)
103
+ POINT.encode({ x: 7, y: -8 }, big, POINT.size) // 写入第 2 槽
104
+ const bdv = new DataView(big)
105
+ t.check('slot 0 stays zero', 0, bdv.getInt32(0, true))
106
+ t.check('slot 1.x written at offset', 7, bdv.getInt32(POINT.size, true))
107
+ t.check('slot 1.y written at offset', -8, bdv.getInt32(POINT.size + 4, true))
108
+ t.checkTrue('offset form returns the buf itself', POINT.encode({ x: 1, y: 2 }, big, 0) === big)
109
+
110
+ t.section('registered encoder dispatch: kernel32!IsBadReadPtr observes the pointer C gets')
111
+ const isBad = bind('kernel32.dll', 'IsBadReadPtr', '<POINT>ptr u64 -> i32', { POINT: POINTArr })
112
+ t.check('3-point array: whole 24-byte region readable', 0, isBad([{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }], 24n))
113
+ t.check('number passthrough: prebuilt PAB .ptr', 0, isBad(pab.ptr, 16n))
114
+ t.check('number passthrough: NULL literal', 1, isBad(NULL, 1n))
115
+ t.checkTrue('bare object rejected at encoder',
116
+ throws(() => isBad({ x: 1, y: 2 } as never, 8n)).includes('non-empty'))
117
+ t.checkTrue('[] rejected at encoder (NULL stays explicit)',
118
+ throws(() => isBad([], 8n)).includes('non-empty'))
119
+ t.checkTrue('null rejected with NULL hint',
120
+ throws(() => isBad(null as never, 8n)).includes('NULL'))
121
+
122
+ t.section('content fidelity: msvcrt!memcpy reads encoder array source')
123
+ const memcpy = bind('msvcrt.dll', 'memcpy', '<POINT>ptr <POINT>ptr u64 -> <>ptr', { POINT: POINTArr })
124
+ const dst = POINT.alloc()
125
+ memcpy(dst.ptr, [{ x: 7, y: -8 }], 8n)
126
+ const back = POINT.decode(dst.ptr)
127
+ t.check('copied x', 7, back.x)
128
+ t.check('copied y', -8, back.y)
129
+
130
+ t.section("decode(alloc()): '!' unfilled field fails (C did not fill)")
131
+ const OutH = struct('OutH', { h: '<HANDLE>ptr!' })
132
+ t.checkTrue("'!' 0 slot throws on decode",
133
+ throws(() => OutH.decode(OutH.alloc())).includes("'!'"))
134
+
135
+ t.section('bind-time lexicon checks (fail at module load)')
136
+ t.checkTrue('<POINT+>ptr rejected at bind (not in lexicon)',
137
+ throws(() => bind('kernel32.dll', 'IsBadReadPtr', '<POINT+>ptr u64 -> i32', { POINT: POINTArr })).includes('Unknown token'))
138
+ t.checkTrue('<POINT*>ptr rejected at bind (out-of-lexicon)',
139
+ throws(() => bind('kernel32.dll', 'IsBadReadPtr', '<POINT*>ptr u64 -> i32', { POINT: POINTArr })).includes('Unknown token'))
140
+ }
141
+ }
@@ -289,6 +289,63 @@ export const suite = {
289
289
  t.checkTrue('GetWindowRect(hwnd, RECT.alloc().ptr) succeeds', getWindowRect(desktop, rectOut.ptr) !== 0);
290
290
  const rectV = RECT.decode(rectOut);
291
291
  t.checkTrue('RECT.decode(encode()) decodes out-param', rectV.right > rectV.left && rectV.bottom > rectV.top);
292
+ t.section('by-value struct <N>: lexicon / type layer / bind-time fail-loud');
293
+ // '<N>' 裸品牌 = 按值传参位(<N>ptr 传地址)。无修饰可剥:带 ! / 尾缀 / 空名 /
294
+ // 数字开头的形正则不匹配,落绑定期通用 Unknown token(与 '<X+>ptr' 同策略)。
295
+ t.checkTrue("normToken('<POINT>') 原样归一", normToken('<POINT>') === '<POINT>');
296
+ for (const bad of ['<POINT>!', '<POINT>x', '<>', '<1abc>']) {
297
+ try {
298
+ normToken(bad);
299
+ t.checkTrue(`normToken 拒 '${bad}'`, false);
300
+ }
301
+ catch (e) {
302
+ t.checkTrue(`normToken 拒 '${bad}'`, String(e).includes('Unknown token'));
303
+ }
304
+ }
305
+ // 类型层:未注册布局的按值位塌 never(LE={} → L['POINT'] = never,同拼错 token 策略)
306
+ expectType();
307
+ // 注册布局:值域 = encode 入参形(对象),无 number 地址直通;ptr 位对照有
308
+ const typeOnly = () => {
309
+ const byVal = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT> -> i32', { RECT, POINT });
310
+ expectType();
311
+ const byPtr = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT>ptr -> i32', { RECT, POINT });
312
+ expectType();
313
+ // @ts-expect-error 按值位不收 number(地址词汇属 <N>ptr 位)
314
+ byVal(NULL, 42);
315
+ // @ts-expect-error 按值位收对象、不收数组
316
+ byVal(NULL, [1, 2]);
317
+ return 0;
318
+ };
319
+ t.checkTrue('type-only block wired', typeof typeOnly === 'function');
320
+ // 绑定期 fail-loud:模块加载即炸,不留到首个调用
321
+ try {
322
+ bind('gdi32.dll', 'DeleteObject', '<PTMISS> -> i32');
323
+ t.checkTrue('by-value 未注册布局 fail-loud', false);
324
+ }
325
+ catch (e) {
326
+ t.checkTrue('by-value 未注册布局 fail-loud', String(e).includes('<PTMISS>'));
327
+ }
328
+ try {
329
+ bind('gdi32.dll', 'DeleteObject', '<WCHAR> -> i32', { WCHAR });
330
+ t.checkTrue('by-value 非结构 codec (无 size) fail-loud', false);
331
+ }
332
+ catch (e) {
333
+ t.checkTrue('by-value 非结构 codec (无 size) fail-loud', String(e).includes('size'));
334
+ }
335
+ try {
336
+ bind('gdi32.dll', 'DeleteObject', 'i32 -> <POINT>', { POINT });
337
+ t.checkTrue('by-value 返回位 fail-loud', false);
338
+ }
339
+ catch (e) {
340
+ t.checkTrue('by-value 返回位 fail-loud', String(e).includes('return position'));
341
+ }
342
+ try {
343
+ closure('<POINT> -> i32', () => 0);
344
+ t.checkTrue('by-value 位 closure fail-loud', false);
345
+ }
346
+ catch (e) {
347
+ t.checkTrue('by-value 位 closure fail-loud', String(e).includes('closure'));
348
+ }
292
349
  // 成员 '<RECT>ptr':decode 得到 Ptr<'RECT'>,可直接喂 <RECT>ptr 形参(品牌在类型层流动)。
293
350
  const RECTPTR = struct('RECTPTR', { r: '<RECT>ptr' });
294
351
  const box = RECTPTR.decode(RECTPTR.encode({ r: rectOut.ptr }));
@@ -334,6 +391,10 @@ export const suite = {
334
391
  // (返回若含 Ptr<'TM'> 分支——无 decode 的旧行为——此访问编译不过)
335
392
  if (tmObj !== 0)
336
393
  expectType(true);
394
+ // gmtime 同构第二例:UTC 锚点确定(time_t=0 → 1970-01-01,不依赖 TZ 环境)
395
+ const gmtimeDec = bind('msvcrt.dll', 'gmtime', '<BYTE>ptr -> <TM>ptr', undefined, { TM });
396
+ const gmObj = gmtimeDec(tbuf);
397
+ t.checkTrue('gmtime + decoders auto-decodes (UTC anchor tm_year=70, tm_mday=1)', gmObj !== 0 && gmObj.tm_year === 70 && gmObj.tm_mday === 1);
337
398
  expectType(true);
338
399
  expectType(true);
339
400
  expectType(true);
@@ -362,6 +423,27 @@ export const suite = {
362
423
  outOnlyErr = String(e);
363
424
  }
364
425
  t.checkTrue('out-only codec rejected at encode time', outOnlyErr.includes('no encoder'));
426
+ // native readBytes:批量 memcpy 往返 + fail-loud 守卫(decode 指针分支的新底层)
427
+ const rbB = new PtrArrayBuffer(8);
428
+ new Uint8Array(rbB).set([1, 2, 3, 4, 250, 251, 252, 253]);
429
+ const rbCopy = ffi.readBytes(rbB.ptr, 8);
430
+ t.check('readBytes(ptr, 8) memcpy roundtrip', '1,2,3,4,250,251,252,253', new Uint8Array(rbCopy).join(','));
431
+ let rbNullErr = '';
432
+ try {
433
+ ffi.readBytes(0, 4);
434
+ }
435
+ catch (e) {
436
+ rbNullErr = String(e);
437
+ }
438
+ t.checkTrue('readBytes(0, n) fails loud (null pointer)', rbNullErr.includes('null pointer'));
439
+ let rbNegErr = '';
440
+ try {
441
+ ffi.readBytes(rbB.ptr, -1);
442
+ }
443
+ catch (e) {
444
+ rbNegErr = String(e);
445
+ }
446
+ t.checkTrue('readBytes(p, -1) fails loud (RangeError)', rbNegErr.includes('RangeError'));
365
447
  t.section('closures: direct ABI drive via ffiCall');
366
448
  const addClos = closure('i32 i32 -> i32', (a, b) => a + b);
367
449
  const r1 = new ArrayBuffer(8);
@@ -286,6 +286,58 @@ export const suite = {
286
286
  const rectV = RECT.decode(rectOut)
287
287
  t.checkTrue('RECT.decode(encode()) decodes out-param', rectV.right > rectV.left && rectV.bottom > rectV.top)
288
288
 
289
+ t.section('by-value struct <N>: lexicon / type layer / bind-time fail-loud')
290
+ // '<N>' 裸品牌 = 按值传参位(<N>ptr 传地址)。无修饰可剥:带 ! / 尾缀 / 空名 /
291
+ // 数字开头的形正则不匹配,落绑定期通用 Unknown token(与 '<X+>ptr' 同策略)。
292
+ t.checkTrue("normToken('<POINT>') 原样归一", normToken('<POINT>') === '<POINT>')
293
+ for (const bad of ['<POINT>!', '<POINT>x', '<>', '<1abc>']) {
294
+ try { normToken(bad); t.checkTrue(`normToken 拒 '${bad}'`, false) }
295
+ catch (e) { t.checkTrue(`normToken 拒 '${bad}'`, String(e).includes('Unknown token')) }
296
+ }
297
+
298
+ // 类型层:未注册布局的按值位塌 never(LE={} → L['POINT'] = never,同拼错 token 策略)
299
+ expectType<Equal<ParamsOf<'<POINT> -> i32'>[0], never>>()
300
+ // 注册布局:值域 = encode 入参形(对象),无 number 地址直通;ptr 位对照有
301
+ const typeOnly: unknown = () => {
302
+ const byVal = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT> -> i32', { RECT, POINT })
303
+ expectType<Equal<Extract<Parameters<typeof byVal>[1], number>, never>>()
304
+ const byPtr = bind('user32.dll', 'PtInRect', '<RECT>ptr <POINT>ptr -> i32', { RECT, POINT })
305
+ expectType<Extract<Parameters<typeof byPtr>[1], number> extends never ? false : true>()
306
+ // @ts-expect-error 按值位不收 number(地址词汇属 <N>ptr 位)
307
+ byVal(NULL, 42)
308
+ // @ts-expect-error 按值位收对象、不收数组
309
+ byVal(NULL, [1, 2])
310
+ return 0
311
+ }
312
+ t.checkTrue('type-only block wired', typeof typeOnly === 'function')
313
+
314
+ // 绑定期 fail-loud:模块加载即炸,不留到首个调用
315
+ try {
316
+ bind('gdi32.dll', 'DeleteObject', '<PTMISS> -> i32')
317
+ t.checkTrue('by-value 未注册布局 fail-loud', false)
318
+ } catch (e) {
319
+ t.checkTrue('by-value 未注册布局 fail-loud', String(e).includes('<PTMISS>'))
320
+ }
321
+ try {
322
+ bind('gdi32.dll', 'DeleteObject', '<WCHAR> -> i32', { WCHAR })
323
+ t.checkTrue('by-value 非结构 codec (无 size) fail-loud', false)
324
+ } catch (e) {
325
+ t.checkTrue('by-value 非结构 codec (无 size) fail-loud', String(e).includes('size'))
326
+ }
327
+ try {
328
+ bind('gdi32.dll', 'DeleteObject', 'i32 -> <POINT>', { POINT })
329
+ t.checkTrue('by-value 返回位 fail-loud', false)
330
+ } catch (e) {
331
+ t.checkTrue('by-value 返回位 fail-loud', String(e).includes('return position'))
332
+ }
333
+ try {
334
+ closure('<POINT> -> i32', () => 0)
335
+ t.checkTrue('by-value 位 closure fail-loud', false)
336
+ } catch (e) {
337
+ t.checkTrue('by-value 位 closure fail-loud', String(e).includes('closure'))
338
+ }
339
+
340
+
289
341
  // 成员 '<RECT>ptr':decode 得到 Ptr<'RECT'>,可直接喂 <RECT>ptr 形参(品牌在类型层流动)。
290
342
  const RECTPTR = struct('RECTPTR', {r: '<RECT>ptr'})
291
343
  const box = RECTPTR.decode(RECTPTR.encode({ r: rectOut.ptr }))
@@ -336,6 +388,12 @@ export const suite = {
336
388
  // (返回若含 Ptr<'TM'> 分支——无 decode 的旧行为——此访问编译不过)
337
389
  if (tmObj !== 0) expectType<Equal<typeof tmObj.tm_sec, number>>(true)
338
390
 
391
+ // gmtime 同构第二例:UTC 锚点确定(time_t=0 → 1970-01-01,不依赖 TZ 环境)
392
+ const gmtimeDec = bind('msvcrt.dll', 'gmtime', '<BYTE>ptr -> <TM>ptr', undefined, { TM })
393
+ const gmObj = gmtimeDec(tbuf)
394
+ t.checkTrue('gmtime + decoders auto-decodes (UTC anchor tm_year=70, tm_mday=1)',
395
+ gmObj !== 0 && gmObj.tm_year === 70 && gmObj.tm_mday === 1)
396
+
339
397
  // Codec:返回位类型断言(decoders 显式 / 缺省回退 encoders)
340
398
  // 断言「替换语义」两半:解码结果 = 结构对象 | 0(Ptr 品牌被替换掉);
341
399
  // 无 decode 保留 0 | Ptr 品牌形态。
@@ -377,6 +435,19 @@ export const suite = {
377
435
  }
378
436
  t.checkTrue('out-only codec rejected at encode time', outOnlyErr.includes('no encoder'))
379
437
 
438
+ // native readBytes:批量 memcpy 往返 + fail-loud 守卫(decode 指针分支的新底层)
439
+ const rbB = new PtrArrayBuffer(8)
440
+ new Uint8Array(rbB).set([1, 2, 3, 4, 250, 251, 252, 253])
441
+ const rbCopy = ffi.readBytes(rbB.ptr, 8)
442
+ t.check('readBytes(ptr, 8) memcpy roundtrip',
443
+ '1,2,3,4,250,251,252,253', new Uint8Array(rbCopy).join(','))
444
+ let rbNullErr = ''
445
+ try { ffi.readBytes(0, 4) } catch (e) { rbNullErr = String(e) }
446
+ t.checkTrue('readBytes(0, n) fails loud (null pointer)', rbNullErr.includes('null pointer'))
447
+ let rbNegErr = ''
448
+ try { ffi.readBytes(rbB.ptr, -1) } catch (e) { rbNegErr = String(e) }
449
+ t.checkTrue('readBytes(p, -1) fails loud (RangeError)', rbNegErr.includes('RangeError'))
450
+
380
451
  t.section('closures: direct ABI drive via ffiCall')
381
452
  const addClos = closure('i32 i32 -> i32', (a, b) => a + b)
382
453
  const r1 = new ArrayBuffer(8)