quickwin 3.0.2 → 3.0.4

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 CHANGED
@@ -6,22 +6,18 @@ QuickJS runtime for Windows, with native GUI, networking, WASM, FFI and more.
6
6
 
7
7
  Supports Windows XP+ (32/64-bit) with a minimal binary size (~1.5MB). The 32-bit build is CI-tested in a QEMU Windows XP VM.
8
8
 
9
- Early-stage project with known bugs.
9
+ ## Quick Start
10
10
 
11
- Install:
11
+ Scaffold a desktop app with [quickwin-starter](https://github.com/quickwinlabs/quickwin-starter):
12
12
 
13
13
  ```bash
14
- # Option 1: npm local install (recommended)
15
- npm install quickwin
16
- npx quickwin node_modules/quickwin/main.js # bundled main.js demo; use npx quickwin script.js for your own scripts
17
-
18
- # Option 2: download directly from GitHub Release
19
- iwr https://github.com/quickwinlabs/quickwin/releases/latest/download/qwin.exe -OutFile qwin.exe
20
- iwr https://github.com/quickwinlabs/quickwin/releases/latest/download/main.js -OutFile main.js
21
- .\qwin.exe main.js
14
+ npm create -y quickwin@latest myapp
15
+ cd myapp
16
+ npm install
17
+ npm run build # produces dist\app.exe
22
18
  ```
23
19
 
24
- The npm package ships 4 executables: `qwin.exe` (64-bit) / `qwin-x86.exe` (32-bit, XP-compatible) plus their no-WASM variants `qwin-nowasm.exe` / `qwin-nowasm-x86.exe`.
20
+ The default template is React (JSX + system tray); `-- --template=minimal` starts from a bare skeleton.
25
21
 
26
22
  ## Features
27
23
 
@@ -36,7 +32,10 @@ The npm package ships 4 executables: `qwin.exe` (64-bit) / `qwin-x86.exe` (32-bi
36
32
 
37
33
  ## CLI
38
34
 
35
+ The runtime itself: install with `npm install quickwin`, or download `qwin.exe` and `main.js` from [Releases](https://github.com/quickwinlabs/quickwin/releases). The npm package ships 4 executables: `qwin.exe` (64-bit) / `qwin-x86.exe` (32-bit, XP-compatible) plus their no-WASM variants `qwin-nowasm.exe` / `qwin-nowasm-x86.exe`.
36
+
39
37
  ```bash
38
+ npx quickwin node_modules/quickwin/main.js # run the bundled demo to see it working
40
39
  quickwin script.js # run a script
41
40
  quickwin -o CON script.js # run with console (AllocConsole)
42
41
  quickwin -o LOG script.js # run with auto-generated log file
package/README.md CHANGED
@@ -6,22 +6,18 @@ Windows 平台上的 QuickJS 运行时,支持原生 GUI、网络、WASM、FFI
6
6
 
7
7
  支持 Windows XP+(32/64 位),保持最小二进制体积(约 1.5MB)。32 位构建已在 QEMU Windows XP 虚拟机中通过 CI 测试。
8
8
 
9
- 项目处于早期,功能待完善,包含已知 bug。
9
+ ## 快速开始
10
10
 
11
- 安装方式:
11
+ 用脚手架新建一个桌面应用([quickwin-starter](https://github.com/quickwinlabs/quickwin-starter)):
12
12
 
13
13
  ```bash
14
- # 方式一:npm 本地安装(推荐)
15
- npm install quickwin
16
- npx quickwin node_modules/quickwin/main.js # 包自带 main.js 示例;用 npx quickwin script.js 运行你自己的脚本
17
-
18
- # 方式二:直接从 GitHub Release 下载
19
- iwr https://github.com/quickwinlabs/quickwin/releases/latest/download/qwin.exe -OutFile qwin.exe
20
- iwr https://github.com/quickwinlabs/quickwin/releases/latest/download/main.js -OutFile main.js
21
- .\qwin.exe main.js
14
+ npm create -y quickwin@latest myapp
15
+ cd myapp
16
+ npm install
17
+ npm run build # 产出 dist\app.exe
22
18
  ```
23
19
 
24
- npm 包自带 4 个可执行文件:`qwin.exe`(64 位)/ `qwin-x86.exe`(32 位,兼容 XP)及其无 WASM 版本 `qwin-nowasm.exe` / `qwin-nowasm-x86.exe`。
20
+ 默认 React 模板(JSX + 系统托盘),`-- --template=minimal` 可从更朴素的骨架起步。
25
21
 
26
22
  ## 特性
27
23
 
@@ -36,7 +32,10 @@ npm 包自带 4 个可执行文件:`qwin.exe`(64 位)/ `qwin-x86.exe`(32
36
32
 
37
33
  ## CLI
38
34
 
35
+ 运行时本体:`npm install quickwin` 安装,或从 [Releases](https://github.com/quickwinlabs/quickwin/releases) 下载 `qwin.exe` 与 `main.js`。npm 包自带 4 个可执行文件:`qwin.exe`(64 位)/ `qwin-x86.exe`(32 位,兼容 XP)及其无 WASM 版本 `qwin-nowasm.exe` / `qwin-nowasm-x86.exe`。
36
+
39
37
  ```bash
38
+ npx quickwin node_modules/quickwin/main.js # 跑包自带的示例
40
39
  quickwin script.js # 运行脚本
41
40
  quickwin -o CON script.js # 带控制台运行(AllocConsole)
42
41
  quickwin -o LOG script.js # 自动生成日志文件运行
package/lib/ffi/bind.d.ts CHANGED
@@ -44,7 +44,13 @@ export declare const BYTE: Required<Pick<Codec<'BYTE', PtrArrayBuffer<any>, numb
44
44
  * encoders 喂入参位(encode 值域 → 形参类型),decoders 喂返回位(decode 值域
45
45
  * → 返回 V | null,替代 Ptr | null)。decoders 缺省时回退到 encoders——双向 codec
46
46
  * 只需传一次(与运行时 `decoders ?? encoders` 同构:都只在实参缺省时生效,
47
- * 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。 */
47
+ * 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。
48
+ *
49
+ * codec 契约(参数帧复用的前提,调用方须遵守):
50
+ * 1. encode/decode 内不得回调任何已 bind 的函数。参数帧按签名只分配一次并逐调复用,
51
+ * 回调重入发生在本层 ffiCall 之前,会把本层已写好的槽抹掉(静默错参)。
52
+ * 2. encode(v, buf, off) 必须写满槽宽:少写会残留上一调的字节。
53
+ * 输入校验/转码若需要调 Win32 API,请在调用 bind 出的函数**之前**做。 */
48
54
  export declare function bind<const S extends string, const LE extends CodecMap = {}, const LD extends CodecMap = LE>(dll: string, name: string, sig: S, encoders?: LE, decoders?: LD): BindFn<S, BindEncoders<LE>, BindDecoders<LD>>;
49
55
  /** 批量绑定(map = 函数名 → 签名串)。encoders/decoders 语义同 bind()。 */
50
56
  export declare function bindLib<const M extends Record<string, string>, const LE extends CodecMap = {}, const LD extends CodecMap = LE>(dll: string, map: M, encoders?: LE, decoders?: LD): { [K in keyof M]: BindFn<M[K], BindEncoders<LE>, BindDecoders<LD>>; };
package/lib/ffi/bind.js CHANGED
@@ -72,7 +72,8 @@ function makeFn(proc, sig, encoders, decoders) {
72
72
  throw new Error(`ffi-bind: ${sig}: by-value position <${name}> requires a struct ` +
73
73
  `layout with size >= 1 (got ${codec.size}; non-struct codecs expose no size)`);
74
74
  }
75
- return (...a) => callPacked(proc, ps, a, encoders, decoders);
75
+ const plan = buildPlan(ps, encoders, decoders);
76
+ return (...a) => callPlan(proc, plan, a);
76
77
  }
77
78
  // 每个参数在 argFrame 里占的字节数。与 src/quickjs-ffi-type.h 的 qwin_ffi_arg_size[]
78
79
  // 保持一致(ia32:≤4B 类型进 4 字节、更大的进 8 字节;x64:恒 8 字节槽)。
@@ -84,149 +85,212 @@ const IA32_ARG_SIZE = {
84
85
  };
85
86
  // x64 桩无条件双读 slots[0..3],故 argFrame 至少 4 个槽(32 字节)
86
87
  const X64_MIN_SLOTS = 4;
87
- function callPacked(proc, { argTokens, retToken, sig }, args, encoders, decoders) {
88
- const is64 = os.arch === 'x64';
89
- const getWidth = (t, is64) => {
90
- // 按值形由 widthOf 分派(槽宽依赖布局 size),此处只认标量/指针档;守卫同时
91
- // 收窄类型(IA32_ARG_SIZE 索引不认 val 形)。
92
- if (isCValToken(t))
93
- throw new Error(`ffi-bind: getWidth: by-value token "${t}" (dispatched by widthOf)`);
94
- if (is64)
95
- return 8;
96
- if (isCPtrToken(t))
97
- return PTR_SIZE;
98
- return IA32_ARG_SIZE[t];
99
- };
100
- // 按值位的槽宽:x64 恒 8 字节(尺寸 ∈{1,2,4,8} 按「同尺寸整数」进槽,其余尺寸进
101
- // 槽的是指向临时副本的指针);ia32 = ALIGN(size, 4) —— 结构字节原样压栈,无按
102
- // 引用一说(libffi ffi.c 的 ia32 分支同规则,桩只做一次 rep movsb 搬运)。
103
- const widthOf = (t) => {
104
- if (isCValToken(t)) {
105
- if (is64)
106
- return 8;
107
- const layout = valCodec(valName(t), encoders);
108
- // 绑定期 makeFn 已校验(存在 + size >= 1);此处兜动态/无类型路径。
109
- if (!layout)
110
- throw new Error(`ffi-bind: <${valName(t)}> by-value position has no registered layout`);
111
- const size = layout.size;
112
- return size % 4 === 0 ? size : size + 4 - (size % 4);
88
+ // os.arch 是编译期的进程指针宽度(见 worker.ts 同款说明),全程恒定:提到模块级,
89
+ // 别在每调路径上读属性。
90
+ const IS64 = os.arch === 'x64';
91
+ // 指针槽按二补位位模式写(读回的负值再回传时保真):setBigUint64/setUint32 内部即
92
+ // ToBigUint64/ToUint32,负值自然取模成全 1;QuickJS 只收 BigInt,故 x64 仍需 BigInt(v)。
93
+ // 宽度分支在模块加载时就定死,call 期零判断。
94
+ const writePtr = PTR_SIZE === 8
95
+ ? (dv, off, v) => { dv.setBigUint64(off, BigInt(v), true); }
96
+ : (dv, off, v) => { dv.setUint32(off, v, true); };
97
+ // 返回槽读法:与 ctype.readScalar 的对应分支逐条等价,但按类型定型到闭包——readScalar
98
+ // 每次调用要走 token 字符串 switch,实测 ~0.5μs/call,是 0 参调用里最大的一笔。
99
+ // 指针位有符号读(对齐 C 版 JS_NewInt64):-1 哨兵等负值保真。
100
+ const RET_READER = {
101
+ u8: dv => dv.getUint8(0),
102
+ i8: dv => dv.getInt8(0),
103
+ u16: dv => dv.getUint16(0, true),
104
+ i16: dv => dv.getInt16(0, true),
105
+ u32: dv => dv.getUint32(0, true),
106
+ i32: dv => dv.getInt32(0, true),
107
+ u64: dv => dv.getBigUint64(0, true),
108
+ i64: dv => dv.getBigInt64(0, true),
109
+ f32: dv => dv.getFloat32(0, true),
110
+ f64: dv => dv.getFloat64(0, true),
111
+ ptr: PTR_SIZE === 8 ? dv => Number(dv.getBigInt64(0, true)) : dv => dv.getInt32(0, true),
112
+ };
113
+ /** 返回槽:所有 bind 出来的函数共用一个 8 字节缓冲。不变量:两个桩都在 call **之后**
114
+ * 才写 out,而 JS 在 ffiCall 返回后立刻由 RET_READER 读出——中间不得插入任何用户代码
115
+ * (decode 必须在 raw 取出之后)。故 native 期间的回调重入即使写了同一块缓冲,外层读到的
116
+ * 仍是本层自己的返回值(每一层自己的桩写都是该层读到前的最后一次写)。 */
117
+ const RET_DV = new DataView(new ArrayBuffer(8));
118
+ // 标量/指针标量槽写器按 token 定型(bind 期分派完,call 期只剩一次 DataView 写)。
119
+ // 32 位槽不显式 `>>> 0` / `Number()`:setUint32 内部即 ToUint32/ToNumber,逐位等价。
120
+ function mkScalarWriter(tok, off) {
121
+ switch (tok) {
122
+ // 所有 ≤4 字节槽一次 setUint32:小整数按 int 提升写(i8 = -1 必须给 0xFFFFFFFF),
123
+ // u32/i32 槽宽本就是 4 字节——ToUint32 取模让两种情形写法一致,合并成一档。
124
+ case 'u8':
125
+ case 'i8':
126
+ case 'u16':
127
+ case 'i16':
128
+ case 'u32':
129
+ case 'i32':
130
+ return (dv, v) => { dv.setUint32(off, v, true); };
131
+ case 'u64':
132
+ return (dv, v) => { dv.setBigUint64(off, v, true); };
133
+ case 'i64':
134
+ return (dv, v) => { dv.setBigInt64(off, v, true); };
135
+ case 'f32':
136
+ return (dv, v) => { dv.setFloat32(off, v, true); };
137
+ case 'f64':
138
+ return (dv, v) => { dv.setFloat64(off, v, true); };
139
+ // 指针标量槽只收 number(NULL 品牌零也是 number):类型层已排除 null/undefined,
140
+ // 这里不再运行时校验;带布局的 <N>ptr 走 mkPtrWriter,那边仍 fail-loud。
141
+ case 'ptr':
142
+ return (dv, v) => { writePtr(dv, off, v); };
143
+ }
144
+ }
145
+ // 指针位写器:number(裸地址 / NULL 品牌零 / 品牌指针)直通不重编码,其余交 codec 编码。
146
+ // 布局存在性保持「懒校验」——未注册布局的 <NOPE>ptr 只在真正调用时 throw,bind 期不炸。
147
+ function mkPtrWriter(name, codec, nonnull, sig, at, off, encoders) {
148
+ const encode = codec?.encode;
149
+ return (dv, arg) => {
150
+ if (typeof arg === 'number') {
151
+ if (nonnull && arg === 0)
152
+ throw new Error(`ffi-bind: ${sig}: ${at} <${name}>ptr! got 0 ` +
153
+ `(null pointer forbidden here; the token marks this position non-null)`);
154
+ writePtr(dv, off, arg);
155
+ return; // number(裸地址 / NULL / 品牌指针)直通,不进下面的 codec 分支
156
+ }
157
+ if (arg === null || arg === undefined) {
158
+ // JS null/undefined 不是指针词汇(空位只写 NULL)——fail-loud,不静默当 0。
159
+ throw new Error(`ffi-bind: <${name}>ptr got ${String(arg)}; ` +
160
+ `pass the imported NULL constant for an empty pointer`);
161
+ }
162
+ if (!codec) {
163
+ // 未知布局落到这:只收 NULL / 有效地址 / codec 值,不静默当 0。
164
+ throw new Error(name === ''
165
+ ? `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`
166
+ : `ffi-bind: unknown layout "<${name}>ptr" (available: ${Object.keys(encoders).join(', ') || 'none'})`);
113
167
  }
114
- // val 形已在上分支 return,此处恒为标量/指针形(类型层收窄,见 C_BasicType_Token)。
115
- return getWidth(t, is64);
168
+ if (!encode)
169
+ throw new Error(`ffi-bind: <${name}>ptr has no encoder (out-only codec)`);
170
+ const buf = encode(arg);
171
+ // 类型层已保证 encode 返回 PtrArrayBuffer;这里再 fail-loud 兜一层
172
+ // (动态传入的无类型 codec 仍可能违反),然后直接取 .ptr——不再重算。
173
+ const p = buf.ptr;
174
+ if (typeof p !== 'number')
175
+ throw new Error('ffi-bind: codec.encode must return a PtrArrayBuffer, got plain ArrayBuffer');
176
+ if (nonnull && p === 0)
177
+ throw new Error(`ffi-bind: ${sig}: ${at} <${name}>ptr! encode produced 0 ` +
178
+ `(null pointer forbidden here)`);
179
+ writePtr(dv, off, p);
180
+ return buf;
116
181
  };
117
- let total = 0;
118
- for (const t of argTokens)
119
- total += widthOf(t);
120
- if (is64 && argTokens.length < X64_MIN_SLOTS)
121
- total = X64_MIN_SLOTS * 8;
122
- const argFrame = new ArrayBuffer(total);
123
- const dv = new DataView(argFrame);
124
- const held = []; // 指针布局编码缓冲需存活到调用结束
182
+ }
183
+ // 按值位写器:x64 尺寸 ∈{1,2,4,8} 按「同尺寸整数」落槽(字节在槽低位,callee 右对齐读;
184
+ // 帧首分配即零填充,槽高位无歧义——复用帧同样成立,写器每调写入的区间恒定),
185
+ // 其余尺寸按引用传 caller 分配的对齐临时副本;ia32 结构字节原样压栈(任意尺寸)。
186
+ // 布局与尺寸已由 makeFn 在 bind 期校验。
187
+ function mkValWriter(name, codec, size, off) {
188
+ const encode = codec.encode;
189
+ const badArg = (arg) => {
190
+ throw new Error(`ffi-bind: <${name}> value position got ${String(arg)}; ` +
191
+ `pass a struct object (use <${name}>ptr to pass a pointer)`);
192
+ };
193
+ if (!IS64 || (size <= 8 && (size & (size - 1)) === 0))
194
+ // 帧由 dv 反查(SlotWriter 不再单传 frame):若将来 argFrame 走池化子视图,
195
+ // 这里要像 struct.ts 的字节访问那样补上 dv.byteOffset。
196
+ return (dv, arg) => {
197
+ if (arg === null || arg === undefined)
198
+ badArg(arg);
199
+ encode(arg, dv.buffer, off);
200
+ };
201
+ return (dv, arg) => {
202
+ if (arg === null || arg === undefined)
203
+ badArg(arg);
204
+ // 16 字节对齐自算(不依赖分配器的对齐保证);返回值让调用方 held 到最后
205
+ const buf = new ArrayBuffer(size + 15);
206
+ const base = ffi.bufferPtr(buf);
207
+ const pad = (16 - (base % 16)) % 16;
208
+ encode(arg, buf, pad);
209
+ writePtr(dv, off, base + pad);
210
+ return buf;
211
+ };
212
+ }
213
+ // bind 期算一次:槽偏移/写器/返回读法全部固化,call 期只剩搬运。
214
+ function buildPlan(ps, encoders, decoders) {
215
+ const { argTokens, retToken, sig } = ps;
216
+ const slots = [];
125
217
  let off = 0;
126
218
  for (let i = 0; i < argTokens.length; i++) {
127
219
  const argToken = argTokens[i];
128
- const nonnull = argToken.endsWith('!'); // '!' 留在 token 尾缀上,此处按尾缀执法
129
- const arg = args[i];
220
+ const at = 'arg#' + String(i + 1);
130
221
  if (isCPtrToken(argToken)) {
222
+ // '!' 留在 token 尾缀上(运行时要执法的语义跟着 token 走,无独立 flag)
131
223
  const name = ptrName(argToken); // '' = <>ptr 裸地址档
132
- if (typeof arg === 'number') {
133
- // 裸地址 / NULL / 品牌指针(都是 number)透传,不重编码。
134
- // 合法值形态由类型层约束(MaybePtr | codec 形,如 <BYTE>ptr 另收 ArrayBuffer)。
135
- if (nonnull && arg === 0)
136
- throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! got 0 ` +
137
- `(null pointer forbidden here; the token marks this position non-null)`);
138
- writeSlot(dv, off, { k: 'ptr', v: arg });
139
- }
140
- else {
141
- if (arg === null || arg === undefined) {
142
- // JS null/undefined 不是指针词汇(空位只写 NULL)——fail-loud,不静默当 0。
143
- throw new Error(`ffi-bind: <${name}>ptr got ${String(arg)}; ` +
144
- `pass the imported NULL constant for an empty pointer`);
145
- }
146
- const codec = encoders[name];
147
- if (!codec) {
148
- // 未知布局落到这:只收 NULL / 有效地址 / codec 值,不静默当 0。
149
- throw new Error(name === ''
150
- ? `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`
151
- : `ffi-bind: unknown layout "<${name}>ptr" (available: ${Object.keys(encoders).join(', ') || 'none'})`);
152
- }
153
- if (!codec.encode) {
154
- // codec 存在但只声明了 decode/alloc(只读出),入参位无序列化可用。
155
- throw new Error(`ffi-bind: <${name}>ptr has no encoder (out-only codec)`);
156
- }
157
- const buf = codec.encode(arg);
158
- held.push(buf);
159
- // 类型层已保证 encode 返回 PtrArrayBuffer;这里再 fail-loud 兜一层
160
- // (动态传入的无类型 codec 仍可能违反),然后直接取 .ptr——不再重算。
161
- const p = buf.ptr;
162
- if (typeof p !== 'number')
163
- throw new Error('ffi-bind: codec.encode must return a PtrArrayBuffer, got plain ArrayBuffer');
164
- if (nonnull && p === 0)
165
- throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}>ptr! encode produced 0 ` +
166
- `(null pointer forbidden here)`);
167
- writeSlot(dv, off, { k: 'ptr', v: p });
168
- }
224
+ slots.push(mkPtrWriter(name, encoders[name], argToken.endsWith('!'), sig, at, off, encoders));
225
+ off += IS64 ? 8 : PTR_SIZE;
226
+ continue;
169
227
  }
170
- else if (isCValToken(argToken)) {
171
- // 按值位:对象直传(值域由注册布局推导),无地址词汇 —— NULL / number
172
- // 进这里是必然错位(引擎按值语义,指针位请写 <N>ptr),fail-loud。
228
+ if (isCValToken(argToken)) {
173
229
  const name = valName(argToken);
230
+ // makeFn 已校验:布局存在、是结构(有 size)且 size >= 1
174
231
  const codec = valCodec(name, encoders);
175
- if (!codec)
176
- throw new Error(`ffi-bind: ${sig}: arg#${i + 1} <${name}> by-value position ` +
177
- `has no registered layout`);
178
- if (arg === null || arg === undefined)
179
- throw new Error(`ffi-bind: <${name}> value position got ${String(arg)}; ` +
180
- `pass a struct object (use <${name}>ptr to pass a pointer)`);
181
232
  const size = codec.size;
182
- if (!is64 || (size <= 8 && (size & (size - 1)) === 0)) {
183
- // 字节直接落槽:ia32 结构按值压栈(任意尺寸);x64 尺寸 ∈{1,2,4,8}
184
- // 按「同尺寸整数」传值——字节在 8 字节槽低位,callee 右对齐读。
185
- // argFrame 构造时即零填充,槽高位为 0(callee 忽略高位,无歧义)。
186
- codec.encode(arg, argFrame, off);
187
- }
188
- else {
189
- // x64 其余尺寸(>8 或非 2 的幂)按引用:槽放指针,指向 caller 分配的
190
- // 16 字节对齐临时副本(MS x64 官方规则:单参数永不跨多个寄存器;对齐
191
- // 偏移自算,不依赖分配器的对齐保证)。held 保持引用直到调用返回。
192
- const buf = new ArrayBuffer(size + 15);
193
- const base = ffi.bufferPtr(buf);
194
- const pad = (16 - (base % 16)) % 16;
195
- codec.encode(arg, buf, pad);
196
- held.push(buf);
197
- writeSlot(dv, off, { k: 'ptr', v: base + pad });
198
- }
199
- }
200
- else {
201
- writeSlot(dv, off, { k: argToken, v: arg });
233
+ slots.push(mkValWriter(name, codec, size, off));
234
+ // ia32 = ALIGN(size, 4):结构字节原样压栈(libffi ffi.c 的 ia32 分支同规则)
235
+ off += IS64 ? 8 : (size % 4 === 0 ? size : size + 4 - (size % 4));
236
+ continue;
202
237
  }
203
- off += widthOf(argToken);
238
+ slots.push(mkScalarWriter(argToken, off));
239
+ off += IS64 ? 8 : IA32_ARG_SIZE[argToken];
204
240
  }
205
- const retBuf = new ArrayBuffer(8);
241
+ // x64 头 4 槽恒被桩读取,帧不足 4 槽时补零到 32 字节
242
+ const total = IS64 && argTokens.length < X64_MIN_SLOTS ? X64_MIN_SLOTS * 8 : off;
243
+ const ret = cTokenToType(retToken); // 按值形返回已在 parseSig 拒掉
206
244
  const retIsFp = retToken === 'f32' || retToken === 'f64';
207
- ffi.ffiCall(proc, argFrame, retBuf, retIsFp ? 1 : 0);
208
- const raw = readRet(cTokenToType(retToken), retBuf);
209
- // 返回位 '!' = 「C 保证非零」的作者担保,标错由这里当场 throw 兜底(显式崩溃而非静默流窜)。
210
- if (retToken.endsWith('!') && raw === 0)
211
- throw new Error(`ffi-bind: ${sig}: return ${retToken} got 0 ` +
212
- `(the token promises C never returns null here)`);
213
- // 返回位自动解码:品牌指针且该键声明了 decode → 交还 JS 值;
214
- // 空指针 0 恒原样返回(无可读内容,不进 decode),裸 <>ptr / 无 decode 的键原样返回。
215
- if (isCPtrToken(retToken) && raw !== 0) {
245
+ // 返回位 '!' = 「C 保证非零」的作者担保,标错由这里当场 throw 兜底(显式崩溃而非静默流窜)
246
+ const zeroMsg = retToken.endsWith('!')
247
+ ? `ffi-bind: ${sig}: return ${retToken} got 0 (the token promises C never returns null here)`
248
+ : null;
249
+ // 返回位自动解码:品牌指针且该键声明了 decode → 交还 JS 值;空指针 0 / 裸 <>ptr /
250
+ // 无 decode 的键原样返回。
251
+ let decode = null;
252
+ if (isCPtrToken(retToken)) {
216
253
  const name = ptrName(retToken);
217
- if (name !== '') {
218
- const codec = decoders[name];
219
- if (codec?.decode)
220
- return codec.decode(raw);
221
- }
254
+ const dec = name !== '' ? decoders[name]?.decode : undefined;
255
+ if (dec)
256
+ decode = (raw) => dec(raw);
222
257
  }
223
- return raw;
258
+ const read = ret === 'void'
259
+ ? () => undefined
260
+ : () => {
261
+ // 返槽读法按类型定型(RET_READER),不再走 readScalar 的 token 字符串 switch
262
+ const raw = RET_READER[ret](RET_DV);
263
+ if (zeroMsg !== null && raw === 0)
264
+ throw new Error(zeroMsg);
265
+ if (decode !== null && raw !== 0)
266
+ return decode(raw);
267
+ return raw;
268
+ };
269
+ return { total, slots, retIsFp, read, dv: null };
224
270
  }
225
- // 把标量参数写进 argFrame 槽位。ptr 为裸地址直通;引用型参数见 writePtrSlot。
226
- function readRet(ret, retBuf) {
227
- if (ret === 'void')
228
- return undefined;
229
- return readScalar(new DataView(retBuf), 0, ret);
271
+ function callPlan(proc, plan, args) {
272
+ const slots = plan.slots;
273
+ const n = slots.length;
274
+ // 参数帧复用(见 CallPlan.dv):首调分配,其后原样重用——写入区间恒定,无需清零。
275
+ let dv = plan.dv;
276
+ if (dv === null) {
277
+ dv = new DataView(new ArrayBuffer(plan.total));
278
+ plan.dv = dv;
279
+ }
280
+ // codec 编码缓冲要活到 read 之后(decode 可能顺着回传的指针读调用方缓冲):靠这个
281
+ // 局部变量存活,返回即不可达、交 GC 回收。原先恒分配空数组,无编码参数时这次省掉。
282
+ let held = null;
283
+ for (let i = 0; i < n; i++) {
284
+ const h = slots[i](dv, args[i]);
285
+ if (h !== undefined) {
286
+ if (held === null)
287
+ held = [h];
288
+ else
289
+ held.push(h);
290
+ }
291
+ }
292
+ ffi.ffiCall(proc, dv.buffer, RET_DV.buffer, plan.retIsFp ? 1 : 0);
293
+ return plan.read();
230
294
  }
231
295
  // 内建 codec:类型校验放在这里,与「用户布局 codec」同一分派点。
232
296
  // BYTE/WCHAR 拒绝裸 number(无句柄可 pin),提示改用 <>ptr。
@@ -295,7 +359,13 @@ const builtinCodecs = { WCHAR, BYTE };
295
359
  * encoders 喂入参位(encode 值域 → 形参类型),decoders 喂返回位(decode 值域
296
360
  * → 返回 V | null,替代 Ptr | null)。decoders 缺省时回退到 encoders——双向 codec
297
361
  * 只需传一次(与运行时 `decoders ?? encoders` 同构:都只在实参缺省时生效,
298
- * 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。 */
362
+ * 显式传空对象不 fallback)。内建 WCHAR/BYTE 无条件参与两张表。
363
+ *
364
+ * codec 契约(参数帧复用的前提,调用方须遵守):
365
+ * 1. encode/decode 内不得回调任何已 bind 的函数。参数帧按签名只分配一次并逐调复用,
366
+ * 回调重入发生在本层 ffiCall 之前,会把本层已写好的槽抹掉(静默错参)。
367
+ * 2. encode(v, buf, off) 必须写满槽宽:少写会残留上一调的字节。
368
+ * 输入校验/转码若需要调 Win32 API,请在调用 bind 出的函数**之前**做。 */
299
369
  export function bind(dll, name, sig, encoders, decoders) {
300
370
  const proc = ffi.GetProcAddress(loadDll(dll), name);
301
371
  if (!proc)
@@ -99,7 +99,10 @@ type Entry = {
99
99
  };
100
100
  }[C_BasicType_No_Void];
101
101
  /** 内存写:按 sizeof 精确写,供 struct 字段编码用。小整数只占自己的字节,
102
- * 否则 `{a:u8,b:u8}` 这类紧凑布局会被 setUint32 越界覆盖相邻字段。 */
102
+ * 否则 `{a:u8,b:u8}` 这类紧凑布局会被 setUint32 越界覆盖相邻字段。
103
+ * 32 位槽不显式 `>>> 0` / `Number()`:setUint32/setUint8… 内部即 ToUint32/ToNumber,
104
+ * 逐位等价;只有给这些槽塞 BigInt 会从「静默截断」变成 TypeError(类型层本就禁它)。
105
+ * 指针槽的取模见下(setBigUint64 内部即 ToBigUint64)。 */
103
106
  export declare function writeScalar(dv: DataView, off: number, { k, v }: Entry): void;
104
107
  /** 槽写:供 bind 的参数槽/返回槽用。小整数扩展到 4 字节 —— 可变参数按 int 提升读,
105
108
  * `i8 = -1` 必须给 `0xFFFFFFFF` 而非 `0x000000FF`。u32/i32/u64/i64/f32/f64/ptr
package/lib/ffi/ctype.js CHANGED
@@ -121,26 +121,29 @@ export function readScalar(dv, off, k) {
121
121
  }
122
122
  }
123
123
  /** 内存写:按 sizeof 精确写,供 struct 字段编码用。小整数只占自己的字节,
124
- * 否则 `{a:u8,b:u8}` 这类紧凑布局会被 setUint32 越界覆盖相邻字段。 */
124
+ * 否则 `{a:u8,b:u8}` 这类紧凑布局会被 setUint32 越界覆盖相邻字段。
125
+ * 32 位槽不显式 `>>> 0` / `Number()`:setUint32/setUint8… 内部即 ToUint32/ToNumber,
126
+ * 逐位等价;只有给这些槽塞 BigInt 会从「静默截断」变成 TypeError(类型层本就禁它)。
127
+ * 指针槽的取模见下(setBigUint64 内部即 ToBigUint64)。 */
125
128
  export function writeScalar(dv, off, { k, v }) {
126
129
  switch (k) {
127
130
  case 'u8':
128
- dv.setUint8(off, Number(v));
131
+ dv.setUint8(off, v);
129
132
  break;
130
133
  case 'i8':
131
- dv.setInt8(off, Number(v));
134
+ dv.setInt8(off, v);
132
135
  break;
133
136
  case 'u16':
134
- dv.setUint16(off, Number(v), true);
137
+ dv.setUint16(off, v, true);
135
138
  break;
136
139
  case 'i16':
137
- dv.setInt16(off, Number(v), true);
140
+ dv.setInt16(off, v, true);
138
141
  break;
139
142
  case 'u32':
140
- dv.setUint32(off, (v) >>> 0, true);
143
+ dv.setUint32(off, v, true);
141
144
  break;
142
145
  case 'i32':
143
- dv.setUint32(off, (v) >>> 0, true);
146
+ dv.setUint32(off, v, true);
144
147
  break;
145
148
  case 'u64':
146
149
  dv.setBigUint64(off, v, true);
@@ -149,22 +152,23 @@ export function writeScalar(dv, off, { k, v }) {
149
152
  dv.setBigInt64(off, v, true);
150
153
  break;
151
154
  case 'f32':
152
- dv.setFloat32(off, (v), true);
155
+ dv.setFloat32(off, v, true);
153
156
  break;
154
157
  case 'f64':
155
- dv.setFloat64(off, (v), true);
158
+ dv.setFloat64(off, v, true);
156
159
  break;
157
160
  case 'ptr': {
158
161
  // JS null/undefined 不是指针词汇(空位写 NULL)——fail-loud,不做 null→0 转换。
159
162
  if (v == null) {
160
163
  throw new Error('ffi: got null/undefined for a ptr slot; pass the imported NULL constant for empty');
161
164
  }
162
- // 读回的负值(LRESULT/句柄)再作参数回传时按二补位位模式写:asUintN /
163
- // >>> 0 取模保真,否则 x64 传负 setBigUint64 直接 RangeError。
165
+ // 读回的负值(LRESULT/句柄)再作参数回传时按二补位位模式写:setBigUint64 /
166
+ // setUint32 内部即 ToBigUint64 / ToUint32,-1n 与负 number 都自然取模成全 1。
167
+ // QuickJS 的 setBigUint64 只收 BigInt(传 number 会 TypeError),故仍需 BigInt(v)。
164
168
  if (PTR_SIZE === 8)
165
- dv.setBigUint64(off, BigInt.asUintN(64, BigInt(v)), true);
169
+ dv.setBigUint64(off, BigInt(v), true);
166
170
  else
167
- dv.setUint32(off, v >>> 0, true);
171
+ dv.setUint32(off, v, true);
168
172
  break;
169
173
  }
170
174
  }
@@ -175,7 +179,7 @@ export function writeScalar(dv, off, { k, v }) {
175
179
  export function writeSlot(dv, off, e) {
176
180
  const { k, v } = e;
177
181
  if (k === 'u8' || k === 'i8' || k === 'u16' || k === 'i16') {
178
- dv.setUint32(off, Number(v) >>> 0, true);
182
+ dv.setUint32(off, v, true);
179
183
  return;
180
184
  }
181
185
  writeScalar(dv, off, e);
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "quickwin",
4
- "version": "3.0.2",
4
+ "version": "3.0.4",
5
5
  "description": "QuickJS Win32 runtime — GUI, networking, WASM, FFI, and more",
6
+ "types": "quickwin.d.ts",
6
7
  "bin": {
7
8
  "quickwin": "qwin.exe",
8
9
  "quickwin-nowasm": "qwin-nowasm.exe",
Binary file
package/qwin-nowasm.exe CHANGED
Binary file
package/qwin-x86.exe CHANGED
Binary file
package/qwin.exe CHANGED
Binary file