cj-tauri 0.8.0 → 1.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.
Files changed (124) hide show
  1. package/README.md +3 -3
  2. package/cjpm.toml +52 -25
  3. package/cli/cjpm.toml +1 -1
  4. package/cli/src/doctor.cj +304 -0
  5. package/cli/src/main.cj +33 -7
  6. package/cli/src/project.cj +59 -1
  7. package/cli/src/resolve.cj +348 -304
  8. package/cli/src/scaffold.cj +139 -22
  9. package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
  10. package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
  11. package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
  12. package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
  13. package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
  14. package/cli/templates/ohos-pc/README.md +79 -0
  15. package/cli/templates/ohos-pc/build-hap.sh +190 -0
  16. package/cli/templates/ohos-pc/build-profile.json5 +46 -0
  17. package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
  18. package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
  19. package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
  20. package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
  21. package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
  22. package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
  23. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
  24. package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
  25. package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
  26. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
  27. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
  28. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
  29. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
  30. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
  31. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
  32. package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
  33. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
  34. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
  35. package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
  36. package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
  37. package/cli/templates/ohos-pc/oh-package.json5 +10 -0
  38. package/native/build_linux.sh +30 -2
  39. package/native/build_win.bat +62 -61
  40. package/native/core/bridge_core.c +1626 -0
  41. package/native/{bridge_core.h → core/bridge_core.h} +246 -6
  42. package/native/platforms/harmonyos/README.md +106 -0
  43. package/native/platforms/linux/bridge_linux.c +2485 -0
  44. package/native/platforms/windows/bridge_win.c +3248 -0
  45. package/native/tests/test_bridge_core.c +528 -1
  46. package/package.json +12 -3
  47. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  48. package/src/api_system.cj +10 -2
  49. package/src/app.cj +248 -85
  50. package/src/arkweb_proxy.cj +56 -0
  51. package/src/arkweb_script.cj +154 -0
  52. package/src/capability.cj +140 -0
  53. package/src/clipboard_bridge.cj +55 -0
  54. package/src/dragdrop.cj +26 -0
  55. package/src/embedded_assets.cj +98 -0
  56. package/src/hexutil.cj +53 -0
  57. package/src/host.cj +197 -26
  58. package/src/host_arkweb.cj +740 -0
  59. package/src/host_factory.cj +48 -0
  60. package/src/host_webkit.cj +202 -26
  61. package/src/host_webview2.cj +176 -3
  62. package/src/hotkey.cj +66 -0
  63. package/src/ipc_hub.cj +49 -0
  64. package/src/ipc_message.cj +2 -1
  65. package/src/jsutil.cj +33 -0
  66. package/src/log_bridge.cj +107 -0
  67. package/src/logging.cj +127 -0
  68. package/src/menu.cj +197 -196
  69. package/src/plugin.cj +40 -0
  70. package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
  71. package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
  72. package/src/plugins/fs/plugin_fs.cj +194 -0
  73. package/src/plugins/http/plugin_http.cj +459 -0
  74. package/src/plugins/log/plugin_log.cj +105 -0
  75. package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
  76. package/src/plugins/mqtt/mqtt_codec.cj +661 -0
  77. package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
  78. package/src/plugins/notification/plugin_notification.cj +146 -0
  79. package/src/plugins/plugins.cj +5 -0
  80. package/src/plugins/redis/plugin_redis.cj +2409 -0
  81. package/src/plugins/redis/redis_codec.cj +556 -0
  82. package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
  83. package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
  84. package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
  85. package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
  86. package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
  87. package/src/plugins/store/plugin_store.cj +287 -0
  88. package/src/plugins/ws/plugin_ws.cj +470 -0
  89. package/src/process_hidden.cj +174 -0
  90. package/src/scope.cj +391 -0
  91. package/src/shell_event.cj +69 -68
  92. package/src/tests/capability_test.cj +53 -0
  93. package/src/tests/fake_host.cj +85 -20
  94. package/src/tests/ipc_hub_test.cj +13 -0
  95. package/src/tests/jsutil_test.cj +62 -0
  96. package/src/tests/logging_test.cj +62 -0
  97. package/src/tests/mqtt_codec_test.cj +405 -0
  98. package/src/tests/plugin_clipboard_test.cj +222 -0
  99. package/src/tests/plugin_dialog_test.cj +98 -4
  100. package/src/tests/plugin_http_test.cj +124 -0
  101. package/src/tests/plugin_menu_test.cj +1 -0
  102. package/src/tests/plugin_notification_test.cj +205 -0
  103. package/src/tests/plugin_serial_test.cj +269 -267
  104. package/src/tests/plugin_shell_test.cj +189 -188
  105. package/src/tests/plugin_sqlite_test.cj +230 -0
  106. package/src/tests/plugin_store_test.cj +260 -0
  107. package/src/tests/plugin_test.cj +542 -524
  108. package/src/tests/plugin_ws_test.cj +40 -0
  109. package/src/tests/process_hidden_test.cj +129 -0
  110. package/src/tests/redis_codec_test.cj +396 -0
  111. package/src/tests/scope_test.cj +350 -0
  112. package/src/tests/shell_event_test.cj +0 -1
  113. package/src/tests/system_host_test.cj +81 -79
  114. package/src/tests/urlutil_test.cj +53 -0
  115. package/src/tests/version_test.cj +75 -65
  116. package/src/tray.cj +90 -0
  117. package/src/urlutil.cj +159 -0
  118. package/src/version.cj +23 -2
  119. package/src/window.cj +51 -42
  120. package/native/bridge_core.c +0 -797
  121. package/native/bridge_linux.c +0 -1268
  122. package/native/bridge_win.c +0 -1633
  123. package/src/plugin_fs.cj +0 -118
  124. /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
