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.
- package/README.en.md +18 -24
- package/README.md +17 -23
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20155 -1420
- 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 +1424 -1180
- package/examples/pdf_viewer.tsx +16 -67
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1418 -1075
- package/examples/test_react_lazy.js +1391 -1059
- package/examples/test_react_listbox.js +1390 -1058
- package/examples/test_root_window.js +1384 -1052
- package/examples/test_tab.js +1391 -1059
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +219 -0
- package/lib/certs.ts +229 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +88 -7
- package/lib/fetch.ts +73 -7
- package/lib/ffi/bind.js +441 -0
- package/lib/ffi/bind.ts +539 -0
- package/lib/ffi/ctype.js +194 -0
- package/lib/ffi/ctype.ts +369 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +490 -0
- package/lib/ffi/struct.ts +753 -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 +50 -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 +5 -3
- package/lib/react-qw/index.ts +8 -3
- 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 +20 -23
- package/lib/text-measure.ts +22 -26
- package/lib/websocket.js +44 -4
- package/lib/websocket.ts +40 -4
- package/lib/windows/advapi32.js +53 -0
- package/lib/windows/advapi32.ts +64 -0
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +19 -0
- package/lib/windows/comdlg32.js +8 -0
- package/lib/windows/comdlg32.ts +10 -0
- package/lib/windows/gdi32.js +392 -0
- package/lib/windows/gdi32.ts +420 -0
- package/lib/windows/kernel32.js +262 -0
- package/lib/windows/kernel32.ts +291 -0
- package/lib/windows/msimg32.js +38 -0
- package/lib/windows/msimg32.ts +45 -0
- package/lib/windows/ntdll.js +9 -0
- package/lib/windows/ntdll.ts +11 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/psapi.js +32 -0
- package/lib/windows/psapi.ts +37 -0
- package/lib/windows/shell32.js +13 -0
- package/lib/windows/shell32.ts +16 -0
- package/lib/windows/structs.js +519 -0
- package/lib/windows/structs.ts +566 -0
- package/lib/windows/user32.js +572 -0
- package/lib/windows/user32.ts +607 -0
- package/lib/windows/version.js +29 -0
- package/lib/windows/version.ts +35 -0
- package/lib/windows/winspool.js +71 -0
- package/lib/windows/winspool.ts +80 -0
- package/package.json +1 -1
- package/quickwin.d.ts +31 -40
- package/quickwin_const.d.ts +137 -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 +19 -4
- package/test/run.ts +20 -5
- package/test/test_basic_fetch.js +29 -0
- package/test/test_basic_fetch.ts +26 -0
- package/test/test_ffi.js +27 -45
- package/test/test_ffi.ts +35 -54
- package/test/test_ffi_abi.js +290 -0
- package/test/test_ffi_abi.ts +274 -0
- package/test/test_ffi_array.js +116 -0
- package/test/test_ffi_array.ts +141 -0
- package/test/test_ffi_bind.js +620 -8
- package/test/test_ffi_bind.ts +630 -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_files.js +102 -0
- package/test/test_files.ts +121 -0
- package/test/test_gdi_draw.js +681 -0
- package/test/test_gdi_draw.ts +725 -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 +74 -11
- package/test/test_net_fetch.ts +73 -10
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_printer.js +137 -0
- package/test/test_printer.ts +148 -0
- package/test/test_proc.js +141 -0
- package/test/test_proc.ts +163 -0
- package/test/test_react_complex.js +1401 -1069
- package/test/test_react_counter.js +1391 -1059
- package/test/test_react_flex.js +1411 -1072
- package/test/test_react_hidden.js +1394 -1062
- package/test/test_react_listview_link.js +1391 -1059
- package/test/test_react_notify.js +18638 -0
- package/test/test_react_render.js +1391 -1059
- package/test/test_registry.js +74 -0
- package/test/test_registry.ts +95 -0
- package/test/test_sysinfo.js +122 -0
- package/test/test_sysinfo.ts +141 -0
- package/test/test_wolfssl_read.js +45 -43
- package/test/test_wolfssl_read.ts +42 -40
- 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/lib/ffi/bind.ts
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import * as ffi from 'ffi'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as std from 'std'
|
|
4
|
+
import * as win from 'win'
|
|
5
|
+
import '../text-codec.js'
|
|
6
|
+
import { type C_BasicType, C_BasicType_No_Void, C_BasicType_Token, C_BasicType_Token_No_Void, cTokenToType, isCPtrToken, isCValToken, valName, ArgJsTypeOfToken, RetJsTypeOfToken, normToken, PTR_SIZE, ptrName, type Ptr, readScalar, writeSlot, PtrArrayBuffer } from './ctype.js'
|
|
7
|
+
|
|
8
|
+
// 形状刻意是「方法」而非裸函数:ffi-struct 的 struct()/union() 结果(StructDef)
|
|
9
|
+
// 结构上即满足 encode 重载(无 buf 新建带品牌 .ptr 的 PtrArrayBuffer / 带 buf 写入既有
|
|
10
|
+
// buffer 返回其本身),用户布局可直接透传。
|
|
11
|
+
// Codec 三件全可选(可只入参 / 只出参):encode JS→native 序列化、decode 读回 JS 值、
|
|
12
|
+
// alloc 出参预分配(分配描述符 T 由各 codec 自定——内建收元素数/字节数;StructDef 不提供
|
|
13
|
+
// alloc —— 其 encode() 无 buf 形态已覆盖零 buffer 分配,返回值自带 .ptr)。
|
|
14
|
+
// decode 双态单参:native 品牌指针(返回位分派 / bufferPtr)逐字节读、调用方持有的
|
|
15
|
+
// ArrayBuffer 直读。刻意没有 offset——out 参数恒从 buffer 起点读,嵌套偏移由布局
|
|
16
|
+
// 字段(doDecode 的 base)承担,顶层再收 offset 只会掩盖"读错位置"这类错误。
|
|
17
|
+
// 内建 WCHAR/BYTE 的 encode/alloc 一律返回 PtrArrayBuffer:既是 decode 的直读 buffer,
|
|
18
|
+
// .ptr 又带品牌直接喂 <N>ptr 形参——不再有 { buf, ptr } 结构包装。
|
|
19
|
+
type Codec<N extends string = string, V = unknown, T = unknown> = {
|
|
20
|
+
// encode 顶层不收 offset(与 decode 对齐):PAB 的 .ptr 恒指向起点,写入位置由
|
|
21
|
+
// buf 自身表达;偏移写入(数组物化原语)由 StructDef.encode(v, buf, offset) 承担。
|
|
22
|
+
encode?(v: V, buf?: PtrArrayBuffer<any>): PtrArrayBuffer<any>
|
|
23
|
+
decode?(p: Ptr<N>| ArrayBuffer): V
|
|
24
|
+
alloc?(a: T): PtrArrayBuffer<N>
|
|
25
|
+
}
|
|
26
|
+
// 约束层三件与 StructDef 形状一致,def 可整体透传;提取层按键存在性走(见下)。
|
|
27
|
+
// 类型层硬约束:encode/alloc 只返回 PtrArrayBuffer —— 裸 ArrayBuffer 出不了
|
|
28
|
+
// codec(callPacked 参数位统一取 .ptr,调用方拿到的也是 .ptr,.buf 词汇从 codec 出口消失);
|
|
29
|
+
// NULL(0)是引擎层词汇(number 直通世界),不作为 codec 出口形态。
|
|
30
|
+
export type CodecMap = Record<string, {
|
|
31
|
+
encode?(v: any, buf?: PtrArrayBuffer<any>): PtrArrayBuffer<any>
|
|
32
|
+
decode?(p: any): any
|
|
33
|
+
alloc?(a: any): PtrArrayBuffer<any>
|
|
34
|
+
}>
|
|
35
|
+
|
|
36
|
+
// 按值位('<N>')消费的布局形态:size(决定槽宽与「传值 / 按引用」分派)+
|
|
37
|
+
// encode(v, buf, offset)(写入任意 buffer 的既有偏移——argFrame 槽内写入、
|
|
38
|
+
// 16 对齐临时副本两条路径共用)。StructDef 结构上即满足,内建 WCHAR/BYTE 无 size。
|
|
39
|
+
type StructValCodec = {
|
|
40
|
+
size: number
|
|
41
|
+
encode(v: unknown, buf: ArrayBuffer, offset: number): unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// CodecMap 的值是结构形(size 只在 StructDef 上、不属 Codec 契约),统一走 unknown
|
|
45
|
+
// 二跳提取;存在性与 size 由 makeFn 绑定期校验。
|
|
46
|
+
function valCodec(name: string, encoders: CodecMap): StructValCodec | undefined {
|
|
47
|
+
return encoders[name] as unknown as StructValCodec | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const _dllCache: Map<string, win.HMODULE> = new Map()
|
|
51
|
+
|
|
52
|
+
function loadDll(dll: string): win.HMODULE {
|
|
53
|
+
const cached = _dllCache.get(dll)
|
|
54
|
+
if (cached !== undefined) return cached
|
|
55
|
+
const loaded = win.LoadLibrary(dll)
|
|
56
|
+
if (!loaded) throw new Error(`ffi-bind: LoadLibrary("${dll}") failed`)
|
|
57
|
+
_dllCache.set(dll, loaded)
|
|
58
|
+
return loaded
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 解析后的签名:'!' 修饰留在 token 尾缀上(token 即契约——运行时要执法的语义跟着
|
|
62
|
+
// token 走,0 检查看尾缀即可,无独立 flag 可被解构丢失);'@' 由 normToken 剥掉
|
|
63
|
+
// (纯类型层语义,运行时不校验)。签名原文留作错误上下文。互斥单修饰语法下合法
|
|
64
|
+
// token 的 '!' 修饰必以 '!' 结尾(品牌名里的 '!' 在 <>ptr 之前,不影响尾判)。
|
|
65
|
+
type ParsedSig = {
|
|
66
|
+
argTokens: C_BasicType_Token_No_Void[]
|
|
67
|
+
retToken: C_BasicType_Token
|
|
68
|
+
sig: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseSig(sig: string): ParsedSig {
|
|
72
|
+
const parts = sig.split(' -> ')
|
|
73
|
+
if (parts.length !== 2) {
|
|
74
|
+
throw new Error(`ffi-bind: invalid signature "${sig}" (expected "arg1 arg2 -> ret")`)
|
|
75
|
+
}
|
|
76
|
+
// 零参数签名是 ' -> u32',此时 parts[0]==='',split(' ') 会产出 [''];
|
|
77
|
+
// 空串必须先滤掉,否则 normToken('') 抛 Unknown token 整个 suite 崩。
|
|
78
|
+
const rawArgs = parts[0]!.split(' ').filter(t => t !== '')
|
|
79
|
+
const argTokens: C_BasicType_Token_No_Void[] = []
|
|
80
|
+
for (const raw of rawArgs) {
|
|
81
|
+
// '@Name' 枚举标注由 normToken 剥(槽宽/ABI 只看底档),名字合法性由类型层拦
|
|
82
|
+
// (未知枚举塌 never);结构侧的 '@'(键 alignas / 值 encoding)不走本路径——
|
|
83
|
+
// bind 签名是 token 修饰的唯一入口。
|
|
84
|
+
const tok = normToken(raw)
|
|
85
|
+
if (tok === 'void') {
|
|
86
|
+
throw new Error("Void can not in args")
|
|
87
|
+
}
|
|
88
|
+
argTokens.push(tok)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 裸 'ptr'/非法 token 由类型层拦截(C_BasicType_Token 已 Exclude 'ptr'、拼错 token 塌成 never);
|
|
92
|
+
// 运行时再比较只会得到 TS2367「两类型无重叠」——类型已表达的约束不重复校验。
|
|
93
|
+
const retToken = normToken(parts[1]!)
|
|
94
|
+
// 按值形 '<N>' 仅支持入参位:大结构返回需要隐藏指针/sret 级 ABI(本运行时的
|
|
95
|
+
// retBuf 恒 8 字节,只支持标量/浮点/指针返回),绑定期 throw 而非静默降级。
|
|
96
|
+
// 类型层对返回位的 '<N>' 不设专门分支(fallthrough),本 throw 是唯一防线。
|
|
97
|
+
if (isCValToken(retToken))
|
|
98
|
+
throw new Error(`ffi-bind: "${sig}": by-value struct at the return position ` +
|
|
99
|
+
`(<${valName(retToken)}>) is not supported — argument positions only; ` +
|
|
100
|
+
`pass a struct out through an out-param slot instead (<N>ptr + caller buffer)`)
|
|
101
|
+
return {
|
|
102
|
+
argTokens,
|
|
103
|
+
retToken,
|
|
104
|
+
sig,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function makeFn(proc: number, sig: string, encoders: CodecMap, decoders: CodecMap): (...a: unknown[]) => unknown {
|
|
109
|
+
const ps = parseSig(sig)
|
|
110
|
+
// 按值位的能力校验前移到绑定期(模块加载即炸,不留到首个调用):布局缺位 / 非结构
|
|
111
|
+
// 布局(无 size)/ 退化 size 都是签名层面的错误,报错指向具体位与可用布局清单。
|
|
112
|
+
for (const t of ps.argTokens) {
|
|
113
|
+
if (!isCValToken(t)) continue
|
|
114
|
+
const name = valName(t)
|
|
115
|
+
const codec = valCodec(name, encoders)
|
|
116
|
+
if (!codec || typeof codec.encode !== 'function')
|
|
117
|
+
throw new Error(`ffi-bind: ${sig}: by-value position <${name}> has no registered layout ` +
|
|
118
|
+
`(available: ${Object.keys(encoders).join(', ') || 'none'})`)
|
|
119
|
+
if (typeof codec.size !== 'number' || codec.size < 1)
|
|
120
|
+
throw new Error(`ffi-bind: ${sig}: by-value position <${name}> requires a struct ` +
|
|
121
|
+
`layout with size >= 1 (got ${codec.size}; non-struct codecs expose no size)`)
|
|
122
|
+
}
|
|
123
|
+
return (...a: unknown[]) => callPacked(proc, ps, a, encoders, decoders)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 每个参数在 argFrame 里占的字节数。与 quickjs-ffi-type.h 的 qwin_ffi_arg_size[]
|
|
127
|
+
// 保持一致(ia32:≤4B 类型进 4 字节、更大的进 8 字节;x64:恒 8 字节槽)。
|
|
128
|
+
// 注意这是「槽宽」,不是类型的 sizeof——同一定义也用于返回槽。指针布局恒指针宽。
|
|
129
|
+
const IA32_ARG_SIZE: Record<C_BasicType_No_Void, number> = {
|
|
130
|
+
u8: 4, i8: 4, u16: 4, i16: 4, u32: 4, i32: 4,
|
|
131
|
+
u64: 8, i64: 8, f32: 4, f64: 8,
|
|
132
|
+
ptr: PTR_SIZE,
|
|
133
|
+
}
|
|
134
|
+
// x64 桩无条件双读 slots[0..3],故 argFrame 至少 4 个槽(32 字节)
|
|
135
|
+
const X64_MIN_SLOTS = 4
|
|
136
|
+
|
|
137
|
+
function callPacked(proc: number, { argTokens, retToken, sig }: ParsedSig, args: any[], encoders: CodecMap, decoders: CodecMap): unknown {
|
|
138
|
+
const is64 = os.arch === 'x64'
|
|
139
|
+
|
|
140
|
+
const getWidth = (t: C_BasicType_Token_No_Void, is64: boolean): number => {
|
|
141
|
+
// 按值形由 widthOf 分派(槽宽依赖布局 size),此处只认标量/指针档;守卫同时
|
|
142
|
+
// 收窄类型(IA32_ARG_SIZE 索引不认 val 形)。
|
|
143
|
+
if (isCValToken(t))
|
|
144
|
+
throw new Error(`ffi-bind: getWidth: by-value token "${t}" (dispatched by widthOf)`)
|
|
145
|
+
if (is64) return 8
|
|
146
|
+
if (isCPtrToken(t)) return PTR_SIZE
|
|
147
|
+
return IA32_ARG_SIZE[t]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 按值位的槽宽:x64 恒 8 字节(尺寸 ∈{1,2,4,8} 按「同尺寸整数」进槽,其余尺寸进
|
|
151
|
+
// 槽的是指向临时副本的指针);ia32 = ALIGN(size, 4) —— 结构字节原样压栈,无按
|
|
152
|
+
// 引用一说(libffi ffi.c 的 ia32 分支同规则,桩只做一次 rep movsb 搬运)。
|
|
153
|
+
const widthOf = (t: C_BasicType_Token_No_Void): number => {
|
|
154
|
+
if (isCValToken(t)) {
|
|
155
|
+
if (is64) return 8
|
|
156
|
+
const layout = valCodec(valName(t), encoders)
|
|
157
|
+
// 绑定期 makeFn 已校验(存在 + size >= 1);此处兜动态/无类型路径。
|
|
158
|
+
if (!layout)
|
|
159
|
+
throw new Error(`ffi-bind: <${valName(t)}> by-value position has no registered layout`)
|
|
160
|
+
const size = layout.size
|
|
161
|
+
return size % 4 === 0 ? size : size + 4 - (size % 4)
|
|
162
|
+
}
|
|
163
|
+
// val 形已在上分支 return,此处恒为标量/指针形(类型层收窄,见 C_BasicType_Token)。
|
|
164
|
+
return getWidth(t, is64)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let total = 0
|
|
168
|
+
for (const t of argTokens) total += widthOf(t)
|
|
169
|
+
if (is64 && argTokens.length < X64_MIN_SLOTS) total = X64_MIN_SLOTS * 8
|
|
170
|
+
|
|
171
|
+
const argFrame = new ArrayBuffer(total)
|
|
172
|
+
const dv = new DataView(argFrame)
|
|
173
|
+
const held: ArrayBuffer[] = [] // 指针布局编码缓冲需存活到调用结束
|
|
174
|
+
|
|
175
|
+
let off = 0
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < argTokens.length; i++) {
|
|
178
|
+
const argToken = argTokens[i]!
|
|
179
|
+
const nonnull = argToken.endsWith('!') // '!' 留在 token 尾缀上,此处按尾缀执法
|
|
180
|
+
const arg = args[i]
|
|
181
|
+
if (isCPtrToken(argToken)) {
|
|
182
|
+
const name = ptrName(argToken) // '' = <>ptr 裸地址档
|
|
183
|
+
if (typeof arg === 'number') {
|
|
184
|
+
// 裸地址 / NULL / 品牌指针(都是 number)透传,不重编码。
|
|
185
|
+
// 合法值形态由类型层约束(MaybePtr | codec 形,如 <BYTE>ptr 另收 ArrayBuffer)。
|
|
186
|
+
if (nonnull && arg === 0)
|
|
187
|
+
throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! got 0 ` +
|
|
188
|
+
`(null pointer forbidden here; the token marks this position non-null)`)
|
|
189
|
+
writeSlot(dv, off, { k: 'ptr', v: arg })
|
|
190
|
+
} else {
|
|
191
|
+
if (arg === null || arg === undefined) {
|
|
192
|
+
// JS null/undefined 不是指针词汇(空位只写 NULL)——fail-loud,不静默当 0。
|
|
193
|
+
throw new Error(`ffi-bind: <${name}>ptr got ${String(arg)}; ` +
|
|
194
|
+
`pass the imported NULL constant for an empty pointer`)
|
|
195
|
+
}
|
|
196
|
+
const codec = encoders[name]
|
|
197
|
+
if (!codec) {
|
|
198
|
+
// 未知布局落到这:只收 NULL / 有效地址 / codec 值,不静默当 0。
|
|
199
|
+
throw new Error(name === ''
|
|
200
|
+
? `ffi-bind: <>ptr expects number (NULL | Ptr), got ${typeof arg}; use <BYTE>ptr with a PtrArrayBuffer (new PtrArrayBuffer(n)), or ffi.bufferPtr(buf) for an address`
|
|
201
|
+
: `ffi-bind: unknown layout "<${name}>ptr" (available: ${Object.keys(encoders).join(', ') || 'none'})`)
|
|
202
|
+
}
|
|
203
|
+
if (!codec.encode) {
|
|
204
|
+
// codec 存在但只声明了 decode/alloc(只读出),入参位无序列化可用。
|
|
205
|
+
throw new Error(`ffi-bind: <${name}>ptr has no encoder (out-only codec)`)
|
|
206
|
+
}
|
|
207
|
+
const buf = codec.encode(arg)
|
|
208
|
+
held.push(buf)
|
|
209
|
+
// 类型层已保证 encode 返回 PtrArrayBuffer;这里再 fail-loud 兜一层
|
|
210
|
+
// (动态传入的无类型 codec 仍可能违反),然后直接取 .ptr——不再重算。
|
|
211
|
+
const p = (buf as PtrArrayBuffer<any>).ptr
|
|
212
|
+
if (typeof p !== 'number')
|
|
213
|
+
throw new Error('ffi-bind: codec.encode must return a PtrArrayBuffer, got plain ArrayBuffer')
|
|
214
|
+
if (nonnull && p === 0)
|
|
215
|
+
throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! encode produced 0 ` +
|
|
216
|
+
`(null pointer forbidden here)`)
|
|
217
|
+
writeSlot(dv, off, { k: 'ptr', v: p })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (isCValToken(argToken)) {
|
|
221
|
+
// 按值位:对象直传(值域由注册布局推导),无地址词汇 —— NULL / number
|
|
222
|
+
// 进这里是必然错位(引擎按值语义,指针位请写 <N>ptr),fail-loud。
|
|
223
|
+
const name = valName(argToken)
|
|
224
|
+
const codec = valCodec(name, encoders)
|
|
225
|
+
if (!codec)
|
|
226
|
+
throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}> by-value position ` +
|
|
227
|
+
`has no registered layout`)
|
|
228
|
+
if (arg === null || arg === undefined)
|
|
229
|
+
throw new Error(`ffi-bind: <${name}> value position got ${String(arg)}; ` +
|
|
230
|
+
`pass a struct object (use <${name}>ptr to pass a pointer)`)
|
|
231
|
+
const size = codec.size
|
|
232
|
+
if (!is64 || (size <= 8 && (size & (size - 1)) === 0)) {
|
|
233
|
+
// 字节直接落槽:ia32 结构按值压栈(任意尺寸);x64 尺寸 ∈{1,2,4,8}
|
|
234
|
+
// 按「同尺寸整数」传值——字节在 8 字节槽低位,callee 右对齐读。
|
|
235
|
+
// argFrame 构造时即零填充,槽高位为 0(callee 忽略高位,无歧义)。
|
|
236
|
+
codec.encode(arg, argFrame, off)
|
|
237
|
+
} else {
|
|
238
|
+
// x64 其余尺寸(>8 或非 2 的幂)按引用:槽放指针,指向 caller 分配的
|
|
239
|
+
// 16 字节对齐临时副本(MS x64 官方规则:单参数永不跨多个寄存器;对齐
|
|
240
|
+
// 偏移自算,不依赖分配器的对齐保证)。held 保持引用直到调用返回。
|
|
241
|
+
const buf = new ArrayBuffer(size + 15)
|
|
242
|
+
const base = ffi.bufferPtr(buf)
|
|
243
|
+
const pad = (16 - (base % 16)) % 16
|
|
244
|
+
codec.encode(arg, buf, pad)
|
|
245
|
+
held.push(buf)
|
|
246
|
+
writeSlot(dv, off, { k: 'ptr', v: base + pad })
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
writeSlot(dv, off, { k: argToken, v: arg })
|
|
251
|
+
}
|
|
252
|
+
off += widthOf(argToken)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const retBuf = new ArrayBuffer(8)
|
|
256
|
+
const retIsFp = retToken === 'f32' || retToken === 'f64'
|
|
257
|
+
ffi.ffiCall(proc, argFrame, retBuf, retIsFp ? 1 : 0)
|
|
258
|
+
const raw = readRet(cTokenToType(retToken), retBuf)
|
|
259
|
+
// 返回位 '!' = 「C 保证非零」的作者担保,标错由这里当场 throw 兜底(显式崩溃而非静默流窜)。
|
|
260
|
+
if (retToken.endsWith('!') && raw === 0)
|
|
261
|
+
throw new Error(`ffi-bind: ${sig}: return ${retToken} got 0 ` +
|
|
262
|
+
`(the token promises C never returns null here)`)
|
|
263
|
+
// 返回位自动解码:品牌指针且该键声明了 decode → 交还 JS 值;
|
|
264
|
+
// 空指针 0 恒原样返回(无可读内容,不进 decode),裸 <>ptr / 无 decode 的键原样返回。
|
|
265
|
+
if (isCPtrToken(retToken) && raw !== 0) {
|
|
266
|
+
const name = ptrName(retToken)
|
|
267
|
+
if (name !== '') {
|
|
268
|
+
const codec = decoders[name]
|
|
269
|
+
if (codec?.decode) return codec.decode(raw)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return raw
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 把标量参数写进 argFrame 槽位。ptr 为裸地址直通;引用型参数见 writePtrSlot。
|
|
276
|
+
function readRet(ret: C_BasicType, retBuf: ArrayBuffer): unknown {
|
|
277
|
+
if (ret === 'void') return undefined
|
|
278
|
+
return readScalar(new DataView(retBuf), 0, ret)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
type ParseArgStr<S extends string, L, Acc extends unknown[] = []> =
|
|
282
|
+
S extends `${infer F} ${infer R}`
|
|
283
|
+
? ParseArgStr<R, L, [...Acc, ArgJsTypeOfToken<F, L>]>
|
|
284
|
+
: S extends ''
|
|
285
|
+
? Acc : [...Acc, ArgJsTypeOfToken<S, L>]
|
|
286
|
+
|
|
287
|
+
// 按键存在性提取 codec 值域(方法全可选后,`extends Encoder<infer V>` 会整体塌 never)。
|
|
288
|
+
type EncodeValue<T> = 'encode' extends keyof T
|
|
289
|
+
? (T extends { encode?: (v: infer V, ...r: any[]) => ArrayBuffer } ? V : never)
|
|
290
|
+
: never
|
|
291
|
+
type DecodeValue<T> = 'decode' extends keyof T
|
|
292
|
+
? (T extends { decode?: (p: any, ...r: any[]) => infer V } ? V : never)
|
|
293
|
+
: never
|
|
294
|
+
|
|
295
|
+
// 入参布局表:用户 codec 解包成 JS 形,并注入两个内建 codec 的值域,
|
|
296
|
+
// 使 <BYTE>ptr / <WCHAR>ptr 形参在类型层拿到 PtrArrayBuffer / string。
|
|
297
|
+
// BYTE 用 PtrArrayBuffer<any> 收所有品牌:PAB<N> 因 .ptr 里的条件类型是不变型,
|
|
298
|
+
// 写 string/'' 都收不下 struct.encode 的具体品牌——any 双向通,形状(必须是 PAB)
|
|
299
|
+
// 才是硬约束,品牌本就由调用点自行声明。
|
|
300
|
+
type BindEncoders<M> = {
|
|
301
|
+
[K in keyof M]: EncodeValue<M[K]>
|
|
302
|
+
} & {
|
|
303
|
+
WCHAR: string;
|
|
304
|
+
BYTE: PtrArrayBuffer<any>;
|
|
305
|
+
}
|
|
306
|
+
// 返回位布局表:只含 decode 值域。内建 WCHAR 注入与运行时(builtinCodecs.WCHAR
|
|
307
|
+
// 无条件合并进 decoders)对齐——<WCHAR>ptr 返回直接拿 string;BYTE 无 decode,
|
|
308
|
+
// 不注入,返回保持 MaybePtr<'BYTE'>(NULL | Ptr<'BYTE'>)。
|
|
309
|
+
type BindDecoders<M> = { [K in keyof M]: DecodeValue<M[K]> } & { WCHAR: string }
|
|
310
|
+
|
|
311
|
+
type BindFn<S extends string, E, D> =
|
|
312
|
+
S extends `${infer ArgStr} -> ${infer RetStr}` ? ((...args: ParseArgStr<ArgStr, E>) => RetJsTypeOfToken<RetStr, D>) : never
|
|
313
|
+
|
|
314
|
+
// 内建 codec:类型校验放在这里,与「用户布局 codec」同一分派点。
|
|
315
|
+
// BYTE/WCHAR 拒绝裸 number(无句柄可 pin),提示改用 <>ptr。
|
|
316
|
+
/** 内建 UTF-16 编解码:encode 入参序列化(UTF-16LE + NUL);decode 双态单参——
|
|
317
|
+
* 品牌指针逐字节读到 NUL(野指针无终止 4MiB 上限 fail-fast)、ArrayBuffer 直读到
|
|
318
|
+
* NUL 或末尾(out 参数未写 NUL 时按读满处理);alloc 预分配返回带 .ptr 的
|
|
319
|
+
* PtrArrayBuffer(a = 字符数,缺省 256)。类型取必需形态——
|
|
320
|
+
* 内建三件齐备,调用无需空断言。刻意 Pick 而非 Required<Codec>:只声明消费的
|
|
321
|
+
* 三件(encode/decode/alloc),新增可选能力位不被拉成必填。 */
|
|
322
|
+
export const WCHAR: Required<Pick<Codec<'WCHAR', string, number>, 'encode' | 'decode' | 'alloc'>> = {
|
|
323
|
+
encode: (v: string) => {
|
|
324
|
+
// TextEncoder 没有 encodeInto:先编码再拷入带 .ptr 的 buffer(单次 memcpy,
|
|
325
|
+
// 编码逻辑仍全局唯一——这行是全部 UTF-16 字符串编码的唯一实现)。
|
|
326
|
+
const u8 = new TextEncoder('utf-16le').encode(v + '\0')
|
|
327
|
+
const out = new PtrArrayBuffer<'WCHAR'>(u8.byteLength)
|
|
328
|
+
new Uint8Array(out).set(u8)
|
|
329
|
+
return out
|
|
330
|
+
},
|
|
331
|
+
decode: (p) => {
|
|
332
|
+
// 两分支同构(UTF-16LE code unit、拼接到 NUL 止),唯一差异是读法。
|
|
333
|
+
if (typeof p === 'number') {
|
|
334
|
+
let s = ''
|
|
335
|
+
let addr: number = p
|
|
336
|
+
const start = addr
|
|
337
|
+
for (;;) {
|
|
338
|
+
const lo = ffi.readByte(addr)
|
|
339
|
+
const hi = ffi.readByte(addr + 1)
|
|
340
|
+
if (lo === 0 && hi === 0) return s
|
|
341
|
+
s += String.fromCharCode(lo | (hi << 8))
|
|
342
|
+
addr += 2
|
|
343
|
+
if (addr - start > 1 << 21) {
|
|
344
|
+
throw new Error('ffi-bind: WCHAR decode exceeded 4MiB without NUL')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const u8 = new Uint8Array(p)
|
|
349
|
+
const limit = u8.length & ~1 // 奇数末尾的残半字节丢弃
|
|
350
|
+
let s = ''
|
|
351
|
+
for (let i = 0; i < limit; i += 2) {
|
|
352
|
+
const c = u8[i]! | (u8[i + 1]! << 8)
|
|
353
|
+
if (c === 0) return s
|
|
354
|
+
s += String.fromCharCode(c)
|
|
355
|
+
}
|
|
356
|
+
return s
|
|
357
|
+
},
|
|
358
|
+
alloc: (size = 256) => new PtrArrayBuffer<'WCHAR'>(size * 2),
|
|
359
|
+
}
|
|
360
|
+
/** 内建字节缓冲:encode identity 直通(收 PtrArrayBuffer、原地存活,native 写回同一
|
|
361
|
+
* buffer);alloc 预分配返回带 .ptr 的 PtrArrayBuffer(a = 字节数)。无 decode——
|
|
362
|
+
* 地址不携带长度(无终止符),读回直接在 buffer 上 DataView / TypedArray 取。 */
|
|
363
|
+
export const BYTE: Required<Pick<Codec<'BYTE', PtrArrayBuffer<any>, number>, 'encode' | 'alloc'>> = {
|
|
364
|
+
encode: (v) => {
|
|
365
|
+
// identity 原地直通(native 写回同一 buffer),不能拷贝——只收自带 .ptr 的
|
|
366
|
+
// PtrArrayBuffer;普通 ArrayBuffer 过来 fail-loud,先包一层 new PtrArrayBuffer(n)。
|
|
367
|
+
if (!(v instanceof PtrArrayBuffer))
|
|
368
|
+
throw new Error('ffi-bind: <BYTE>ptr wants a PtrArrayBuffer (new PtrArrayBuffer(n) / BYTE.alloc / struct.encode), not a plain ArrayBuffer')
|
|
369
|
+
return v
|
|
370
|
+
},
|
|
371
|
+
alloc: (byteLen) => new PtrArrayBuffer<'BYTE'>(byteLen),
|
|
372
|
+
}
|
|
373
|
+
// 运行时两张表是同一对象:参数位只调 .encode、返回位只调 .decode,互不干扰。
|
|
374
|
+
const builtinCodecs: CodecMap = { WCHAR, BYTE }
|
|
375
|
+
/** 绑定单个函数。encoders/decoders 是两张可选布局表(结构上是 Codec):
|
|
376
|
+
* encoders 喂入参位(encode 值域 → 形参类型),decoders 喂返回位(decode 值域
|
|
377
|
+
* → 返回 V | null,替代 Ptr | null)。decoders 缺省时回退到 encoders——双向 codec
|
|
378
|
+
* 只需传一次(与运行时 `decoders ?? encoders` 同构:都只在实参缺省时生效,
|
|
379
|
+
* 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。 */
|
|
380
|
+
export function bind<const S extends string,
|
|
381
|
+
const LE extends CodecMap = {},
|
|
382
|
+
const LD extends CodecMap = LE>(
|
|
383
|
+
dll: string, name: string, sig: S, encoders?: LE, decoders?: LD) {
|
|
384
|
+
const proc = win.GetProcAddress(loadDll(dll), name)
|
|
385
|
+
if (!proc) throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`)
|
|
386
|
+
const effDec = decoders ?? encoders
|
|
387
|
+
return makeFn(proc, sig,
|
|
388
|
+
{ ...builtinCodecs, ...encoders },
|
|
389
|
+
{ ...builtinCodecs, ...(effDec ?? {}) }
|
|
390
|
+
) as unknown as BindFn<S, BindEncoders<LE>, BindDecoders<LD>>
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 批量绑定(map = 函数名 → 签名串)。encoders/decoders 语义同 bind()。 */
|
|
394
|
+
export function bindLib<const M extends Record<string, string>,
|
|
395
|
+
const LE extends CodecMap = {},
|
|
396
|
+
const LD extends CodecMap = LE>(
|
|
397
|
+
dll: string, map: M, encoders?: LE, decoders?: LD) {
|
|
398
|
+
const out: Record<string, (...a: unknown[]) => unknown> = {}
|
|
399
|
+
const h = loadDll(dll)
|
|
400
|
+
const effDec = decoders ?? encoders
|
|
401
|
+
const encTable = { ...builtinCodecs, ...encoders }
|
|
402
|
+
const decTable = { ...builtinCodecs, ...(effDec ?? {}) }
|
|
403
|
+
for (const [name, sig] of Object.entries(map)) {
|
|
404
|
+
const proc = win.GetProcAddress(h, name)
|
|
405
|
+
if (!proc) throw new Error(`ffi-bind: proc "${name}" not found in ${dll}`)
|
|
406
|
+
out[name] = makeFn(proc, sig, encTable, decTable)
|
|
407
|
+
}
|
|
408
|
+
return out as { [K in keyof M]: BindFn<M[K], BindEncoders<LE>, BindDecoders<LD>> }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ---- 闭包(回调):JS 函数 → 可传给 Win32 API 的函数指针 ---- */
|
|
412
|
+
|
|
413
|
+
// 泄漏警告:target = dispose 本体 —— 警告条件字面对齐「释放通道死亡」:
|
|
414
|
+
// `const {dispose} = closure(...)` 解构出的就是 target 本身,持有期间绝不触发;
|
|
415
|
+
// dispose 体内 unregister(dispose)(自引用 token),释放即撤销,之后丢弃不误报。
|
|
416
|
+
// 回调只打印、绝不自动 free:ptr 会被复制进任意 native API,JS 对象的可达性
|
|
417
|
+
// 推不出 native 是否还在用它,自动释放会把「泄漏」升级成「野指针崩溃」
|
|
418
|
+
// (wrapper 被 C 侧强引用,不 dispose 的代价只是滞留到进程结束)。
|
|
419
|
+
// heldValue 纯数据、不引用 target —— 若反向强持,target 永不可达,警告永不触发。
|
|
420
|
+
// 注意:触发时机依赖 QuickJS 的 GC→job 队列链路,未做强时序断言(best-effort
|
|
421
|
+
// 诊断 —— 即使不触发也只是退回静默泄漏,无任何负面后果)。
|
|
422
|
+
const leakRegistry: FinalizationRegistry<{ sig: string, ptr: number }> =
|
|
423
|
+
new FinalizationRegistry((held) => {
|
|
424
|
+
std.printf('[ffi] closure leaked (GCed without dispose): sig="%s" ptr=0x%x\n',
|
|
425
|
+
held.sig, held.ptr)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// 共享解码/编码助手:由每个闭包的 wrapper 调用(wrapper 闭包捕获 args/ret/fn)。
|
|
429
|
+
// args/ret 为「底档 + '!' 绑定」的记录(与 ParsedSig 同形态——修饰随 token 走,
|
|
430
|
+
// 消费者要么整体拿到、要么整体拿不到)。'!' 违约(C 传入 0 / fn 返回 0)打印诊断并
|
|
431
|
+
// 中止本次分发(fn 不被调用 / 结果槽保持 0),与 fn-throw 同风格:此处不 throw,
|
|
432
|
+
// 异常通道只留给用户回调(C 桩捕获 wrapper 异常并清零,quickjs-ffi-closure.c)。
|
|
433
|
+
function dispatchClosure(
|
|
434
|
+
args: { tok: C_BasicType_No_Void, nonnull: boolean }[],
|
|
435
|
+
ret: { tok: C_BasicType, nonnull: boolean },
|
|
436
|
+
fn: (...a: unknown[]) => unknown,
|
|
437
|
+
frameBuf: ArrayBuffer, retBuf: ArrayBuffer, sig: string): void {
|
|
438
|
+
const dv = new DataView(retBuf)
|
|
439
|
+
const is64 = os.arch === 'x64'
|
|
440
|
+
const fv = new DataView(frameBuf)
|
|
441
|
+
const a: unknown[] = []
|
|
442
|
+
let off = 0
|
|
443
|
+
for (let i = 0; i < args.length; i++) {
|
|
444
|
+
const spec = args[i]!
|
|
445
|
+
const k = spec.tok
|
|
446
|
+
let v: unknown
|
|
447
|
+
if (is64) {
|
|
448
|
+
const fp = k === 'f32' || k === 'f64'
|
|
449
|
+
const base = fp
|
|
450
|
+
? (i < 4 ? 128 + i * 8 : 32 + (i - 4) * 8) // xmm 区(前 4)/ 溢出区
|
|
451
|
+
: (i < 4 ? i * 8 : 32 + (i - 4) * 8) // 整数寄存器区 / 溢出区
|
|
452
|
+
v = readScalar(fv, base, k)
|
|
453
|
+
} else {
|
|
454
|
+
v = readScalar(fv, off, k)
|
|
455
|
+
off += IA32_ARG_SIZE[k]
|
|
456
|
+
}
|
|
457
|
+
// 参数位 '!' = 「C 保证非空」的作者担保,C 传 0 即标错 → 跳过回调。
|
|
458
|
+
if (spec.nonnull && v === 0) {
|
|
459
|
+
std.printf('[ffi] closure %s: arg#%d got 0 (! promise violated by caller), skipping callback\n', sig, i + 1)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
a.push(v)
|
|
463
|
+
}
|
|
464
|
+
let r: any
|
|
465
|
+
try {
|
|
466
|
+
r = fn(...a)
|
|
467
|
+
} catch (err) {
|
|
468
|
+
std.printf('[ffi] closure callback threw: %s\n', String(err))
|
|
469
|
+
return // 结果槽保持 0
|
|
470
|
+
}
|
|
471
|
+
// 返回位 '!' = 「JS 保证非零」的作者担保,fn 返回 0 即标错 → 不写结果槽。
|
|
472
|
+
if (ret.nonnull && r === 0) {
|
|
473
|
+
std.printf('[ffi] closure %s: return got 0 (! promise violated by callback), leaving result slot 0\n', sig)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
if (ret.tok !== 'void') {
|
|
477
|
+
writeSlot(dv, 0, { k: ret.tok, v: r })
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** 把 JS 函数变成可传给 Win32 API 的函数指针(同步同线程回调)。
|
|
482
|
+
* sig 为回调签名:实参只许 BasicKind(ptr 槽 0 保真读回 NULL | Ptr),
|
|
483
|
+
* 返回只许 void|u8..u64|i8..i64|f32|f64|ptr(u64/i64 走 EDX:EAX,x64 走 RAX)。
|
|
484
|
+
* '!' 标记位运行时校验(与 bind 调用位同语义,见 dispatchClosure):参数位 C 传入 0
|
|
485
|
+
* → 打印诊断并跳过回调;返回位 fn 返回 0 → 保持结果槽 0。
|
|
486
|
+
* opts.stdcall 仅 ia32 有效(默认 true,Win32 回调标准 CALLBACK);msvcrt 等
|
|
487
|
+
* cdecl 库回调传 { stdcall: false }。x64 恒由调用方清栈,无需指定。
|
|
488
|
+
* 参数上限:x64 至多 16 个、ia32 栈区至多 64 字节(C 桩定长捕获窗),超限抛错。
|
|
489
|
+
* 返回 { ptr, dispose }:ptr 即函数指针(传给 API 的 ptr 参数);dispose 注销
|
|
490
|
+
* 并释放回调函数,幂等;闭包期间回调被强引用,不会被 GC 回收。
|
|
491
|
+
* 注意:dispose 后 ptr 不得再被任何 native 方引用。
|
|
492
|
+
* 从未 dispose 就丢弃通道(整个返回对象或解构出来的 dispose 均算)
|
|
493
|
+
* → 打印泄漏警告(只警告、不自动释放,见 leakRegistry)。 */
|
|
494
|
+
export function closure<S extends string>(sig: S, fn: BindFn<S, {}, {}>,
|
|
495
|
+
opts?: { stdcall?: boolean }): { ptr: number; dispose(): void } {
|
|
496
|
+
const { argTokens, retToken } = parseSig(sig)
|
|
497
|
+
// 按值位仅支持 bind 的入参方向:closure 的 C→JS 方向由 quickjs-ffi-closure-*.S
|
|
498
|
+
// 按固定槽宽读参数(x64 固定 8 字节槽、ia32 IA32_ARG_SIZE 表),不区分「槽内是
|
|
499
|
+
// 结构字节还是指向临时副本的指针」——该分派未实现,创建期 fail-fast。
|
|
500
|
+
for (const t of argTokens)
|
|
501
|
+
if (isCValToken(t))
|
|
502
|
+
throw new Error(`ffi-bind: closure signature cannot take a by-value struct ` +
|
|
503
|
+
`(<${valName(t)}>); by-value is bind-only — the C→JS direction has no ` +
|
|
504
|
+
`struct slot decoding (pass a pointer and decode on the JS side)`)
|
|
505
|
+
// 上方已拒 '<N>' 按值形:此处 tok 恒为标量/指针档(cTokenToType 对 val 形原样返回,
|
|
506
|
+
// 但那条路径不可达——IA32_ARG_SIZE 索引与 dispatchClosure 只认标量档)。
|
|
507
|
+
const argSpecs = argTokens.map(t => ({ tok: cTokenToType(t) as C_BasicType_No_Void, nonnull: t.endsWith('!') }))
|
|
508
|
+
const retSpec = { tok: cTokenToType(retToken), nonnull: retToken.endsWith('!') }
|
|
509
|
+
const is64 = os.arch === 'x64'
|
|
510
|
+
const stackCapture = argSpecs.reduce((s, x) => s + IA32_ARG_SIZE[x.tok], 0)
|
|
511
|
+
// C 桩捕获窗定长(quickjs-ffi-closure.c:x64 栈区 96B=12 槽,第 5 参起在栈上,
|
|
512
|
+
// 即至多 16 参;ia32 16×4B)——超限 dispatch 会读到捕获缓冲之外的垃圾,或
|
|
513
|
+
// RangeError 被吞成返回 0,故创建期 fail-fast。扩容改 C 侧 buf 尺寸并同步放宽。
|
|
514
|
+
if (is64 ? argSpecs.length > 16 : stackCapture > 64) {
|
|
515
|
+
throw new Error(`ffi-bind: closure signature exceeds capture window ` +
|
|
516
|
+
`(${is64 ? 'max 16 params' : 'max 64 stack bytes'}): ${sig}`)
|
|
517
|
+
}
|
|
518
|
+
const argBytes = (opts?.stdcall === false || is64) ? 0 : stackCapture
|
|
519
|
+
const retKind = retSpec.tok === 'f32' ? 1
|
|
520
|
+
: retSpec.tok === 'f64' ? 2
|
|
521
|
+
: (retSpec.tok === 'u64' || retSpec.tok === 'i64') ? 3
|
|
522
|
+
: 0
|
|
523
|
+
// per-closure wrapper:闭包捕获 argSpecs/retSpec/fn/sig,C 侧只存它 + ctx;回调
|
|
524
|
+
// 永远回到创建它的 context(无跨 context 全局、无 registry)。
|
|
525
|
+
const wrapper = (frameBuf: ArrayBuffer, retBuf: ArrayBuffer): void => {
|
|
526
|
+
dispatchClosure(argSpecs, retSpec, fn as (...a: unknown[]) => unknown, frameBuf, retBuf, sig)
|
|
527
|
+
}
|
|
528
|
+
const ptr = ffi.closureNew(argBytes, retKind, wrapper)
|
|
529
|
+
let done = false
|
|
530
|
+
const dispose = (): void => {
|
|
531
|
+
if (done) return
|
|
532
|
+
done = true
|
|
533
|
+
leakRegistry.unregister(dispose) // 释放即撤销:之后通道死亡不再警告
|
|
534
|
+
ffi.closureFree(ptr)
|
|
535
|
+
}
|
|
536
|
+
const ret = { ptr, dispose }
|
|
537
|
+
leakRegistry.register(dispose, { sig, ptr }) // target = 通道本体(非 ret)
|
|
538
|
+
return ret
|
|
539
|
+
}
|