quickwin 1.0.2 → 1.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.
- package/README.en.md +204 -204
- package/README.md +203 -203
- package/examples/PdfCanvas.tsx +83 -83
- package/examples/exec_server.ts +35 -35
- package/examples/pdf_preview2.ts +375 -375
- package/examples/pdf_viewer.tsx +279 -279
- package/examples/test_gallery.tsx +285 -285
- package/examples/test_react_lazy.tsx +63 -63
- package/examples/test_react_listbox.tsx +71 -71
- package/examples/test_root_window.tsx +41 -41
- package/examples/test_tab.tsx +75 -75
- package/examples/tray_demo.tsx +81 -81
- package/lib/assert.ts +6 -6
- package/lib/cache-utils.ts +40 -40
- package/lib/dumpRects.ts +25 -25
- package/lib/fetch.ts +502 -502
- package/lib/http-common.ts +399 -399
- package/lib/http-server.ts +424 -424
- package/lib/polyfill.ts +171 -171
- package/lib/react-qw/components/index.ts +17 -17
- package/lib/react-qw/index.ts +90 -90
- package/lib/react-qw/jsx.d.ts +34 -34
- package/lib/react-qw/layout.ts +111 -111
- package/lib/react-qw/props.ts +36 -36
- package/lib/react-qw/reconciler.ts +373 -373
- package/lib/stream.ts +21 -21
- package/lib/text-codec.ts +171 -171
- package/lib/text-measure.ts +54 -54
- package/lib/url.ts +201 -201
- package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
- package/lib/vendor/web-streams/ponyfill.mjs +8 -8
- package/lib/websocket.js +8 -0
- package/lib/websocket.ts +563 -555
- package/package.json +50 -44
- 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/run.ts +96 -96
- package/test/test_basic.ts +9 -9
- package/test/test_basic_fetch.ts +33 -33
- package/test/test_brotli.ts +52 -52
- package/test/test_fetch_cache.ts +187 -187
- package/test/test_ffi.ts +174 -174
- package/test/test_frame_encoding.ts +132 -132
- package/test/test_helper.ts +80 -80
- package/test/test_http_import.ts +74 -74
- package/test/test_http_server.ts +136 -136
- package/test/test_import_dir.ts +5 -5
- package/test/test_ipv6.ts +129 -129
- package/test/test_meta_url.ts +4 -4
- package/test/test_mupdf_render.ts +74 -74
- package/test/test_mupdf_twice.ts +81 -81
- package/test/test_mupdf_wasm.ts +30 -30
- package/test/test_net_event.ts +55 -55
- package/test/test_net_fetch.ts +238 -238
- package/test/test_net_websocket.ts +164 -164
- package/test/test_polyfill.ts +159 -159
- package/test/test_url.ts +183 -183
- package/test/test_wasm_basic.ts +73 -73
- package/test/test_wasm_import_global.ts +45 -45
- package/test/test_wasm_sjlj.ts +125 -125
- package/test/test_wasm_types.ts +124 -124
- package/test/test_worker.ts +24 -24
- package/test/test_worker_fetch.ts +27 -27
- package/test/test_worker_net.ts +54 -54
- package/test/test_worker_wasm_concurrent.ts +45 -45
- package/test/worker_fetch.ts +13 -13
- package/test/worker_helper.ts +1 -1
- package/test/worker_http.ts +20 -20
- package/test/worker_import_lib.ts +13 -13
- package/test/worker_module.ts +17 -17
- package/test/worker_wasm_concurrent.ts +22 -22
- package/tsconfig.json +27 -27
- package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
- package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
- package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
- package/vendor/mupdf-wasm/mupdf.js +3317 -3317
package/README.md
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
> [English](README.en.md) · **中文**
|
|
2
|
-
|
|
3
|
-
# QuickWin
|
|
4
|
-
|
|
5
|
-
Windows 平台上的 QuickJS 运行时,支持原生 GUI、网络、WASM、FFI 等能力。
|
|
6
|
-
|
|
7
|
-
目标:兼容 Windows XP+(32/64 位),保持最小二进制体积。
|
|
8
|
-
|
|
9
|
-
现状:当前支持 Windows 7+(64 位),体积约 1.5MB。
|
|
10
|
-
|
|
11
|
-
项目处于早期,功能待完善,包含已知 bug。
|
|
12
|
-
|
|
13
|
-
直接从 GitHub Release 下载运行:
|
|
14
|
-
|
|
15
|
-
```powershell
|
|
16
|
-
iwr https://github.com/xna00/quickwin/releases/latest/download/qwin.exe -OutFile qwin.exe
|
|
17
|
-
iwr https://github.com/xna00/quickwin/releases/latest/download/main.js -OutFile main.js
|
|
18
|
-
.\qwin.exe main.js
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 特性
|
|
22
|
-
|
|
23
|
-
- **Win32 原生 GUI** —— 原生窗口、按钮、编辑框、列表框、托盘图标、弹出菜单
|
|
24
|
-
- **React 渲染器** —— 用 JSX 声明式 GUI,支持 `useState`/`useEffect`,diff 增量更新([react-qw](lib/react-qw/))
|
|
25
|
-
- **HTTP/HTTPS** —— `fetch()` API、Brotli 解压、chunked 传输、条件缓存
|
|
26
|
-
- **WebSocket** —— 完整的 RFC 6455 实现,支持 ws:// 与 wss://
|
|
27
|
-
- **WebAssembly** —— 基于 WAMR,支持标准 `WebAssembly.*` API
|
|
28
|
-
- **FFI** —— 通过 libffi 调用任意 DLL 函数
|
|
29
|
-
- **Polyfills** —— `TextEncoder`、`URL`、`URLSearchParams`、`btoa`/`atob`、`crypto.subtle`、`setTimeout`
|
|
30
|
-
- **动态导入** —— `import('https://esm.sh/...')`,无需安装任何依赖
|
|
31
|
-
|
|
32
|
-
## CLI
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
quickwin script.js # 运行脚本
|
|
36
|
-
quickwin -o CON script.js # 带控制台运行(AllocConsole)
|
|
37
|
-
quickwin -o LOG script.js # 自动生成日志文件运行
|
|
38
|
-
quickwin -e "console.log('hi')" # 执行表达式
|
|
39
|
-
quickwin -- script.js --flag # -- 停止选项解析
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### 选项
|
|
43
|
-
|
|
44
|
-
| Flag | Description |
|
|
45
|
-
|------|-------------|
|
|
46
|
-
| `-e <expr>` | 执行表达式而不是文件 |
|
|
47
|
-
| `-o CON` | 分配控制台(`AllocConsole`);仅 GUI 子系统构建所需 |
|
|
48
|
-
| `-o LOG` | 将 stdout+stderr 重定向到 `log_YYYY_MM_DD_HH_MM_SS.txt`(exe 所在目录) |
|
|
49
|
-
| `-o <file>` | 将 stdout+stderr 重定向到指定文件 |
|
|
50
|
-
| `-d` | 开启 HTTP 调试日志 |
|
|
51
|
-
| `--` | 停止选项解析,其余参数传给脚本 |
|
|
52
|
-
|
|
53
|
-
所有未知参数都会透明转发给 `scriptArgs` —— 不报错、不消费。选项解析后的第一个非参数即脚本文件。
|
|
54
|
-
|
|
55
|
-
## 内嵌脚本
|
|
56
|
-
|
|
57
|
-
可以把一个 JS 脚本直接内嵌进 `qwin.exe` 二进制 —— 无需重新编译。
|
|
58
|
-
|
|
59
|
-
### 格式(追加到 exe 末尾)
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
[JS bytes (N)] [N: uint32 LE] [magic "QWJS"]
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 用法
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# 内嵌一个脚本
|
|
69
|
-
powershell -ExecutionPolicy Bypass -File scripts/embed-js.ps1 -ExePath _build/qwin.exe -JsFile script.js
|
|
70
|
-
|
|
71
|
-
# 或通过 make
|
|
72
|
-
make embed-js JS_EMBED=script.js
|
|
73
|
-
|
|
74
|
-
# 内嵌 Brotli 压缩的脚本(exe 更小,运行时解压)
|
|
75
|
-
make embed-js-br JS_EMBED=script.js
|
|
76
|
-
|
|
77
|
-
# 运行它(不需要文件参数)
|
|
78
|
-
qwin.exe
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
不带脚本文件参数运行时,`qwin.exe` 会检查自身末尾是否有内嵌 JS:找到则执行内嵌代码,否则回退到 `main.js`。同时支持原始(`QWJS`)与 Brotli 压缩(`QWBR`)两种内嵌格式;压缩格式在启动时解压。
|
|
82
|
-
|
|
83
|
-
## 模块
|
|
84
|
-
|
|
85
|
-
| Module | Import | Description |
|
|
86
|
-
|--------|--------|-------------|
|
|
87
|
-
| `std` | built-in | 文件 I/O、环境变量、URL 下载 |
|
|
88
|
-
| `os` | built-in | 文件系统、进程、Worker、定时器 |
|
|
89
|
-
| `gui` | built-in | Win32 窗口/控件/消息/托盘 API |
|
|
90
|
-
| `sock` | built-in | Socket 网络(`AddrFamily`、`FdEvent` 等) |
|
|
91
|
-
| `wolfssl` | built-in | TLS/SSL(`VerifyMode`、`ReturnCode` 等) |
|
|
92
|
-
| `ffi` | built-in | 外部函数接口 |
|
|
93
|
-
| `win` | built-in | DLL 加载(`LoadLibrary`、`GetProcAddress`) |
|
|
94
|
-
| `brotli` | built-in | Brotli 解压 |
|
|
95
|
-
| `wamr` | built-in | 底层 WAMR API |
|
|
96
|
-
| `fetch` | `import './lib/fetch.js'` | 向 globalThis 添加 `fetch()`、`Response`、`Headers` |
|
|
97
|
-
| `websocket` | `import './lib/websocket.js'` | 向 globalThis 添加 `WebSocket` |
|
|
98
|
-
| `polyfill` | `import './lib/polyfill.js'` | 向 globalThis 添加 `TextEncoder`、`URL`、`btoa`/`atob`、`setTimeout` |
|
|
99
|
-
| `preact` | `lib/preact/...` | JSX → Win32 渲染器(`render`、`useState`、`useEffect`) |
|
|
100
|
-
| `react-qw` | `lib/react-qw/` | 面向 Win32 GUI 的 React 自定义渲染器([文档](lib/react-qw/)) |
|
|
101
|
-
|
|
102
|
-
## Worker
|
|
103
|
-
|
|
104
|
-
面向 CPU 密集或 I/O 密集工作创建后台线程。Worker 运行在独立的 QuickJS runtime 中,各自拥有自己的事件循环。
|
|
105
|
-
|
|
106
|
-
### 主线程
|
|
107
|
-
|
|
108
|
-
```js
|
|
109
|
-
import * as os from 'os'
|
|
110
|
-
|
|
111
|
-
const worker = new os.Worker('./worker.js')
|
|
112
|
-
|
|
113
|
-
worker.onmessage = (e) => {
|
|
114
|
-
console.log('received:', e.data)
|
|
115
|
-
worker.onmessage = null // 完成后清理
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
worker.postMessage({ type: 'start', value: 42 })
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Worker 线程
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
// worker.js
|
|
125
|
-
import * as os from 'os'
|
|
126
|
-
|
|
127
|
-
const parent = os.Worker.parent
|
|
128
|
-
|
|
129
|
-
parent.onmessage = (e) => {
|
|
130
|
-
if (e.data.type === 'start') {
|
|
131
|
-
parent.postMessage({ type: 'result', value: e.data.value * 2 })
|
|
132
|
-
} else if (e.data.type === 'done') {
|
|
133
|
-
parent.onmessage = null // 清理以允许事件循环退出
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### API
|
|
139
|
-
|
|
140
|
-
| API | Description |
|
|
141
|
-
|-----|-------------|
|
|
142
|
-
| `new os.Worker(specifier)` | 创建 worker。`specifier` 是文件路径或 URL(支持 `https://` ESM 导入) |
|
|
143
|
-
| `worker.postMessage(data)` | 向 worker 发送消息(JSON 可序列化) |
|
|
144
|
-
| `worker.onmessage = fn` | 设置来自 worker 的消息回调。完成后设为 `null` 以释放消息端口 |
|
|
145
|
-
| `os.Worker.parent` | (Worker 侧)父线程的引用 |
|
|
146
|
-
|
|
147
|
-
### 清理
|
|
148
|
-
|
|
149
|
-
通信完成后,双方都必须把 `onmessage` 设为 `null`。这会释放消息端口,使事件循环能够干净退出。忘记设置会导致进程挂起。
|
|
150
|
-
|
|
151
|
-
## 示例
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
.\qwin.exe examples/preact_demo.js # JSX + hooks 的计数器 GUI
|
|
155
|
-
.\qwin.exe examples/tray_demo.js # 系统托盘应用
|
|
156
|
-
.\qwin.exe examples/pdf_preview.js # 基于 mupdf 的 PDF 阅读器
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## 从源码构建
|
|
160
|
-
|
|
161
|
-
### 前置依赖
|
|
162
|
-
|
|
163
|
-
- MSYS2 UCRT64 或 MINGW64
|
|
164
|
-
- Node.js(用于通过 tsc 编译 TypeScript)
|
|
165
|
-
- Git(用于子模块)
|
|
166
|
-
|
|
167
|
-
### 构建
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
git clone --recursive https://github.com/anomalyco/quickwin.git
|
|
171
|
-
cd quickwin
|
|
172
|
-
|
|
173
|
-
.\run.ps1 "make wamr" # 构建 WAMR 库(仅首次)
|
|
174
|
-
.\run.ps1 "make minimal" # 构建 qwin.exe(-Os + LTO + UPX)
|
|
175
|
-
.\run.ps1 "make js" # 编译 TypeScript
|
|
176
|
-
.\run.ps1 "make test" # 运行全部测试
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
`make nowasm` 会产出更小的 `_build/qwin-nowasm.exe`,不含 WASM/WAMR
|
|
180
|
-
(没有 `WebAssembly` 全局对象);对这种版本运行 `make test TEST=-wasm` 跳过 WASM 测试。
|
|
181
|
-
|
|
182
|
-
### 构建目标
|
|
183
|
-
|
|
184
|
-
| Target | Description |
|
|
185
|
-
|--------|-------------|
|
|
186
|
-
| `make` / `make nodebug` | 快速构建 |
|
|
187
|
-
| `make minimal` | `-Os` + LTO + `-mwindows` + UPX,无控制台,需要控制台时加 `-o CON` |
|
|
188
|
-
| `make nowasm` | 无 WASM/WAMR 构建 → `_build/qwin-nowasm.exe`(约 1.2MB,无 `WebAssembly` 全局) |
|
|
189
|
-
| `make release` | `-O2` + LTO + strip,约 1.5MB |
|
|
190
|
-
| `make debug` | 带 bridge 日志的调试构建 |
|
|
191
|
-
| `make js` | 通过 tsc 编译 TypeScript |
|
|
192
|
-
| `make wasm` | 编译 WAT → WASM fixtures |
|
|
193
|
-
| `make test` | 运行全部测试 |
|
|
194
|
-
| `make test TEST=-net` | 跳过网络测试(快速) |
|
|
195
|
-
| `make test TEST=wasm` | 仅运行 WASM 测试 |
|
|
196
|
-
| `make wamr` | 重建 WAMR 库 |
|
|
197
|
-
| `make embed-js` | 把 `embed.js` 内嵌进 exe(用 `JS_EMBED=file.js`) |
|
|
198
|
-
| `make embed-js-br` | 把 Brotli 压缩的 JS 内嵌进 exe |
|
|
199
|
-
| `make npm-pkg` | 打包到 `dist/quickwin/` |
|
|
200
|
-
| `make clean` | 清理构建产物 |
|
|
201
|
-
|
|
202
|
-
## 许可证
|
|
203
|
-
|
|
1
|
+
> [English](README.en.md) · **中文**
|
|
2
|
+
|
|
3
|
+
# QuickWin
|
|
4
|
+
|
|
5
|
+
Windows 平台上的 QuickJS 运行时,支持原生 GUI、网络、WASM、FFI 等能力。
|
|
6
|
+
|
|
7
|
+
目标:兼容 Windows XP+(32/64 位),保持最小二进制体积。
|
|
8
|
+
|
|
9
|
+
现状:当前支持 Windows 7+(64 位),体积约 1.5MB。
|
|
10
|
+
|
|
11
|
+
项目处于早期,功能待完善,包含已知 bug。
|
|
12
|
+
|
|
13
|
+
直接从 GitHub Release 下载运行:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
iwr https://github.com/xna00/quickwin/releases/latest/download/qwin.exe -OutFile qwin.exe
|
|
17
|
+
iwr https://github.com/xna00/quickwin/releases/latest/download/main.js -OutFile main.js
|
|
18
|
+
.\qwin.exe main.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 特性
|
|
22
|
+
|
|
23
|
+
- **Win32 原生 GUI** —— 原生窗口、按钮、编辑框、列表框、托盘图标、弹出菜单
|
|
24
|
+
- **React 渲染器** —— 用 JSX 声明式 GUI,支持 `useState`/`useEffect`,diff 增量更新([react-qw](lib/react-qw/))
|
|
25
|
+
- **HTTP/HTTPS** —— `fetch()` API、Brotli 解压、chunked 传输、条件缓存
|
|
26
|
+
- **WebSocket** —— 完整的 RFC 6455 实现,支持 ws:// 与 wss://
|
|
27
|
+
- **WebAssembly** —— 基于 WAMR,支持标准 `WebAssembly.*` API
|
|
28
|
+
- **FFI** —— 通过 libffi 调用任意 DLL 函数
|
|
29
|
+
- **Polyfills** —— `TextEncoder`、`URL`、`URLSearchParams`、`btoa`/`atob`、`crypto.subtle`、`setTimeout`
|
|
30
|
+
- **动态导入** —— `import('https://esm.sh/...')`,无需安装任何依赖
|
|
31
|
+
|
|
32
|
+
## CLI
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
quickwin script.js # 运行脚本
|
|
36
|
+
quickwin -o CON script.js # 带控制台运行(AllocConsole)
|
|
37
|
+
quickwin -o LOG script.js # 自动生成日志文件运行
|
|
38
|
+
quickwin -e "console.log('hi')" # 执行表达式
|
|
39
|
+
quickwin -- script.js --flag # -- 停止选项解析
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 选项
|
|
43
|
+
|
|
44
|
+
| Flag | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `-e <expr>` | 执行表达式而不是文件 |
|
|
47
|
+
| `-o CON` | 分配控制台(`AllocConsole`);仅 GUI 子系统构建所需 |
|
|
48
|
+
| `-o LOG` | 将 stdout+stderr 重定向到 `log_YYYY_MM_DD_HH_MM_SS.txt`(exe 所在目录) |
|
|
49
|
+
| `-o <file>` | 将 stdout+stderr 重定向到指定文件 |
|
|
50
|
+
| `-d` | 开启 HTTP 调试日志 |
|
|
51
|
+
| `--` | 停止选项解析,其余参数传给脚本 |
|
|
52
|
+
|
|
53
|
+
所有未知参数都会透明转发给 `scriptArgs` —— 不报错、不消费。选项解析后的第一个非参数即脚本文件。
|
|
54
|
+
|
|
55
|
+
## 内嵌脚本
|
|
56
|
+
|
|
57
|
+
可以把一个 JS 脚本直接内嵌进 `qwin.exe` 二进制 —— 无需重新编译。
|
|
58
|
+
|
|
59
|
+
### 格式(追加到 exe 末尾)
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
[JS bytes (N)] [N: uint32 LE] [magic "QWJS"]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 用法
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 内嵌一个脚本
|
|
69
|
+
powershell -ExecutionPolicy Bypass -File scripts/embed-js.ps1 -ExePath _build/qwin.exe -JsFile script.js
|
|
70
|
+
|
|
71
|
+
# 或通过 make
|
|
72
|
+
make embed-js JS_EMBED=script.js
|
|
73
|
+
|
|
74
|
+
# 内嵌 Brotli 压缩的脚本(exe 更小,运行时解压)
|
|
75
|
+
make embed-js-br JS_EMBED=script.js
|
|
76
|
+
|
|
77
|
+
# 运行它(不需要文件参数)
|
|
78
|
+
qwin.exe
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
不带脚本文件参数运行时,`qwin.exe` 会检查自身末尾是否有内嵌 JS:找到则执行内嵌代码,否则回退到 `main.js`。同时支持原始(`QWJS`)与 Brotli 压缩(`QWBR`)两种内嵌格式;压缩格式在启动时解压。
|
|
82
|
+
|
|
83
|
+
## 模块
|
|
84
|
+
|
|
85
|
+
| Module | Import | Description |
|
|
86
|
+
|--------|--------|-------------|
|
|
87
|
+
| `std` | built-in | 文件 I/O、环境变量、URL 下载 |
|
|
88
|
+
| `os` | built-in | 文件系统、进程、Worker、定时器 |
|
|
89
|
+
| `gui` | built-in | Win32 窗口/控件/消息/托盘 API |
|
|
90
|
+
| `sock` | built-in | Socket 网络(`AddrFamily`、`FdEvent` 等) |
|
|
91
|
+
| `wolfssl` | built-in | TLS/SSL(`VerifyMode`、`ReturnCode` 等) |
|
|
92
|
+
| `ffi` | built-in | 外部函数接口 |
|
|
93
|
+
| `win` | built-in | DLL 加载(`LoadLibrary`、`GetProcAddress`) |
|
|
94
|
+
| `brotli` | built-in | Brotli 解压 |
|
|
95
|
+
| `wamr` | built-in | 底层 WAMR API |
|
|
96
|
+
| `fetch` | `import './lib/fetch.js'` | 向 globalThis 添加 `fetch()`、`Response`、`Headers` |
|
|
97
|
+
| `websocket` | `import './lib/websocket.js'` | 向 globalThis 添加 `WebSocket` |
|
|
98
|
+
| `polyfill` | `import './lib/polyfill.js'` | 向 globalThis 添加 `TextEncoder`、`URL`、`btoa`/`atob`、`setTimeout` |
|
|
99
|
+
| `preact` | `lib/preact/...` | JSX → Win32 渲染器(`render`、`useState`、`useEffect`) |
|
|
100
|
+
| `react-qw` | `lib/react-qw/` | 面向 Win32 GUI 的 React 自定义渲染器([文档](lib/react-qw/)) |
|
|
101
|
+
|
|
102
|
+
## Worker
|
|
103
|
+
|
|
104
|
+
面向 CPU 密集或 I/O 密集工作创建后台线程。Worker 运行在独立的 QuickJS runtime 中,各自拥有自己的事件循环。
|
|
105
|
+
|
|
106
|
+
### 主线程
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import * as os from 'os'
|
|
110
|
+
|
|
111
|
+
const worker = new os.Worker('./worker.js')
|
|
112
|
+
|
|
113
|
+
worker.onmessage = (e) => {
|
|
114
|
+
console.log('received:', e.data)
|
|
115
|
+
worker.onmessage = null // 完成后清理
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
worker.postMessage({ type: 'start', value: 42 })
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Worker 线程
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// worker.js
|
|
125
|
+
import * as os from 'os'
|
|
126
|
+
|
|
127
|
+
const parent = os.Worker.parent
|
|
128
|
+
|
|
129
|
+
parent.onmessage = (e) => {
|
|
130
|
+
if (e.data.type === 'start') {
|
|
131
|
+
parent.postMessage({ type: 'result', value: e.data.value * 2 })
|
|
132
|
+
} else if (e.data.type === 'done') {
|
|
133
|
+
parent.onmessage = null // 清理以允许事件循环退出
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### API
|
|
139
|
+
|
|
140
|
+
| API | Description |
|
|
141
|
+
|-----|-------------|
|
|
142
|
+
| `new os.Worker(specifier)` | 创建 worker。`specifier` 是文件路径或 URL(支持 `https://` ESM 导入) |
|
|
143
|
+
| `worker.postMessage(data)` | 向 worker 发送消息(JSON 可序列化) |
|
|
144
|
+
| `worker.onmessage = fn` | 设置来自 worker 的消息回调。完成后设为 `null` 以释放消息端口 |
|
|
145
|
+
| `os.Worker.parent` | (Worker 侧)父线程的引用 |
|
|
146
|
+
|
|
147
|
+
### 清理
|
|
148
|
+
|
|
149
|
+
通信完成后,双方都必须把 `onmessage` 设为 `null`。这会释放消息端口,使事件循环能够干净退出。忘记设置会导致进程挂起。
|
|
150
|
+
|
|
151
|
+
## 示例
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
.\qwin.exe examples/preact_demo.js # JSX + hooks 的计数器 GUI
|
|
155
|
+
.\qwin.exe examples/tray_demo.js # 系统托盘应用
|
|
156
|
+
.\qwin.exe examples/pdf_preview.js # 基于 mupdf 的 PDF 阅读器
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 从源码构建
|
|
160
|
+
|
|
161
|
+
### 前置依赖
|
|
162
|
+
|
|
163
|
+
- MSYS2 UCRT64 或 MINGW64
|
|
164
|
+
- Node.js(用于通过 tsc 编译 TypeScript)
|
|
165
|
+
- Git(用于子模块)
|
|
166
|
+
|
|
167
|
+
### 构建
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
git clone --recursive https://github.com/anomalyco/quickwin.git
|
|
171
|
+
cd quickwin
|
|
172
|
+
|
|
173
|
+
.\run.ps1 "make wamr" # 构建 WAMR 库(仅首次)
|
|
174
|
+
.\run.ps1 "make minimal" # 构建 qwin.exe(-Os + LTO + UPX)
|
|
175
|
+
.\run.ps1 "make js" # 编译 TypeScript
|
|
176
|
+
.\run.ps1 "make test" # 运行全部测试
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`make nowasm` 会产出更小的 `_build/qwin-nowasm.exe`,不含 WASM/WAMR
|
|
180
|
+
(没有 `WebAssembly` 全局对象);对这种版本运行 `make test TEST=-wasm` 跳过 WASM 测试。
|
|
181
|
+
|
|
182
|
+
### 构建目标
|
|
183
|
+
|
|
184
|
+
| Target | Description |
|
|
185
|
+
|--------|-------------|
|
|
186
|
+
| `make` / `make nodebug` | 快速构建 |
|
|
187
|
+
| `make minimal` | `-Os` + LTO + `-mwindows` + UPX,无控制台,需要控制台时加 `-o CON` |
|
|
188
|
+
| `make nowasm` | 无 WASM/WAMR 构建 → `_build/qwin-nowasm.exe`(约 1.2MB,无 `WebAssembly` 全局) |
|
|
189
|
+
| `make release` | `-O2` + LTO + strip,约 1.5MB |
|
|
190
|
+
| `make debug` | 带 bridge 日志的调试构建 |
|
|
191
|
+
| `make js` | 通过 tsc 编译 TypeScript |
|
|
192
|
+
| `make wasm` | 编译 WAT → WASM fixtures |
|
|
193
|
+
| `make test` | 运行全部测试 |
|
|
194
|
+
| `make test TEST=-net` | 跳过网络测试(快速) |
|
|
195
|
+
| `make test TEST=wasm` | 仅运行 WASM 测试 |
|
|
196
|
+
| `make wamr` | 重建 WAMR 库 |
|
|
197
|
+
| `make embed-js` | 把 `embed.js` 内嵌进 exe(用 `JS_EMBED=file.js`) |
|
|
198
|
+
| `make embed-js-br` | 把 Brotli 压缩的 JS 内嵌进 exe |
|
|
199
|
+
| `make npm-pkg` | 打包到 `dist/quickwin/` |
|
|
200
|
+
| `make clean` | 清理构建产物 |
|
|
201
|
+
|
|
202
|
+
## 许可证
|
|
203
|
+
|
|
204
204
|
MIT
|
package/examples/PdfCanvas.tsx
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import { forwardRef, useRef } from 'react'
|
|
2
|
-
import * as gui from 'gui'
|
|
3
|
-
import * as win from 'win'
|
|
4
|
-
import * as ffi from 'ffi'
|
|
5
|
-
import type { WStyle } from '../lib/react-qw/jsx.d.ts'
|
|
6
|
-
|
|
7
|
-
const _user32 = win.LoadLibrary('user32.dll')
|
|
8
|
-
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
9
|
-
if (!_user32 || !_gdi32) throw new Error('Failed to load user32/gdi32')
|
|
10
|
-
|
|
11
|
-
function loadProc(lib: win.HMODULE, name: string): number {
|
|
12
|
-
const ptr = win.GetProcAddress(lib, name)
|
|
13
|
-
if (!ptr) throw new Error('Failed to load ' + name)
|
|
14
|
-
return ptr
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const GetDC_ = loadProc(_user32, 'GetDC')
|
|
18
|
-
const ReleaseDC_ = loadProc(_user32, 'ReleaseDC')
|
|
19
|
-
const SetDIBitsToDevice_ = loadProc(_gdi32, 'SetDIBitsToDevice')
|
|
20
|
-
|
|
21
|
-
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
22
|
-
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
23
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
24
|
-
|
|
25
|
-
function makeBitmapInfo(w: number, h: number): ArrayBuffer {
|
|
26
|
-
const bmi = new ArrayBuffer(40)
|
|
27
|
-
const bv = new DataView(bmi)
|
|
28
|
-
bv.setUint32(0, 40, true)
|
|
29
|
-
bv.setInt32(4, w, true)
|
|
30
|
-
bv.setInt32(8, -h, true)
|
|
31
|
-
bv.setUint16(12, 1, true)
|
|
32
|
-
bv.setUint16(14, 24, true)
|
|
33
|
-
return bmi
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface PdfCanvasProps {
|
|
37
|
-
pixmap?: { data: ArrayBuffer; w: number; h: number }
|
|
38
|
-
style?: WStyle
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const PdfCanvas = forwardRef<gui.HWND, PdfCanvasProps>(
|
|
42
|
-
({ pixmap, style }, ref) => {
|
|
43
|
-
const canvasRef = useRef<gui.HWND>(null)
|
|
44
|
-
const pixmapRef = useRef(pixmap)
|
|
45
|
-
pixmapRef.current = pixmap
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<w type="STATIC"
|
|
49
|
-
ws={gui.WindowStyle.VISIBLE | gui.WindowStyle.CLIPCHILDREN}
|
|
50
|
-
style={style}
|
|
51
|
-
ref={(h: gui.HWND) => {
|
|
52
|
-
canvasRef.current = h
|
|
53
|
-
if (typeof ref === 'function') ref(h)
|
|
54
|
-
else if (ref) (ref as React.RefObject<gui.HWND | null>).current = h
|
|
55
|
-
}}
|
|
56
|
-
onEvent={(e) => {
|
|
57
|
-
const hwnd = e.hwnd
|
|
58
|
-
if (e.msg === 0x14) return 1
|
|
59
|
-
if (e.msg === gui.WmMsg.PAINT) {
|
|
60
|
-
const pm = pixmapRef.current
|
|
61
|
-
if (!pm) return 0
|
|
62
|
-
const hdc = ffi.ffiCall(GetDC_, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
63
|
-
if (hdc) {
|
|
64
|
-
const bmi = makeBitmapInfo(pm.w, pm.h)
|
|
65
|
-
ffi.ffiCall(SetDIBitsToDevice_, [
|
|
66
|
-
ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
67
|
-
FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
68
|
-
FFI_PTR, FFI_PTR, FFI_U32
|
|
69
|
-
], [
|
|
70
|
-
hdc, 0, 0, pm.w, pm.h,
|
|
71
|
-
0, 0, 0, pm.h,
|
|
72
|
-
pm.data, bmi, 0
|
|
73
|
-
], FFI_S32)
|
|
74
|
-
ffi.ffiCall(ReleaseDC_, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
75
|
-
}
|
|
76
|
-
return 0
|
|
77
|
-
}
|
|
78
|
-
return
|
|
79
|
-
}}
|
|
80
|
-
/>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
)
|
|
1
|
+
import { forwardRef, useRef } from 'react'
|
|
2
|
+
import * as gui from 'gui'
|
|
3
|
+
import * as win from 'win'
|
|
4
|
+
import * as ffi from 'ffi'
|
|
5
|
+
import type { WStyle } from '../lib/react-qw/jsx.d.ts'
|
|
6
|
+
|
|
7
|
+
const _user32 = win.LoadLibrary('user32.dll')
|
|
8
|
+
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
9
|
+
if (!_user32 || !_gdi32) throw new Error('Failed to load user32/gdi32')
|
|
10
|
+
|
|
11
|
+
function loadProc(lib: win.HMODULE, name: string): number {
|
|
12
|
+
const ptr = win.GetProcAddress(lib, name)
|
|
13
|
+
if (!ptr) throw new Error('Failed to load ' + name)
|
|
14
|
+
return ptr
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const GetDC_ = loadProc(_user32, 'GetDC')
|
|
18
|
+
const ReleaseDC_ = loadProc(_user32, 'ReleaseDC')
|
|
19
|
+
const SetDIBitsToDevice_ = loadProc(_gdi32, 'SetDIBitsToDevice')
|
|
20
|
+
|
|
21
|
+
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
22
|
+
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
23
|
+
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
24
|
+
|
|
25
|
+
function makeBitmapInfo(w: number, h: number): ArrayBuffer {
|
|
26
|
+
const bmi = new ArrayBuffer(40)
|
|
27
|
+
const bv = new DataView(bmi)
|
|
28
|
+
bv.setUint32(0, 40, true)
|
|
29
|
+
bv.setInt32(4, w, true)
|
|
30
|
+
bv.setInt32(8, -h, true)
|
|
31
|
+
bv.setUint16(12, 1, true)
|
|
32
|
+
bv.setUint16(14, 24, true)
|
|
33
|
+
return bmi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PdfCanvasProps {
|
|
37
|
+
pixmap?: { data: ArrayBuffer; w: number; h: number }
|
|
38
|
+
style?: WStyle
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const PdfCanvas = forwardRef<gui.HWND, PdfCanvasProps>(
|
|
42
|
+
({ pixmap, style }, ref) => {
|
|
43
|
+
const canvasRef = useRef<gui.HWND>(null)
|
|
44
|
+
const pixmapRef = useRef(pixmap)
|
|
45
|
+
pixmapRef.current = pixmap
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<w type="STATIC"
|
|
49
|
+
ws={gui.WindowStyle.VISIBLE | gui.WindowStyle.CLIPCHILDREN}
|
|
50
|
+
style={style}
|
|
51
|
+
ref={(h: gui.HWND) => {
|
|
52
|
+
canvasRef.current = h
|
|
53
|
+
if (typeof ref === 'function') ref(h)
|
|
54
|
+
else if (ref) (ref as React.RefObject<gui.HWND | null>).current = h
|
|
55
|
+
}}
|
|
56
|
+
onEvent={(e) => {
|
|
57
|
+
const hwnd = e.hwnd
|
|
58
|
+
if (e.msg === 0x14) return 1
|
|
59
|
+
if (e.msg === gui.WmMsg.PAINT) {
|
|
60
|
+
const pm = pixmapRef.current
|
|
61
|
+
if (!pm) return 0
|
|
62
|
+
const hdc = ffi.ffiCall(GetDC_, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
63
|
+
if (hdc) {
|
|
64
|
+
const bmi = makeBitmapInfo(pm.w, pm.h)
|
|
65
|
+
ffi.ffiCall(SetDIBitsToDevice_, [
|
|
66
|
+
ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
67
|
+
FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
68
|
+
FFI_PTR, FFI_PTR, FFI_U32
|
|
69
|
+
], [
|
|
70
|
+
hdc, 0, 0, pm.w, pm.h,
|
|
71
|
+
0, 0, 0, pm.h,
|
|
72
|
+
pm.data, bmi, 0
|
|
73
|
+
], FFI_S32)
|
|
74
|
+
ffi.ffiCall(ReleaseDC_, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
75
|
+
}
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
)
|
package/examples/exec_server.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { createServer } from '../lib/http-server.js'
|
|
2
|
-
import * as std from 'std'
|
|
3
|
-
|
|
4
|
-
function readAll(file: std.FILE): string {
|
|
5
|
-
const chunks: Uint8Array[] = []
|
|
6
|
-
const buf = new Uint8Array(4096)
|
|
7
|
-
while (true) {
|
|
8
|
-
const n = file.read(buf.buffer, 0, 4096)
|
|
9
|
-
if (n <= 0) break
|
|
10
|
-
chunks.push(buf.slice(0, n))
|
|
11
|
-
}
|
|
12
|
-
let total = 0
|
|
13
|
-
for (const c of chunks) total += c.length
|
|
14
|
-
const out = new Uint8Array(total)
|
|
15
|
-
let offset = 0
|
|
16
|
-
for (const c of chunks) { out.set(c, offset); offset += c.length }
|
|
17
|
-
return new TextDecoder().decode(out)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const server = createServer(async (req) => {
|
|
21
|
-
if (req.method === 'POST' && new URL(req.url).pathname === '/exec') {
|
|
22
|
-
const body = await req.json() as { cmd: string }
|
|
23
|
-
const file = std.popen(body.cmd, 'r')
|
|
24
|
-
if (!file) return new Response('popen failed', { status: 500 })
|
|
25
|
-
const output = readAll(file)
|
|
26
|
-
file.close()
|
|
27
|
-
return new Response(output, {
|
|
28
|
-
headers: { 'Content-Type': 'application/octet-stream' }
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
return new Response('Not Found', { status: 404 })
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
server.listen(8080)
|
|
35
|
-
console.log('listening on http://localhost:8080')
|
|
1
|
+
import { createServer } from '../lib/http-server.js'
|
|
2
|
+
import * as std from 'std'
|
|
3
|
+
|
|
4
|
+
function readAll(file: std.FILE): string {
|
|
5
|
+
const chunks: Uint8Array[] = []
|
|
6
|
+
const buf = new Uint8Array(4096)
|
|
7
|
+
while (true) {
|
|
8
|
+
const n = file.read(buf.buffer, 0, 4096)
|
|
9
|
+
if (n <= 0) break
|
|
10
|
+
chunks.push(buf.slice(0, n))
|
|
11
|
+
}
|
|
12
|
+
let total = 0
|
|
13
|
+
for (const c of chunks) total += c.length
|
|
14
|
+
const out = new Uint8Array(total)
|
|
15
|
+
let offset = 0
|
|
16
|
+
for (const c of chunks) { out.set(c, offset); offset += c.length }
|
|
17
|
+
return new TextDecoder().decode(out)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const server = createServer(async (req) => {
|
|
21
|
+
if (req.method === 'POST' && new URL(req.url).pathname === '/exec') {
|
|
22
|
+
const body = await req.json() as { cmd: string }
|
|
23
|
+
const file = std.popen(body.cmd, 'r')
|
|
24
|
+
if (!file) return new Response('popen failed', { status: 500 })
|
|
25
|
+
const output = readAll(file)
|
|
26
|
+
file.close()
|
|
27
|
+
return new Response(output, {
|
|
28
|
+
headers: { 'Content-Type': 'application/octet-stream' }
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return new Response('Not Found', { status: 404 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
server.listen(8080)
|
|
35
|
+
console.log('listening on http://localhost:8080')
|