cj-tauri 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,618 +1,675 @@
1
- /*
2
- * cj-tauri C 桥公共核心(批次 1):句柄化的宿主状态 + 平台无关逻辑
3
- *
4
- * 分层(AGENTS.md §2:平台差异只允许出现在 @When 与 bridge_*.c):
5
- * bridge_core.c/.h 平台无关:JS 回投队列与批处理、对话框单槽状态机、窗口配置存储、
6
- * 预执行脚本注册表、生命周期标志、全部 cj_bridge_* 导出(单一来源)
7
- * bridge_linux.c GTK / WebKitGTK 原语:建窗装配、gtk_main、原生对话框、idle 投递
8
- * bridge_win.c Win32 / WebView2 / COM 原语:窗口类与消息循环、COM 回调、原生对话框
9
- *
10
- * 为什么句柄化:原先是进程级单例(g_window / g_hwnd),多窗口无从落地——
11
- * 见 docs/架构演进-多平台与多窗口.md §2.1。这里把「哪个窗口」变成显式句柄,单窗口行为不变。
12
- *
13
- * 分工铁律:
14
- * 1. core **不 include 任何平台头**(pthread / windows.h / gtk),需要平台能力一律走 cj_plat_*;
15
- * 2. 平台文件**不实现任何 cj_bridge_***(导出全在 core,两平台导出名与签名因此逐字一致);
16
- * 3. 平台文件只实现下面的「平台原语清单」。
17
- *
18
- * 句柄生命周期(docs/架构演进-多平台与多窗口.md §6.1):
19
- * 窗口关闭 ≠ 释放句柄。窗口销毁时平台调 cj_core_window_destroyed(),只置标志 + 通知回调 +
20
- * 释放原生/COM 资源;句柄内存由 cj_bridge_destroy() 在 C 侧回收(malloc 与 free 同侧)。
21
- * 仓颉侧保证 destroy 至多调用一次并把字段置空(C 侧的 destroy 对 NULL 安全)。
22
- */
23
- #ifndef CJ_BRIDGE_CORE_H
24
- #define CJ_BRIDGE_CORE_H
25
-
26
- #include <stdio.h>
27
- #include <stdlib.h>
28
- #include <string.h>
29
-
30
- /* 导出宏:导出定义全在 bridge_core.c,平台文件只实现 cj_plat_*(不重复定义导出)。
31
- Windows 要 dllexport;Linux 默认可见,仍显式声明一遍。 */
32
- #if defined(_WIN32)
33
- #define CJ_BRIDGE_API __declspec(dllexport)
34
- #else
35
- #define CJ_BRIDGE_API __attribute__((visibility("default")))
36
- #endif
37
-
38
- /* ===== 仓颉侧回调(@C 函数)===== */
39
- typedef void (*cj_on_message_fn)(const char *json); /* JS 消息到达(UTF-8 JSON)*/
40
-
41
- /* 前向声明:下面三个回调签名都要用到宿主句柄,而 cj_host 结构体还在后面才定义。
42
- 必须是**文件作用域**的前向声明——只在参数列表里写 struct cj_host,clang 会把它当成
43
- 「仅本原型内可见」的另一个类型(-Wvisibility),赋函数指针时就报类型不兼容。 */
44
- struct cj_host;
45
-
46
- /* **为什么这些回调的首参是宿主句柄**:仓颉的 @C 函数是模块级函数,每个宿主注册的是同一个
47
- 函数指针,不把 h 传回去就分不清是哪个窗口触发的——多窗口下就是「后装配的窗口覆盖先装配的」
48
- 那个静态单槽坑(docs/架构演进-多平台与多窗口.md §7.5)的根治办法。**新增回调一律照此带宿主身份。**
49
-
50
- cj_on_message_fn 是唯一的例外,且是**有意**的:报文自带发起窗口的 window label
51
- (架构文档 §7.3),仓颉侧给所有窗口装的是同一只派发器,不需要按句柄分表
52
- (见 src/app.cj 的 wireHostCallbacks)。其余回调没有这种自带身份的载荷,只能靠句柄。 */
53
- typedef void (*cj_on_destroy_fn)(struct cj_host *host); /* 窗口销毁 / 退出(每个宿主恰好一次)*/
54
- typedef void (*cj_on_dialog_fn)(struct cj_host *host, const char *path); /* 原生对话框选中的路径(取消 = 空串)*/
55
-
56
- /* 宿主级 shell 事件回调:**菜单 / 托盘 / 拖放共用一个入口**(RFC-002 §5.5),
57
- 第一个参数回传宿主句柄,第二个是 JSON 载荷(与 on_message 同一载体,仓颉侧已有 JSON 解析)。
58
-
59
- {"kind":"menu","id":"file.save","enabled":true,"checked":false}
60
- {"kind":"tray","id":"toggle"} ← 7.C
61
- {"kind":"drop","paths":["C:\\a.png"]} ← 7.C
62
-
63
- **签名这里必须写 `struct cj_host *`(不能用 `cj_host *`)**:本 typedef 要出现在 cj_host
64
- 结构体定义之前(结构体里有 on_shell 字段),此时那个 typedef 名字还不存在,只有上面刚做的
65
- 结构体前向声明可用。同一类型,Cangjie 侧看不出来——它只拿到一个不透明指针。 */
66
- typedef void (*cj_on_shell_fn)(struct cj_host *host, const char *json);
67
-
68
- /* 前端 __CJ_TAURI__.reload() 的控制消息:宿主级操作,不进 IPC hub
69
- (与 native/bridge_js.h 里的字面量必须一致) */
70
- #define CJT_RELOAD_MSG "__cj_tauri_reload__"
71
-
72
- /* 单批上限:一次 UI 空闲窗口最多合并多少条回投。写成常量而不是「取空为止」,
73
- 是为了长队列也肯让出 UI 线程(剩下的由自续唤醒接手)。 */
74
- #define CJ_JS_BATCH_MAX 64
75
-
76
- /* 宿主能力位(cj_bridge_host_capabilities 的返回值,RFC-002 §5.4):
77
- 纯静态事实,由平台在 cj_plat_init 时给出(例:Linux 侧托盘没编进来 → 不置 CJ_CAP_TRAY),
78
- 仓颉侧据此构造 HostCapabilities,`system:host` 如实上报,不许猜。 */
79
- #define CJ_CAP_MENU 1
80
- #define CJ_CAP_TRAY 2
81
- #define CJ_CAP_DRAG_DROP 4
82
- #define CJ_CAP_NOTIFICATION 8 /* 平台通知可用(Linux = freedesktop Notifications)*/
83
- #define CJ_CAP_HOTKEY 16 /* 全局快捷键可用(Linux = X11 grab;Wayland 不置位)*/
84
- #define CJ_CAP_SUBTITLE 32 /* 窗口顶部那一行能显示副标题(Linux = 与菜单栏同排的 label;
85
- Windows 的系统菜单没有自绘区,未实现、不置位)*/
86
-
87
- /* ===== 回投队列节点(core 拥有;UTF-8 存储,宽字符转换由平台在 cj_plat_post_js 里做)===== */
88
- typedef struct js_node {
89
- char *js;
90
- struct js_node *next;
91
- } js_node;
92
-
93
- /* ===== 原生对话框请求(core 分配 / 释放;平台只填 ok 与 path)===== */
94
- typedef struct dlg_req {
95
- int kind; /* 0 打开文件 1 保存文件 2 info 3 warning 4 error 5 confirm 6 选择目录 */
96
- char *title;
97
- char *message;
98
- char *filter; /* "描述|模式|描述|模式";空串 = 不过滤 */
99
- char *path; /* 文件类结果:选中路径(取消时为空)*/
100
- int ok; /* 1 = 用户点了确认 */
101
- } dlg_req;
102
-
103
- /* ===== 同步原语句柄(平台私有结构:Linux = mutex + cond;Windows = CRITICAL_SECTION +
104
- CONDITION_VARIABLE)。core 只用它们,绝不直接碰 pthread / Win32。 ===== */
105
- typedef struct cj_sync cj_sync;
106
-
107
- /* ===== 宿主句柄:core 拥有的状态 + 一个平台私有指针 ===== */
108
- typedef struct cj_host {
109
- /* --- 回调(cj_bridge_init 注入)--- */
110
- cj_on_message_fn on_message;
111
- cj_on_destroy_fn on_destroy;
112
- cj_on_dialog_fn on_dialog;
113
- cj_on_shell_fn on_shell; /* cj_bridge_set_shell_callback 注入;未注册 = 事件只留日志 */
114
-
115
- /* --- JS 回投队列(FIFO:每条 JS 独立执行,防止覆盖)--- */
116
- cj_sync *js_lock;
117
- js_node *js_head;
118
- js_node *js_tail;
119
-
120
- /* --- 对话框单槽:对话框是模态的,同时只允许一个(嵌套调用直接拒绝而不是自锁)--- */
121
- cj_sync *dlg_lock;
122
- int dlg_busy; /* 有人正在进行中 */
123
- int dlg_ready; /* 结果就绪(与 dlg_lock 配套的谓词)*/
124
- dlg_req *dlg_cur; /* 进行中的请求;窗口先关时由 cj_core_window_destroyed 作废 */
125
-
126
- /* --- 剪贴板单槽(同一时刻只允许一次读写;窗口销毁时作废等待方)---
127
- 读结果经 cb_text 交回(core malloc,调用方 cj_bridge_clipboard_free 释放),
128
- cb_ok=0 表示失败/作废(cb_text 为 NULL)。 */
129
- cj_sync *clip_lock;
130
- int clip_ready;
131
- int clip_ok;
132
- char *clip_text; /* 读操作的结果(UI 线程 malloc,等待方负责释放)*/
133
- int clip_busy;
134
-
135
- /* --- 窗口配置:由仓颉侧在 cj_bridge_start 之前经 cj_bridge_set_window 注入 --- */
136
- char *win_title;
137
- int win_w;
138
- int win_h;
139
- char *win_icon; /* NULL = 系统默认图标 */
140
- int devtools; /* 开发者工具开关 */
141
-
142
- /* 窗口初始可见性(cj_bridge_set_window_visible 存入):1 = 显示(默认,老应用行为不变)、
143
- 0 = 窗口建好即隐藏。**只解决「建窗那一刻该不该显示」**——运行期改可见性由平台直接投
144
- UI 线程执行,不需要它。 */
145
- int win_visible;
146
-
147
- /* 最小化到托盘(cj_bridge_set_minimize_to_tray 存入):1 = 平台把「最小化」改写成「隐藏」。
148
- 真到最小化那一刻平台还会复核托盘是否真装上了(没有就如实降级为普通最小化 + 一行 stderr)。 */
149
- int minimize_to_tray;
150
-
151
- /* --- 预执行脚本(cj_bridge_add_init_script 追加,WebView 创建时逐条注入到
152
- document-start;插件的 jsShim 走这条通道,注册顺序排在 BRIDGE_JS 之后)--- */
153
- char **init_scripts;
154
- int init_script_count;
155
- int init_script_cap;
156
-
157
- /* --- 页面来源二选一(URL 优先):start 之前注入,start 之后由平台导航 --- */
158
- char *pending_html;
159
- char *pending_url;
160
-
161
- /* --- 菜单线路文本(cj_bridge_set_menu 存入;平台建窗时读它,运行期改则由平台重建)--- */
162
- char *pending_menu_text;
163
-
164
- /* --- 拖放策略(cj_bridge_set_drag_drop 存入:enabled/files 两个开关)。
165
- enabled=0 平台不注册 drop target(默认关——现有应用零改动);
166
- 平台建 view 时读它决定要不要挂 drag dest,运行期再改由平台重挂 --- */
167
- int pending_drop_enabled;
168
- int pending_drop_files;
169
-
170
- /* --- 全局快捷键线路(cj_bridge_set_global_hotkeys 存入,空串 = 一个都没注册)。
171
- 格式一行一条:`<accel>:<id>`(accel 如 Ctrl+Alt+C,id 是应用给的标识)。
172
- 平台负责解析 accel 并 grab;触发时经 cj_core_hotkey_pressed 交回 core。
173
- 运行期再调 = 整表替换(先 ungrab 再按新表 grab) --- */
174
- char *pending_hotkeys;
175
-
176
- /* --- 托盘线路(cj_bridge_set_tray 存入,空串 = 移除托盘)。
177
- 格式(与 src/tray.cj 的 TrayModel.toWire() 逐字对应):
178
- 第 1 行 `icon\t<路径>`(空路径 = 主题默认图标)
179
- 第 2 行 `tooltip\t<文本>`(可空)
180
- 其余行 = 菜单项,与菜单线路同格式(kind\tid\tlabel\tflags\taccel,无前导制表符 = 平铺一层)
181
- 平台建宿主线程时读它装配;运行期再调 = 整体重建。菜单项被点经
182
- cj_core_tray_clicked 交回 core({"kind":"tray","id":…},与菜单点击同走 on_shell) --- */
183
- char *pending_tray;
184
-
185
- /* --- 能力位(cj_plat_init 时由 cj_plat_host_capabilities 填,此后只读)--- */
186
- int capabilities;
187
-
188
- /* --- 生命周期 --- */
189
- volatile int ready; /* 宿主 UI 就绪(平台在窗口 + WebView 建好后置 1)*/
190
- volatile int should_quit; /* 事件循环已退出 / 要求退出(平台在启动失败等路径可直接置 1)*/
191
- int window_gone; /* 窗口已销毁、原生资源已释放;句柄壳仍可安全调用 destroy */
192
- int destroy_notified; /* onDestroy 是否已触发(quit 与关窗只通知一次)*/
193
-
194
- /* --- 平台私有(GTK 窗口/view;HWND + COM 接口 + 宿主线程 id)--- */
195
- void *plat;
196
- } cj_host;
197
-
198
- /* =========================================================================
199
- * 平台原语清单(每个平台实现一份,共 24 个)
200
- * ========================================================================= */
201
-
202
- /* ---- 同步(Linux: pthread_mutex + pthread_cond / Windows: CS + CONDITION_VARIABLE)---- */
203
- cj_sync *cj_plat_sync_create(void); /* 非递归锁 + 配套条件变量 */
204
- void cj_plat_sync_destroy(cj_sync *s);
205
- void cj_plat_sync_lock(cj_sync *s);
206
- void cj_plat_sync_unlock(cj_sync *s);
207
- /* 谓词等待:内部 while (!*ready) 等待(允许虚假唤醒);返回时 *ready 已为真 */
208
- void cj_plat_sync_wait(cj_sync *s, int *ready);
209
- void cj_plat_sync_signal(cj_sync *s);
210
-
211
- /* ---- 宿主 ---- */
212
- void cj_plat_init(cj_host *h); /* 一次性平台初始化(Windows: COM vtbl;Linux: 无)*/
213
- /* 释放宿主级平台资源(模块句柄、平台状态),幂等。
214
- 注意:**不**释放 COM 对象——WebView2 的 controller / environment 按「活到进程退出」口径处理,
215
- 平台文件只把指针置空(见 bridge_win.c 的 host_thread_main 收尾段),两平台的 fini 都不碰 COM。
216
- 等待必须有界:宿主线程收尾要进入仓颉运行时(onDestroy 回调),阻塞式 join 会与运行时「停处理器」
217
- 握手互等(Linux 侧实测 6 轮挂 1 轮,见 AGENTS.md §4),所以这里只做有界等待,不做无限 join。
218
- 返回 1=状态已释放、句柄可以回收;0=宿主线程可能还在收尾,调用方须保留句柄与状态,不要 free。 */
219
- int cj_plat_fini(cj_host *h);
220
- void cj_plat_start(cj_host *h); /* 起宿主线程:建窗口 + WebView + 跑事件循环,非阻塞返回 */
221
- void cj_plat_quit(cj_host *h); /* 请求退出:走与「用户关窗」同一条销毁路径 */
222
- void cj_plat_wake_ui(cj_host *h);/* 让 UI 线程尽快跑一次 cj_core_flush_js */
223
- void cj_plat_post_js(cj_host *h, const char *script_utf8); /* UI 线程执行一段 JS(含失败日志)*/
224
- int cj_plat_is_ui_thread(cj_host *h); /* 1 = 调用方已站在 UI 线程上 */
225
- void cj_plat_post_dialog(cj_host *h, dlg_req *req); /* 投到 UI 线程 → cj_core_dialog_tick */
226
-
227
- /* 通用「投到 UI 线程执行」原语(剪贴板等后续宿主能力共用;对话框那套是它之前的专用通道):
228
- 在 UI 线程上回调 fn(ud)(一次),投不出去返回 0(调用方自行作废等待)。
229
- 平台实现:Linux = g_idle_add;Windows = PostMessageW 新消息,UI 线程取 lParam 调 fn。 */
230
- typedef void (*cj_ui_fn)(void *ud);
231
- int cj_plat_post_ui(cj_host *h, cj_ui_fn fn, void *ud);
232
- int cj_plat_run_dialog(cj_host *h, dlg_req *req); /* UI 线程直接弹原生对话框,写 req */
233
- void cj_plat_open_devtools(cj_host *h);
234
- void cj_plat_load_url(cj_host *h, const char *url);
235
- void cj_plat_reload(cj_host *h);
236
-
237
- /* ---- 菜单 / 能力位(RFC-002;7.A 只做菜单栏,托盘 / 拖放在 7.B / 7.C)----
238
- wire 是 `MenuModel.toWire()` 的线路文本(格式见 cj_core_menu_walk 的注释),空串 = 清空菜单栏。
239
- 调用时机两头都要支持:**未就绪**时只记下(平台建窗时读 h->pending_menu_text);**已就绪**时由平台
240
- 投到 UI 线程重建菜单。cj_plat_menu_item_state 只改运行期状态(可用 / 勾选),要快、可能频繁。 */
241
- void cj_plat_set_menu(cj_host *h, const char *wire);
242
-
243
- /* 平台原语:把副标题写到窗口顶部那一行(UI 线程调用)。返回 0 = 已提交,
244
- -3 = 宿主未就绪,-4 = 本平台没这块(Windows 的系统菜单没有自绘区)。 */
245
- int cj_plat_set_titlebar_subtitle(cj_host *h, const char *text);
246
- void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
247
- void cj_plat_set_drag_drop(cj_host *h, int enabled, int files);
248
-
249
- /* 全局快捷键(RFC-003 第 5 期首个落地项):wire 一行一条 `<accel>:<id>`(见 pending_hotkeys 注释)。
250
- 调用时机两头都要支持:未就绪时只记下(平台建宿主线程后读 h->pending_hotkeys),
251
- 已就绪时整表替换(先 ungrab 旧表再 grab 新表)。平台不支持(Wayland / Windows 桩)时
252
- 打一行 stderr 如实说明,不算错误。 */
253
- void cj_plat_set_global_hotkeys(cj_host *h, const char *wire);
254
-
255
- /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释,空串 = 移除托盘。
256
- 未就绪时只记下(宿主线程装配时读 h->pending_tray);已就绪时投 UI 线程重建。
257
- 平台不支持 / 依赖没编进来时打一行 stderr 如实说明(可选编译,RFC-002 §6.3)。 */
258
- void cj_plat_set_tray(cj_host *h, const char *wire);
259
-
260
- /* 窗口可见性(RFC-003 第 5 期余项:最小化到托盘的还原路径)。
261
- 未就绪时平台**什么都不做**——core 已把值写进 h->win_visible,平台建窗时读它定稿;
262
- 已就绪时投 UI 线程执行(Linux = gtk_widget_show/hide + gtk_window_present,Windows = ShowWindow)。 */
263
- void cj_plat_set_window_visible(cj_host *h, int visible);
264
-
265
- /* 最小化到托盘策略(须在建窗前调):平台把窗口过程 / 窗口状态信号里的「最小化」改写成「隐藏」。
266
- 平台在**真要最小化那一刻**复核托盘是否真的装上了(能力位 / 应用有没有 setTray):
267
- 没装就降级为普通最小化并打一行 stderr——窗口藏起来却没有还原入口是比「不支持」更糟的结果。 */
268
- void cj_plat_set_minimize_to_tray(cj_host *h, int enabled);
269
-
270
- /* 宿主能力位(CJ_CAP_* 的按位或):纯静态事实,cj_plat_init 里由平台给出。 */
271
- int cj_plat_host_capabilities(cj_host *h);
272
-
273
- /* ---- 串口(plugin_serial)----
274
- 与窗口 / WebView 无关,所以平台原语**不带 cj_host**:串口是设备 I/O,不是宿主能力位,
275
- 也不该为了拿一个句柄强迫应用先建窗口(无头侧跑串口服务是合法用法)。
276
-
277
- 分工:**句柄表与真正的 termios / Win32 调用在平台侧**——平台本来就有 pthread / CS 这类同步设施,
278
- 静态初始化即可,core 不必再发明一次「一次性初始化」;core 只做**路径白名单**与参数校验
279
- (安全边界 + 平台无关,因而能被桩平台自检覆盖)。
280
-
281
- 返回码口径(core 与平台一致,上层因此不必写平台分支):
282
- >0 = 成功(句柄,仅本进程有意义;页面/前端拿不到它对应的 fd)
283
- -1 = 参数非法(含路径不在白名单内)
284
- -2 = 句柄表满
285
- -3 = 打开 / 配置失败(明细写进 err)
286
- -4 = 句柄无效 / 该平台未实现
287
- read / write:>=0 实际字节数(0 = 超时无数据),<0 = 失败。 */
288
- typedef struct cj_serial_cfg {
289
- int baud; /* 数值波特率(平台自己查 speed_t / DCB 表);不认识的值由平台拒绝 */
290
- int data_bits; /* 5..8 */
291
- int parity; /* 0=无 1=奇 2=偶 */
292
- int stop_bits; /* 1 或 2 */
293
- } cj_serial_cfg;
294
-
295
- long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len);
296
- int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms, char *err, int err_len);
297
- int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
298
- char *err, int err_len);
299
- int cj_plat_serial_close(long long h, char *err, int err_len);
300
-
301
- /* 枚举本机串口:一行一个名字写进 out(\n 分隔),返回条数;-1 参数非法,-4 未实现。
302
- Linux 侧尚未实现(-4)——serial:list 在 Linux 仍走仓颉侧的 /dev 目录扫描。 */
303
- int cj_plat_serial_list(char *out, int out_len);
304
-
305
- /* 设备路径白名单(纯函数、平台无关):只放行常见串口设备节点 / 伪终端 / udev 稳定链接,
306
- 含 ".." 的一律拒绝。**这是安全边界**——路径是页面可控字符串,core 这里强制校验一次,
307
- 平台实现就不必各自重写策略(仓颉侧另有一份镜像用于「跨 FFI 之前」给出可读错误,C 这份是权威)。
308
- 非导出符号:桩平台自检直接调它。 */
309
- int cj_serial_path_allowed(const char *path);
310
-
311
- /* =========================================================================
312
- * core 提供给平台的状态迁移(平台的事件循环里调用)
313
- * ========================================================================= */
314
- void cj_core_set_ready(cj_host *h); /* 窗口 + WebView 就绪(平台在装配完成后调)*/
315
- void cj_core_flush_js(cj_host *h); /* UI 线程:取队列拼批回投(idle / WM_CJT_FLUSH 里调)*/
316
- void cj_core_dialog_tick(cj_host *h, dlg_req *req); /* UI 线程:弹对话框 → 回调 → 唤醒等待方 */
317
- void cj_core_dialog_abort(cj_host *h); /* 作废进行中的对话框并唤醒等待方(ok=0,幂等)*/
318
- int cj_core_clipboard_tick(cj_host *h, int is_write, char *text); /* UI 线程:执行剪贴板调用 → 唤醒等待方(返回 1=成功)*/
319
- void cj_core_clipboard_abort(cj_host *h); /* 作废进行中的剪贴板请求并唤醒等待方(幂等)*/
320
- void cj_core_window_destroyed(cj_host *h);/* 窗口销毁:置标志 + 通知 onDestroy(一次)+ 作废对话框 */
321
-
322
- /* ---- 菜单线路格式的共享解析(两平台翻译层共用,避免各写一遍前缀/分割逻辑)----
323
- 格式(RFC-002 §5.4,与仓颉侧 `MenuModel.toWire()` 逐字对应):
324
-
325
- <行首制表符 = 层级><kind>\t<id>\t<label>\t<flags>\t<accel>\n
326
-
327
- kind:`n` 普通 / `c` 勾选 / `s` 分隔线 / `m` 子菜单;
328
- flags:十进制,bit0 = 可用、bit1 = 勾选(`3` = 又可用又勾选)。
329
- 容错口径:字段缺失按空串 / 0 处理,kind 为空的行整行跳过——**不崩**优先于报错,
330
- 真正的结构错误在仓颉侧就该被丢弃(toWire 不出口含制表符/换行的字段)。
331
-
332
- 回调期间字段指针指向 core 内部的副本,**回调返回后失效**(需要留存请自行拷贝)。 */
333
- typedef void (*cj_menu_row_fn)(void *ctx, int depth, char kind, const char *id,
334
- const char *label, int flags, const char *accel);
335
- void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx);
336
-
337
- /* 平台把「菜单项被点了」交回 core:core 记日志 + 经 h->on_menu 回调仓颉侧(带宿主身份)。 */
338
- void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked);
339
-
340
- /* 全局快捷键触发(平台在 grab 的事件回调里调):id 是应用注册时给的标识。
341
- core 记日志 + 以 shell 报文 {"kind":"hotkey","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
342
- 仓颉侧按句柄路由——快捷键事件与菜单点击同走 on_shell 通道,不新增回调类型)。 */
343
- void cj_core_hotkey_pressed(cj_host *h, const char *id);
344
-
345
- /* 托盘菜单项被点(平台在 appindicator 的 activate 回调里调):id 是 TrayModel 里应用给的标识。
346
- core 记日志 + 以 shell 报文 {"kind":"tray","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
347
- 按句柄路由;前端事件名 tray:click 由仓颉侧 shellEventNameFor 映射——RFC-002 §5.5 既有约定)。 */
348
- void cj_core_tray_clicked(cj_host *h, const char *id);
349
-
350
- /* 拖放落地(平台在 drag-data-received 里调):uri_list 是 text/uri-list 原文
351
- (\r\n 或 \n 分隔,可含 file:// URI 与百分比编码)。
352
- core 负责解析:跳过注释行/空行、file:// 解码成路径(%XX 十六进制解码)、
353
- 非 file:// 的 URI 原样保留(v1 载荷只有文件,页面自己决定要不要)。
354
- 结果以 shell 报文 {"kind":"drop","paths":[…]} 经 on_shell 回投(首参宿主句柄,
355
- 仓颉侧按句柄路由)。paths 上限 32 个、单个路径上限 4096 字节(超限丢弃并记日志)。 */
356
- void cj_core_drop_files(cj_host *h, const char *uri_list);
357
-
358
- /* =========================================================================
359
- * 仓颉 → C 桥:导出(全部实现在 bridge_core.c,两平台同一份)
360
- * ========================================================================= */
361
-
362
- /* 建宿主:分配公共状态(默认标题尺寸 900x640、devtools 开)+ 建两把锁。失败返回 NULL。 */
363
- CJ_BRIDGE_API cj_host *cj_bridge_create(void);
364
-
365
- /* 释放宿主编:先 cj_plat_fini,再回收公共状态。对 NULL 安全。
366
- 契约:仓颉侧在 waitForExit 返回后调用**至多一次**,调用后句柄不可再用(字段置空)。 */
367
- CJ_BRIDGE_API void cj_bridge_destroy(cj_host *h);
368
-
369
- /* 注入回调(JS 消息到达 / 窗口销毁)+ 平台一次性初始化 */
370
- CJ_BRIDGE_API void cj_bridge_init(cj_host *h, cj_on_message_fn m, cj_on_destroy_fn d);
371
-
372
- /* 窗口配置(标题 / 尺寸):必须在 cj_bridge_start 之前调用 */
373
- CJ_BRIDGE_API void cj_bridge_set_window(cj_host *h, const char *title, int width, int height);
374
-
375
- /* 窗口图标(png / ico 路径):必须在 cj_bridge_start 之前调用;空路径 = 系统默认图标 */
376
- CJ_BRIDGE_API void cj_bridge_set_icon(cj_host *h, const char *path);
377
-
378
- /* 开发者工具开关:enabled=0 表示禁止打开(须在 cj_bridge_start 之前调用) */
379
- CJ_BRIDGE_API void cj_bridge_set_devtools(cj_host *h, int enabled);
380
-
381
- /* 注册一段预执行脚本(须在 cj_bridge_start 之前调用;空串忽略) */
382
- CJ_BRIDGE_API void cj_bridge_add_init_script(cj_host *h, const char *js);
383
-
384
- /* 起宿主:html 非空则记作内联页面(空串 = 页面由 cj_bridge_load_url 指定)。
385
- 平台在独立线程里建窗 + WebView + 跑事件循环,本函数立即返回。 */
386
- CJ_BRIDGE_API void cj_bridge_start(cj_host *h, const char *html);
387
-
388
- /* 原生 → JS:投递一段脚本(入队,UI 线程批量执行)。
389
- 前端 promise 的 resolve/reject 都走这条通道,静默失败会让前端永久 pending。 */
390
- CJ_BRIDGE_API void cj_bridge_run_js(cj_host *h, const char *js);
391
-
392
- /* 按 URL 加载页面(http(s):// 或 file://):
393
- * - cj_bridge_start 之前调用:只记下 URL,宿主就绪时加载(与 HTML 二选一,URL 优先);
394
- * - 启动之后调用:投递到 UI 线程重新导航(运行期切页面)。
395
- * 空 URL 忽略。 */
396
- CJ_BRIDGE_API void cj_bridge_load_url(cj_host *h, const char *url);
397
-
398
- /* 重新加载当前页面:投递到 UI 线程执行 */
399
- CJ_BRIDGE_API void cj_bridge_reload(cj_host *h);
400
-
401
- /* 运行时打开开发者工具:投递到 UI 线程执行 */
402
- CJ_BRIDGE_API void cj_bridge_open_devtools(cj_host *h);
403
-
404
- /* 请求退出:与「用户关窗」走同一条销毁路径(onDestroy 恰好触发一次) */
405
- CJ_BRIDGE_API void cj_bridge_quit(cj_host *h);
406
-
407
- /* 宿主 UI 是否就绪 / 事件循环是否已退出(供仓颉侧 run 阻塞轮询) */
408
- CJ_BRIDGE_API int cj_bridge_is_ready(cj_host *h);
409
- CJ_BRIDGE_API int cj_bridge_should_quit(cj_host *h);
410
-
411
- /* 原生对话框回调(选中路径回送仓颉侧) */
412
- CJ_BRIDGE_API void cj_bridge_set_dialog_callback(cj_host *h, cj_on_dialog_fn cb);
413
-
414
- /* 阻塞式原生对话框:返回 1 = 用户确认(文件类路径已回调送回),0 = 取消 / 宿主未就绪 /
415
- 已有对话框在进行中。调用方已站在 UI 线程时内部走直接弹的快路径(投递 + 等待会自锁)。 */
416
- CJ_BRIDGE_API int cj_bridge_show_dialog(cj_host *h, int kind, const char *title,
417
- const char *message, const char *filter);
418
-
419
- /* 菜单栏:wire 是 MenuModel.toWire() 的线路文本(空串 = 移除菜单栏)。
420
- 未 start 时只记下、建窗时生效;start 之后调用=运行期换菜单(平台投到 UI 线程重建)。 */
421
- CJ_BRIDGE_API void cj_bridge_set_menu(cj_host *h, const char *wire);
422
-
423
- /* 运行期改单个菜单项的状态(按 id 找;找不到只记日志不报错)。 */
424
- CJ_BRIDGE_API void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
425
-
426
- /* 拖放策略(RFC-002 §5):enabled=0 平台不注册 drop target(默认关)。
427
- 两头都能用:未就绪时记 pending(建 view 时读),就绪后平台投 UI 线程重挂 drag dest。 */
428
- CJ_BRIDGE_API void cj_bridge_set_drag_drop(cj_host *h, int enabled, int files);
429
-
430
- /* 窗口顶部副标题:把一行 UTF-8 文本画在原生菜单栏**同一行的右侧**(应用据此省掉
431
- 自己的页面顶栏,标题 / 保存状态 / 字数都能挂在原生那一行上)。允许运行期高频更新,
432
- 实现必须自己投递到 UI 线程(Linux 走 g_idle_add,同 set_menu)。
433
- 返回 0 = 已受理,-1 = 参数非法(含超过 1024 字节),-3 = 宿主未就绪,
434
- -4 = 本平台未实现(应用据 CJ_CAP_SUBTITLE 决定要不要自己画顶栏)。 */
435
- CJ_BRIDGE_API int cj_bridge_set_titlebar_subtitle(cj_host *h, const char *text);
436
-
437
- /* 全局快捷键:wire 一行一条 `<accel>:<id>`(空串 = 全部注销)。
438
- 两头都能用:未就绪时记 pending(宿主线程起来后读),就绪后整表替换(ungrab 旧表再 grab 新表)。 */
439
- CJ_BRIDGE_API void cj_bridge_set_global_hotkeys(cj_host *h, const char *wire);
440
-
441
- /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释(icon/tooltip 两行 + 菜单项),空串 = 移除。
442
- 两头都能用:未就绪时记 pending(宿主线程装配时读),就绪后平台投 UI 线程重建。 */
443
- CJ_BRIDGE_API void cj_bridge_set_tray(cj_host *h, const char *wire);
444
-
445
- /* 窗口可见性(对标 Tauri 的 window.show()/hide()):visible=1 显示并提到前台、0 隐藏。
446
- 两头都能用:未 start 时只记下(平台建窗时按 h->win_visible 定稿),start 之后投 UI 线程执行。 */
447
- CJ_BRIDGE_API void cj_bridge_set_window_visible(cj_host *h, int visible);
448
-
449
- /* 最小化到托盘策略(须在 cj_bridge_start 之前调):1 = 平台把「最小化」改写成「隐藏」。
450
- 平台在真要最小化那一刻复核托盘是否装上,没装则降级为普通最小化 + 一行 stderr。 */
451
- CJ_BRIDGE_API void cj_bridge_set_minimize_to_tray(cj_host *h, int enabled);
452
-
453
- /* shell 事件回调(菜单点击等;载荷见 cj_on_shell_fn 的注释,首参为宿主句柄)。 */
454
- CJ_BRIDGE_API void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb);
455
-
456
- /* 宿主能力位(CJ_CAP_* 的按位或):cj_plat_init 时填好,供仓颉侧 HostCapabilities /
457
- `system:host` 如实上报。未 init 返回 0。 */
458
- CJ_BRIDGE_API int cj_bridge_host_capabilities(cj_host *h);
459
-
460
- /* 句柄同一性判定(返回 1 = 同一个宿主,任一方为 NULL 返回 0)。
461
- 为什么要有它:shell 事件回调首参是**发生事件的宿主句柄**,仓颉侧得判断「是不是我这个窗口」,
462
- 但仓颉的 `CPointer<Unit>` 不支持 `==`(实测编译器直接报 invalid binary operator),
463
- 也没有可靠的指针→整数转换。句柄本来就归 core 所有,判定交回 C 侧最省事、也最不会猜错。
464
- 纯 core 函数:不需要任何平台原语。 */
465
- CJ_BRIDGE_API int cj_bridge_host_eq(cj_host *a, cj_host *b);
466
-
467
- /* =========================================================================
468
- * 串口(plugin_serial):设备 I/O,与宿主 / 窗口无关
469
- *
470
- * 这几个导出**不带 cj_host**:串口不是宿主能力,也不该强迫应用先建窗口才能用。
471
- * 配置在仓颉侧已解析成数值,C 侧只认数值(字符串解析归仓颉,便于单测)。
472
- * ========================================================================= */
473
-
474
- /* 打开并配置串口:成功返回 >0 句柄;失败返回负码(-1 参数/白名单、-2 句柄表满、
475
- -3 平台打不开)。path 必须先过 cj_serial_path_allowed——**这是安全边界**。
476
- 打开是非阻塞的(O_NOCTTY | O_NONBLOCK + 裸 termios),不会为了等载波信号钉住调用线程。 */
477
- CJ_BRIDGE_API long long cj_bridge_serial_open(const char *path, int baud, int data_bits,
478
- int parity, int stop_bits);
479
-
480
- /* 读:最多 len 字节,最多等 timeout_ms 毫秒(0 = 立即返回,负值按 0 处理)。
481
- 返回读到的字节数(0 = 超时无数据,不是错误);<0 失败。缓冲由调用方提供。 */
482
- CJ_BRIDGE_API int cj_bridge_serial_read(long long h, unsigned char *buf, int len, int timeout_ms);
483
-
484
- /* 写:在 timeout_ms 预算内尽量写完;返回实际写入字节数(可能小于 len),<0 失败。 */
485
- CJ_BRIDGE_API int cj_bridge_serial_write(long long h, const unsigned char *buf, int len,
486
- int timeout_ms);
487
-
488
- /* 关闭并释放句柄(幂等:重复关同一句柄返回 -4)。 */
489
- CJ_BRIDGE_API int cj_bridge_serial_close(long long h);
490
-
491
- /* 枚举本机串口(serial:list 的平台层):把「COM + 全数字」一行一个写进 out,
492
- 返回条数(0 = 没有串口,不是错误)。平台不实现时返回 -4(err 口径同其余原语)。
493
- out_len 是 out 的容量;放不下时截断到已写入的整行。 */
494
- CJ_BRIDGE_API int cj_bridge_serial_list(char *out, int out_len);
495
-
496
- /* 最近一次失败的明细(UTF-8,**线程局部**——命令各自跑在 worker 线程上,静态单槽会被互相覆盖)。
497
- 每次串口导出调用都会先清空它,失败时写入;返回的指针在该线程下次调用前有效。 */
498
- CJ_BRIDGE_API const char *cj_bridge_serial_last_error(void);
499
-
500
- /* =========================================================================
501
- * 剪贴板(plugin_clipboard):宿主能力——GTK 的 clipboard 依赖 gtk_init 后的环境,
502
- * 且 GTK 规定剪贴板调用必须在 GTK 线程上,所以**带 cj_host**(借它的 UI 线程分流),
503
- * 与对话框同一套「已在 UI 线程就直接做、否则投递 + 有界等待」的样板。
504
- * 返回码口径:0 = 成功,-1 = 参数非法,-3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
505
- * ========================================================================= */
506
-
507
- /* 写文本:text 须为合法 UTF-8,长度上限 8 MB(core 校验)。0 = 成功。 */
508
- CJ_BRIDGE_API int cj_bridge_clipboard_write_text(cj_host *h, const char *text);
509
-
510
- /* 读文本:返回 malloc 的 UTF-8 字符串(可能为 NULL = 剪贴板为空 / 不是文本)。
511
- 用 cj_bridge_clipboard_free 释放,**不许**在仓颉侧 free。 */
512
- CJ_BRIDGE_API char *cj_bridge_clipboard_read_text(cj_host *h);
513
-
514
- /* 释放 read_text 返回的缓冲(NULL 安全,幂等)。 */
515
- CJ_BRIDGE_API void cj_bridge_clipboard_free(char *s);
516
-
517
- /* 平台原语:真正的剪贴板读写。err 口径同其余原语(-4 未实现由平台自己报)。 */
518
- int cj_plat_clipboard_write_text(cj_host *h, const char *text, char *err, int err_len);
519
- char *cj_plat_clipboard_read_text(cj_host *h, char *err, int err_len);
520
-
521
- /* =========================================================================
522
- * 系统通知(plugin_notification):与剪贴板同一类宿主能力,但**发后即忘**——
523
- * 通知提交后显示与否不回传(freedesktop 协议本就不给同步回执),core 校验 + 投
524
- * UI 线程即返回,成败只在 stderr 留行。返回码:0 = 已提交,-1 = 参数非法,
525
- * -3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
526
- * ========================================================================= */
527
-
528
- /* 发一条通知:title 非空且 ≤256 字节,body ≤8 KiB(core 校验)。0 = 已提交。 */
529
- CJ_BRIDGE_API int cj_bridge_notification_notify(cj_host *h, const char *title, const char *body);
530
-
531
- /* 平台原语:真正发通知(UI 线程调用)。 */
532
- int cj_plat_notification_notify(cj_host *h, const char *title, const char *body,
533
- char *err, int err_len);
534
-
535
- /* =========================================================================
536
- * 日志落盘(RFC-003 第 5 期):把 stderr 接管到文件,给「没有控制台」的产物留凭证
537
- *
538
- * 为什么在 fd 层接管:单文件产物是 Windows GUI 子系统,双击时 stderr 无处可去;
539
- * 而诊断输出分布在 core、平台实现与仓颉侧上百处。解 `freopen` 一次,**所有** stderr
540
- * 写者自动落进同一个 FILE*(顺序天然一致),两端调用点一处都不用改。stdout 一律不碰
541
- * (`CJ_TAURI_MANIFEST` 的插件清单走 stdout)。
542
- * ========================================================================= */
543
-
544
- /* 开启日志落盘:path 非空且可写时返回 1,否则返回 0 并在**还在的** stderr 上说明原因。
545
- max_kb ≤ 0 表示不轮转(交给外部 logrotate);keep = 保留几份档案(log.1 … log.keep),
546
- ≤0 表示轮转时直接丢弃旧内容。**只能在第一条日志之前调**——已开启后重复调用返回 0
547
- (stderr 已经被接管,换不了目标;想换就重启应用换参数)。 */
548
- CJ_BRIDGE_API int cj_bridge_set_log_file(const char *path, int max_kb, int keep);
549
-
550
- /* 回读生效中的日志路径;未开启返回空串。供 system:host 如实上报。 */
551
- CJ_BRIDGE_API const char *cj_bridge_log_file(void);
552
-
553
- /* 追加一行调用方提供的日志(同样走轮转;用于 log 插件把前端报错并进同一份文件)。
554
- 返回 0 = 已写入(或未开启但已落 stderr),-1 = 参数非法。 */
555
- CJ_BRIDGE_API int cj_bridge_log_write(const char *line);
556
-
557
- /* core 内的诊断输出统一走它:**未开启落盘时逐字节等同 `fprintf(stderr, …)`**
558
- (现有 E2E needle 与 `[cj-bridge]` 前缀断言因此一条都不受影响);开启后才加
559
- `[YYYY-MM-DD HH:MM:SS] ` 前缀并按大小轮转。 */
560
- void cj_core_logf(const char *fmt, ...)
561
- #if defined(__GNUC__)
562
- __attribute__((format(printf, 1, 2)))
563
- #endif
564
- ;
565
- #define cj_logf(...) cj_core_logf(__VA_ARGS__)
566
-
567
- /* =========================================================================
568
- * 静默子进程:后台启动控制台程序,与宿主 / 窗口无关(不带 cj_host)
569
- *
570
- * 为什么必须下沉到 C 桥:cj-tauri 应用都是 GUI 子系统(Windows 的 `--subsystem=windows`),
571
- * 父进程**没有控制台**;此时拉起一个控制台子系统的子进程,Windows 会给它**单独配一个控制台
572
- * 窗口**——用户看到的就是一个黑窗口。仓颉的 `std.process.launch` 没有创建标志可传,
573
- * 所以这条只能在桥里做:Windows 走 `CreateProcessW` + `DETACHED_PROCESS`(连隐藏 console 都不建),
574
- * Linux 走 `fork` + `execv` + `setsid`(脱开终端,Ctrl-C 不连带打死)。
575
- *
576
- * 三个导出两平台同名同签名(AGENTS §2):上层因此没有平台分叉的调用点。
577
- * ========================================================================= */
578
-
579
- /* 静默启动:exe 是程序路径(UTF-8),args 是参数串(UTF-8,**以 0x1F 分隔**,空串 = 无参数)。
580
- 引号 / 空格由平台按各自规则转义,调用方**不要**自己加引号。
581
- 返回 pid(> 0);失败返回 0,原因由平台写进 stderr(core 会再补一行带 exe 的上下文)。 */
582
- CJ_BRIDGE_API long long cj_bridge_spawn_hidden(const char *exe, const char *args);
583
-
584
- /* 终止该 pid(幂等:进程已经不在了也返回 1)。返回 1 = 已结束,0 = 无权 / 失败。 */
585
- CJ_BRIDGE_API int cj_bridge_kill_pid(long long pid);
586
-
587
- /* 该 pid 是否还活着:1 = 活着,0 = 已退出或查不到(**含没权限查**——按「不在」上报,
588
- 比谎报活着好:调用方据它决定要不要重新拉起)。 */
589
- CJ_BRIDGE_API int cj_bridge_pid_alive(long long pid);
590
-
591
- /* 把产物内嵌的资源释放到 dir(单文件包用:scripts/pack-win-single.sh 把随包的程序与依赖 DLL
592
- 作为 RT_RCDATA 资源嵌进 exe,资源名就是文件名)。
593
- 返回值:> 0 = 已就绪的文件数(本次释放的 + 本来就在的),0 = **产物里没有内嵌资源**
594
- (普通构建、非单文件包都是这个),-1 = 参数为空或写盘失败(原因在 stderr)。
595
- 幂等:目标文件已存在且大小与资源一致就跳过——每次启动调它不会重写一份几十 MB 的资源。
596
- 目录由调用方给(桥不认识应用名):典型是 %LOCALAPPDATA%\应用名\runtime。 */
597
- CJ_BRIDGE_API int cj_bridge_extract_assets(const char *dir);
598
-
599
- /* 按**绝对路径**把一个动态库加载进本进程(单文件包专用:随包释放出来的 OpenSSL 这类
600
- 「运行期按名字 LoadLibrary」的组件,不预加载就永远找不到)。
601
- 为什么需要它(2026-10-21 实测,另一台机器):仓颉运行时的 TLS 加载器按**裸名**找
602
- `libssl-3-x64.dll` / `libcrypto-3-x64.dll`,SDK 与 stdx 都不带;开发机上它恰好命中 PATH 里的
603
- Git 自带那份,于是「本机全通、别人双击就挂」。症状极具误导性:http 照常 200(cjh 的
604
- /api/health 通),**只有 ws 握手失败**——`ws: connect failed: Can not load openssl library or
605
- function RAND_bytes.`(RFC6455 升级要算随机 nonce,明文 ws:// 也逃不掉)。
606
- 做法是进程启动早期先按绝对路径 LoadLibrary/dlopen 一次:之后同类裸名加载会命中已加载模块,
607
- 不必改系统 PATH、也不必把 DLL 摆到 exe 旁边(单文件承诺不破)。
608
- 返回 1 = 已加载(含本来就在的),0 = 失败(原因在 stderr)。 */
609
- CJ_BRIDGE_API int cj_bridge_preload_library(const char *path);
610
-
611
- /* 平台原语(err 口径同其余原语:写不进就留空,core 会打「平台没有给出原因」)。 */
612
- long long cj_plat_spawn_hidden(const char *exe, const char *args, char *err, int err_len);
613
- int cj_plat_kill_pid(long long pid);
614
- int cj_plat_pid_alive(long long pid);
615
- int cj_plat_extract_assets(const char *dir, char *err, int err_len);
616
- int cj_plat_preload_library(const char *path, char *err, int err_len);
617
-
618
- #endif /* CJ_BRIDGE_CORE_H */
1
+ /*
2
+ * cj-tauri C 桥公共核心(批次 1):句柄化的宿主状态 + 平台无关逻辑
3
+ *
4
+ * 分层(AGENTS.md §2:平台差异只允许出现在 @When 与 bridge_*.c):
5
+ * bridge_core.c/.h 平台无关:JS 回投队列与批处理、对话框单槽状态机、窗口配置存储、
6
+ * 预执行脚本注册表、生命周期标志、全部 cj_bridge_* 导出(单一来源)
7
+ * bridge_linux.c GTK / WebKitGTK 原语:建窗装配、gtk_main、原生对话框、idle 投递
8
+ * bridge_win.c Win32 / WebView2 / COM 原语:窗口类与消息循环、COM 回调、原生对话框
9
+ *
10
+ * 为什么句柄化:原先是进程级单例(g_window / g_hwnd),多窗口无从落地——
11
+ * 见 docs/架构演进-多平台与多窗口.md §2.1。这里把「哪个窗口」变成显式句柄,单窗口行为不变。
12
+ *
13
+ * 分工铁律:
14
+ * 1. core **不 include 任何平台头**(pthread / windows.h / gtk),需要平台能力一律走 cj_plat_*;
15
+ * 2. 平台文件**不实现任何 cj_bridge_***(导出全在 core,两平台导出名与签名因此逐字一致);
16
+ * 3. 平台文件只实现下面的「平台原语清单」。
17
+ *
18
+ * 句柄生命周期(docs/架构演进-多平台与多窗口.md §6.1):
19
+ * 窗口关闭 ≠ 释放句柄。窗口销毁时平台调 cj_core_window_destroyed(),只置标志 + 通知回调 +
20
+ * 释放原生/COM 资源;句柄内存由 cj_bridge_destroy() 在 C 侧回收(malloc 与 free 同侧)。
21
+ * 仓颉侧保证 destroy 至多调用一次并把字段置空(C 侧的 destroy 对 NULL 安全)。
22
+ */
23
+ #ifndef CJ_BRIDGE_CORE_H
24
+ #define CJ_BRIDGE_CORE_H
25
+
26
+ #include <stdio.h>
27
+ #include <stdlib.h>
28
+ #include <string.h>
29
+
30
+ /* 导出宏:导出定义全在 bridge_core.c,平台文件只实现 cj_plat_*(不重复定义导出)。
31
+ Windows 要 dllexport;Linux 默认可见,仍显式声明一遍。 */
32
+ #if defined(_WIN32)
33
+ #define CJ_BRIDGE_API __declspec(dllexport)
34
+ #else
35
+ #define CJ_BRIDGE_API __attribute__((visibility("default")))
36
+ #endif
37
+
38
+ /* ===== 仓颉侧回调(@C 函数)===== */
39
+ typedef void (*cj_on_message_fn)(const char *json); /* JS 消息到达(UTF-8 JSON)*/
40
+
41
+ /* 前向声明:下面三个回调签名都要用到宿主句柄,而 cj_host 结构体还在后面才定义。
42
+ 必须是**文件作用域**的前向声明——只在参数列表里写 struct cj_host,clang 会把它当成
43
+ 「仅本原型内可见」的另一个类型(-Wvisibility),赋函数指针时就报类型不兼容。 */
44
+ struct cj_host;
45
+
46
+ /* **为什么这些回调的首参是宿主句柄**:仓颉的 @C 函数是模块级函数,每个宿主注册的是同一个
47
+ 函数指针,不把 h 传回去就分不清是哪个窗口触发的——多窗口下就是「后装配的窗口覆盖先装配的」
48
+ 那个静态单槽坑(docs/架构演进-多平台与多窗口.md §7.5)的根治办法。**新增回调一律照此带宿主身份。**
49
+
50
+ cj_on_message_fn 是唯一的例外,且是**有意**的:报文自带发起窗口的 window label
51
+ (架构文档 §7.3),仓颉侧给所有窗口装的是同一只派发器,不需要按句柄分表
52
+ (见 src/app.cj 的 wireHostCallbacks)。其余回调没有这种自带身份的载荷,只能靠句柄。 */
53
+ typedef void (*cj_on_destroy_fn)(struct cj_host *host); /* 窗口销毁 / 退出(每个宿主恰好一次)*/
54
+ typedef void (*cj_on_dialog_fn)(struct cj_host *host, const char *path); /* 原生对话框选中的路径(取消 = 空串)*/
55
+
56
+ /* 宿主级 shell 事件回调:**菜单 / 托盘 / 拖放共用一个入口**(RFC-002 §5.5),
57
+ 第一个参数回传宿主句柄,第二个是 JSON 载荷(与 on_message 同一载体,仓颉侧已有 JSON 解析)。
58
+
59
+ {"kind":"menu","id":"file.save","enabled":true,"checked":false}
60
+ {"kind":"tray","id":"toggle"} ← 7.C
61
+ {"kind":"drop","paths":["C:\\a.png"]} ← 7.C
62
+
63
+ **签名这里必须写 `struct cj_host *`(不能用 `cj_host *`)**:本 typedef 要出现在 cj_host
64
+ 结构体定义之前(结构体里有 on_shell 字段),此时那个 typedef 名字还不存在,只有上面刚做的
65
+ 结构体前向声明可用。同一类型,Cangjie 侧看不出来——它只拿到一个不透明指针。 */
66
+ typedef void (*cj_on_shell_fn)(struct cj_host *host, const char *json);
67
+
68
+ /* 前端 __CJ_TAURI__.reload() 的控制消息:宿主级操作,不进 IPC hub
69
+ (与 native/bridge_js.h 里的字面量必须一致) */
70
+ #define CJT_RELOAD_MSG "__cj_tauri_reload__"
71
+
72
+ /* 单批上限:一次 UI 空闲窗口最多合并多少条回投。写成常量而不是「取空为止」,
73
+ 是为了长队列也肯让出 UI 线程(剩下的由自续唤醒接手)。 */
74
+ #define CJ_JS_BATCH_MAX 64
75
+
76
+ /* 宿主能力位(cj_bridge_host_capabilities 的返回值,RFC-002 §5.4):
77
+ 纯静态事实,由平台在 cj_plat_init 时给出(例:Linux 侧托盘没编进来 → 不置 CJ_CAP_TRAY),
78
+ 仓颉侧据此构造 HostCapabilities,`system:host` 如实上报,不许猜。 */
79
+ #define CJ_CAP_MENU 1
80
+ #define CJ_CAP_TRAY 2
81
+ #define CJ_CAP_DRAG_DROP 4
82
+ #define CJ_CAP_NOTIFICATION 8 /* 平台通知可用(Linux = freedesktop Notifications)*/
83
+ #define CJ_CAP_HOTKEY 16 /* 全局快捷键可用(Linux = X11 grab;Wayland 不置位)*/
84
+ #define CJ_CAP_SUBTITLE 32 /* 窗口顶部那一行能显示副标题(Linux = 与菜单栏同排的 label;
85
+ Windows 的系统菜单没有自绘区,未实现、不置位)*/
86
+
87
+ /* ===== 回投队列节点(core 拥有;UTF-8 存储,宽字符转换由平台在 cj_plat_post_js 里做)===== */
88
+ typedef struct js_node {
89
+ char *js;
90
+ struct js_node *next;
91
+ } js_node;
92
+
93
+ /* ===== 原生对话框请求(core 分配 / 释放;平台只填 ok 与 path)===== */
94
+ typedef struct dlg_req {
95
+ int kind; /* 0 打开文件 1 保存文件 2 info 3 warning 4 error 5 confirm 6 选择目录 */
96
+ char *title;
97
+ char *message;
98
+ char *filter; /* "描述|模式|描述|模式";空串 = 不过滤 */
99
+ char *path; /* 文件类结果:选中路径(取消时为空)*/
100
+ int ok; /* 1 = 用户点了确认 */
101
+ } dlg_req;
102
+
103
+ /* ===== 同步原语句柄(平台私有结构:Linux = mutex + cond;Windows = CRITICAL_SECTION +
104
+ CONDITION_VARIABLE)。core 只用它们,绝不直接碰 pthread / Win32。 ===== */
105
+ typedef struct cj_sync cj_sync;
106
+
107
+ /* ===== 宿主句柄:core 拥有的状态 + 一个平台私有指针 ===== */
108
+ typedef struct cj_host {
109
+ /* --- 回调(cj_bridge_init 注入)--- */
110
+ cj_on_message_fn on_message;
111
+ cj_on_destroy_fn on_destroy;
112
+ cj_on_dialog_fn on_dialog;
113
+ cj_on_shell_fn on_shell; /* cj_bridge_set_shell_callback 注入;未注册 = 事件只留日志 */
114
+
115
+ /* --- JS 回投队列(FIFO:每条 JS 独立执行,防止覆盖)--- */
116
+ cj_sync *js_lock;
117
+ js_node *js_head;
118
+ js_node *js_tail;
119
+
120
+ /* --- 对话框单槽:对话框是模态的,同时只允许一个(嵌套调用直接拒绝而不是自锁)--- */
121
+ cj_sync *dlg_lock;
122
+ int dlg_busy; /* 有人正在进行中 */
123
+ int dlg_ready; /* 结果就绪(与 dlg_lock 配套的谓词)*/
124
+ dlg_req *dlg_cur; /* 进行中的请求;窗口先关时由 cj_core_window_destroyed 作废 */
125
+
126
+ /* --- 剪贴板单槽(同一时刻只允许一次读写;窗口销毁时作废等待方)---
127
+ 读结果经 cb_text 交回(core malloc,调用方 cj_bridge_clipboard_free 释放),
128
+ cb_ok=0 表示失败/作废(cb_text 为 NULL)。 */
129
+ cj_sync *clip_lock;
130
+ int clip_ready;
131
+ int clip_ok;
132
+ char *clip_text; /* 读操作的结果(UI 线程 malloc,等待方负责释放)*/
133
+ int clip_busy;
134
+
135
+ /* --- 窗口配置:由仓颉侧在 cj_bridge_start 之前经 cj_bridge_set_window 注入 --- */
136
+ char *win_title;
137
+ int win_w;
138
+ int win_h;
139
+ char *win_icon; /* NULL = 系统默认图标 */
140
+ int devtools; /* 开发者工具开关 */
141
+
142
+ /* 窗口初始可见性(cj_bridge_set_window_visible 存入):1 = 显示(默认,老应用行为不变)、
143
+ 0 = 窗口建好即隐藏。**只解决「建窗那一刻该不该显示」**——运行期改可见性由平台直接投
144
+ UI 线程执行,不需要它。 */
145
+ int win_visible;
146
+
147
+ /* 最小化到托盘(cj_bridge_set_minimize_to_tray 存入):1 = 平台把「最小化」改写成「隐藏」。
148
+ 真到最小化那一刻平台还会复核托盘是否真装上了(没有就如实降级为普通最小化 + 一行 stderr)。 */
149
+ int minimize_to_tray;
150
+
151
+ /* 附庸窗属主(cj_bridge_set_owner_window 存入):非 NULL = 本窗挂在 owner 下
152
+ (不重复占任务栏、跟随属主显隐)。平台建窗后读它定稿;owner 为 NULL = 解除隶属。 */
153
+ struct cj_host *owner;
154
+
155
+ /* --- 预执行脚本(cj_bridge_add_init_script 追加,WebView 创建时逐条注入到
156
+ document-start;插件的 jsShim 走这条通道,注册顺序排在 BRIDGE_JS 之后)--- */
157
+ char **init_scripts;
158
+ int init_script_count;
159
+ int init_script_cap;
160
+
161
+ /* --- 页面来源二选一(URL 优先):start 之前注入,start 之后由平台导航 --- */
162
+ char *pending_html;
163
+ char *pending_url;
164
+
165
+ /* --- 菜单线路文本(cj_bridge_set_menu 存入;平台建窗时读它,运行期改则由平台重建)--- */
166
+ char *pending_menu_text;
167
+
168
+ /* --- 拖放策略(cj_bridge_set_drag_drop 存入:enabled/files 两个开关)。
169
+ enabled=0 平台不注册 drop target(默认关——现有应用零改动);
170
+ 平台建 view 时读它决定要不要挂 drag dest,运行期再改由平台重挂 --- */
171
+ int pending_drop_enabled;
172
+ int pending_drop_files;
173
+
174
+ /* --- 全局快捷键线路(cj_bridge_set_global_hotkeys 存入,空串 = 一个都没注册)。
175
+ 格式一行一条:`<accel>:<id>`(accel 如 Ctrl+Alt+C,id 是应用给的标识)。
176
+ 平台负责解析 accel 并 grab;触发时经 cj_core_hotkey_pressed 交回 core。
177
+ 运行期再调 = 整表替换(先 ungrab 再按新表 grab) --- */
178
+ char *pending_hotkeys;
179
+
180
+ /* --- 托盘线路(cj_bridge_set_tray 存入,空串 = 移除托盘)。
181
+ 格式(与 src/tray.cj 的 TrayModel.toWire() 逐字对应):
182
+ 第 1 行 `icon\t<路径>`(空路径 = 主题默认图标)
183
+ 第 2 行 `tooltip\t<文本>`(可空)
184
+ 其余行 = 菜单项,与菜单线路同格式(kind\tid\tlabel\tflags\taccel,无前导制表符 = 平铺一层)
185
+ 平台建宿主线程时读它装配;运行期再调 = 整体重建。菜单项被点经
186
+ cj_core_tray_clicked 交回 core({"kind":"tray","id":…},与菜单点击同走 on_shell) --- */
187
+ char *pending_tray;
188
+
189
+ /* --- 能力位(cj_plat_init 时由 cj_plat_host_capabilities 填,此后只读)--- */
190
+ int capabilities;
191
+
192
+ /* --- 生命周期 --- */
193
+ volatile int ready; /* 宿主 UI 就绪(平台在窗口 + WebView 建好后置 1)*/
194
+ volatile int should_quit; /* 事件循环已退出 / 要求退出(平台在启动失败等路径可直接置 1)*/
195
+ int window_gone; /* 窗口已销毁、原生资源已释放;句柄壳仍可安全调用 destroy */
196
+ int destroy_notified; /* onDestroy 是否已触发(quit 与关窗只通知一次)*/
197
+
198
+ /* --- 平台私有(GTK 窗口/view;HWND + COM 接口 + 宿主线程 id)--- */
199
+ void *plat;
200
+ } cj_host;
201
+
202
+ /* =========================================================================
203
+ * 平台原语清单(每个平台实现一份,共 24 个)
204
+ * ========================================================================= */
205
+
206
+ /* ---- 同步(Linux: pthread_mutex + pthread_cond / Windows: CS + CONDITION_VARIABLE)---- */
207
+ cj_sync *cj_plat_sync_create(void); /* 非递归锁 + 配套条件变量 */
208
+ void cj_plat_sync_destroy(cj_sync *s);
209
+ void cj_plat_sync_lock(cj_sync *s);
210
+ void cj_plat_sync_unlock(cj_sync *s);
211
+ /* 谓词等待:内部 while (!*ready) 等待(允许虚假唤醒);返回时 *ready 已为真 */
212
+ void cj_plat_sync_wait(cj_sync *s, int *ready);
213
+ void cj_plat_sync_signal(cj_sync *s);
214
+
215
+ /* ---- 宿主 ---- */
216
+ void cj_plat_init(cj_host *h); /* 一次性平台初始化(Windows: COM vtbl;Linux: 无)*/
217
+ /* 释放宿主级平台资源(模块句柄、平台状态),幂等。
218
+ 注意:**不**释放 COM 对象——WebView2 的 controller / environment 按「活到进程退出」口径处理,
219
+ 平台文件只把指针置空(见 bridge_win.c 的 host_thread_main 收尾段),两平台的 fini 都不碰 COM。
220
+ 等待必须有界:宿主线程收尾要进入仓颉运行时(onDestroy 回调),阻塞式 join 会与运行时「停处理器」
221
+ 握手互等(Linux 侧实测 6 轮挂 1 轮,见 AGENTS.md §4),所以这里只做有界等待,不做无限 join。
222
+ 返回 1=状态已释放、句柄可以回收;0=宿主线程可能还在收尾,调用方须保留句柄与状态,不要 free。 */
223
+ int cj_plat_fini(cj_host *h);
224
+ void cj_plat_start(cj_host *h); /* 起宿主线程:建窗口 + WebView + 跑事件循环,非阻塞返回 */
225
+ void cj_plat_quit(cj_host *h); /* 请求退出:走与「用户关窗」同一条销毁路径 */
226
+ void cj_plat_wake_ui(cj_host *h);/* 让 UI 线程尽快跑一次 cj_core_flush_js */
227
+ void cj_plat_post_js(cj_host *h, const char *script_utf8); /* UI 线程执行一段 JS(含失败日志)*/
228
+ int cj_plat_is_ui_thread(cj_host *h); /* 1 = 调用方已站在 UI 线程上 */
229
+ void cj_plat_post_dialog(cj_host *h, dlg_req *req); /* 投到 UI 线程 → cj_core_dialog_tick */
230
+
231
+ /* 通用「投到 UI 线程执行」原语(剪贴板等后续宿主能力共用;对话框那套是它之前的专用通道):
232
+ 在 UI 线程上回调 fn(ud)(一次),投不出去返回 0(调用方自行作废等待)。
233
+ 平台实现:Linux = g_idle_add;Windows = PostMessageW 新消息,UI 线程取 lParam 调 fn。 */
234
+ typedef void (*cj_ui_fn)(void *ud);
235
+ int cj_plat_post_ui(cj_host *h, cj_ui_fn fn, void *ud);
236
+ int cj_plat_run_dialog(cj_host *h, dlg_req *req); /* UI 线程直接弹原生对话框,写 req */
237
+ void cj_plat_open_devtools(cj_host *h);
238
+ void cj_plat_load_url(cj_host *h, const char *url);
239
+ void cj_plat_reload(cj_host *h);
240
+
241
+ /* ---- 菜单 / 能力位(RFC-002;7.A 只做菜单栏,托盘 / 拖放在 7.B / 7.C)----
242
+ wire 是 `MenuModel.toWire()` 的线路文本(格式见 cj_core_menu_walk 的注释),空串 = 清空菜单栏。
243
+ 调用时机两头都要支持:**未就绪**时只记下(平台建窗时读 h->pending_menu_text);**已就绪**时由平台
244
+ 投到 UI 线程重建菜单。cj_plat_menu_item_state 只改运行期状态(可用 / 勾选),要快、可能频繁。 */
245
+ void cj_plat_set_menu(cj_host *h, const char *wire);
246
+
247
+ /* 平台原语:把副标题写到窗口顶部那一行(UI 线程调用)。返回 0 = 已提交,
248
+ -3 = 宿主未就绪,-4 = 本平台没这块(Windows 的系统菜单没有自绘区)。 */
249
+ int cj_plat_set_titlebar_subtitle(cj_host *h, const char *text);
250
+ void cj_plat_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
251
+ void cj_plat_set_drag_drop(cj_host *h, int enabled, int files);
252
+
253
+ /* 全局快捷键(RFC-003 第 5 期首个落地项):wire 一行一条 `<accel>:<id>`(见 pending_hotkeys 注释)。
254
+ 调用时机两头都要支持:未就绪时只记下(平台建宿主线程后读 h->pending_hotkeys),
255
+ 已就绪时整表替换(先 ungrab 旧表再 grab 新表)。平台不支持(Wayland / Windows 桩)时
256
+ 打一行 stderr 如实说明,不算错误。 */
257
+ void cj_plat_set_global_hotkeys(cj_host *h, const char *wire);
258
+
259
+ /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释,空串 = 移除托盘。
260
+ 未就绪时只记下(宿主线程装配时读 h->pending_tray);已就绪时投 UI 线程重建。
261
+ 平台不支持 / 依赖没编进来时打一行 stderr 如实说明(可选编译,RFC-002 §6.3)。 */
262
+ void cj_plat_set_tray(cj_host *h, const char *wire);
263
+
264
+ /* 窗口可见性(RFC-003 第 5 期余项:最小化到托盘的还原路径)。
265
+ 未就绪时平台**什么都不做**——core 已把值写进 h->win_visible,平台建窗时读它定稿;
266
+ 已就绪时投 UI 线程执行(Linux = gtk_widget_show/hide + gtk_window_present,Windows = ShowWindow)。 */
267
+ void cj_plat_set_window_visible(cj_host *h, int visible);
268
+
269
+ /* 最小化到托盘策略(须在建窗前调):平台把窗口过程 / 窗口状态信号里的「最小化」改写成「隐藏」。
270
+ 平台在**真要最小化那一刻**复核托盘是否真的装上了(能力位 / 应用有没有 setTray):
271
+ 没装就降级为普通最小化并打一行 stderr——窗口藏起来却没有还原入口是比「不支持」更糟的结果。 */
272
+ void cj_plat_set_minimize_to_tray(cj_host *h, int enabled);
273
+
274
+ /* 附庸窗属主(cj_bridge_set_owner_window 存入 h->owner):平台把 h 挂到 owner 下。
275
+ 未就绪时**什么都不做**——平台建窗后读 h->owner 定稿;就绪后投 UI 线程执行
276
+ (Windows = GWLP_HWNDPARENT + WS_EX_TOOLWINDOW,Linux = gtk_window_set_transient_for +
277
+ skip_taskbar_hint)。owner 为 NULL 表示解除隶属。 */
278
+ void cj_plat_set_owner_window(cj_host *h, cj_host *owner);
279
+
280
+ /* ---- 同窗内嵌视图(PLT-006 真标签页)----
281
+ 在**已存在的宿主窗口**里再嵌一块独立的 WebView,占据客户区的一块矩形,同一时刻只显示一个。
282
+ 成功返回视图句柄(>0),0 = 建不出来(宿主未就绪 / 本平台没这块 / 环境复用失败)。
283
+
284
+ 为什么要有它:一个 cj_host 就是一扇系统窗口,而浏览器 tab 的语义是「一扇窗里换内容」。
285
+ 用第二个宿主表达必然多一扇窗——这正是附庸窗形态的由来,也是它看起来像「又开了一个程序」的原因。
286
+ 视图由平台持有与回收:**宿主销毁时平台必须先收掉自己名下的全部视图**(core 不管生命周期)。
287
+
288
+ 线程口径:create / destroy 由命令 worker 线程调用,平台自己投宿主 UI 线程(Windows 必须如此:
289
+ 在同步调用栈上 CreateCoreWebView2Controller 会死锁,见 Tauri 记录的 WebView2 issue);
290
+ set_visible / set_bounds 允许高频,平台必须保证线程安全(Windows 走 PostMessageW,
291
+ Linux 走 g_idle_add)。 */
292
+ long long cj_plat_view_create(cj_host *h, const char *url, int x, int y, int w, int hgt);
293
+ /* 其余四个都带 h:视图没有自己的消息循环,平台要靠宿主句柄才能把动作投回宿主 UI 线程
294
+ (Windows = PostMessageW(hwnd),Linux = g_idle_add)。句柄本身只在一个宿主内有效。 */
295
+ void cj_plat_view_set_visible(cj_host *h, long long view, int visible);
296
+ void cj_plat_view_set_bounds(cj_host *h, long long view, int x, int y, int w, int hgt);
297
+ void cj_plat_view_navigate(cj_host *h, long long view, const char *url);
298
+ void cj_plat_view_destroy(cj_host *h, long long view);
299
+
300
+ /* 宿主能力位(CJ_CAP_* 的按位或):纯静态事实,cj_plat_init 里由平台给出。 */
301
+ int cj_plat_host_capabilities(cj_host *h);
302
+
303
+ /* ---- 串口(plugin_serial)----
304
+ 与窗口 / WebView 无关,所以平台原语**不带 cj_host**:串口是设备 I/O,不是宿主能力位,
305
+ 也不该为了拿一个句柄强迫应用先建窗口(无头侧跑串口服务是合法用法)。
306
+
307
+ 分工:**句柄表与真正的 termios / Win32 调用在平台侧**——平台本来就有 pthread / CS 这类同步设施,
308
+ 静态初始化即可,core 不必再发明一次「一次性初始化」;core 只做**路径白名单**与参数校验
309
+ (安全边界 + 平台无关,因而能被桩平台自检覆盖)。
310
+
311
+ 返回码口径(core 与平台一致,上层因此不必写平台分支):
312
+ >0 = 成功(句柄,仅本进程有意义;页面/前端拿不到它对应的 fd)
313
+ -1 = 参数非法(含路径不在白名单内)
314
+ -2 = 句柄表满
315
+ -3 = 打开 / 配置失败(明细写进 err)
316
+ -4 = 句柄无效 / 该平台未实现
317
+ read / write:>=0 实际字节数(0 = 超时无数据),<0 = 失败。 */
318
+ typedef struct cj_serial_cfg {
319
+ int baud; /* 数值波特率(平台自己查 speed_t / DCB 表);不认识的值由平台拒绝 */
320
+ int data_bits; /* 5..8 */
321
+ int parity; /* 0=无 1=奇 2=偶 */
322
+ int stop_bits; /* 1 或 2 */
323
+ } cj_serial_cfg;
324
+
325
+ long long cj_plat_serial_open(const char *path, const cj_serial_cfg *cfg, char *err, int err_len);
326
+ int cj_plat_serial_read(long long h, unsigned char *buf, int len, int timeout_ms, char *err, int err_len);
327
+ int cj_plat_serial_write(long long h, const unsigned char *buf, int len, int timeout_ms,
328
+ char *err, int err_len);
329
+ int cj_plat_serial_close(long long h, char *err, int err_len);
330
+
331
+ /* 枚举本机串口:一行一个名字写进 out(\n 分隔),返回条数;-1 参数非法,-4 未实现。
332
+ Linux 侧尚未实现(-4)——serial:list 在 Linux 仍走仓颉侧的 /dev 目录扫描。 */
333
+ int cj_plat_serial_list(char *out, int out_len);
334
+
335
+ /* 设备路径白名单(纯函数、平台无关):只放行常见串口设备节点 / 伪终端 / udev 稳定链接,
336
+ 含 ".." 的一律拒绝。**这是安全边界**——路径是页面可控字符串,core 这里强制校验一次,
337
+ 平台实现就不必各自重写策略(仓颉侧另有一份镜像用于「跨 FFI 之前」给出可读错误,C 这份是权威)。
338
+ 非导出符号:桩平台自检直接调它。 */
339
+ int cj_serial_path_allowed(const char *path);
340
+
341
+ /* =========================================================================
342
+ * core 提供给平台的状态迁移(平台的事件循环里调用)
343
+ * ========================================================================= */
344
+ void cj_core_set_ready(cj_host *h); /* 窗口 + WebView 就绪(平台在装配完成后调)*/
345
+ void cj_core_flush_js(cj_host *h); /* UI 线程:取队列拼批回投(idle / WM_CJT_FLUSH 里调)*/
346
+ void cj_core_dialog_tick(cj_host *h, dlg_req *req); /* UI 线程:弹对话框 → 回调 → 唤醒等待方 */
347
+ void cj_core_dialog_abort(cj_host *h); /* 作废进行中的对话框并唤醒等待方(ok=0,幂等)*/
348
+ int cj_core_clipboard_tick(cj_host *h, int is_write, char *text); /* UI 线程:执行剪贴板调用 → 唤醒等待方(返回 1=成功)*/
349
+ void cj_core_clipboard_abort(cj_host *h); /* 作废进行中的剪贴板请求并唤醒等待方(幂等)*/
350
+ void cj_core_window_destroyed(cj_host *h);/* 窗口销毁:置标志 + 通知 onDestroy(一次)+ 作废对话框 */
351
+
352
+ /* ---- 菜单线路格式的共享解析(两平台翻译层共用,避免各写一遍前缀/分割逻辑)----
353
+ 格式(RFC-002 §5.4,与仓颉侧 `MenuModel.toWire()` 逐字对应):
354
+
355
+ <行首制表符 = 层级><kind>\t<id>\t<label>\t<flags>\t<accel>\n
356
+
357
+ kind:`n` 普通 / `c` 勾选 / `s` 分隔线 / `m` 子菜单;
358
+ flags:十进制,bit0 = 可用、bit1 = 勾选(`3` = 又可用又勾选)。
359
+ 容错口径:字段缺失按空串 / 0 处理,kind 为空的行整行跳过——**不崩**优先于报错,
360
+ 真正的结构错误在仓颉侧就该被丢弃(toWire 不出口含制表符/换行的字段)。
361
+
362
+ 回调期间字段指针指向 core 内部的副本,**回调返回后失效**(需要留存请自行拷贝)。 */
363
+ typedef void (*cj_menu_row_fn)(void *ctx, int depth, char kind, const char *id,
364
+ const char *label, int flags, const char *accel);
365
+ void cj_core_menu_walk(const char *wire, cj_menu_row_fn fn, void *ctx);
366
+
367
+ /* 平台把「菜单项被点了」交回 core:core 记日志 + 经 h->on_menu 回调仓颉侧(带宿主身份)。 */
368
+ void cj_core_menu_clicked(cj_host *h, const char *id, int enabled, int checked);
369
+
370
+ /* 全局快捷键触发(平台在 grab 的事件回调里调):id 是应用注册时给的标识。
371
+ core 记日志 + 以 shell 报文 {"kind":"hotkey","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
372
+ 仓颉侧按句柄路由——快捷键事件与菜单点击同走 on_shell 通道,不新增回调类型)。 */
373
+ void cj_core_hotkey_pressed(cj_host *h, const char *id);
374
+
375
+ /* 托盘菜单项被点(平台在 appindicator 的 activate 回调里调):id 是 TrayModel 里应用给的标识。
376
+ core 记日志 + 以 shell 报文 {"kind":"tray","id":"…"} 经 on_shell 回调仓颉侧(首参宿主句柄,
377
+ 按句柄路由;前端事件名 tray:click 由仓颉侧 shellEventNameFor 映射——RFC-002 §5.5 既有约定)。 */
378
+ void cj_core_tray_clicked(cj_host *h, const char *id);
379
+
380
+ /* 拖放落地(平台在 drag-data-received 里调):uri_list 是 text/uri-list 原文
381
+ (\r\n 或 \n 分隔,可含 file:// URI 与百分比编码)。
382
+ core 负责解析:跳过注释行/空行、file:// 解码成路径(%XX 十六进制解码)、
383
+ 非 file:// 的 URI 原样保留(v1 载荷只有文件,页面自己决定要不要)。
384
+ 结果以 shell 报文 {"kind":"drop","paths":[…]} 经 on_shell 回投(首参宿主句柄,
385
+ 仓颉侧按句柄路由)。paths 上限 32 个、单个路径上限 4096 字节(超限丢弃并记日志)。 */
386
+ void cj_core_drop_files(cj_host *h, const char *uri_list);
387
+
388
+ /* =========================================================================
389
+ * 仓颉 → C 桥:导出(全部实现在 bridge_core.c,两平台同一份)
390
+ * ========================================================================= */
391
+
392
+ /* 建宿主:分配公共状态(默认标题尺寸 900x640、devtools 开)+ 建两把锁。失败返回 NULL。 */
393
+ CJ_BRIDGE_API cj_host *cj_bridge_create(void);
394
+
395
+ /* 释放宿主编:先 cj_plat_fini,再回收公共状态。对 NULL 安全。
396
+ 契约:仓颉侧在 waitForExit 返回后调用**至多一次**,调用后句柄不可再用(字段置空)。 */
397
+ CJ_BRIDGE_API void cj_bridge_destroy(cj_host *h);
398
+
399
+ /* 注入回调(JS 消息到达 / 窗口销毁)+ 平台一次性初始化 */
400
+ CJ_BRIDGE_API void cj_bridge_init(cj_host *h, cj_on_message_fn m, cj_on_destroy_fn d);
401
+
402
+ /* 窗口配置(标题 / 尺寸):必须在 cj_bridge_start 之前调用 */
403
+ CJ_BRIDGE_API void cj_bridge_set_window(cj_host *h, const char *title, int width, int height);
404
+
405
+ /* 窗口图标(png / ico 路径):必须在 cj_bridge_start 之前调用;空路径 = 系统默认图标 */
406
+ CJ_BRIDGE_API void cj_bridge_set_icon(cj_host *h, const char *path);
407
+
408
+ /* 开发者工具开关:enabled=0 表示禁止打开(须在 cj_bridge_start 之前调用) */
409
+ CJ_BRIDGE_API void cj_bridge_set_devtools(cj_host *h, int enabled);
410
+
411
+ /* 注册一段预执行脚本(须在 cj_bridge_start 之前调用;空串忽略) */
412
+ CJ_BRIDGE_API void cj_bridge_add_init_script(cj_host *h, const char *js);
413
+
414
+ /* 起宿主:html 非空则记作内联页面(空串 = 页面由 cj_bridge_load_url 指定)。
415
+ 平台在独立线程里建窗 + WebView + 跑事件循环,本函数立即返回。 */
416
+ CJ_BRIDGE_API void cj_bridge_start(cj_host *h, const char *html);
417
+
418
+ /* 原生 → JS:投递一段脚本(入队,UI 线程批量执行)。
419
+ 前端 promise 的 resolve/reject 都走这条通道,静默失败会让前端永久 pending。 */
420
+ CJ_BRIDGE_API void cj_bridge_run_js(cj_host *h, const char *js);
421
+
422
+ /* 按 URL 加载页面(http(s):// 或 file://):
423
+ * - cj_bridge_start 之前调用:只记下 URL,宿主就绪时加载(与 HTML 二选一,URL 优先);
424
+ * - 启动之后调用:投递到 UI 线程重新导航(运行期切页面)。
425
+ * 空 URL 忽略。 */
426
+ CJ_BRIDGE_API void cj_bridge_load_url(cj_host *h, const char *url);
427
+
428
+ /* 重新加载当前页面:投递到 UI 线程执行 */
429
+ CJ_BRIDGE_API void cj_bridge_reload(cj_host *h);
430
+
431
+ /* 运行时打开开发者工具:投递到 UI 线程执行 */
432
+ CJ_BRIDGE_API void cj_bridge_open_devtools(cj_host *h);
433
+
434
+ /* 请求退出:与「用户关窗」走同一条销毁路径(onDestroy 恰好触发一次) */
435
+ CJ_BRIDGE_API void cj_bridge_quit(cj_host *h);
436
+
437
+ /* 宿主 UI 是否就绪 / 事件循环是否已退出(供仓颉侧 run 阻塞轮询) */
438
+ CJ_BRIDGE_API int cj_bridge_is_ready(cj_host *h);
439
+ CJ_BRIDGE_API int cj_bridge_should_quit(cj_host *h);
440
+
441
+ /* 原生对话框回调(选中路径回送仓颉侧) */
442
+ CJ_BRIDGE_API void cj_bridge_set_dialog_callback(cj_host *h, cj_on_dialog_fn cb);
443
+
444
+ /* 阻塞式原生对话框:返回 1 = 用户确认(文件类路径已回调送回),0 = 取消 / 宿主未就绪 /
445
+ 已有对话框在进行中。调用方已站在 UI 线程时内部走直接弹的快路径(投递 + 等待会自锁)。 */
446
+ CJ_BRIDGE_API int cj_bridge_show_dialog(cj_host *h, int kind, const char *title,
447
+ const char *message, const char *filter);
448
+
449
+ /* 菜单栏:wire 是 MenuModel.toWire() 的线路文本(空串 = 移除菜单栏)。
450
+ 未 start 时只记下、建窗时生效;start 之后调用=运行期换菜单(平台投到 UI 线程重建)。 */
451
+ CJ_BRIDGE_API void cj_bridge_set_menu(cj_host *h, const char *wire);
452
+
453
+ /* 运行期改单个菜单项的状态(按 id 找;找不到只记日志不报错)。 */
454
+ CJ_BRIDGE_API void cj_bridge_set_menu_item_state(cj_host *h, const char *id, int enabled, int checked);
455
+
456
+ /* 拖放策略(RFC-002 §5):enabled=0 平台不注册 drop target(默认关)。
457
+ 两头都能用:未就绪时记 pending(建 view 时读),就绪后平台投 UI 线程重挂 drag dest。 */
458
+ CJ_BRIDGE_API void cj_bridge_set_drag_drop(cj_host *h, int enabled, int files);
459
+
460
+ /* 窗口顶部副标题:把一行 UTF-8 文本画在原生菜单栏**同一行的右侧**(应用据此省掉
461
+ 自己的页面顶栏,标题 / 保存状态 / 字数都能挂在原生那一行上)。允许运行期高频更新,
462
+ 实现必须自己投递到 UI 线程(Linux 走 g_idle_add,同 set_menu)。
463
+ 返回 0 = 已受理,-1 = 参数非法(含超过 1024 字节),-3 = 宿主未就绪,
464
+ -4 = 本平台未实现(应用据 CJ_CAP_SUBTITLE 决定要不要自己画顶栏)。 */
465
+ CJ_BRIDGE_API int cj_bridge_set_titlebar_subtitle(cj_host *h, const char *text);
466
+
467
+ /* 全局快捷键:wire 一行一条 `<accel>:<id>`(空串 = 全部注销)。
468
+ 两头都能用:未就绪时记 pending(宿主线程起来后读),就绪后整表替换(ungrab 旧表再 grab 新表)。 */
469
+ CJ_BRIDGE_API void cj_bridge_set_global_hotkeys(cj_host *h, const char *wire);
470
+
471
+ /* 托盘(RFC-002 §7.C):wire 见 pending_tray 注释(icon/tooltip 两行 + 菜单项),空串 = 移除。
472
+ 两头都能用:未就绪时记 pending(宿主线程装配时读),就绪后平台投 UI 线程重建。 */
473
+ CJ_BRIDGE_API void cj_bridge_set_tray(cj_host *h, const char *wire);
474
+
475
+ /* 窗口可见性(对标 Tauri 的 window.show()/hide()):visible=1 显示并提到前台、0 隐藏。
476
+ 两头都能用:未 start 时只记下(平台建窗时按 h->win_visible 定稿),start 之后投 UI 线程执行。 */
477
+ CJ_BRIDGE_API void cj_bridge_set_window_visible(cj_host *h, int visible);
478
+
479
+ /* 最小化到托盘策略(须在 cj_bridge_start 之前调):1 = 平台把「最小化」改写成「隐藏」。
480
+ 平台在真要最小化那一刻复核托盘是否装上,没装则降级为普通最小化 + 一行 stderr。 */
481
+ CJ_BRIDGE_API void cj_bridge_set_minimize_to_tray(cj_host *h, int enabled);
482
+
483
+ /* 附庸窗(对标 owned window):把 h 挂到 owner 下——不重复占任务栏、跟随属主显隐。
484
+ 两头都能用:未 start 时只记下(平台建窗时按 h->owner 定稿,少一次任务栏按钮闪动),
485
+ start 之后投 UI 线程执行。owner 为空指针 = 解除隶属。两平台同名同签名。 */
486
+ CJ_BRIDGE_API void cj_bridge_set_owner_window(cj_host *h, cj_host *owner);
487
+
488
+ /* ===== 同窗内嵌视图(PLT-006 真标签页)=====
489
+ 在 h 这扇窗的客户区里嵌一块独立 WebView 加载 url,矩形为**客户区逻辑坐标**
490
+ (页面上报 getBoundingClientRect 的 CSS 像素即可,Windows DIP / GTK 逻辑像素与之等值)。
491
+ 成功返回视图句柄(>0);0 = 建不出来(h 为空 / url 空 / 宽高非正)。
492
+
493
+ 使用契约:
494
+ - **创建是异步的**,句柄当场返回,加载完成不另发通知。判「真的打开了」看页面自身回投
495
+ (或导航后的标题),不要把这个返回值当加载成功的凭证。
496
+ - 切换 tab 只在 set_visible:**显示一个之前由应用把别的藏起来**,框架不做调度——
497
+ 它不理解你的 tab 语义,也不该替你决定哪块该露出来。
498
+ - 宿主窗口 resize 时视图**不会**自动跟随,应用要按新矩形重调 set_bounds
499
+ (与 Tauri 的 auto_resize 缺省关闭是同一口径)。
500
+ - 视图共享宿主的 WebView2 environment / WebKit context,因此 cookie 与登录态是共用的。
501
+ - destroy 之后句柄立即失效;宿主销毁时平台会先把名下视图全部收掉,应用不必重复调。 */
502
+ CJ_BRIDGE_API long long cj_bridge_view_create(cj_host *h, const char *url,
503
+ int x, int y, int w, int hgt);
504
+ CJ_BRIDGE_API void cj_bridge_view_set_visible(cj_host *h, long long view, int visible);
505
+ CJ_BRIDGE_API void cj_bridge_view_set_bounds(cj_host *h, long long view, int x, int y,
506
+ int w, int hgt);
507
+ CJ_BRIDGE_API void cj_bridge_view_navigate(cj_host *h, long long view, const char *url);
508
+ CJ_BRIDGE_API void cj_bridge_view_destroy(cj_host *h, long long view);
509
+
510
+ /* shell 事件回调(菜单点击等;载荷见 cj_on_shell_fn 的注释,首参为宿主句柄)。 */
511
+ CJ_BRIDGE_API void cj_bridge_set_shell_callback(cj_host *h, cj_on_shell_fn cb);
512
+
513
+ /* 宿主能力位(CJ_CAP_* 的按位或):cj_plat_init 时填好,供仓颉侧 HostCapabilities /
514
+ `system:host` 如实上报。未 init 返回 0。 */
515
+ CJ_BRIDGE_API int cj_bridge_host_capabilities(cj_host *h);
516
+
517
+ /* 句柄同一性判定(返回 1 = 同一个宿主,任一方为 NULL 返回 0)。
518
+ 为什么要有它:shell 事件回调首参是**发生事件的宿主句柄**,仓颉侧得判断「是不是我这个窗口」,
519
+ 但仓颉的 `CPointer<Unit>` 不支持 `==`(实测编译器直接报 invalid binary operator),
520
+ 也没有可靠的指针→整数转换。句柄本来就归 core 所有,判定交回 C 侧最省事、也最不会猜错。
521
+ 纯 core 函数:不需要任何平台原语。 */
522
+ CJ_BRIDGE_API int cj_bridge_host_eq(cj_host *a, cj_host *b);
523
+
524
+ /* =========================================================================
525
+ * 串口(plugin_serial):设备 I/O,与宿主 / 窗口无关
526
+ *
527
+ * 这几个导出**不带 cj_host**:串口不是宿主能力,也不该强迫应用先建窗口才能用。
528
+ * 配置在仓颉侧已解析成数值,C 侧只认数值(字符串解析归仓颉,便于单测)。
529
+ * ========================================================================= */
530
+
531
+ /* 打开并配置串口:成功返回 >0 句柄;失败返回负码(-1 参数/白名单、-2 句柄表满、
532
+ -3 平台打不开)。path 必须先过 cj_serial_path_allowed——**这是安全边界**。
533
+ 打开是非阻塞的(O_NOCTTY | O_NONBLOCK + 裸 termios),不会为了等载波信号钉住调用线程。 */
534
+ CJ_BRIDGE_API long long cj_bridge_serial_open(const char *path, int baud, int data_bits,
535
+ int parity, int stop_bits);
536
+
537
+ /* 读:最多 len 字节,最多等 timeout_ms 毫秒(0 = 立即返回,负值按 0 处理)。
538
+ 返回读到的字节数(0 = 超时无数据,不是错误);<0 失败。缓冲由调用方提供。 */
539
+ CJ_BRIDGE_API int cj_bridge_serial_read(long long h, unsigned char *buf, int len, int timeout_ms);
540
+
541
+ /* 写:在 timeout_ms 预算内尽量写完;返回实际写入字节数(可能小于 len),<0 失败。 */
542
+ CJ_BRIDGE_API int cj_bridge_serial_write(long long h, const unsigned char *buf, int len,
543
+ int timeout_ms);
544
+
545
+ /* 关闭并释放句柄(幂等:重复关同一句柄返回 -4)。 */
546
+ CJ_BRIDGE_API int cj_bridge_serial_close(long long h);
547
+
548
+ /* 枚举本机串口(serial:list 的平台层):把「COM + 全数字」一行一个写进 out,
549
+ 返回条数(0 = 没有串口,不是错误)。平台不实现时返回 -4(err 口径同其余原语)。
550
+ out_len 是 out 的容量;放不下时截断到已写入的整行。 */
551
+ CJ_BRIDGE_API int cj_bridge_serial_list(char *out, int out_len);
552
+
553
+ /* 最近一次失败的明细(UTF-8,**线程局部**——命令各自跑在 worker 线程上,静态单槽会被互相覆盖)。
554
+ 每次串口导出调用都会先清空它,失败时写入;返回的指针在该线程下次调用前有效。 */
555
+ CJ_BRIDGE_API const char *cj_bridge_serial_last_error(void);
556
+
557
+ /* =========================================================================
558
+ * 剪贴板(plugin_clipboard):宿主能力——GTK 的 clipboard 依赖 gtk_init 后的环境,
559
+ * 且 GTK 规定剪贴板调用必须在 GTK 线程上,所以**带 cj_host**(借它的 UI 线程分流),
560
+ * 与对话框同一套「已在 UI 线程就直接做、否则投递 + 有界等待」的样板。
561
+ * 返回码口径:0 = 成功,-1 = 参数非法,-3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
562
+ * ========================================================================= */
563
+
564
+ /* 写文本:text 须为合法 UTF-8,长度上限 8 MB(core 校验)。0 = 成功。 */
565
+ CJ_BRIDGE_API int cj_bridge_clipboard_write_text(cj_host *h, const char *text);
566
+
567
+ /* 读文本:返回 malloc 的 UTF-8 字符串(可能为 NULL = 剪贴板为空 / 不是文本)。
568
+ 用 cj_bridge_clipboard_free 释放,**不许**在仓颉侧 free。 */
569
+ CJ_BRIDGE_API char *cj_bridge_clipboard_read_text(cj_host *h);
570
+
571
+ /* 释放 read_text 返回的缓冲(NULL 安全,幂等)。 */
572
+ CJ_BRIDGE_API void cj_bridge_clipboard_free(char *s);
573
+
574
+ /* 平台原语:真正的剪贴板读写。err 口径同其余原语(-4 未实现由平台自己报)。 */
575
+ int cj_plat_clipboard_write_text(cj_host *h, const char *text, char *err, int err_len);
576
+ char *cj_plat_clipboard_read_text(cj_host *h, char *err, int err_len);
577
+
578
+ /* =========================================================================
579
+ * 系统通知(plugin_notification):与剪贴板同一类宿主能力,但**发后即忘**——
580
+ * 通知提交后显示与否不回传(freedesktop 协议本就不给同步回执),core 校验 + 投
581
+ * UI 线程即返回,成败只在 stderr 留行。返回码:0 = 已提交,-1 = 参数非法,
582
+ * -3 = 宿主未就绪 / 平台失败,-4 = 本平台未实现。
583
+ * ========================================================================= */
584
+
585
+ /* 发一条通知:title 非空且 ≤256 字节,body ≤8 KiB(core 校验)。0 = 已提交。 */
586
+ CJ_BRIDGE_API int cj_bridge_notification_notify(cj_host *h, const char *title, const char *body);
587
+
588
+ /* 平台原语:真正发通知(UI 线程调用)。 */
589
+ int cj_plat_notification_notify(cj_host *h, const char *title, const char *body,
590
+ char *err, int err_len);
591
+
592
+ /* =========================================================================
593
+ * 日志落盘(RFC-003 第 5 期):把 stderr 接管到文件,给「没有控制台」的产物留凭证
594
+ *
595
+ * 为什么在 fd 层接管:单文件产物是 Windows GUI 子系统,双击时 stderr 无处可去;
596
+ * 而诊断输出分布在 core、平台实现与仓颉侧上百处。解 `freopen` 一次,**所有** stderr
597
+ * 写者自动落进同一个 FILE*(顺序天然一致),两端调用点一处都不用改。stdout 一律不碰
598
+ * (`CJ_TAURI_MANIFEST` 的插件清单走 stdout)。
599
+ * ========================================================================= */
600
+
601
+ /* 开启日志落盘:path 非空且可写时返回 1,否则返回 0 并在**还在的** stderr 上说明原因。
602
+ max_kb ≤ 0 表示不轮转(交给外部 logrotate);keep = 保留几份档案(log.1 … log.keep),
603
+ ≤0 表示轮转时直接丢弃旧内容。**只能在第一条日志之前调**——已开启后重复调用返回 0
604
+ (stderr 已经被接管,换不了目标;想换就重启应用换参数)。 */
605
+ CJ_BRIDGE_API int cj_bridge_set_log_file(const char *path, int max_kb, int keep);
606
+
607
+ /* 回读生效中的日志路径;未开启返回空串。供 system:host 如实上报。 */
608
+ CJ_BRIDGE_API const char *cj_bridge_log_file(void);
609
+
610
+ /* 追加一行调用方提供的日志(同样走轮转;用于 log 插件把前端报错并进同一份文件)。
611
+ 返回 0 = 已写入(或未开启但已落 stderr),-1 = 参数非法。 */
612
+ CJ_BRIDGE_API int cj_bridge_log_write(const char *line);
613
+
614
+ /* core 内的诊断输出统一走它:**未开启落盘时逐字节等同 `fprintf(stderr, …)`**
615
+ (现有 E2E needle 与 `[cj-bridge]` 前缀断言因此一条都不受影响);开启后才加
616
+ `[YYYY-MM-DD HH:MM:SS] ` 前缀并按大小轮转。 */
617
+ void cj_core_logf(const char *fmt, ...)
618
+ #if defined(__GNUC__)
619
+ __attribute__((format(printf, 1, 2)))
620
+ #endif
621
+ ;
622
+ #define cj_logf(...) cj_core_logf(__VA_ARGS__)
623
+
624
+ /* =========================================================================
625
+ * 静默子进程:后台启动控制台程序,与宿主 / 窗口无关(不带 cj_host)
626
+ *
627
+ * 为什么必须下沉到 C 桥:cj-tauri 应用都是 GUI 子系统(Windows 的 `--subsystem=windows`),
628
+ * 父进程**没有控制台**;此时拉起一个控制台子系统的子进程,Windows 会给它**单独配一个控制台
629
+ * 窗口**——用户看到的就是一个黑窗口。仓颉的 `std.process.launch` 没有创建标志可传,
630
+ * 所以这条只能在桥里做:Windows 走 `CreateProcessW` + `DETACHED_PROCESS`(连隐藏 console 都不建),
631
+ * Linux 走 `fork` + `execv` + `setsid`(脱开终端,Ctrl-C 不连带打死)。
632
+ *
633
+ * 三个导出两平台同名同签名(AGENTS §2):上层因此没有平台分叉的调用点。
634
+ * ========================================================================= */
635
+
636
+ /* 静默启动:exe 是程序路径(UTF-8),args 是参数串(UTF-8,**以 0x1F 分隔**,空串 = 无参数)。
637
+ 引号 / 空格由平台按各自规则转义,调用方**不要**自己加引号。
638
+ 返回 pid(> 0);失败返回 0,原因由平台写进 stderr(core 会再补一行带 exe 的上下文)。 */
639
+ CJ_BRIDGE_API long long cj_bridge_spawn_hidden(const char *exe, const char *args);
640
+
641
+ /* 终止该 pid(幂等:进程已经不在了也返回 1)。返回 1 = 已结束,0 = 无权 / 失败。 */
642
+ CJ_BRIDGE_API int cj_bridge_kill_pid(long long pid);
643
+
644
+ /* 该 pid 是否还活着:1 = 活着,0 = 已退出或查不到(**含没权限查**——按「不在」上报,
645
+ 比谎报活着好:调用方据它决定要不要重新拉起)。 */
646
+ CJ_BRIDGE_API int cj_bridge_pid_alive(long long pid);
647
+
648
+ /* 把产物内嵌的资源释放到 dir(单文件包用:scripts/pack-win-single.sh 把随包的程序与依赖 DLL
649
+ 作为 RT_RCDATA 资源嵌进 exe,资源名就是文件名)。
650
+ 返回值:> 0 = 已就绪的文件数(本次释放的 + 本来就在的),0 = **产物里没有内嵌资源**
651
+ (普通构建、非单文件包都是这个),-1 = 参数为空或写盘失败(原因在 stderr)。
652
+ 幂等:目标文件已存在且大小与资源一致就跳过——每次启动调它不会重写一份几十 MB 的资源。
653
+ 目录由调用方给(桥不认识应用名):典型是 %LOCALAPPDATA%\应用名\runtime。 */
654
+ CJ_BRIDGE_API int cj_bridge_extract_assets(const char *dir);
655
+
656
+ /* 按**绝对路径**把一个动态库加载进本进程(单文件包专用:随包释放出来的 OpenSSL 这类
657
+ 「运行期按名字 LoadLibrary」的组件,不预加载就永远找不到)。
658
+ 为什么需要它(2026-10-21 实测,另一台机器):仓颉运行时的 TLS 加载器按**裸名**找
659
+ `libssl-3-x64.dll` / `libcrypto-3-x64.dll`,SDK 与 stdx 都不带;开发机上它恰好命中 PATH 里的
660
+ Git 自带那份,于是「本机全通、别人双击就挂」。症状极具误导性:http 照常 200(cjh 的
661
+ /api/health 通),**只有 ws 握手失败**——`ws: connect failed: Can not load openssl library or
662
+ function RAND_bytes.`(RFC6455 升级要算随机 nonce,明文 ws:// 也逃不掉)。
663
+ 做法是进程启动早期先按绝对路径 LoadLibrary/dlopen 一次:之后同类裸名加载会命中已加载模块,
664
+ 不必改系统 PATH、也不必把 DLL 摆到 exe 旁边(单文件承诺不破)。
665
+ 返回 1 = 已加载(含本来就在的),0 = 失败(原因在 stderr)。 */
666
+ CJ_BRIDGE_API int cj_bridge_preload_library(const char *path);
667
+
668
+ /* 平台原语(err 口径同其余原语:写不进就留空,core 会打「平台没有给出原因」)。 */
669
+ long long cj_plat_spawn_hidden(const char *exe, const char *args, char *err, int err_len);
670
+ int cj_plat_kill_pid(long long pid);
671
+ int cj_plat_pid_alive(long long pid);
672
+ int cj_plat_extract_assets(const char *dir, char *err, int err_len);
673
+ int cj_plat_preload_library(const char *path, char *err, int err_len);
674
+
675
+ #endif /* CJ_BRIDGE_CORE_H */