
前言先让两个监听者收到一次信号这次做 signal4cj我先给页面放了两个计数器监听 A 和监听 B。点一次发送两边都加一移除 B 后再发送只有 A 变化。这样一来注册、接收、撤销这几件事能直接从画面看出来也方便检查库的行为有没有真正接到鸿蒙进程。它对应的上游是 signal-hook。这里的 signal 指 Unix 系统信号不是聊天软件也不是普通组件点击事件。演示按钮最终调用 kill(getpid(), SIGUSR1)由系统把信号送回本应用再经过原生处理器和仓颉分发路径更新计数。按钮本身不修改 A、B 的数值。CJMP 社区公共逻辑库与平台接入示例按 CJMP 0.2.2 的 native 逻辑模块组织。公共订阅接口在仓颉 common 包sigaction 和管道操作留在鸿蒙后端ArkTS 负责展示。文章沿着一次信号的实际去向往下看最后再回到设备测试和源码复现。环境展示signal-hook 固定上游源码CPF-RN基础工具与安装资料CJMP 开发准备CJMP 0.2.2 配套说明项目本次实际配套框架CJMP Tools 0.2.2 / logic-module / nativeIDEDevEco Studio 6.1.1.300SDK / 编译器HarmonyOS 6.1.1(24) / 仓颉与 cjpm 1.1.3模拟器x86_646.1.1.350真机MatePad Edgearm64-v8a6.1.1.120环境搭建直接引用 CPF-RN 的基础工具文件RN 专有依赖不照搬到这个 CJMP 工程。CJMP 0.2.2 文档给出的仓颉配套是 1.1.0-beta.22本机实际为 1.1.3构建目录与 C ABI 桥按实际 SDK 调整。目前完成的是 API 24 双端实测API 26 仍需要换配套后重新验证。信号处理器里做什么系统信号可能打断任意线程。当中调用仓颉回调、拼接字符串或刷新 ArkUI都不适合放进这个特殊执行环境。原生入口因此只做两件事置 pending 标志向非阻塞管道写一个唤醒字节。真正的业务回调等 pump 在正常调用路径里处理。static void on_signal(int sig) { int savederrno, iindex_of(sig); if(i0 atomic_load_explicit(enabled[i],memory_order_relaxed)) { atomic_store_explicit(pending[i],1,memory_order_relaxed); unsigned char byte(unsigned char)sig; (void)write(wakefd[1],byte,1); /* Non-blocking; pending survives EAGAIN. */ } errnosaved; }这里保留并恢复 errno避免信号打断的原操作发现错误码被改了。pending 使用无锁原子整数编译时要求对应平台的 int 原子始终无锁处理器没有加互斥锁也没有分配内存。管道满时write 可能失败但 pending 仍然保留通知不依赖每个唤醒字节都成功写入。这并不意味着能精确记录每次信号。普通信号本来就可能合并这一版还主动按类型合并 pending连续发送二十次再 pump一名监听者本次只收到一次通知。要统计每条任务应该用有序消息队列等机制不能把这个标志当作可靠计数器。仓颉回调在 pump 中分发public func pump(timeoutMillis: Int32): Int64 { ensureOpen();let bitsunsafe {sg_poll(timeoutMillis)} if(bits0){throw IllegalStateException(poll failed, errno${-bits})} var count: Int640 // Snapshot: subscriptions made in a callback are deferred to the next pump. let snapshotlisteners.toArray() for(slot in snapshot) { let bit: Int32if(slot.signaluser1){1}else{2} if(slot.active (bits bit)!0){slot.callback(slot.signal);count} } return count }pump 先读取管道并取走 pending再遍历当前监听者的快照。每个订阅有独立 ID撤销只把对应项标为 inactive。回调里新增的监听者不会挤进正在处理的这批快照已经撤销的项也会在调用前重新判断。这些操作都是普通线程路径上的仓颉逻辑。public func unsubscribe(id: Int64): Bool { ensureOpen() for(slot in listeners){if(slot.idid slot.active){slot.activefalse;return true}} return false }示例先注册 A、B两边收到第一次通知后都是 1。点击移除 B再发送一次A 变成 2B 保持 1。日志也只新增 A 的一行。这里同时检查了画面数字和日志避免只看某个内部返回值就认为撤销成功。调用规则也要提前说清全进程只能有一个分发器普通 API 由拥有线程串行调用第二个分发器会在获取所有权时被拒绝。回调如果抛异常会终止当前分发并向调用者传播该批 pending 已经消费不会自动重新执行剩余回调。业务侧应在自己的回调边界处理可预期异常。注册和关闭不能抢占运行时SIGUSR1、SIGUSR2 可以成为这一版的注册目标但仍需检查进程当时的处理器。已有自定义处理器时返回 EBUSY而不是强行替换。上游具备旧处理器链式调用能力这一版没有把那套完整注册表移植进来因此采用明确拒绝的策略避免覆盖仓颉运行时或其他库。两端实测对象是 SIGUSR1SIGUSR2 没有单独扩展验证结论。int32_t sg_restore(int32_t sig) { int iindex_of(sig);if(i0)return EINVAL; if(!atomic_load(enabled[i]))return 0; /* Do not overwrite a handler installed by another component meanwhile. */ struct sigaction current;if(sigaction(sig,NULL,current)0)return errno; if(current.sa_handler!on_signal)return EBUSY; atomic_store(enabled[i],0); if(sigaction(sig,previous[i],NULL)0){int eerrno;atomic_store(enabled[i],1);return e;} atomic_store(pending[i],0);return 0; }close 会确认当前处理器仍然是本库安装的入口再恢复原处理器。若另一个组件已经改过它会报告冲突不能覆盖别人的新状态。全局管道 FD 保留到进程退出避免处理器正在写时 FD 被关闭、甚至被其他文件复用反复开关不会不断创建新管道。停止之后页面显示已停止。此时再点发送接口直接拒绝不会给已经恢复默认处理器的应用发 SIGUSR1。另一个按钮尝试注册 SIGKILL用来验证不支持的目标会在安装前被拒绝演示没有真的向应用发送 SIGKILL。实际改动都在这些文件里harmony/cjmp/signal4cj/ project.conf logic-module/cjpm.toml logic-module/common/signals.cj logic-module/hos/native/signal_core.c logic-module/hos/native/napi_bridge.c logic-module/hos/demo.cj logic-module/hos/test_suite.cj hos/signal4cj/ scripts/build-logic.ps1 example/harmony/app/文件改动目的common/signals.cj提供仓颉订阅、撤销、快照分发及 Resource 关闭native/signal_core.c接入鸿蒙 sigaction、self-pipe、真实信号和所有权检查cjpm.toml / build-logic.ps1构建公共包链接原生后端收集两个 ABI 的依赖napi_bridge.c / demo.cj普通调用路径的 C ABI 接入复制结果并释放返回字符串hos/signal4cjHAR 门面与类型声明提前编译外部 common 源码example/harmony/app实际依赖 HAR 的应用显示计数、状态与设备报告harmony 保存的是实际适配源码example 是独立依赖它的示例。原生后端先编译再由 cjpm 链接进公共包HAR 的构建钩子提前处理位于模块外的 common 目录避免增量构建把修改漏掉。平台依赖按 ELF 信息收集x86_64 与 arm64 文件分别打包。页面通过 signal4cj 门面调用未再实现另一套信号模拟器。运行效果模拟器与 MatePad Edge两端按同一顺序操作发送、移除 B、再次发送、连续发送、停止、尝试发送、重新注册、拒绝不支持信号、执行自检。下面的 GIF 来自实际运行帧包含状态变化和当前设备的测试结果。自动化通过设备接口点击应用不切换 Windows 前台窗口。自检在设备里的仓颉测试包执行包含三十轮真实 SIGUSR1 生命周期。每轮检查两个监听者、撤销幂等性、合并通知、空轮询、非法目标拒绝、关闭幂等性和关闭后调用共三百三十项检查。模拟器与真机都返回 PASS测试没有通过 Python 写入一个预设的成功报告。PASS | 330 checks | 30 real-signal lifecycle rounds截图复核还查出一个显示层问题设备深色主题会让没有显式字体颜色的正文在白色卡片上变得难读。最终给状态文字设置颜色并重建、重跑、重录。读数验证能检查计算和调用真实画面的复核则能发现这种用户实际看不到内容的问题。总结与复现这次接入的核心是把异步系统入口和正常业务执行分开并给注册、撤销、关闭留出可验证的边界。完整的 Rust 注册表、实时信号队列、siginfo、异步运行时适配器以及退出策略都不在本版范围Android/iOS 也没有运行验证。README 对这些边界和回调异常行为有单独说明。下载源码包后打开 example/harmony/app按 README 配置对应 SDK 与运行依赖再生成自己的签名并运行。公共 CJMP 工程可在 harmony/cjmp/signal4cj 下用官方命令构建 HAR。逐文件理由在 PORTING.md两端包哈希、读数和源码一致性记录在 docs/verification。cjmp build har --platform ohos-arm64项目开源地址signal4cjAtomGit。加入 CJMP 社区讨论公共逻辑库与鸿蒙接入