@@ -1,1633 +0,0 @@
1
- /*
2
- * cj-tauri C 桥(Windows 平台层):Win32 窗口 + WebView2 宿主原语
3
- *
4
- * 本文件只实现 bridge_core.h 的「平台原语清单」+ 宿主线程装配;平台无关逻辑
5
- * (IPC 回投队列与批处理、对话框单槽状态机、窗口配置存储、生命周期标志、全部导出)
6
- * 都在 native/bridge_core.c,两平台共用。分层原因见 docs/架构演进-多平台与多窗口.md §2.2。
7
- *
8
- * 分层对标 Tauri 2:本文件 = tao(窗口/事件循环) + wry(WebView 抽象) 的 Windows 后端,
9
- * 上层 IPC 桥 / capability 安全模型(src 目录下的 .cj)跨平台复用。
10
- *
11
- * 与 Linux 桥(bridge_linux.c)的差异:
12
- * - Windows 的 JS 引擎(V8)运行在 msedgewebview2.exe 独立进程,宿主进程内不跑 JS 引擎,
13
- * 因此 Linux 上 JSC 栈边界校验导致的「WebView 调用必须在原生 pthread」约束在此不适用;
14
- * 这里仍保持「独立宿主线程 + JS FIFO 投递」的同构结构,两端 API 与行为一致。
15
- * - JS → 宿主:window.chrome.webview.postMessage(JSON.stringify(obj))
16
- * - 宿主 → JS:ExecuteScript("window.postMessage(json,'*')"),与 Linux 端完全一致
17
- *
18
- * 编译(mingw-w64 / llvm-mingw gcc,见 native/build_win.bat):
19
- * gcc -shared -O2 bridge_core.c bridge_win.c -o libcjtbridge.dll \
20
- * -Wl,--out-implib,libcjtbridge.dll.a \
21
- * -I<WebView2 SDK>/build/native/include \
22
- * -lole32 -loleaut32 -luuid -luser32 -lgdi32
23
- * 运行期要求:libcjtbridge.dll 与 WebView2Loader.dll 位于 exe 目录或 PATH 中。
24
- */
25
- #define UNICODE
26
- #define _UNICODE
27
- /* 条件变量(CONDITION_VARIABLE)要 Vista+ 的头;WebView2 本身就要求 Win10,写 0x0601 足够 */
28
- #define _WIN32_WINNT 0x0601
29
- #include <windows.h>
30
- #include <commdlg.h>
31
- #include <objbase.h>
32
- #include <stdio.h>
33
- #include <stdarg.h>
34
- #include <stdlib.h>
35
- #include <string.h>
36
- #include "WebView2.h"
37
- #include "bridge_core.h"
38
-
39
- /* ===== COM 回调对象(每个宿主一套)=====
40
- 原先 4 个 handler 是静态单例(AddRef/Release 硬编码 return 1),多窗口一落地就分不清是谁的回调;
41
- 现在按宿主内嵌,This 直接转回本结构就能拿到 cj_host。引用计数仍恒 1:对象随宿主句柄存续
42
- (句柄壳活到 cj_bridge_destroy),这是 WebView2 手工 COM 的惯用写法,也是 AGENTS.md §4 里
43
- hr=0x8007139F 那个坑的由来——AddRef 若返回 0 / 不自持有,注册给 WebView2 的回调会被提前回收。 */
44
- typedef struct env_handler {
45
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler iface; /* 首成员:This == &iface */
46
- cj_host *h;
47
- } env_handler;
48
-
49
- typedef struct ctrl_handler {
50
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler iface;
51
- cj_host *h;
52
- } ctrl_handler;
53
-
54
- typedef struct msg_handler {
55
- ICoreWebView2WebMessageReceivedEventHandler iface;
56
- cj_host *h;
57
- } msg_handler;
58
-
59
- typedef struct nav_handler {
60
- ICoreWebView2NavigationCompletedEventHandler iface;
61
- cj_host *h;
62
- } nav_handler;
63
-
64
- /* ===== 菜单栏(RFC-002 §5.4 / §5.5)=====
65
- core 把线路文本交下来,本文件翻译成 Win32 菜单栏。菜单栏是**每窗口**资源(HMENU 挂在窗口上),
66
- 建 / 换 / 改都必须在 UI 线程上做——跨线程调用一律 PostMessage 到宿主线程。 */
67
- #define CJ_MENU_MAX_ITEMS 64
68
- #define CJ_MENU_MAX_DEPTH 8
69
-
70
- typedef struct menu_slot {
71
- UINT cmd_id; /* 建菜单时分配的命令 id(WM_COMMAND 带回来的那个),从 1 开始 */
72
- char *app_id; /* 应用给的稳定 id:回投仓颉侧,也是 set_menu_item_state 的查找键 */
73
- char kind; /* 'n' 普通 / 'c' 勾选(点击时由我们自己翻转勾选状态)*/
74
- int enabled;
75
- int checked;
76
- } menu_slot;
77
-
78
- /* ===== 每个宿主的平台私有状态 =====
79
- 原先是一堆进程级单例(g_hwnd / g_controller / g_environment / g_webview / g_host_thread_id …)。
80
- 现在挂在自己的 cj_host->plat 上,平台函数一律从入参句柄取状态。 */
81
- typedef struct plat_host {
82
- HWND hwnd; /* 主窗口;WM_DESTROY 后置空 */
83
- ICoreWebView2Controller *controller;
84
- ICoreWebView2Environment *environment; /* 必须自己持有一份引用(见 env_Invoke)*/
85
- ICoreWebView2 *webview;
86
- HANDLE thread; /* 宿主线程句柄(cj_plat_fini 里 join 用)*/
87
- DWORD thread_id; /* 宿主线程 id:判断「调用方是否已在宿主线程」*/
88
- int thread_started;
89
- int thread_set;
90
- volatile int thread_done; /* 宿主线程跑完(收尾最后一步):见 cj_plat_fini */
91
- HMODULE loader; /* WebView2Loader.dll */
92
- env_handler env;
93
- ctrl_handler ctrl;
94
- msg_handler msg;
95
- nav_handler nav;
96
- /* --- 菜单栏(只在 UI 线程上建 / 换 / 改)--- */
97
- HMENU menu; /* 当前菜单栏;NULL = 没有 */
98
- menu_slot menu_items[CJ_MENU_MAX_ITEMS]; /* 命令 id ↔ 应用 id 映射:WM_COMMAND 靠它回投 */
99
- int menu_item_count;
100
- } plat_host;
101
-
102
- /* ===== 同步原语(core 只用这些,绝不直接碰 Win32)=====
103
- 与 Linux 桥的 pthread_mutex + pthread_cond 一一对应;用条件变量而不是「事件」的好处是
104
- 谓词在锁内检查,不存在「先 SetEvent 后 CreateEvent 丢唤醒」的时序坑。 */
105
-
106
- struct cj_sync {
107
- CRITICAL_SECTION cs;
108
- CONDITION_VARIABLE cv;
109
- };
110
-
111
- cj_sync *cj_plat_sync_create(void) {
112
- cj_sync *s = (cj_sync *)calloc(1, sizeof(cj_sync));
113
- if (!s) return NULL;
114
- InitializeCriticalSection(&s->cs);
115
- InitializeConditionVariable(&s->cv);
116
- return s;
117
- }
118
-
119
- void cj_plat_sync_destroy(cj_sync *s) {
120
- if (!s) return;
121
- DeleteCriticalSection(&s->cs); /* CONDITION_VARIABLE 无需销毁 */
122
- free(s);
123
- }
124
-
125
- void cj_plat_sync_lock(cj_sync *s) {
126
- if (s) EnterCriticalSection(&s->cs);
127
- }
128
-
129
- void cj_plat_sync_unlock(cj_sync *s) {
130
- if (s) LeaveCriticalSection(&s->cs);
131
- }
132
-
133
- void cj_plat_sync_wait(cj_sync *s, int *ready) {
134
- if (!s || !ready) return;
135
- while (!*ready) {
136
- SleepConditionVariableCS(&s->cv, &s->cs, INFINITE);
137
- }
138
- }
139
-
140
- void cj_plat_sync_signal(cj_sync *s) {
141
- if (s) WakeConditionVariable(&s->cv);
142
- }
143
-
144
- /* ===== UTF-8 <-> UTF-16 ===== */
145
-
146
- static wchar_t *utf8_to_wide(const char *s) {
147
- if (!s) return NULL;
148
- int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
149
- if (n <= 0) return NULL;
150
- wchar_t *w = (wchar_t *)malloc((size_t)n * sizeof(wchar_t));
151
- if (!w) return NULL;
152
- MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n);
153
- return w;
154
- }
155
-
156
- static char *wide_to_utf8(const wchar_t *w) {
157
- if (!w) return NULL;
158
- int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
159
- if (n <= 0) return NULL;
160
- char *s = (char *)malloc((size_t)n);
161
- if (!s) return NULL;
162
- WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL);
163
- return s;
164
- }
165
-
166
- /* 关键 WebView2 调用的 HRESULT 上报 */
167
- static void log_hr(const char *what, HRESULT hr) {
168
- fprintf(stderr, "[cj-bridge] %s -> hr=0x%08lx\n", what, (unsigned long)hr);
169
- }
170
-
171
- /* ===== 注入前端桥接脚本(暴露 window.__CJ_TAURI__,对标 @tauri-apps/api)===== */
172
-
173
- /* 平台唯一差异行(其余 JS 见 native/bridge_js.h,两平台共用一份,防漂移):
174
- post 通道 = chrome.webview;reload 控制消息同通道发字面量。宽字符:本文件要 wchar_t。 */
175
- #define CJ_LIT(s) L##s
176
- #define CJ_POST_STMT L" window.chrome.webview.postMessage(JSON.stringify(obj));"
177
- #define CJ_RELOAD_STMT L" window.chrome.webview.postMessage('__cj_tauri_reload__');"
178
- #include "bridge_js.h"
179
-
180
- static const wchar_t *BRIDGE_JS = CJ_BRIDGE_JS(CJ_POST_STMT, CJ_RELOAD_STMT);
181
-
182
- /* ===== 投递通道用到的窗口消息(PostMessage 按窗口寻址,句柄从窗口 USERDATA 取)===== */
183
-
184
- #define WM_CJT_FLUSH (WM_APP + 1)
185
- #define WM_CJT_QUIT (WM_APP + 2)
186
- #define WM_CJT_DEVTOOLS (WM_APP + 3)
187
- #define WM_CJT_LOAD_URL (WM_APP + 4)
188
- #define WM_CJT_RELOAD (WM_APP + 5)
189
- #define WM_CJT_DIALOG (WM_APP + 6)
190
- #define WM_CJT_MENU (WM_APP + 7) /* 运行期换菜单:lParam = strdup 的线路文本(UI 线程释放)*/
191
- #define WM_CJT_MENU_ITEM (WM_APP + 8) /* 运行期改单项状态:lParam = menu_state_msg*(UI 线程释放)*/
192
-
193
- /* 运行期改单项状态:跨线程传递的小包裹(谁 post 谁分配,UI 线程处理完释放)*/
194
- typedef struct menu_state_msg {
195
- char *id;
196
- int enabled;
197
- int checked;
198
- } menu_state_msg;
199
-
200
- static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
201
- /* 菜单实现集中在文件末尾;宿主线程与窗口过程要用,先声明 */
202
- static void menu_apply_wire(cj_host *h, plat_host *p, const char *wire);
203
- static void menu_apply_item_state(plat_host *p, const char *id, int enabled, int checked);
204
- static void menu_clear_slots(plat_host *p);
205
-
206
- /* ===== COM 回调实现(完成/事件 handler)===== */
207
-
208
- /* --- 环境创建完成 --- */
209
-
210
- static HRESULT STDMETHODCALLTYPE env_QueryInterface(
211
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *This, REFIID riid, void **ppvObject) {
212
- if (ppvObject) *ppvObject = This;
213
- return S_OK;
214
- }
215
-
216
- static ULONG STDMETHODCALLTYPE env_AddRef(
217
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *This) {
218
- return 1;
219
- }
220
-
221
- static ULONG STDMETHODCALLTYPE env_Release(
222
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *This) {
223
- return 1;
224
- }
225
-
226
- static HRESULT STDMETHODCALLTYPE env_Invoke(
227
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *This,
228
- HRESULT errorCode, ICoreWebView2Environment *result) {
229
- env_handler *self = (env_handler *)This;
230
- cj_host *h = self ? self->h : NULL;
231
- plat_host *p = h ? (plat_host *)h->plat : NULL;
232
-
233
- if (!h || !p) return S_OK;
234
- if (FAILED(errorCode) || !result) {
235
- fprintf(stderr, "[cj-bridge] create WebView2 environment failed: 0x%08lx\n",
236
- (unsigned long)errorCode);
237
- h->should_quit = 1;
238
- cj_core_window_destroyed(h); /* 通知仓颉侧(onDestroy 恰好一次)+ 作废对话框 */
239
- if (p->hwnd) PostMessageW(p->hwnd, WM_CJT_QUIT, 0, 0);
240
- return S_OK;
241
- }
242
- /* 必须自己持有一份环境引用:否则本回调返回后环境对象被释放,
243
- WebView2 会随即关闭浏览器进程,表现为导航不完成、ExecuteScript 返回 0x8007139F */
244
- p->environment = result;
245
- p->environment->lpVtbl->AddRef(p->environment);
246
- result->lpVtbl->CreateCoreWebView2Controller(result, p->hwnd, &p->ctrl.iface);
247
- fprintf(stderr, "[cj-bridge] WebView2 environment ready\n");
248
- return S_OK;
249
- }
250
-
251
- static ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandlerVtbl g_env_vtbl = {
252
- env_QueryInterface, env_AddRef, env_Release, env_Invoke
253
- };
254
-
255
- /* --- 控制器(WebView)创建完成:装配 WebView、注入桥、加载 HTML --- */
256
-
257
- static HRESULT STDMETHODCALLTYPE ctrl_QueryInterface(
258
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler *This, REFIID riid, void **ppvObject) {
259
- if (ppvObject) *ppvObject = This;
260
- return S_OK;
261
- }
262
-
263
- static ULONG STDMETHODCALLTYPE ctrl_AddRef(
264
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler *This) {
265
- return 1;
266
- }
267
-
268
- static ULONG STDMETHODCALLTYPE ctrl_Release(
269
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler *This) {
270
- return 1;
271
- }
272
-
273
- static HRESULT STDMETHODCALLTYPE ctrl_Invoke(
274
- ICoreWebView2CreateCoreWebView2ControllerCompletedHandler *This,
275
- HRESULT errorCode, ICoreWebView2Controller *result) {
276
- ctrl_handler *self = (ctrl_handler *)This;
277
- cj_host *h = self ? self->h : NULL;
278
- plat_host *p = h ? (plat_host *)h->plat : NULL;
279
- RECT rc;
280
- int i;
281
-
282
- if (!h || !p) return S_OK;
283
- if (FAILED(errorCode) || !result) {
284
- fprintf(stderr, "[cj-bridge] create WebView2 controller failed: 0x%08lx\n",
285
- (unsigned long)errorCode);
286
- h->should_quit = 1;
287
- cj_core_window_destroyed(h);
288
- if (p->hwnd) PostMessageW(p->hwnd, WM_CJT_QUIT, 0, 0);
289
- return S_OK;
290
- }
291
- p->controller = result;
292
- p->controller->lpVtbl->AddRef(p->controller);
293
- result->lpVtbl->get_CoreWebView2(result, &p->webview);
294
- if (!p->webview) {
295
- fprintf(stderr, "[cj-bridge] get_CoreWebView2 failed\n");
296
- return S_OK;
297
- }
298
-
299
- /* 先让父窗口可见,再摆放 WebView:不可见的父窗口会挂起渲染 */
300
- ShowWindow(p->hwnd, SW_SHOW);
301
- UpdateWindow(p->hwnd);
302
-
303
- GetClientRect(p->hwnd, &rc);
304
- result->lpVtbl->put_Bounds(result, rc);
305
- result->lpVtbl->put_IsVisible(result, TRUE);
306
-
307
- /* WebView 行为设置 */
308
- {
309
- ICoreWebView2Settings *settings = NULL;
310
- HRESULT hr = p->webview->lpVtbl->get_Settings(p->webview, &settings);
311
- if (SUCCEEDED(hr) && settings) {
312
- settings->lpVtbl->put_IsStatusBarEnabled(settings, FALSE);
313
- settings->lpVtbl->put_IsZoomControlEnabled(settings, FALSE);
314
- settings->lpVtbl->put_AreDefaultContextMenusEnabled(settings, TRUE);
315
- settings->lpVtbl->put_AreDevToolsEnabled(settings, h->devtools ? TRUE : FALSE);
316
- settings->lpVtbl->Release(settings);
317
- }
318
- }
319
-
320
- /* 注入桥接脚本(页面脚本执行前)+ 注册 JS 消息通道 */
321
- p->webview->lpVtbl->AddScriptToExecuteOnDocumentCreated(p->webview, BRIDGE_JS, NULL);
322
- /* 预执行脚本按注册顺序追加在桥之后(WebView2 按注册顺序执行),脚本里可直接用 window.__CJ_TAURI__ */
323
- for (i = 0; i < h->init_script_count; i++) {
324
- wchar_t *w = utf8_to_wide(h->init_scripts[i]);
325
- if (w) {
326
- p->webview->lpVtbl->AddScriptToExecuteOnDocumentCreated(p->webview, w, NULL);
327
- free(w);
328
- }
329
- }
330
- fprintf(stderr, "[cj-bridge] init scripts injected: %d\n", h->init_script_count);
331
- p->webview->lpVtbl->add_WebMessageReceived(p->webview, &p->msg.iface, NULL);
332
- p->webview->lpVtbl->add_NavigationCompleted(p->webview, &p->nav.iface, NULL);
333
-
334
- cj_core_set_ready(h);
335
- fprintf(stderr, "[cj-bridge] controller ready, loading page\n");
336
-
337
- /* 页面来源:URL 优先(cj_bridge_load_url),否则内联 HTML(cj_bridge_start) */
338
- if (h->pending_url) {
339
- wchar_t *w = utf8_to_wide(h->pending_url);
340
- if (w) {
341
- log_hr("Navigate", p->webview->lpVtbl->Navigate(p->webview, w));
342
- free(w);
343
- }
344
- fprintf(stderr, "[cj-bridge] url navigation requested: %s\n", h->pending_url);
345
- } else if (h->pending_html) {
346
- wchar_t *w = utf8_to_wide(h->pending_html);
347
- if (w) {
348
- log_hr("NavigateToString", p->webview->lpVtbl->NavigateToString(p->webview, w));
349
- free(w);
350
- }
351
- fprintf(stderr, "[cj-bridge] html navigation requested\n");
352
- }
353
- /* 宿主就绪前可能已有排队脚本(如启动即推送的事件) */
354
- cj_core_flush_js(h);
355
- return S_OK;
356
- }
357
-
358
- static ICoreWebView2CreateCoreWebView2ControllerCompletedHandlerVtbl g_ctrl_vtbl = {
359
- ctrl_QueryInterface, ctrl_AddRef, ctrl_Release, ctrl_Invoke
360
- };
361
-
362
- /* --- JS → 宿主的消息(chrome.webview.postMessage 的字符串载荷)--- */
363
-
364
- static HRESULT STDMETHODCALLTYPE msg_QueryInterface(
365
- ICoreWebView2WebMessageReceivedEventHandler *This, REFIID riid, void **ppvObject) {
366
- if (ppvObject) *ppvObject = This;
367
- return S_OK;
368
- }
369
-
370
- static ULONG STDMETHODCALLTYPE msg_AddRef(ICoreWebView2WebMessageReceivedEventHandler *This) {
371
- return 1;
372
- }
373
-
374
- static ULONG STDMETHODCALLTYPE msg_Release(ICoreWebView2WebMessageReceivedEventHandler *This) {
375
- return 1;
376
- }
377
-
378
- static HRESULT STDMETHODCALLTYPE msg_Invoke(
379
- ICoreWebView2WebMessageReceivedEventHandler *This,
380
- ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {
381
- msg_handler *self = (msg_handler *)This;
382
- cj_host *h = self ? self->h : NULL;
383
- plat_host *p = h ? (plat_host *)h->plat : NULL;
384
- LPWSTR payload = NULL;
385
-
386
- if (SUCCEEDED(args->lpVtbl->TryGetWebMessageAsString(args, &payload)) && payload) {
387
- char *utf8 = wide_to_utf8(payload);
388
- CoTaskMemFree(payload);
389
- if (utf8) {
390
- /* 前端 __CJ_TAURI__.reload():宿主控制消息,不进 IPC hub */
391
- if (strcmp(utf8, CJT_RELOAD_MSG) == 0) {
392
- fprintf(stderr, "[cj-bridge] frontend requested reload\n");
393
- if (p && p->hwnd) PostMessageW(p->hwnd, WM_CJT_RELOAD, 0, 0);
394
- free(utf8);
395
- return S_OK;
396
- }
397
- fprintf(stderr, "[cj-bridge] js -> native (%d bytes)\n", (int)strlen(utf8));
398
- if (h && h->on_message) h->on_message(utf8); /* 回调仓颉(IPC hub) */
399
- free(utf8);
400
- }
401
- }
402
- return S_OK;
403
- }
404
-
405
- static ICoreWebView2WebMessageReceivedEventHandlerVtbl g_msg_vtbl = {
406
- msg_QueryInterface, msg_AddRef, msg_Release, msg_Invoke
407
- };
408
-
409
- /* --- 导航完成:仅在失败时上报(成功无需打扰用户)--- */
410
-
411
- static HRESULT STDMETHODCALLTYPE nav_QueryInterface(
412
- ICoreWebView2NavigationCompletedEventHandler *This, REFIID riid, void **ppvObject) {
413
- if (ppvObject) *ppvObject = This;
414
- return S_OK;
415
- }
416
-
417
- static ULONG STDMETHODCALLTYPE nav_AddRef(ICoreWebView2NavigationCompletedEventHandler *This) {
418
- return 1;
419
- }
420
-
421
- static ULONG STDMETHODCALLTYPE nav_Release(ICoreWebView2NavigationCompletedEventHandler *This) {
422
- return 1;
423
- }
424
-
425
- static HRESULT STDMETHODCALLTYPE nav_Invoke(
426
- ICoreWebView2NavigationCompletedEventHandler *This,
427
- ICoreWebView2 *sender, ICoreWebView2NavigationCompletedEventArgs *args) {
428
- BOOL ok = FALSE;
429
- COREWEBVIEW2_WEB_ERROR_STATUS status = COREWEBVIEW2_WEB_ERROR_STATUS_UNKNOWN;
430
- args->lpVtbl->get_IsSuccess(args, &ok);
431
- args->lpVtbl->get_WebErrorStatus(args, &status);
432
- if (!ok) {
433
- fprintf(stderr, "[cj-bridge] navigation failed: web error status=%d\n", (int)status);
434
- }
435
- return S_OK;
436
- }
437
-
438
- static ICoreWebView2NavigationCompletedEventHandlerVtbl g_nav_vtbl = {
439
- nav_QueryInterface, nav_AddRef, nav_Release, nav_Invoke
440
- };
441
-
442
- /* ===== 原生对话框(原生弹窗部分;状态机在 core)===== */
443
-
444
- /* "描述|模式|描述|模式" → Win32 双 NUL 过滤器串(末尾落单的描述段按 *.* 处理) */
445
- static wchar_t *build_win_filter(const char *filter) {
446
- const char *p;
447
- wchar_t *out;
448
- size_t cap, len = 0;
449
-
450
- if (!filter || !filter[0]) {
451
- filter = "所有文件 (*.*)|*.*";
452
- }
453
- cap = strlen(filter) * 4 + 8; /* UTF-8 → UTF-16 上界,末尾留双 NUL 位 */
454
- out = (wchar_t *)calloc(cap, sizeof(wchar_t));
455
- if (!out) return NULL;
456
-
457
- p = filter;
458
- while (p && p[0]) {
459
- const char *bar = strchr(p, '|');
460
- size_t seg = bar ? (size_t)(bar - p) : strlen(p);
461
- int n = MultiByteToWideChar(CP_UTF8, 0, p, (int)seg, out + len, (int)(cap - len - 2));
462
- if (n > 0) {
463
- len += (size_t)n;
464
- out[len++] = 0; /* 段结束符 */
465
- }
466
- if (!bar) {
467
- break;
468
- }
469
- if (!bar[1]) { /* 末尾落单的描述段:补一个 *.* 当模式 */
470
- int m = MultiByteToWideChar(CP_UTF8, 0, "*.*", 3, out + len, (int)(cap - len - 2));
471
- if (m > 0) {
472
- len += (size_t)m;
473
- out[len++] = 0;
474
- }
475
- break;
476
- }
477
- p = bar + 1;
478
- }
479
- out[len] = 0; /* 双 NUL 收尾(上一格已经是 0) */
480
- return out;
481
- }
482
-
483
- /* 已经站在宿主线程上:直接弹一次,返回 1 = 确认;选中路径写入 *out_path(由调用方 free) */
484
- static int show_dialog_here(cj_host *h, int kind, const char *title, const char *message,
485
- const char *filter, char **out_path) {
486
- plat_host *p = h ? (plat_host *)h->plat : NULL;
487
- HWND owner = p ? p->hwnd : NULL;
488
- int ok = 0;
489
-
490
- *out_path = NULL;
491
- if (kind <= 1) {
492
- wchar_t path[32768];
493
- wchar_t *title_w = utf8_to_wide(title[0] ? title : (kind == 1 ? "保存文件" : "打开文件"));
494
- wchar_t *filter_w = build_win_filter(filter);
495
- OPENFILENAMEW ofn;
496
-
497
- path[0] = 0;
498
- ZeroMemory(&ofn, sizeof(ofn));
499
- ofn.lStructSize = sizeof(ofn);
500
- ofn.hwndOwner = owner;
501
- ofn.lpstrFile = path;
502
- ofn.nMaxFile = (DWORD)(sizeof(path) / sizeof(path[0]));
503
- ofn.lpstrFilter = filter_w;
504
- ofn.nFilterIndex = 1;
505
- ofn.lpstrTitle = title_w;
506
- ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR |
507
- ((kind == 1) ? OFN_OVERWRITEPROMPT : OFN_FILEMUSTEXIST);
508
- {
509
- BOOL got = (kind == 1) ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn);
510
- if (got) {
511
- *out_path = wide_to_utf8(path);
512
- ok = 1;
513
- } else {
514
- DWORD err = CommDlgExtendedError();
515
- if (err) {
516
- fprintf(stderr, "[cj-bridge] dialog failed: CommDlgExtendedError=0x%lx\n",
517
- (unsigned long)err);
518
- }
519
- }
520
- }
521
- free(title_w);
522
- free(filter_w);
523
- } else {
524
- UINT flags = MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND;
525
- wchar_t *title_w, *msg_w;
526
-
527
- if (kind == 3) {
528
- flags = MB_OK | MB_ICONWARNING | MB_SETFOREGROUND;
529
- } else if (kind == 4) {
530
- flags = MB_OK | MB_ICONERROR | MB_SETFOREGROUND;
531
- } else if (kind == 5) {
532
- flags = MB_OKCANCEL | MB_ICONQUESTION | MB_SETFOREGROUND;
533
- }
534
- title_w = utf8_to_wide(title[0] ? title : "cj-tauri");
535
- msg_w = utf8_to_wide(message[0] ? message : " ");
536
- {
537
- int res = MessageBoxW(owner, msg_w, title_w, flags);
538
- ok = (kind == 5) ? (res == IDOK) : 1;
539
- }
540
- free(title_w);
541
- free(msg_w);
542
- }
543
- return ok;
544
- }
545
-
546
- int cj_plat_run_dialog(cj_host *h, dlg_req *req) {
547
- if (!req) return 0;
548
- req->ok = show_dialog_here(h, req->kind, req->title, req->message, req->filter, &req->path);
549
- return req->ok;
550
- }
551
-
552
- void cj_plat_post_dialog(cj_host *h, dlg_req *req) {
553
- plat_host *p = h ? (plat_host *)h->plat : NULL;
554
- if (!p || !p->hwnd) {
555
- fprintf(stderr, "[cj-bridge] dialog post failed: window not ready\n");
556
- cj_core_dialog_abort(h); /* 投不出去就别让调用方干等 */
557
- return;
558
- }
559
- if (!PostMessageW(p->hwnd, WM_CJT_DIALOG, 0, (LPARAM)req)) {
560
- fprintf(stderr, "[cj-bridge] dialog post failed: %lu\n", (unsigned long)GetLastError());
561
- cj_core_dialog_abort(h);
562
- }
563
- }
564
-
565
- static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
566
- /* 句柄从窗口自己的 USERDATA 取:PostMessage 按窗口寻址,谁收到就是谁的宿主 */
567
- cj_host *h = (cj_host *)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
568
- plat_host *p = h ? (plat_host *)h->plat : NULL;
569
-
570
- switch (msg) {
571
- case WM_NCCREATE: {
572
- /* 唯一安全时机:CreateWindowExW 的 lpParam 在这里交回来,之后这个窗口就知道自己属于谁 */
573
- CREATESTRUCTW *cs = (CREATESTRUCTW *)lp;
574
- if (cs) {
575
- SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams);
576
- }
577
- break; /* 交给 DefWindowProcW 继续(它对 WM_NCCREATE 返回 TRUE)*/
578
- }
579
- case WM_SIZE:
580
- if (p && p->controller) {
581
- RECT rc;
582
- GetClientRect(hwnd, &rc);
583
- p->controller->lpVtbl->put_Bounds(p->controller, rc);
584
- }
585
- return 0;
586
- case WM_CJT_FLUSH:
587
- cj_core_flush_js(h);
588
- return 0;
589
- case WM_CJT_QUIT:
590
- DestroyWindow(hwnd);
591
- return 0;
592
- case WM_CJT_DEVTOOLS:
593
- if (p && p->webview) {
594
- p->webview->lpVtbl->OpenDevToolsWindow(p->webview);
595
- }
596
- return 0;
597
- case WM_CJT_RELOAD:
598
- if (p && p->webview) {
599
- log_hr("Reload", p->webview->lpVtbl->Reload(p->webview));
600
- }
601
- return 0;
602
- case WM_CJT_LOAD_URL: {
603
- /* lParam 是跨线程传过来的宽字符串,由宿主线程负责释放 */
604
- wchar_t *w = (wchar_t *)lp;
605
- if (p && p->webview && w) {
606
- log_hr("Navigate", p->webview->lpVtbl->Navigate(p->webview, w));
607
- }
608
- free(w);
609
- return 0;
610
- }
611
- case WM_CJT_MENU: {
612
- /* lParam 是调用方 strdup 的线路文本:宿主线程用完即释放(与 WM_CJT_LOAD_URL 同规)*/
613
- char *wire = (char *)lp;
614
- if (wire && h && p) {
615
- menu_apply_wire(h, p, wire);
616
- }
617
- free(wire);
618
- return 0;
619
- }
620
- case WM_CJT_MENU_ITEM: {
621
- menu_state_msg *m = (menu_state_msg *)lp;
622
- if (m) {
623
- if (p) {
624
- menu_apply_item_state(p, m->id, m->enabled, m->checked);
625
- }
626
- free(m->id);
627
- free(m);
628
- }
629
- return 0;
630
- }
631
- case WM_COMMAND: {
632
- /* 菜单项:LOWORD(wp) 是建菜单时分配的命令 id;0 = 没有命令(分隔线 / 子菜单标题)*/
633
- UINT cmd = (UINT)LOWORD(wp);
634
- if (h && p && cmd) {
635
- int i;
636
- for (i = 0; i < p->menu_item_count; i++) {
637
- menu_slot *s = &p->menu_items[i];
638
- if (s->cmd_id != cmd) continue;
639
- if (s->kind == 'c') {
640
- /* Win32 不会替我们翻转勾选:自己翻,并同步菜单上的勾 */
641
- s->checked = s->checked ? 0 : 1;
642
- CheckMenuItem(p->menu, cmd,
643
- MF_BYCOMMAND | (s->checked ? MF_CHECKED : MF_UNCHECKED));
644
- }
645
- cj_core_menu_clicked(h, s->app_id, s->enabled, s->checked);
646
- break;
647
- }
648
- }
649
- return 0;
650
- }
651
- case WM_CJT_DIALOG:
652
- /* lParam 是调用方(仓颉线程)堆上的请求:宿主线程只填结果,释放仍归调用方 */
653
- if (lp) {
654
- cj_core_dialog_tick(h, (dlg_req *)lp);
655
- }
656
- return 0;
657
- case WM_DESTROY:
658
- if (p) {
659
- p->hwnd = NULL; /* 窗口没了:别再往它 PostMessage */
660
- }
661
- /* 置 should_quit + 触发 onDestroy(每个宿主恰好一次)+ 作废进行中的对话框 */
662
- cj_core_window_destroyed(h);
663
- PostQuitMessage(0);
664
- return 0;
665
- default:
666
- return DefWindowProcW(hwnd, msg, wp, lp);
667
- }
668
- /* Cases above that break instead of returning (currently WM_NCCREATE) land
669
- here. Returning DefWindowProcW is mandatory: it is the one that answers
670
- TRUE to WM_NCCREATE. Falling off the end leaves the return value
671
- indeterminate, CreateWindowExW reads it as FALSE and the window creation
672
- fails with GetLastError() == 0 -- which is exactly what it looks like. */
673
- return DefWindowProcW(hwnd, msg, wp, lp);
674
- }
675
-
676
- /* ===== 宿主线程(对标 tao 的事件循环线程)===== */
677
-
678
- /* WebView2Loader 的两种加载方式:
679
- · 默认(动态桥 / 便携包):运行期按名字找 WebView2Loader.dll(exe 目录或 PATH);
680
- · 单文件产物(-DCJ_EMBED_WEBVIEW2_LOADER):把 loader 的字节直接编进 exe,首次运行时
681
- 释放到 %TEMP%\cj-tauri-loader\ 再按绝对路径加载(幂等:大小一致就不重写)。
682
- 为什么不是「静态链 WebView2LoaderStatic.lib」:那份 .lib 是 MSVC 编的,要 /GS 的
683
- __security_cookie 与 C++ 运行时符号(?nothrow@std@@、_Init_thread_epoch …),
684
- 2026-10-07 mingw 实测链接失败(ld.lld: undefined symbol: __security_cookie)。 */
685
- #ifdef CJ_EMBED_WEBVIEW2_LOADER
686
- #include "webview2_loader_embed.h" /* 打包脚本生成的 C 头:字节数组 + 长度 */
687
-
688
- static int write_bytes(HANDLE f, const unsigned char *p, DWORD total) {
689
- DWORD off = 0;
690
- while (off < total) {
691
- DWORD wrote = 0;
692
- if (!WriteFile(f, p + off, total - off, &wrote, NULL) || wrote == 0) {
693
- return 0;
694
- }
695
- off += wrote;
696
- }
697
- return 1;
698
- }
699
-
700
- /* 把内嵌的 loader 释放成文件,成功时把绝对路径写进 out。
701
- 路径拼接一律用 lstrcpyW/lstrcatW:mingw 的 swprintf 在默认方言下走 MSVCRT 语义,
702
- `%s` 收的是 char* 而不是 wchar_t* —— 拼出来是垃圾路径,而且失败时一声不响
703
- (2026-10-07 实测:日志只有一句 "failed to unpack embedded WebView2Loader.dll")。 */
704
- static int unpack_embedded_loader(wchar_t *out, DWORD cap) {
705
- wchar_t tmp[MAX_PATH];
706
- wchar_t dir[MAX_PATH];
707
- DWORD n = GetTempPathW(MAX_PATH, tmp);
708
- if (n == 0 || n + 32 >= MAX_PATH) {
709
- fprintf(stderr, "[cj-bridge] embedded loader: GetTempPathW failed: err=%lu\n",
710
- GetLastError());
711
- return 0;
712
- }
713
- lstrcpyW(dir, tmp);
714
- lstrcatW(dir, L"cj-tauri-loader");
715
- if (!CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
716
- fprintf(stderr, "[cj-bridge] embedded loader: CreateDirectoryW(%ls) err=%lu\n",
717
- dir, GetLastError());
718
- /* 建不出来也不立刻放弃:目录可能本来就存在且可写 */
719
- }
720
- lstrcpyW(out, dir);
721
- lstrcatW(out, L"\\WebView2Loader.dll");
722
- {
723
- WIN32_FILE_ATTRIBUTE_DATA attr;
724
- if (GetFileAttributesExW(out, GetFileExInfoStandard, &attr)) {
725
- LARGE_INTEGER sz;
726
- sz.HighPart = (LONG)attr.nFileSizeHigh;
727
- sz.LowPart = attr.nFileSizeLow;
728
- if (sz.QuadPart == (LONGLONG)CJ_WV2_LOADER_SIZE) {
729
- return 1; /* 已经释放过同一份 */
730
- }
731
- }
732
- }
733
- {
734
- HANDLE f = CreateFileW(out, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
735
- FILE_ATTRIBUTE_NORMAL, NULL);
736
- int ok;
737
- if (f == INVALID_HANDLE_VALUE) {
738
- fprintf(stderr, "[cj-bridge] embedded loader: CreateFileW(%ls) err=%lu\n",
739
- out, GetLastError());
740
- return 0;
741
- }
742
- ok = write_bytes(f, CJ_WV2_LOADER_BYTES, (DWORD)CJ_WV2_LOADER_SIZE);
743
- CloseHandle(f);
744
- if (!ok) {
745
- fprintf(stderr, "[cj-bridge] embedded loader: WriteFile failed: err=%lu\n",
746
- GetLastError());
747
- }
748
- return ok;
749
- }
750
- }
751
-
752
- static HMODULE load_webview2_loader(void) {
753
- wchar_t path[MAX_PATH];
754
- if (unpack_embedded_loader(path, MAX_PATH)) {
755
- HMODULE m = LoadLibraryW(path);
756
- if (m) {
757
- fprintf(stderr, "[cj-bridge] WebView2Loader unpacked to %%TEMP%% and loaded\n");
758
- return m;
759
- }
760
- fprintf(stderr, "[cj-bridge] unpacked WebView2Loader refused to load: err=%lu\n",
761
- GetLastError());
762
- } else {
763
- fprintf(stderr, "[cj-bridge] failed to unpack embedded WebView2Loader.dll\n");
764
- }
765
- /* 退回按名字找:失败文案与默认模式一致,便于对照 */
766
- return LoadLibraryW(L"WebView2Loader.dll");
767
- }
768
- #else
769
- static HMODULE load_webview2_loader(void) {
770
- return LoadLibraryW(L"WebView2Loader.dll");
771
- }
772
- #endif
773
-
774
- typedef HRESULT(STDMETHODCALLTYPE *pfn_create_env)(
775
- PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions *,
776
- ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler *);
777
-
778
- static const wchar_t *WINDOW_CLASS = L"CjTauriWindow";
779
-
780
- static DWORD WINAPI host_thread_main(LPVOID param) {
781
- cj_host *h = (cj_host *)param;
782
- plat_host *p = h ? (plat_host *)h->plat : NULL;
783
- pfn_create_env create_env = NULL;
784
- WNDCLASSEXW wc;
785
- MSG msg;
786
- wchar_t *title_w = NULL; /* 宽字符标题(用完即 free)*/
787
- wchar_t *icon_w = NULL;
788
- HICON icon_big = NULL; /* 窗口图标(h->win_icon 非空时才加载)*/
789
- HICON icon_small = NULL;
790
-
791
- if (!p) return 0;
792
- CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
793
- fprintf(stderr, "[cj-bridge] host thread started\n");
794
-
795
- /* WebView2 的入口在 WebView2Loader.dll 中(默认按名字找;单文件产物从内嵌字节释放) */
796
- p->loader = load_webview2_loader();
797
- if (!p->loader) {
798
- fprintf(stderr, "[cj-bridge] WebView2Loader.dll not found: "
799
- "put it next to the exe or on PATH\n");
800
- goto done;
801
- }
802
- create_env = (pfn_create_env)GetProcAddress(p->loader, "CreateCoreWebView2EnvironmentWithOptions");
803
- if (!create_env) {
804
- fprintf(stderr, "[cj-bridge] CreateCoreWebView2EnvironmentWithOptions not found\n");
805
- goto done;
806
- }
807
- {
808
- /* 诊断:x64 宿主是否能看到可用运行时(排障用,可删) */
809
- typedef HRESULT(STDAPICALLTYPE *pfn_getver)(PCWSTR, LPWSTR *);
810
- pfn_getver getver =
811
- (pfn_getver)GetProcAddress(p->loader, "GetAvailableCoreWebView2BrowserVersionString");
812
- if (getver) {
813
- LPWSTR ver = NULL;
814
- HRESULT hr = getver(NULL, &ver);
815
- char *v8 = ver ? wide_to_utf8(ver) : NULL;
816
- fprintf(stderr, "[cj-bridge] WebView2 runtime: hr=0x%08lx ver=%s\n",
817
- (unsigned long)hr, v8 ? v8 : "(none)");
818
- free(v8);
819
- if (ver) CoTaskMemFree(ver);
820
- }
821
- }
822
- fprintf(stderr, "[cj-bridge] WebView2Loader loaded\n");
823
-
824
- /* 标题/图标在 core 里是 UTF-8,窗口类与窗口要宽字符,在这里转换 */
825
- title_w = h->win_title ? utf8_to_wide(h->win_title) : NULL;
826
- icon_w = h->win_icon ? utf8_to_wide(h->win_icon) : NULL;
827
-
828
- /* 窗口图标:在 RegisterClassExW 之前加载并挂到窗口类上,
829
- 这样任务栏与 Alt-Tab 也用同一个图标;失败只是回到系统默认图标,不阻断启动。 */
830
- if (icon_w) {
831
- icon_big = (HICON)LoadImageW(NULL, icon_w, IMAGE_ICON,
832
- GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON),
833
- LR_LOADFROMFILE);
834
- icon_small = (HICON)LoadImageW(NULL, icon_w, IMAGE_ICON,
835
- GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
836
- LR_LOADFROMFILE);
837
- /* GetLastError 只在失败时才有意义:成功时它是上一次调用的残留值(常见 6 = 无效句柄) */
838
- if (icon_big && icon_small) {
839
- fprintf(stderr, "[cj-bridge] window icon: path=%s loaded (big=%p small=%p)\n",
840
- h->win_icon, (void *)icon_big, (void *)icon_small);
841
- } else {
842
- fprintf(stderr, "[cj-bridge] window icon FAILED: path=%s hIcon=%p/%p err=%lu\n",
843
- h->win_icon, (void *)icon_big, (void *)icon_small,
844
- (unsigned long)GetLastError());
845
- }
846
- }
847
-
848
- ZeroMemory(&wc, sizeof(wc));
849
- wc.cbSize = sizeof(wc);
850
- wc.lpfnWndProc = wnd_proc;
851
- wc.hInstance = GetModuleHandleW(NULL);
852
- wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
853
- wc.lpszClassName = WINDOW_CLASS;
854
- wc.hIcon = icon_big; /* 可空:空则用系统默认 */
855
- wc.hIconSm = icon_small;
856
- RegisterClassExW(&wc);
857
-
858
- /* 先记下宿主线程身份:对话框要靠它判断「调用方是不是已经在本线程上」 */
859
- p->thread_id = GetCurrentThreadId();
860
- p->thread_set = 1;
861
-
862
- /* lpParam = 宿主句柄:WM_NCCREATE 里存进窗口 USERDATA,之后 PostMessage 按窗口寻址即找到宿主 */
863
- p->hwnd = CreateWindowExW(0, WINDOW_CLASS, title_w ? title_w : L"cj-tauri", WS_OVERLAPPEDWINDOW,
864
- CW_USEDEFAULT, CW_USEDEFAULT, h->win_w, h->win_h,
865
- NULL, NULL, wc.hInstance, h);
866
- if (!p->hwnd) {
867
- fprintf(stderr, "[cj-bridge] CreateWindowExW failed: %lu\n", GetLastError());
868
- goto done;
869
- }
870
- /* 类图标只管新窗口的默认值;对已存在的窗口再显式设一次,标题栏立刻生效 */
871
- if (icon_big) {
872
- SendMessageW(p->hwnd, WM_SETICON, ICON_BIG, (LPARAM)icon_big);
873
- }
874
- if (icon_small) {
875
- SendMessageW(p->hwnd, WM_SETICON, ICON_SMALL, (LPARAM)icon_small);
876
- }
877
- /* 菜单栏:装配期就先建好(仓颉侧可能在 start() 之前就 set_menu 了);
878
- 运行期再改的走 WM_CJT_MENU 到本线程重建。 */
879
- if (h->pending_menu_text && h->pending_menu_text[0]) {
880
- menu_apply_wire(h, p, h->pending_menu_text);
881
- }
882
- /* 创建 WebView2 环境(完成/失败都在回调 env_Invoke 中处理) */
883
- fprintf(stderr, "[cj-bridge] window created, requesting WebView2 environment\n");
884
- log_hr("CreateCoreWebView2EnvironmentWithOptions",
885
- create_env(NULL, NULL, NULL, &p->env.iface));
886
-
887
- while (GetMessageW(&msg, NULL, 0, 0)) {
888
- TranslateMessage(&msg);
889
- DispatchMessageW(&msg);
890
- }
891
-
892
- done:
893
- if (title_w) free(title_w);
894
- if (icon_w) free(icon_w);
895
- if (p->menu) { /* 窗口没了,菜单栏也要回收(HMENU 归本线程建、也归本线程销毁)*/
896
- DestroyMenu(p->menu);
897
- p->menu = NULL;
898
- }
899
- menu_clear_slots(p);
900
- if (p->controller) {
901
- p->controller->lpVtbl->Close(p->controller);
902
- p->controller = NULL;
903
- }
904
- /* COM 引用(controller / environment / webview)不在这里 Release:
905
- 它们都在本线程的 STA 上创建,且进程随之结束——沿用原实现的「活到进程退出」口径,
906
- 只把指针置空,避免收尾后再被别的路径碰到。 */
907
- p->webview = NULL;
908
- p->hwnd = NULL;
909
- if (p->loader) {
910
- FreeLibrary(p->loader);
911
- p->loader = NULL;
912
- }
913
- CoUninitialize();
914
- /* 兜底:非关窗路径退出消息循环(Loader 缺失 / 环境创建失败)也要通知一次(幂等)*/
915
- cj_core_window_destroyed(h);
916
- /* 最后一步才立这个标记:cj_plat_fini 见到它才 free 平台状态(此刻本线程已不再碰 p)*/
917
- p->thread_done = 1;
918
- return 0;
919
- }
920
-
921
- /* ===== 平台原语(bridge_core.h 的清单;导出不在这里,全在 core)===== */
922
-
923
- void cj_plat_init(cj_host *h) {
924
- plat_host *p;
925
- if (!h) return;
926
- p = (plat_host *)calloc(1, sizeof(plat_host));
927
- if (!p) {
928
- fprintf(stderr, "[cj-bridge] init failed: out of memory\n");
929
- return;
930
- }
931
- p->env.h = h;
932
- p->ctrl.h = h;
933
- p->msg.h = h;
934
- p->nav.h = h;
935
- p->env.iface.lpVtbl = &g_env_vtbl;
936
- p->ctrl.iface.lpVtbl = &g_ctrl_vtbl;
937
- p->msg.iface.lpVtbl = &g_msg_vtbl;
938
- p->nav.iface.lpVtbl = &g_nav_vtbl;
939
- h->plat = p;
940
- fprintf(stderr, "[cj-bridge] init (on_message=%p on_destroy=%p)\n",
941
- (void *)h->on_message, (void *)h->on_destroy);
942
- }
943
-
944
- int cj_plat_fini(cj_host *h) {
945
- plat_host *p;
946
- if (!h) return 1;
947
- p = (plat_host *)h->plat;
948
- if (!p) return 1;
949
- /* 宿主线程还活着(事件循环没退出):不能在这里 free 平台状态——它正被那条线程用着。
950
- 等待必须有界:宿主线程收尾时要进入仓颉运行时(onDestroy),无限等会与运行时的线程
951
- 退出握手互等(Linux 侧实测过,见 bridge_linux.c 同名函数);超时就状态与句柄一起留着。 */
952
- if (p->thread_started && p->thread) {
953
- WaitForSingleObject(p->thread, 5000);
954
- CloseHandle(p->thread);
955
- p->thread = NULL;
956
- p->thread_started = 0;
957
- }
958
- if (!p->thread_done) {
959
- fprintf(stderr, "[cj-bridge] fini: host thread still running, platform state kept\n");
960
- return 0;
961
- }
962
- h->plat = NULL;
963
- free(p);
964
- return 1;
965
- }
966
-
967
- void cj_plat_start(cj_host *h) {
968
- plat_host *p = h ? (plat_host *)h->plat : NULL;
969
- DWORD tid = 0;
970
- if (!p) {
971
- fprintf(stderr, "[cj-bridge] start failed: platform not initialized\n");
972
- if (h) h->should_quit = 1; /* 起不来就别让 run() 干等 */
973
- return;
974
- }
975
- p->thread = CreateThread(NULL, 0, host_thread_main, h, 0, &tid);
976
- if (!p->thread) {
977
- fprintf(stderr, "[cj-bridge] start failed: cannot create host thread\n");
978
- h->should_quit = 1;
979
- return;
980
- }
981
- p->thread_id = tid;
982
- p->thread_started = 1;
983
- }
984
-
985
- void cj_plat_quit(cj_host *h) {
986
- plat_host *p = h ? (plat_host *)h->plat : NULL;
987
- if (p && p->hwnd) {
988
- PostMessageW(p->hwnd, WM_CJT_QUIT, 0, 0);
989
- } else if (h) {
990
- h->should_quit = 1;
991
- }
992
- }
993
-
994
- void cj_plat_wake_ui(cj_host *h) {
995
- plat_host *p = h ? (plat_host *)h->plat : NULL;
996
- if (p && p->hwnd) {
997
- PostMessageW(p->hwnd, WM_CJT_FLUSH, 0, 0);
998
- }
999
- }
1000
-
1001
- void cj_plat_post_js(cj_host *h, const char *script_utf8) {
1002
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1003
- wchar_t *w;
1004
- if (!p || !p->webview || !script_utf8) return;
1005
- w = utf8_to_wide(script_utf8);
1006
- if (!w) return;
1007
- log_hr("ExecuteScript", p->webview->lpVtbl->ExecuteScript(p->webview, w, NULL));
1008
- free(w);
1009
- }
1010
-
1011
- int cj_plat_is_ui_thread(cj_host *h) {
1012
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1013
- return (p && p->thread_set && GetCurrentThreadId() == p->thread_id) ? 1 : 0;
1014
- }
1015
-
1016
- void cj_plat_open_devtools(cj_host *h) {
1017
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1018
- if (!p || !p->hwnd) {
1019
- fprintf(stderr, "[cj-bridge] open devtools ignored: window not ready\n");
1020
- return;
1021
- }
1022
- PostMessageW(p->hwnd, WM_CJT_DEVTOOLS, 0, 0);
1023
- }
1024
-
1025
- void cj_plat_load_url(cj_host *h, const char *url) {
1026
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1027
- if (!h || !url || !url[0]) {
1028
- return;
1029
- }
1030
- if (p && p->hwnd) {
1031
- wchar_t *w = utf8_to_wide(url);
1032
- if (!w) return;
1033
- /* pending_url 由 core 拥有(cj_bridge_load_url 已经记过一份),这里只投当次导航 */
1034
- fprintf(stderr, "[cj-bridge] load url (runtime): %s\n", url);
1035
- PostMessageW(p->hwnd, WM_CJT_LOAD_URL, 0, (LPARAM)w);
1036
- return;
1037
- }
1038
- fprintf(stderr, "[cj-bridge] load url: %s\n", url);
1039
- }
1040
-
1041
- void cj_plat_reload(cj_host *h) {
1042
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1043
- if (!p || !p->hwnd) {
1044
- fprintf(stderr, "[cj-bridge] reload ignored: window not ready\n");
1045
- return;
1046
- }
1047
- PostMessageW(p->hwnd, WM_CJT_RELOAD, 0, 0);
1048
- }
1049
-
1050
- /* ===== 菜单栏翻译层(RFC-002 §5.4):线路文本 → HMENU =====
1051
- 格式解析不在这里重复实现:走 core 的 cj_core_menu_walk(两平台共用一份,桩平台已自检)。
1052
- 本层只做三件事:把行翻译成 Win32 项、维护「命令 id ↔ 应用 id」映射、把点击经 core 回投。 */
1053
-
1054
- static void menu_clear_slots(plat_host *p) {
1055
- int i;
1056
- if (!p) return;
1057
- for (i = 0; i < p->menu_item_count; i++) {
1058
- free(p->menu_items[i].app_id);
1059
- p->menu_items[i].app_id = NULL;
1060
- }
1061
- p->menu_item_count = 0;
1062
- }
1063
-
1064
- static void menu_apply_item_state(plat_host *p, const char *id, int enabled, int checked) {
1065
- int i;
1066
- if (!p || !id || !id[0] || !p->menu) return;
1067
- for (i = 0; i < p->menu_item_count; i++) {
1068
- menu_slot *s = &p->menu_items[i];
1069
- if (!s->app_id || strcmp(s->app_id, id) != 0) continue;
1070
- s->enabled = enabled ? 1 : 0;
1071
- s->checked = checked ? 1 : 0;
1072
- EnableMenuItem(p->menu, s->cmd_id, MF_BYCOMMAND | (s->enabled ? MF_ENABLED : MF_GRAYED));
1073
- CheckMenuItem(p->menu, s->cmd_id,
1074
- MF_BYCOMMAND | (s->checked ? MF_CHECKED : MF_UNCHECKED));
1075
- DrawMenuBar(p->hwnd);
1076
- return;
1077
- }
1078
- fprintf(stderr, "[cj-bridge] menu item not found: %s\n", id);
1079
- }
1080
-
1081
- /* 建菜单时的上下文:层级用一张 HMENU 栈表示(行按文档顺序来,深度只会 +1 或回退)*/
1082
- typedef struct menu_build_ctx {
1083
- plat_host *p;
1084
- HMENU bar;
1085
- HMENU stack[CJ_MENU_MAX_DEPTH]; /* stack[d]:深度 d 的行往哪个菜单里追加 */
1086
- int ok[CJ_MENU_MAX_DEPTH]; /* 该层是否已经有子菜单可挂(没有就退回上一层)*/
1087
- } menu_build_ctx;
1088
-
1089
- static void menu_add_row(void *ctx, int depth, char kind, const char *id,
1090
- const char *label, int flags, const char *accel) {
1091
- menu_build_ctx *c = (menu_build_ctx *)ctx;
1092
- plat_host *p;
1093
- HMENU target;
1094
- wchar_t *wlabel = NULL;
1095
- int enabled = (flags & 1) ? 1 : 0;
1096
- int checked = (flags & 2) ? 1 : 0;
1097
-
1098
- if (!c || !c->p) return;
1099
- p = c->p;
1100
- if (depth < 0) depth = 0;
1101
- if (depth >= CJ_MENU_MAX_DEPTH) depth = CJ_MENU_MAX_DEPTH - 1;
1102
- target = (depth > 0 && c->ok[depth]) ? c->stack[depth] : c->bar;
1103
-
1104
- if (kind == 's') { /* 分隔线:既没有命令 id,也没有文字 */
1105
- AppendMenuW(target, MF_SEPARATOR, 0, NULL);
1106
- return;
1107
- }
1108
-
1109
- /* 标签:accel 非空时用制表符并进去——Win32 会把 \t 之后的部分右对齐,正是快捷键栏的位置 */
1110
- {
1111
- size_t n = strlen(label) + (accel[0] ? strlen(accel) + 1 : 0) + 1;
1112
- char *tmp = (char *)malloc(n);
1113
- if (!tmp) return;
1114
- snprintf(tmp, n, "%s%s%s", label, accel[0] ? "\t" : "", accel);
1115
- wlabel = utf8_to_wide(tmp);
1116
- free(tmp);
1117
- }
1118
- if (!wlabel) return;
1119
-
1120
- if (kind == 'm') { /* 子菜单:建出 HMENU 挂进父项,并让深一层的行知道往哪挂 */
1121
- HMENU sub = CreatePopupMenu();
1122
- if (!sub) {
1123
- free(wlabel);
1124
- return;
1125
- }
1126
- if (!AppendMenuW(target, MF_POPUP | (enabled ? 0 : MF_GRAYED), (UINT_PTR)sub, wlabel)) {
1127
- fprintf(stderr, "[cj-bridge] menu: append submenu failed: %lu\n",
1128
- (unsigned long)GetLastError());
1129
- DestroyMenu(sub);
1130
- free(wlabel);
1131
- return;
1132
- }
1133
- free(wlabel);
1134
- if (depth + 1 < CJ_MENU_MAX_DEPTH) {
1135
- c->stack[depth + 1] = sub;
1136
- c->ok[depth + 1] = 1;
1137
- }
1138
- return;
1139
- }
1140
-
1141
- if (p->menu_item_count >= CJ_MENU_MAX_ITEMS) {
1142
- fprintf(stderr, "[cj-bridge] menu: item limit reached, dropped: %s\n", id);
1143
- free(wlabel);
1144
- return;
1145
- }
1146
- {
1147
- UINT item_flags = MF_STRING;
1148
- UINT cmd_id = (UINT)(p->menu_item_count + 1); /* 0 保留给「没有命令」*/
1149
- menu_slot *s = &p->menu_items[p->menu_item_count];
1150
- if (!enabled) item_flags |= MF_GRAYED;
1151
- if (kind == 'c' && checked) item_flags |= MF_CHECKED;
1152
- if (!AppendMenuW(target, item_flags, cmd_id, wlabel)) {
1153
- fprintf(stderr, "[cj-bridge] menu: append item failed: %lu (id=%s)\n",
1154
- (unsigned long)GetLastError(), id);
1155
- free(wlabel);
1156
- return;
1157
- }
1158
- free(wlabel);
1159
- ZeroMemory(s, sizeof(*s));
1160
- s->cmd_id = cmd_id;
1161
- s->app_id = strdup(id);
1162
- s->kind = kind;
1163
- s->enabled = enabled;
1164
- s->checked = checked;
1165
- p->menu_item_count++;
1166
- }
1167
- }
1168
-
1169
- static void menu_apply_wire(cj_host *h, plat_host *p, const char *wire) {
1170
- menu_build_ctx c;
1171
- HMENU old;
1172
-
1173
- if (!p) return;
1174
- if (!wire || !wire[0]) {
1175
- /* 空串 = 清空菜单栏:SetMenu(hwnd, NULL) 才是真的没有菜单栏(空 popup 会留一条空条)*/
1176
- menu_clear_slots(p);
1177
- if (p->menu) {
1178
- SetMenu(p->hwnd, NULL);
1179
- DestroyMenu(p->menu);
1180
- p->menu = NULL;
1181
- }
1182
- DrawMenuBar(p->hwnd);
1183
- fprintf(stderr, "[cj-bridge] menu cleared\n");
1184
- return;
1185
- }
1186
- ZeroMemory(&c, sizeof(c));
1187
- c.p = p;
1188
- /* 顶层必须是「菜单栏」:SetMenu 只接受 CreateMenu 出来的句柄,塞一个 CreatePopupMenu
1189
- 的 popup 句柄进去会以 ERROR_INVALID_PARAMETER(87) **静默失败**——实测就是这样:
1190
- menu applied 照打、窗口上却查不到菜单(GetMenu 一直是 NULL)。子菜单才用 popup。 */
1191
- c.bar = CreateMenu();
1192
- if (!c.bar) {
1193
- fprintf(stderr, "[cj-bridge] menu: CreateMenu failed: %lu\n",
1194
- (unsigned long)GetLastError());
1195
- return;
1196
- }
1197
- c.stack[0] = c.bar;
1198
- c.ok[0] = 1;
1199
- menu_clear_slots(p); /* 旧映射整体作废:新菜单的命令 id 从 1 重新分配 */
1200
- cj_core_menu_walk(wire, menu_add_row, &c);
1201
- old = p->menu;
1202
- p->menu = c.bar;
1203
- /* SetMenu 会静默失败(返回 FALSE),此时日志照样说「已应用」而窗口上什么都没有——
1204
- Windows 上「真挂上了」的唯一凭证就是这两行:返回值 + 事后 GetMenu 复核。 */
1205
- if (!SetMenu(p->hwnd, p->menu)) {
1206
- fprintf(stderr, "[cj-bridge] menu: SetMenu failed: %lu\n", (unsigned long)GetLastError());
1207
- }
1208
- DrawMenuBar(p->hwnd);
1209
- if (old) DestroyMenu(old);
1210
- fprintf(stderr, "[cj-bridge] menu applied: items=%d hwnd=%p bar=%p GetMenu=%p\n",
1211
- p->menu_item_count, (void *)p->hwnd, (void *)p->menu, (void *)GetMenu(p->hwnd));
1212
- }
1213
-
1214
- void cj_plat_set_menu(cj_host *h, const char *wire) {
1215
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1216
- if (!p || !wire) return;
1217
- if (!p->hwnd) {
1218
- /* 未就绪:core 已把线路文本记在 h->pending_menu_text,宿主线程建窗时取用 */
1219
- fprintf(stderr, "[cj-bridge] menu deferred: window not ready\n");
1220
- return;
1221
- }
1222
- if (cj_plat_is_ui_thread(h)) {
1223
- menu_apply_wire(h, p, wire);
1224
- return;
1225
- }
1226
- {
1227
- char *copy = strdup(wire); /* 跨线程:谁 post 谁分配,UI 线程在 WM_CJT_MENU 分支里释放 */
1228
- if (!copy) return;
1229
- if (!PostMessageW(p->hwnd, WM_CJT_MENU, 0, (LPARAM)copy)) {
1230
- fprintf(stderr, "[cj-bridge] menu post failed: %lu\n", (unsigned long)GetLastError());
1231
- free(copy);
1232
- return;
1233
- }
1234
- fprintf(stderr, "[cj-bridge] menu posted to ui thread (%d bytes)\n", (int)strlen(wire));
1235
- }
1236
- }
1237
-
1238
- void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked) {
1239
- plat_host *p = h ? (plat_host *)h->plat : NULL;
1240
- if (!p || !id || !id[0]) return;
1241
- if (!p->hwnd || !p->menu) {
1242
- fprintf(stderr, "[cj-bridge] menu item state ignored: menu not ready (%s)\n", id);
1243
- return;
1244
- }
1245
- if (cj_plat_is_ui_thread(h)) {
1246
- menu_apply_item_state(p, id, enabled, checked);
1247
- return;
1248
- }
1249
- {
1250
- menu_state_msg *m = (menu_state_msg *)malloc(sizeof(*m));
1251
- if (!m) return;
1252
- m->id = strdup(id);
1253
- m->enabled = enabled ? 1 : 0;
1254
- m->checked = checked ? 1 : 0;
1255
- if (!m->id || !PostMessageW(p->hwnd, WM_CJT_MENU_ITEM, 0, (LPARAM)m)) {
1256
- fprintf(stderr, "[cj-bridge] menu item state post failed: %lu\n",
1257
- (unsigned long)GetLastError());
1258
- free(m->id);
1259
- free(m);
1260
- }
1261
- }
1262
- }
1263
-
1264
- int cj_plat_host_capabilities(cj_host *h) {
1265
- (void)h;
1266
- /* 本平台的静态能力:菜单栏已落地;托盘 / 拖放到 7.B / 7.C 做完再把对应的位置打开 */
1267
- return CJ_CAP_MENU;
1268
- }
1269
-
1270
- /* =========================================================================
1271
- * 串口(plugin_serial 的平台层):Windows 侧
1272
- *
1273
- * 四个原语的**名字与返回码口径**与 Linux 侧逐字一致(见 bridge_core.h),所以上层不必写平台分支。
1274
- * 实现:CreateFileW("\\\\.\\COMx", …, FILE_FLAG_OVERLAPPED) + DCB 配参;
1275
- * 读写都走 OVERLAPPED——超时由 WaitForSingleObject 的预算控制,不会把调用线程钉死在
1276
- * 驱动内部的阻塞读上(SetCommTimeouts 的总超时只对「等到第一个字节」可控,
1277
- * OVERLAPPED 才能像 Linux 的 poll 那样精确按剩余预算等)。
1278
- * ========================================================================= */
1279
-
1280
- #define CJ_SERIAL_MAX_HANDLES 8
1281
-
1282
- typedef struct serial_slot {
1283
- int used;
1284
- long long id;
1285
- HANDLE h;
1286
- char path[256];
1287
- } serial_slot;
1288
-
1289
- static serial_slot g_serial_slots[CJ_SERIAL_MAX_HANDLES];
1290
- static long long g_serial_next_id = 1;
1291
- static CRITICAL_SECTION g_serial_lock;
1292
- static INIT_ONCE g_serial_lock_once = INIT_ONCE_STATIC_INIT;
1293
-
1294
- static BOOL CALLBACK serial_lock_init_once(PINIT_ONCE once, PVOID param, PVOID *ctx) {
1295
- (void)once; (void)param; (void)ctx;
1296
- InitializeCriticalSection(&g_serial_lock);
1297
- return TRUE;
1298
- }
1299
-
1300
- static void serial_lock(void) {
1301
- InitOnceExecuteOnce(&g_serial_lock_once, serial_lock_init_once, NULL, NULL);
1302
- EnterCriticalSection(&g_serial_lock);
1303
- }
1304
-
1305
- static void serial_unlock(void) {
1306
- LeaveCriticalSection(&g_serial_lock);
1307
- }
1308
-
1309
- static void serial_err(char *err, int err_len, const char *fmt, ...) {
1310
- va_list ap;
1311
- if (!err || err_len <= 0) return;
1312
- va_start(ap, fmt);
1313
- vsnprintf(err, (size_t)err_len, fmt, ap);
1314
- va_end(ap);
1315
- }
1316
-
1317
- static serial_slot *serial_find_locked(long long id) {
1318
- int i;
1319
- for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
1320
- if (g_serial_slots[i].used && g_serial_slots[i].id == id) return &g_serial_slots[i];
1321
- }
1322
- return NULL;
1323
- }
1324
-
1325
- /* 句柄 → HANDLE;无效时填 err 返回 NULL。HANDLE 值本身取出后在锁外用:
1326
- close 会先置 used=0 再 CloseHandle,所以锁外使用期间句柄不会被本表回收。 */
1327
- static HANDLE serial_handle_of(long long h, char *err, int err_len) {
1328
- serial_slot *s;
1329
- HANDLE hd = NULL;
1330
- serial_lock();
1331
- s = serial_find_locked(h);
1332
- if (s) hd = s->h;
1333
- serial_unlock();
1334
- if (!hd) serial_err(err, err_len, "serial: 句柄 %lld 无效(未打开或已关闭)", h);
1335
- return hd;
1336
- }
1337
-
1338
- /* "COM3"(白名单放行的形状)→ "\\.\COM3":Win32 打开串口必须走设备命名空间,
1339
- 普通文件语义的 "COM3" 会被当成当前目录下的同名文件。 */
1340
- static int serial_win_path(const char *path, wchar_t *out, int out_chars) {
1341
- wchar_t w[256];
1342
- int n;
1343
- if (MultiByteToWideChar(CP_UTF8, 0, path, -1, w, 256) <= 0) return 0;
1344
- if (wcsncmp(w, L"\\\\.\\", 4) == 0) {
1345
- n = _snwprintf(out, (size_t)out_chars, L"%s", w);
1346
- } else {
1347
- n = _snwprintf(out, (size_t)out_chars, L"\\\\.\\%s", w);
1348
- }
1349
- return (n > 0 && n < out_chars) ? 1 : 0;
1350
- }
1351
-
1352
- /* 数值波特率 → DCB:DCB.BaudRate 本身收数值,但沿用 Linux「只收常见档位」的口径——
1353
- 不认识的值拒绝而不是透传给驱动(两端行为一致,上层不用猜哪边支持哪档) */
1354
- static int serial_baud_known(int baud) {
1355
- switch (baud) {
1356
- case 300: case 600: case 1200: case 2400: case 4800: case 9600:
1357
- case 19200: case 38400: case 57600: case 115200: case 230400:
1358
- case 460800: case 500000: case 921600: case 1000000:
1359
- return 1;
1360
- default:
1361
- return 0;
1362
- }
1363
- }
1364
-
1365
- static long long serial_now_ms(void) {
1366
- return (long long)GetTickCount64();
1367
- }
1368
-
1369
- long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len) {
1370
- wchar_t wpath[280];
1371
- HANDLE hd;
1372
- DCB dcb;
1373
- COMMTIMEOUTS to;
1374
- int i;
1375
- long long id;
1376
-
1377
- if (!path || !cfg) {
1378
- serial_err(err, err_len, "serial: 空路径 / 空配置");
1379
- return -1;
1380
- }
1381
- if (!serial_baud_known(cfg->baud)) {
1382
- serial_err(err, err_len, "serial: 本平台不支持的波特率 %d(只收常见档位)", cfg->baud);
1383
- return -1;
1384
- }
1385
- if (!serial_win_path(path, wpath, (int)(sizeof(wpath) / sizeof(wpath[0])))) {
1386
- serial_err(err, err_len, "serial: 设备路径转宽字符失败(过长或非 UTF-8?): %s", path);
1387
- return -1;
1388
- }
1389
-
1390
- serial_lock();
1391
- for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
1392
- if (!g_serial_slots[i].used) break;
1393
- }
1394
- if (i == CJ_SERIAL_MAX_HANDLES) {
1395
- serial_unlock();
1396
- serial_err(err, err_len, "serial: 句柄表满(最多 %d 个并发串口)", CJ_SERIAL_MAX_HANDLES);
1397
- return -2;
1398
- }
1399
-
1400
- /* 独占打开(共享模式 0):串口是物理独占资源,两个进程同开一个口只会互咬数据 */
1401
- hd = CreateFileW(wpath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
1402
- FILE_FLAG_OVERLAPPED, NULL);
1403
- if (hd == INVALID_HANDLE_VALUE) {
1404
- serial_err(err, err_len, "serial: CreateFileW(%s) 失败: lu=%lu(口不存在 / 被占用?)",
1405
- path, (unsigned long)GetLastError());
1406
- serial_unlock();
1407
- return -3;
1408
- }
1409
-
1410
- /* GetCommState 拿驱动现状打底再覆盖:DCB 结构里有几十个流控/二进制位,
1411
- 全零起步会把 fBinary=0 这类非法组合交给驱动,行为随驱动版本漂移 */
1412
- memset(&dcb, 0, sizeof(dcb));
1413
- dcb.DCBlength = sizeof(dcb);
1414
- if (!GetCommState(hd, &dcb)) {
1415
- serial_err(err, err_len, "serial: GetCommState(%s) 失败: lu=%lu",
1416
- path, (unsigned long)GetLastError());
1417
- CloseHandle(hd);
1418
- serial_unlock();
1419
- return -3;
1420
- }
1421
- dcb.BaudRate = (DWORD)cfg->baud;
1422
- dcb.ByteSize = (BYTE)cfg->data_bits;
1423
- dcb.Parity = (cfg->parity == 1) ? ODDPARITY : (cfg->parity == 2) ? EVENPARITY : NOPARITY;
1424
- dcb.StopBits = (cfg->stop_bits == 2) ? TWOSTOPBITS : ONESTOPBIT;
1425
- dcb.fBinary = TRUE;
1426
- dcb.fParity = (cfg->parity != 0) ? TRUE : FALSE;
1427
- /* 硬件/软件流控不替应用开(与 Linux 侧同一条铁律:框架自作主张会改握手语义) */
1428
- dcb.fOutxCtsFlow = FALSE;
1429
- dcb.fOutxDsrFlow = FALSE;
1430
- dcb.fDtrControl = DTR_CONTROL_DISABLE;
1431
- dcb.fRtsControl = RTS_CONTROL_DISABLE;
1432
- dcb.fOutX = FALSE;
1433
- dcb.fInX = FALSE;
1434
- if (!SetCommState(hd, &dcb)) {
1435
- serial_err(err, err_len, "serial: SetCommState(%s) 失败: lu=%lu(设备不接受这组参数?)",
1436
- path, (unsigned long)GetLastError());
1437
- CloseHandle(hd);
1438
- serial_unlock();
1439
- return -3;
1440
- }
1441
-
1442
- /* 超时全部归零:等待交给 OVERLAPPED 的 WaitForSingleObject(我们控制预算),
1443
- 驱动层不再叠加自己的阻塞语义。PURGE 清掉打开瞬间的残留字节 */
1444
- memset(&to, 0, sizeof(to));
1445
- to.ReadIntervalTimeout = MAXDWORD;
1446
- to.ReadTotalTimeoutConstant = 0;
1447
- to.ReadTotalTimeoutMultiplier = 0;
1448
- to.WriteTotalTimeoutConstant = 0;
1449
- to.WriteTotalTimeoutMultiplier = 0;
1450
- if (!SetCommTimeouts(hd, &to)) {
1451
- serial_err(err, err_len, "serial: SetCommTimeouts(%s) 失败: lu=%lu",
1452
- path, (unsigned long)GetLastError());
1453
- CloseHandle(hd);
1454
- serial_unlock();
1455
- return -3;
1456
- }
1457
- PurgeComm(hd, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR);
1458
-
1459
- id = g_serial_next_id++;
1460
- if (g_serial_next_id <= 0) g_serial_next_id = 1; /* 溢出兜底:句柄永不为 0 */
1461
- g_serial_slots[i].used = 1;
1462
- g_serial_slots[i].id = id;
1463
- g_serial_slots[i].h = hd;
1464
- snprintf(g_serial_slots[i].path, sizeof(g_serial_slots[i].path), "%s", path);
1465
- serial_unlock();
1466
-
1467
- /* 落地一行:参数与句柄都打全,串口问题基本都是拿这行对账(AGENTS §1 的取证口径) */
1468
- fprintf(stderr, "[cj-bridge] serial win32: path=%s baud=%d data=%d parity=%d stop=%d handle=%p\n",
1469
- path, cfg->baud, cfg->data_bits, cfg->parity, cfg->stop_bits, (void *)hd);
1470
- return id;
1471
- }
1472
-
1473
- /* 读:OVERLAPPED 发起,等事件最多 timeout_ms;0 = 超时无数据(与 Linux poll 口径一致) */
1474
- int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms, char *err, int err_len) {
1475
- HANDLE hd = serial_handle_of(h, err, err_len);
1476
- OVERLAPPED ov;
1477
- DWORD got = 0;
1478
- BOOL ok;
1479
-
1480
- if (!hd) return -4;
1481
-
1482
- memset(&ov, 0, sizeof(ov));
1483
- ov.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
1484
- if (!ov.hEvent) {
1485
- serial_err(err, err_len, "serial: CreateEvent 失败: lu=%lu", (unsigned long)GetLastError());
1486
- return -3;
1487
- }
1488
- ok = ReadFile(hd, buf, (DWORD)len, &got, &ov);
1489
- if (!ok && GetLastError() == ERROR_IO_PENDING) {
1490
- DWORD w = WaitForSingleObject(ov.hEvent, timeout_ms > 0 ? (DWORD)timeout_ms : 0);
1491
- if (w == WAIT_TIMEOUT) {
1492
- /* 预算用尽:撤销这次读再走,驱动里不挂未完成请求 */
1493
- CancelIo(hd);
1494
- GetOverlappedResult(hd, &ov, &got, TRUE); /* 等 CancelIo 本身落定 */
1495
- CloseHandle(ov.hEvent);
1496
- return 0;
1497
- }
1498
- ok = (w == WAIT_OBJECT_0) ? GetOverlappedResult(hd, &ov, &got, FALSE) : FALSE;
1499
- if (!ok && GetLastError() == ERROR_OPERATION_ABORTED) {
1500
- CloseHandle(ov.hEvent);
1501
- return 0;
1502
- }
1503
- } else if (!ok && GetLastError() == ERROR_IO_INCOMPLETE) {
1504
- /* 理论上只在无事件句柄时出现;按「本轮无数据」降级,别当错误放大 */
1505
- CloseHandle(ov.hEvent);
1506
- return 0;
1507
- }
1508
- CloseHandle(ov.hEvent);
1509
- if (!ok) {
1510
- serial_err(err, err_len, "serial: ReadFile 失败: lu=%lu", (unsigned long)GetLastError());
1511
- return -3;
1512
- }
1513
- return (int)got;
1514
- }
1515
-
1516
- /* 写:在 timeout_ms 总预算内有界写完(与 Linux 侧同一口径:返回已写入量,预算用尽即交回) */
1517
- int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
1518
- char *err, int err_len) {
1519
- HANDLE hd = serial_handle_of(h, err, err_len);
1520
- long long deadline = serial_now_ms() + (timeout_ms > 0 ? (long long)timeout_ms : 0);
1521
- int written = 0;
1522
-
1523
- if (!hd) return -4;
1524
-
1525
- while (written < len) {
1526
- OVERLAPPED ov;
1527
- DWORD put = 0, w;
1528
- long long budget;
1529
- BOOL ok;
1530
-
1531
- memset(&ov, 0, sizeof(ov));
1532
- ov.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
1533
- if (!ov.hEvent) {
1534
- serial_err(err, err_len, "serial: CreateEvent 失败: lu=%lu", (unsigned long)GetLastError());
1535
- return written > 0 ? written : -3;
1536
- }
1537
- ok = WriteFile(hd, buf + written, (DWORD)(len - written), &put, &ov);
1538
- if (!ok && GetLastError() == ERROR_IO_PENDING) {
1539
- budget = deadline - serial_now_ms();
1540
- w = WaitForSingleObject(ov.hEvent, budget > 0 ? (DWORD)budget : 0);
1541
- if (w == WAIT_TIMEOUT) {
1542
- CancelIo(hd);
1543
- GetOverlappedResult(hd, &ov, &put, TRUE);
1544
- CloseHandle(ov.hEvent);
1545
- break; /* 预算用尽:返回已写入量 */
1546
- }
1547
- ok = (w == WAIT_OBJECT_0) ? GetOverlappedResult(hd, &ov, &put, FALSE) : FALSE;
1548
- if (!ok && GetLastError() == ERROR_OPERATION_ABORTED) {
1549
- CloseHandle(ov.hEvent);
1550
- break;
1551
- }
1552
- }
1553
- CloseHandle(ov.hEvent);
1554
- if (!ok) {
1555
- serial_err(err, err_len, "serial: WriteFile 失败: lu=%lu", (unsigned long)GetLastError());
1556
- return written > 0 ? written : -3;
1557
- }
1558
- if (put == 0) break; /* 驱动收了请求却 0 字节:别空转,交回上层 */
1559
- written += (int)put;
1560
- }
1561
- return written;
1562
- }
1563
-
1564
- int cj_plat_serial_close(long long h, char *err, int err_len) {
1565
- HANDLE hd = NULL;
1566
- int i;
1567
-
1568
- serial_lock();
1569
- for (i = 0; i < CJ_SERIAL_MAX_HANDLES; i++) {
1570
- if (g_serial_slots[i].used && g_serial_slots[i].id == h) {
1571
- hd = g_serial_slots[i].h;
1572
- g_serial_slots[i].used = 0;
1573
- g_serial_slots[i].id = 0;
1574
- g_serial_slots[i].h = NULL;
1575
- g_serial_slots[i].path[0] = '\0';
1576
- break;
1577
- }
1578
- }
1579
- serial_unlock();
1580
- if (!hd) {
1581
- serial_err(err, err_len, "serial: 句柄 %lld 无效(未打开或已关闭)", h);
1582
- return -4;
1583
- }
1584
- if (!CloseHandle(hd)) {
1585
- serial_err(err, err_len, "serial: CloseHandle 失败: lu=%lu", (unsigned long)GetLastError());
1586
- return -3;
1587
- }
1588
- return 0;
1589
- }
1590
-
1591
- /* 串口枚举(cj_bridge_serial_list 的平台层):QueryDosDeviceW 扫全部 MS-DOS 设备名,
1592
- 收「COM + 全数字」的形状。相比翻注册表(枚举值随驱动形态漂移:有 00xx 子键的、
1593
- 有 Device Parameters 的),QueryDosDevice 一次调用给全系统当前真实存在的设备名,
1594
- 虚拟口(com0com、驱动模拟)也在其中。 */
1595
- int cj_plat_serial_list(char *out, int out_len) {
1596
- wchar_t *buf;
1597
- DWORD need = 65536, got, i;
1598
- int count = 0, pos = 0;
1599
-
1600
- if (!out || out_len <= 0) return -1;
1601
- out[0] = '\0';
1602
- for (;;) {
1603
- buf = (wchar_t *)malloc((size_t)need * sizeof(wchar_t));
1604
- if (!buf) return -1;
1605
- got = QueryDosDeviceW(NULL, buf, need);
1606
- if (got > 0) break;
1607
- free(buf);
1608
- if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return -1;
1609
- need *= 2; /* 缓冲不够就翻倍重试;QueryDosDevice 无返回所需大小的口径 */
1610
- }
1611
- for (i = 0; buf[i] != L'\0'; i += wcslen(buf + i) + 1) {
1612
- wchar_t *name = buf + i;
1613
- const wchar_t *p = name;
1614
- int digits = 0;
1615
- if (wcsncmp(p, L"COM", 3) != 0) continue;
1616
- p += 3;
1617
- while (*p >= L'0' && *p <= L'9') { p++; digits++; }
1618
- if (digits == 0 || digits > 3 || *p != L'\0') continue; /* COMfoo / 超长号一律不要 */
1619
- {
1620
- char ascii[16];
1621
- int n = WideCharToMultiByte(CP_UTF8, 0, name, -1, ascii, (int)sizeof(ascii), NULL, NULL);
1622
- if (n <= 0) continue;
1623
- if (pos + n >= out_len) break; /* 放不下就到此为止:串口数量不会真到溢出 */
1624
- if (count > 0) out[pos++] = '\n';
1625
- memcpy(out + pos, ascii, (size_t)n);
1626
- pos += n - 1; /* 不含收尾 '\0' */
1627
- count++;
1628
- }
1629
- }
1630
- free(buf);
1631
- out[pos] = '\0';
1632
- return count;
1633
- }