将 ohos_coap 移植到仓颉:从 libcoap、C ABI 到 HarmonyOS HAR 与应用

本文完成一个具体的移植工程:保留 ohos_coap 使用的 libcoap C 核心,新增 C ABI 桥接层,再用仓颉实现客户端、服务端和应用演示。读者可以从空工程开始,按文中的目录、完整源码和命令生成 coap.har 与 HAP。

下载完整工程 ZIP(约 1.3 MB)。包内包含两套工程、固定版本的 libcoap 源码、两种架构的桥接库、许可证与教程。解压后先按第 3 节准备 SDK,再打开 ohos-coap-app,从第 10 节执行构建;若希望理解并手工搭建全部过程,从第 4 节逐步操作。

验证日期:2026 年 9 月 30 日。 Windows 上的仓颉回环、独立 UDP 对端检查、HarmonyOS arm64 与 x86_64 原生库交叉编译、双架构 HAR/HAP 构建均已通过。尚未在 HarmonyOS 设备或模拟器上运行。 本机模拟器需要启用 Hyper-V,此次未启用;下文的设备运行步骤是交付给读者的验收步骤,不能当作已经完成的运行证据。当前 HAP 未签名。

网页中的仓颉代码采用本地词法着色;主题代码框顶部仍可能显示 TEXT,实际代码与复制内容均为仓颉。C、PowerShell 等代码沿用网站现有高亮。编译结果用于验证语法正确性。

1. 移植对象和交付范围

ohos_coap 上游仓库 提供面向 ArkTS 的 NAPI 包装,底层协议处理来自 libcoap。上游页面已标记归档并提供 GitCode 新地址。本文锁定现有 Gitee 提交,避免后续默认分支变化影响复现。

项目本文固定值
ohos_coap 提交4ad8844e1748706283721d46b8c65ca3e46a337e
libcoap 子模块提交5fd2f89ef068214130e5d60b7087ef48711fa615
libcoap 实际源码版本4.3.4,以该提交的 CMakeLists.txt 为准
DevEco Studio / 仓颉插件26.0.0.821,Windows
HarmonyOS 仓颉编译器1.2.0-beta.rc3
工程目标 SDK26.0.0
工程兼容 SDK6.1.1(24)
Native SDK Clang15.0.4
产物架构arm64-v8a、x86_64

上游 README 介绍中仍写着 libcoap 4.3.1;本文检出的子模块 CMake 声明为 4.3.4。构建和生命周期判断都按后者进行。本文使用的是该固定提交中的原生 C 核心,没有编译上游 NAPI 包装层;也没有把上游 thirdModule 的补丁流程自动套到不同版本上。涉及线上部署时,需要对这个固定版本及所需补丁另做维护评估。

这份移植的公开接口采用仓颉对象与函数,功能对应关系如下;它不是 ArkTS 接口的逐字兼容层。

功能仓颉交付
客户端 GET / POST / PUTrequestUri(uri, method: 1 / 2 / 3)
CON / NONkind: 0 / 1
文本 / 二进制载荷setPayload / setBinaryPayload
Content-Format、端口、MID、TokensetFormat、setPort、setMID、setToken
等待时长、最大重传次数setWaitSeconds、setRetryCount
自动 Block1 / Block2默认块模式 3,完整消息体;由 libcoap 处理
自定义选项setCustomOption,只接受不由桥接层管理的选项
服务端 GET / POSTCoapServer.register(path, method, handler)
请求正文、路径、查询与选项CoapRequest 的复制快照
服务端响应、停止CoapReply、stop() 后等待运行任务结束
应用与库entry HAP、coap HAR,均包含两种架构

当前未封装持续 Observe 订阅、资源属性、对端地址查询、DELETE、服务端 PUT、文件/媒体发送助手、TLS/DTLS、TCP、WebSocket、OSCORE。服务端 PUT 请求会得到 4.05;客户端 PUT 可以请求支持 PUT 的外部服务。应用运行、设备网络权限实际表现以及局域网互通仍需设备验收。

2. 三层结构与边界

调用链是:仓颉应用 → 仓颉 HAR → libcoap_cj_bridge.so → libcoap → UDP。协议报文、ACK、重传和块传输交给 libcoap,桥接层只提供一组固定宽度的 C 函数和不透明句柄。

桥接层导出的函数使用 int32_t、int64_t、uint16_t、uint32_t、uint64_t。仓颉分别声明 Int32、Int64、UInt16、UInt32、UInt64,避免让平台相关的 size_t 或 C 结构体布局进入公开 ABI。void * 对应 CPointer<Unit>,UTF-8 C 字符串对应 CString。

内存所有权需要先确定,再写调用代码:

  • 传给 C 的字节数组: 仓颉只在同步 setter 或回复函数期间 pin 数组;C 在返回前复制数据,finally 释放 pin。C 不保存仓颉数组指针。
  • 传给 C 的字符串: mallocCString 和 LibC.free 配对。URI、资源路径、绑定地址拒绝内嵌 NUL,防止 C 看到截断后的字符串。
  • C 返回的指针: 在原生句柄释放或回调结束前立即复制为仓颉数组/字符串。
  • 大响应正文: 桥接层分配 C 堆副本,由 libcoap 的 release callback 释放。固定的 4.3.4 实现也会在失败路径释放它,桥接层不能再释放一次。
  • 服务端回调: C 只保存整数 handler ID;仓颉在受锁保护的注册表中保留闭包。@C 入口捕获 Exception,未生成响应时由 C 返回 5.00。严重运行时错误仍不属于普通业务异常的恢复契约。
  • 停止顺序: stop() 设置原子标志,轮询任务退出后释放 C 上下文,再清除注册表;调用方必须等待对应 Future。

默认块模式 3 是 COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY,让应用收到组装后的消息体。libcoap 4.3.4 块传输文档 说明了这些模式和大数据接口的释放回调。本实现的单次输入和最终复制正文限制为 1 MiB;这个限制不等于 libcoap 重组阶段所有内存占用的总上限。

3. 准备开发环境

先安装 DevEco Studio 26 与对应仓颉插件,登录拥有下载权限的华为账号,安装随插件提供的 HarmonyOS 仓颉 SDK。安装入口与版本要求以华为官方仓颉开发环境说明 为准。

本机曾出现“插件暂不支持非中国地区”的启动提示,最终确认是 DevEco 没有登录华为账号,登录后解决。遇到同类提示应先检查账号登录、体验版权限和插件版本;不要把这次问题直接归因于 Windows 地区。

本文的构建步骤不需要启动 Hyper-V。模拟器可能需要虚拟化支持;也可以在最后连接适配的真机。通用 Windows 仓颉 SDK 与 HarmonyOS 仓颉 SDK 的目标不同,不能拿 Windows DLL 放进 HAR。

下面约定工程根目录为 C:\dev\coap-cangjie。该目录可以替换;建议避开同步盘,减少生成目录属性和文件占用问题。在 PowerShell 7 中准备路径:

$projectRoot = 'C:\dev\coap-cangjie'
$devEco = 'C:\Program Files\Huawei\DevEco Studio'
$cjSdk = "$env:USERPROFILE\.cangjie-sdk\26.0\cangjie"
$nativeSdk = Join-Path $devEco 'sdk\default\openharmony\native'
New-Item -ItemType Directory -Path $projectRoot -Force | Out-Null
Set-Location $projectRoot
. (Join-Path $cjSdk 'build-tools\envsetup.ps1')
$env:PATH = (Join-Path $nativeSdk 'build-tools\cmake\bin') + ';' + $env:PATH
cjc -v
cjpm --version
cmake --version
ninja --version

cjc 必须来自这里的 1.2 SDK。cmake / ninja 可直接使用 Native SDK 随附的版本;交叉编译使用 SDK 的 Clang 与 sysroot。可选的 Windows 回环检查另外需要 MinGW GCC;本机版本为 GCC 15.2.0、CMake 4.2.3。不要拿安装包文件名代替实际命令版本检查。

4. 创建应用和 HAR 工程

在 DevEco 中创建 [Cangjie] Empty Ability 应用,保存到 $projectRoot\ohos-coap-app,选择 SDK 26.0.0。然后在工程中使用 New → Module → [Cangjie] Static Library,模块名填写 coap。不同版本菜单措辞可能略有区别,以当前插件模板为准。

本文工程中,模板生成的包名分别为 ohos_app_cangjie_entry 和 ohos_app_cangjie_coap。如果你的模板包名不同,要统一调整 .cj 的 package、import、cjpm.toml 中的 name、combined 构建配置和 module.json5 的 srcEntry。最简单的复现方式是沿用本文包名。

向导负责生成图标、字符串资源、构建插件、Ability 模板等基础文件。保留这些文件,下文会给出需要覆盖或核对的配置和业务源码。Static Library 是 IDE 的 HAR 模块模板名称;这个版本模板的 CJPM output-type 实际为 dynamic,打包后包含仓颉 .so,不能擅自改成纯静态目标。

完成后的主要目录如下:

coap-cangjie/
├─ ohos-coap-libcoap-port/
│  ├─ Build-Native.ps1
│  ├─ check-native.py
│  ├─ native/
│  │  ├─ CMakeLists.txt
│  │  ├─ coap_bridge.h
│  │  └─ coap_bridge.c
│  ├─ third_party/libcoap/       # 固定提交的 C 核心
│  └─ host-check/               # 可选 Windows 仓颉检查
└─ ohos-coap-app/
   ├─ Build-App.ps1
   ├─ build-profile.json5
   ├─ entry/
   │  ├─ cjpm.toml
   │  ├─ build-profile.json5
   │  ├─ oh-package.json5
   │  └─ src/main/{module.json5,cangjie/}
   └─ coap/
      ├─ cjpm.toml
      ├─ build-profile.json5
      ├─ libs/{arm64-v8a,x86_64}/libcoap_cj_bridge.so
      └─ src/main/
         ├─ cangjie/{native,client,server,demo}.cj
         └─ resources/rawfile/libcoap-LICENSE.txt

5. 取得固定版本的原生核心

在工程根目录执行下面的命令。先固定上游提交,再更新子模块;不要先检出子模块的最新 HEAD。

git clone https://gitee.com/openharmony-tpc/ohos_coap.git ohos_coap-upstream
git -C ohos_coap-upstream checkout 4ad8844e1748706283721d46b8c65ca3e46a337e
git -C ohos_coap-upstream submodule update --init --recursive
git -C ohos_coap-upstream/libcoap/src/main/cpp/libcoapSource rev-parse HEAD

最后输出必须为 5fd2f89ef068214130e5d60b7087ef48711fa615。随后复制源码,排除 Git 元数据;这是一个完整源码快照,不依赖上游 ArkTS 模块的构建系统。

New-Item -ItemType Directory -Path 'ohos-coap-libcoap-port\third_party' -Force | Out-Null
robocopy 'ohos_coap-upstream\libcoap\src\main\cpp\libcoapSource' 'ohos-coap-libcoap-port\third_party\libcoap' /E /XD .git /XF .git
if ($LASTEXITCODE -ge 8) { throw 'libcoap source copy failed' }
Copy-Item -LiteralPath 'ohos_coap-upstream\LICENSE' -Destination 'ohos-coap-libcoap-port\LICENSE'

Windows 上若 Git 未创建真实符号链接,README 和 examples/contiki/coap_config.h 可能只是链接目标名。可分别复制 README.md、coap_config.h.contiki 为这两个文件的内容副本。本工程的 Windows/OHOS 目标没有编译 Contiki 示例;源码快照保留这种展开记录,不改动协议实现。

6. 编写 C ABI 桥接层

以下三个文件保存到 ohos-coap-libcoap-port/native。代码是完整实现,不需要另找隐藏的辅助函数。

6.1 CMakeLists.txt

libcoap 构建为 PIC 静态库并链接进入一个桥接 .so。关闭本文未交付的协议功能、示例和文档目标,避免引入额外的 TLS 依赖。

cmake_minimum_required(VERSION 3.16)
project(ohos_coap_cangjie_bridge LANGUAGES C)

set(LIBCOAP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libcoap" CACHE PATH "Pinned libcoap source directory")
if(NOT EXISTS "${LIBCOAP_SOURCE_DIR}/CMakeLists.txt")
  message(FATAL_ERROR "Initialize third_party/libcoap, or set LIBCOAP_SOURCE_DIR")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(ENABLE_DTLS OFF CACHE BOOL "" FORCE)
set(ENABLE_TCP OFF CACHE BOOL "" FORCE)
set(ENABLE_WS OFF CACHE BOOL "" FORCE)
set(ENABLE_OSCORE OFF CACHE BOOL "" FORCE)
set(ENABLE_AF_UNIX OFF CACHE BOOL "" FORCE)
set(ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(ENABLE_DOCS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTS OFF CACHE BOOL "" FORCE)
set(WITH_OBSERVE_PERSIST OFF CACHE BOOL "" FORCE)
set(WITH_EPOLL OFF CACHE BOOL "" FORCE)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_subdirectory("${LIBCOAP_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/libcoap")

add_library(coap_cj_bridge SHARED coap_bridge.c)
target_compile_features(coap_cj_bridge PRIVATE c_std_11)
target_compile_definitions(coap_cj_bridge PRIVATE CC_COAP_EXPORTS)
target_link_libraries(coap_cj_bridge PRIVATE coap-3)
target_include_directories(coap_cj_bridge PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
if(MINGW)
  # Avoid mixing MinGW 15 clock_gettime64 with the SDK's older pthread DLL.
  target_link_libraries(coap_cj_bridge PRIVATE ws2_32 "-Wl,-Bstatic" winpthread "-Wl,-Bdynamic")
  target_link_options(coap_cj_bridge PRIVATE -static-libgcc)
endif()
if(NOT WIN32)
  set_target_properties(coap_cj_bridge PROPERTIES C_VISIBILITY_PRESET hidden)
endif()

6.2 coap_bridge.h

客户端句柄只执行一次请求;返回正文指针属于句柄。服务端 exchange 指针仅在同步回调内有效。

#ifndef COAP_CANGJIE_BRIDGE_H
#define COAP_CANGJIE_BRIDGE_H
#include <stdint.h>
#if defined(_WIN32)
#define CC_API __declspec(dllexport)
#else
#define CC_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif

/* Handles are single-owner. Input bytes are copied before a setter returns.
 * Borrowed result pointers remain valid until the owning handle is freed.
 * Client status 0 means a CoAP response, including a 4.xx or 5.xx response.
 * Configuration keys: 1=format, 2=port, 3=MID, 4=timeout ms,
 * 5=max retransmissions, 6=libcoap block mode. */
CC_API void *cc_client_new(const char *uri, int32_t method, int32_t type);
CC_API int32_t cc_client_set_uint(void *client, int32_t key, int64_t value);
CC_API int32_t cc_client_set_payload(void *client, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_set_token(void *client, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_add_option(void *client, uint16_t number, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_run(void *client);
CC_API int32_t cc_client_response_code(void *client);
CC_API uint64_t cc_client_response_size(void *client);
CC_API const uint8_t *cc_client_response_data(void *client);
CC_API const char *cc_client_error(void *client);
CC_API void cc_client_free(void *client);

/* Poll and registration run on the server's owner thread. The callback is
 * synchronous during poll; the exchange expires as soon as it returns.
 * Stop on the Cangjie side and join the polling task before freeing a server. */
typedef void (*cc_handler)(int64_t handler_id, void *exchange);
CC_API void *cc_server_new(const char *address, uint16_t port);
CC_API int32_t cc_server_register(void *server, const char *path, int32_t method, int64_t handler_id);
CC_API int32_t cc_server_poll(void *server, uint32_t wait_ms, cc_handler handler);
CC_API const char *cc_server_error(void *server);
CC_API void cc_server_free(void *server);
CC_API int32_t cc_exchange_method(void *exchange);
CC_API const char *cc_exchange_path(void *exchange);
CC_API const uint8_t *cc_exchange_payload(void *exchange);
CC_API uint64_t cc_exchange_payload_size(void *exchange);
CC_API const uint8_t *cc_exchange_query(void *exchange);
CC_API uint64_t cc_exchange_query_size(void *exchange);
CC_API const uint8_t *cc_exchange_option(void *exchange, uint16_t number);
CC_API uint64_t cc_exchange_option_size(void *exchange, uint16_t number);
CC_API int32_t cc_exchange_option_number_at(void *exchange, uint32_t index);
CC_API const uint8_t *cc_exchange_option_data_at(void *exchange, uint32_t index);
CC_API uint64_t cc_exchange_option_size_at(void *exchange, uint32_t index);
CC_API int32_t cc_exchange_reply(void *exchange, int32_t code, uint16_t format, const uint8_t *data, uint64_t size);
CC_API void cc_set_log(int32_t enabled);

#ifdef __cplusplus
}
#endif
#endif

6.3 coap_bridge.c

这里调用 libcoap 的 URI 解析、上下文、会话、PDU、选项、大数据和轮询 API。自定义选项拒绝保留编号 0、Observe 6,以及桥接层管理的 URI、Content-Format、块与大小选项;客户端结束一次请求就释放会话与上下文,不会维持观察订阅。

#include "coap_bridge.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <sys/socket.h>
#endif
#include <coap3/coap.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CC_MAX_BODY (1024u * 1024u)
static atomic_flag startup_lock = ATOMIC_FLAG_INIT;
static int initialized;

static void initialize(void) {
  while (atomic_flag_test_and_set(&startup_lock)) {}
  if (!initialized) { coap_startup(); initialized = 1; }
  atomic_flag_clear(&startup_lock);
}

static int replace_bytes(uint8_t **destination, const uint8_t *source, uint64_t size) {
  if (size > CC_MAX_BODY || (size && !source)) return 0;
  uint8_t *copy = size ? malloc((size_t)size) : NULL;
  if (size && !copy) return 0;
  if (size) memcpy(copy, source, (size_t)size);
  free(*destination); *destination = copy;
  return 1;
}

static int resolve_address(const char *host, uint16_t port, coap_address_t *address) {
  struct addrinfo hints = {0}, *result = NULL;
  hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM;
  if (getaddrinfo(host, NULL, &hints, &result) || !result) return 0;
  struct addrinfo *entry;
  for (entry = result; entry; entry = entry->ai_next) {
    if (entry->ai_family == AF_INET || entry->ai_family == AF_INET6) break;
  }
  if (!entry || entry->ai_addrlen > sizeof(address->addr)) { freeaddrinfo(result); return 0; }
  coap_address_init(address);
  address->size = (socklen_t)entry->ai_addrlen;
  memcpy(&address->addr, entry->ai_addr, entry->ai_addrlen);
  coap_address_set_port(address, port);
  freeaddrinfo(result);
  return 1;
}

typedef struct {
  char *uri;
  int32_t method, type, format, port, mid;
  uint32_t timeout_ms, retransmit, block_mode;
  uint8_t *payload, token[8], *response;
  size_t payload_size, token_size, response_size;
  int token_set, used, done, status, response_code;
  char error[192];
  coap_optlist_t *options;
} client_t;

static int fail(client_t *client, int status, const char *message) {
  client->status = status;
  snprintf(client->error, sizeof(client->error), "%s", message);
  return status;
}

void *cc_client_new(const char *uri, int32_t method, int32_t type) {
  initialize();
  if (!uri || method < 1 || method > 3 || type < 0 || type > 1) return NULL;
  client_t *client = calloc(1, sizeof(*client));
  if (!client) return NULL;
  client->uri = malloc(strlen(uri) + 1);
  if (!client->uri) { free(client); return NULL; }
  strcpy(client->uri, uri);
  client->method = method; client->type = type;
  client->format = client->port = client->mid = -1;
  client->timeout_ms = 10000; client->retransmit = 4;
  client->block_mode = COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY;
  return client;
}

int32_t cc_client_set_uint(void *handle, int32_t key, int64_t value) {
  client_t *c = handle;
  if (!c || c->used) return -1;
  switch (key) {
    case 1: if (value < 0 || value > 65535) break; c->format = (int32_t)value; return 0;
    case 2: if (value < 1 || value > 65535) break; c->port = (int32_t)value; return 0;
    case 3: if (value < 0 || value > 65535) break; c->mid = (int32_t)value; return 0;
    case 4: if (value < 1 || value > 600000) break; c->timeout_ms = (uint32_t)value; return 0;
    case 5: if (value < 0 || value > 8) break; c->retransmit = (uint32_t)value; return 0;
    case 6: if (value != 0 && value != 1 && value != 3) break; c->block_mode = (uint32_t)value; return 0;
    default: break;
  }
  return fail(c, 1, "invalid client configuration");
}

int32_t cc_client_set_payload(void *handle, const uint8_t *data, uint64_t size) {
  client_t *c = handle;
  if (!c || c->used || !replace_bytes(&c->payload, data, size)) return -1;
  c->payload_size = (size_t)size; return 0;
}

int32_t cc_client_set_token(void *handle, const uint8_t *data, uint64_t size) {
  client_t *c = handle;
  if (!c || c->used || size > 8 || (size && !data)) return -1;
  if (size) memcpy(c->token, data, (size_t)size);
  c->token_size = (size_t)size; c->token_set = 1; return 0;
}

int32_t cc_client_add_option(void *handle, uint16_t number, const uint8_t *data, uint64_t size) {
  client_t *c = handle;
  /* URI and payload options are owned by the URI parser / libcoap; option 0 is
   * reserved, Observe 6 needs a subscription API we do not expose, and Size2 28
   * / Size1 60 body sizing belongs to libcoap blockwise handling. */
  if (!c || c->used || size > 65804 || (size && !data) || number == 0 || number == 3 || number == 6 ||
      number == 7 || number == 11 || number == 12 || number == 15 || number == 23 || number == 27 ||
      number == 28 || number == 60) return -1;
  coap_optlist_t *option = coap_new_optlist(number, (size_t)size, data);
  if (!option) return -1;
  return coap_insert_optlist(&c->options, option) ? 0 : -1;
}

static coap_response_t received(coap_session_t *session, const coap_pdu_t *sent,
                                const coap_pdu_t *response, const coap_mid_t mid) {
  (void)sent; (void)mid;
  client_t *c = coap_session_get_app_data(session);
  coap_bin_const_t token = coap_pdu_get_token(response);
  if (!c || token.length != c->token_size ||
      (token.length && memcmp(token.s, c->token, token.length))) return COAP_RESPONSE_FAIL;
  size_t length = 0, offset = 0, total = 0; const uint8_t *data = NULL;
  c->response_code = coap_pdu_get_code(response);
  if (coap_get_data_large(response, &length, &data, &offset, &total)) {
    if (offset != 0 || length != total || !replace_bytes(&c->response, data, length)) {
      fail(c, 2, "response exceeds limit or block body is incomplete");
    } else { c->response_size = length; }
  }
  c->done = 1;
  return COAP_RESPONSE_OK;
}

static void rejected(coap_session_t *session, const coap_pdu_t *sent,
                     const coap_nack_reason_t reason, const coap_mid_t mid) {
  (void)sent; (void)mid;
  client_t *c = coap_session_get_app_data(session);
  if (c) { fail(c, 3, reason == COAP_NACK_RST ? "peer reset the request" : "delivery failed or retries exhausted"); c->done = 1; }
}

int32_t cc_client_run(void *handle) {
  client_t *c = handle;
  if (!c || c->used) return -1;
  c->used = 1; c->status = 0;
  coap_uri_t uri; coap_address_t destination;
  coap_context_t *context = NULL; coap_session_t *session = NULL;
  coap_pdu_t *pdu = NULL; coap_optlist_t *options = NULL;
  char *host = NULL; uint8_t *scratch = NULL;
  if (coap_split_uri((const uint8_t *)c->uri, strlen(c->uri), &uri) < 0 || uri.scheme != COAP_URI_SCHEME_COAP) {
    fail(c, 1, "only a valid coap:// UDP URI is supported"); goto done;
  }
  host = malloc(uri.host.length + 1);
  if (!host) { fail(c, 2, "allocation failed"); goto done; }
  memcpy(host, uri.host.s, uri.host.length); host[uri.host.length] = '\0';
  if (c->port >= 0) uri.port = (uint16_t)c->port;
  if (!resolve_address(host, uri.port, &destination)) { fail(c, 2, "host resolution failed"); goto done; }
  context = coap_new_context(NULL);
  if (!context) { fail(c, 2, "cannot create CoAP context"); goto done; }
  coap_context_set_block_mode(context, c->block_mode);
  coap_register_response_handler(context, received);
  coap_register_nack_handler(context, rejected);
  session = coap_new_client_session(context, NULL, &destination, COAP_PROTO_UDP);
  if (!session) { fail(c, 2, "cannot create UDP session"); goto done; }
  coap_session_set_app_data(session, c);
  coap_session_set_max_retransmit(session, (uint16_t)c->retransmit);
  if (!c->token_set) {
    c->token_size = sizeof(c->token);
    coap_session_new_token(session, &c->token_size, c->token);
  }
  pdu = coap_new_pdu((coap_pdu_type_t)c->type, (coap_pdu_code_t)c->method, session);
  if (!pdu || !coap_add_token(pdu, c->token_size, c->token)) { fail(c, 2, "cannot create request PDU"); goto done; }
  if (c->mid >= 0) coap_pdu_set_mid(pdu, (coap_mid_t)c->mid);
  size_t scratch_size = strlen(c->uri) * 2 + 32;
  scratch = malloc(scratch_size);
  if (!scratch || coap_uri_into_options(&uri, &destination, &options, 1, scratch, scratch_size) < 0) {
    fail(c, 1, "cannot encode URI options"); goto done;
  }
  for (coap_optlist_t *o = c->options; o; o = o->next) {
    coap_optlist_t *copy = coap_new_optlist(o->number, o->length, o->data);
    if (!copy || !coap_insert_optlist(&options, copy)) { fail(c, 2, "option allocation failed"); goto done; }
  }
  if (c->format >= 0) {
    uint8_t encoded[2]; size_t length = coap_encode_var_safe(encoded, sizeof(encoded), (unsigned)c->format);
    coap_optlist_t *o = coap_new_optlist(COAP_OPTION_CONTENT_FORMAT, length, encoded);
    if (!o || !coap_insert_optlist(&options, o)) { fail(c, 2, "format allocation failed"); goto done; }
  }
  if ((options && !coap_add_optlist_pdu(pdu, &options)) ||
      (c->payload_size && !coap_add_data_large_request(session, pdu, c->payload_size, c->payload, NULL, NULL))) {
    fail(c, 2, "cannot add request options or payload"); goto done;
  }
  coap_mid_t sent_mid = coap_send(session, pdu); pdu = NULL; /* coap_send owns the PDU. */
  if (sent_mid == COAP_INVALID_MID) { fail(c, 2, "cannot send request"); goto done; }
  coap_tick_t start, now; coap_ticks(&start);
  while (!c->done) {
    coap_ticks(&now);
    uint64_t elapsed = (uint64_t)(now - start) * 1000 / COAP_TICKS_PER_SECOND;
    if (elapsed >= c->timeout_ms) { fail(c, 4, "request timed out"); break; }
    uint32_t remaining = c->timeout_ms - (uint32_t)elapsed;
    if (coap_io_process(context, remaining > 100 ? 100 : remaining) < 0) { fail(c, 2, "CoAP I/O failed"); break; }
  }
done:
  if (pdu) coap_delete_pdu(pdu);
  if (session) coap_session_release(session);
  if (context) coap_free_context(context);
  coap_delete_optlist(options); free(host); free(scratch);
  return c->status;
}

int32_t cc_client_response_code(void *handle) { return handle ? ((client_t *)handle)->response_code : 0; }
uint64_t cc_client_response_size(void *handle) { return handle ? ((client_t *)handle)->response_size : 0; }
const uint8_t *cc_client_response_data(void *handle) { return handle ? ((client_t *)handle)->response : NULL; }
const char *cc_client_error(void *handle) { return handle ? ((client_t *)handle)->error : "invalid client"; }
void cc_client_free(void *handle) {
  client_t *c = handle; if (!c) return;
  coap_delete_optlist(c->options); free(c->uri); free(c->payload); free(c->response); free(c);
}

typedef struct server_t server_t;
typedef struct resource_t {
  struct resource_t *next; server_t *owner; char *path;
  coap_resource_t *resource; int64_t get_id, post_id;
} resource_t;
struct server_t { coap_context_t *context; resource_t *resources; cc_handler handler; char error[192]; };
typedef struct {
  resource_t *resource; coap_session_t *session;
  const coap_pdu_t *request; coap_pdu_t *response; const coap_string_t *query;
  const uint8_t *payload; size_t payload_size; int replied;
} exchange_t;

static void dispatched(coap_resource_t *resource, coap_session_t *session,
                       const coap_pdu_t *request, const coap_string_t *query,
                       coap_pdu_t *response) {
  resource_t *r = coap_resource_get_userdata(resource);
  exchange_t exchange = {0};
  exchange.resource = r; exchange.session = session; exchange.request = request;
  exchange.response = response; exchange.query = query;
  size_t offset = 0, total = 0;
  coap_get_data_large(request, &exchange.payload_size, &exchange.payload, &offset, &total);
  if (offset || exchange.payload_size > CC_MAX_BODY || exchange.payload_size != total) {
    coap_pdu_set_code(response, COAP_RESPONSE_CODE_REQUEST_TOO_LARGE); return;
  }
  int64_t id = coap_pdu_get_code(request) == COAP_REQUEST_CODE_GET ? r->get_id : r->post_id;
  if (r->owner->handler && id > 0) r->owner->handler(id, &exchange);
  if (!exchange.replied) coap_pdu_set_code(response, COAP_RESPONSE_CODE_INTERNAL_ERROR);
}

void *cc_server_new(const char *address, uint16_t port) {
  initialize();
  if (!address || !port) return NULL;
  coap_address_t listen_address;
  if (!resolve_address(address, port, &listen_address)) return NULL;
  server_t *s = calloc(1, sizeof(*s)); if (!s) return NULL;
  s->context = coap_new_context(NULL);
  if (!s->context || !coap_new_endpoint(s->context, &listen_address, COAP_PROTO_UDP)) {
    if (s->context) coap_free_context(s->context); free(s); return NULL;
  }
  coap_context_set_block_mode(s->context, COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY);
  return s;
}

int32_t cc_server_register(void *handle, const char *path, int32_t method, int64_t id) {
  server_t *s = handle;
  if (!s || !path || id <= 0 || (method != 1 && method != 2)) return -1;
  if (*path == '/') ++path;
  resource_t *r = s->resources;
  while (r && strcmp(r->path, path)) r = r->next;
  if (!r) {
    r = calloc(1, sizeof(*r)); if (!r) return -1;
    r->path = malloc(strlen(path) + 1);
    if (!r->path) { free(r); return -1; }
    strcpy(r->path, path); r->owner = s;
    coap_str_const_t *uri = coap_new_str_const((const uint8_t *)path, strlen(path));
    r->resource = uri ? coap_resource_init(uri, COAP_RESOURCE_FLAGS_RELEASE_URI) : NULL;
    if (!r->resource) { if (uri) coap_delete_str_const(uri); free(r->path); free(r); return -1; }
    coap_resource_set_userdata(r->resource, r);
    coap_add_resource(s->context, r->resource);
    r->next = s->resources; s->resources = r;
  }
  if (method == 1) r->get_id = id; else r->post_id = id;
  coap_register_request_handler(r->resource, (coap_request_t)method, dispatched);
  return 0;
}

int32_t cc_server_poll(void *handle, uint32_t wait_ms, cc_handler handler) {
  server_t *s = handle; if (!s || !wait_ms || !handler) return -1;
  s->handler = handler;
  int status = coap_io_process(s->context, wait_ms);
  s->handler = NULL;
  if (status < 0) snprintf(s->error, sizeof(s->error), "server I/O failed");
  return status;
}
const char *cc_server_error(void *handle) { return handle ? ((server_t *)handle)->error : "invalid server"; }
void cc_server_free(void *handle) {
  server_t *s = handle; if (!s) return;
  coap_free_context(s->context);
  while (s->resources) { resource_t *r = s->resources; s->resources = r->next; free(r->path); free(r); }
  free(s);
}
int32_t cc_exchange_method(void *handle) { return coap_pdu_get_code(((exchange_t *)handle)->request); }
const char *cc_exchange_path(void *handle) { return ((exchange_t *)handle)->resource->path; }
const uint8_t *cc_exchange_payload(void *handle) { return ((exchange_t *)handle)->payload; }
uint64_t cc_exchange_payload_size(void *handle) { return ((exchange_t *)handle)->payload_size; }
const uint8_t *cc_exchange_query(void *handle) { exchange_t *e = handle; return e->query ? e->query->s : NULL; }
uint64_t cc_exchange_query_size(void *handle) { exchange_t *e = handle; return e->query ? e->query->length : 0; }
const uint8_t *cc_exchange_option(void *handle, uint16_t number) {
  coap_opt_iterator_t iterator; coap_opt_t *option = coap_check_option(((exchange_t *)handle)->request, number, &iterator);
  return option ? coap_opt_value(option) : NULL;
}
uint64_t cc_exchange_option_size(void *handle, uint16_t number) {
  coap_opt_iterator_t iterator; coap_opt_t *option = coap_check_option(((exchange_t *)handle)->request, number, &iterator);
  return option ? coap_opt_length(option) : 0;
}
static void release_body(coap_session_t *session, void *data) { (void)session; free(data); }
static coap_opt_t *option_at(void *handle, uint32_t index, uint16_t *number) {
  coap_opt_iterator_t iterator;
  if (!coap_option_iterator_init(((exchange_t *)handle)->request, &iterator, COAP_OPT_ALL)) return NULL;
  coap_opt_t *option;
  while ((option = coap_option_next(&iterator))) {
    if (index-- == 0) { *number = iterator.number; return option; }
  }
  return NULL;
}
int32_t cc_exchange_option_number_at(void *handle, uint32_t index) {
  uint16_t number; return option_at(handle, index, &number) ? number : -1;
}
const uint8_t *cc_exchange_option_data_at(void *handle, uint32_t index) {
  uint16_t number; coap_opt_t *option = option_at(handle, index, &number);
  return option ? coap_opt_value(option) : NULL;
}
uint64_t cc_exchange_option_size_at(void *handle, uint32_t index) {
  uint16_t number; coap_opt_t *option = option_at(handle, index, &number);
  return option ? coap_opt_length(option) : 0;
}
int32_t cc_exchange_reply(void *handle, int32_t code, uint16_t format, const uint8_t *data, uint64_t size) {
  exchange_t *e = handle;
  if (!e || e->replied || code < 64 || code > 191 || size > CC_MAX_BODY || (size && !data)) return -1;
  uint8_t *copy = NULL;
  if (!replace_bytes(&copy, data, size)) return -1;
  coap_pdu_set_code(e->response, (coap_pdu_code_t)code);
  if (!coap_add_data_large_response(e->resource->resource, e->session, e->request, e->response,
      e->query, format, -1, 0, (size_t)size, copy, release_body, copy)) {
    /* libcoap invokes release_body on failure too. */
    coap_pdu_set_code(e->response, COAP_RESPONSE_CODE_INTERNAL_ERROR); return -1;
  }
  e->replied = 1; return 0;
}
void cc_set_log(int32_t enabled) { initialize(); coap_set_log_level(enabled ? COAP_LOG_DEBUG : COAP_LOG_ERR); }

cc_client_run 的返回值表示传输/桥接状态:0 为已收到 CoAP 响应,1 为配置或 URI 错误,2 为创建、内存或 I/O 错误,3 为 NACK/交付失败,4 为等待截止超时。4.xx 和 5.xx 是正常收到的协议响应,仍然返回状态 0;随后读取响应 code。cc_client_set_payload 等 setter 的非零值表示配置失败。

等待计时从发送请求后开始。主机名解析使用同步 getaddrinfo,因此 setWaitSeconds 不限制 DNS 解析阶段的总耗时。

7. 编写仓颉 HAR

下面四个文件保存到 ohos-coap-app/coap/src/main/cangjie。模板的示例 index.cj 可保留;本文业务不调用其中的示例函数。

7.1 native.cj:声明 ABI 并集中处理内存

package ohos_app_cangjie_coap

foreign {
    func cc_client_new(uri: CString, method: Int32, kind: Int32): CPointer<Unit>

    func cc_client_set_uint(h: CPointer<Unit>, key: Int32, value: Int64): Int32

    func cc_client_set_payload(h: CPointer<Unit>, data: CPointer<UInt8>, size: UInt64): Int32

    func cc_client_set_token(h: CPointer<Unit>, data: CPointer<UInt8>, size: UInt64): Int32

    func cc_client_add_option(h: CPointer<Unit>, number: UInt16, data: CPointer<UInt8>, size: UInt64): Int32

    func cc_client_run(h: CPointer<Unit>): Int32

    func cc_client_response_code(h: CPointer<Unit>): Int32

    func cc_client_response_size(h: CPointer<Unit>): UInt64

    func cc_client_response_data(h: CPointer<Unit>): CPointer<UInt8>

    func cc_client_error(h: CPointer<Unit>): CString

    func cc_client_free(h: CPointer<Unit>): Unit

    func cc_server_new(address: CString, port: UInt16): CPointer<Unit>

    func cc_server_register(h: CPointer<Unit>, path: CString, method: Int32, id: Int64): Int32

    func cc_server_poll(h: CPointer<Unit>, ms: UInt32, handler: CFunc<(Int64, CPointer<Unit>) -> Unit>): Int32

    func cc_server_error(h: CPointer<Unit>): CString

    func cc_server_free(h: CPointer<Unit>): Unit

    func cc_exchange_method(h: CPointer<Unit>): Int32

    func cc_exchange_path(h: CPointer<Unit>): CString

    func cc_exchange_payload(h: CPointer<Unit>): CPointer<UInt8>

    func cc_exchange_payload_size(h: CPointer<Unit>): UInt64

    func cc_exchange_query(h: CPointer<Unit>): CPointer<UInt8>

    func cc_exchange_query_size(h: CPointer<Unit>): UInt64

    func cc_exchange_option_number_at(h: CPointer<Unit>, index: UInt32): Int32

    func cc_exchange_option_data_at(h: CPointer<Unit>, index: UInt32): CPointer<UInt8>

    func cc_exchange_option_size_at(h: CPointer<Unit>, index: UInt32): UInt64

    func cc_exchange_reply(h: CPointer<Unit>, code: Int32, format: UInt16, data: CPointer<UInt8>, size: UInt64): Int32

    func cc_set_log(enabled: Int32): Unit
}

func copyBytes(pointer: CPointer<UInt8>, size: UInt64): Array<UInt8> {
    if (size > 1048576 || (size > 0 && pointer.isNull())) {
        throw Exception("invalid native buffer")
    }
    return Array<UInt8>(Int64(size), {i => unsafe { pointer.read(i) }})
}

// Build the closure and length before pinning. The closure must only call C.
func withBytes(bytes: Array<UInt8>, call: (CPointer<UInt8>, UInt64) -> Int32): Int32 {
    let size = UInt64(bytes.size)
    if (size == 0) {
        return call(CPointer<UInt8>(), 0)
    }
    unsafe {
        let pin = acquireArrayRawData(bytes)
        try {
            return call(pin.pointer, size)
        } finally {
            releaseArrayRawData(pin)
        }
    }
}

func check(status: Int32): Unit {
    if (status != 0) {
        throw Exception("invalid CoAP configuration (${status})")
    }
}

7.2 client.cj:请求构建与响应

CoapClient 是可变构建器,应由一个任务独占。setter 保存配置,请求时创建新的原生句柄。二进制响应读取 payload;只有确认内容是合法 UTF-8 时才读取 text。

package ohos_app_cangjie_coap

import std.collection.ArrayList

public class CoapResponse {
    public let code: Int32
    public let payload: Array<UInt8>
    public init(code: Int32, payload: Array<UInt8>) {
        this.code = code
        this.payload = payload
    }
    public prop isSuccess: Bool {
        get() {
            code >= 64 && code < 96
        }
    }
    public prop text: String {
        get() {
            String.fromUtf8(payload)
        }
    }
}

public class CoapOption {
    public let number: UInt16
    public let value: Array<UInt8>
    public init(number: UInt16, value: Array<UInt8>) {
        this.number = number
        this.value = value.clone()
    }
}

// A mutable request builder belongs to one task. Each request creates a fresh C handle.
public class CoapClient {
    private let settings = ArrayList<(Int32, Int64)>()
    private let options = ArrayList<CoapOption>()
    private var payload = Array<UInt8>()
    private var token: ?Array<UInt8> = None

    public init() {}
    public func setFormat(value: UInt16): Unit {
        settings.add((1, Int64(value)))
    }
    public func setPort(value: UInt16): Unit {
        settings.add((2, Int64(value)))
    }
    public func setMID(value: UInt16): Unit {
        settings.add((3, Int64(value)))
    }
    public func setWaitSeconds(value: Int64): Unit {
        if (value < 1 || value > 600) {
            throw Exception("wait seconds must be 1..600")
        }
        settings.add((4, value * 1000))
    }
    public func setRetryCount(value: Int64): Unit {
        settings.add((5, value))
    }
    public func setBlockMode(value: Int64): Unit {
        settings.add((6, value))
    }
    public func setPayload(value: String): Unit {
        payload = value.toArray()
    }
    public func setBinaryPayload(value: Array<UInt8>): Unit {
        payload = value.clone()
    }
    public func setToken(value: Array<UInt8>): Unit {
        token = Some(value.clone())
    }
    public func setCustomOption(number: UInt16, value: Array<UInt8>): Unit {
        options.add(CoapOption(number, value))
    }

    // method: GET=1, POST=2, PUT=3; kind: CON=0, NON=1. Run in spawn from a UI.
    public func requestUri(uri: String, method!: Int32 = 1, kind!: Int32 = 0): CoapResponse {
        if (uri.contains("\u{0}")) {
            throw Exception("URI contains NUL")
        }
        let cUri = unsafe { LibC.mallocCString(uri) }
        let handle = try {
            unsafe { cc_client_new(cUri, method, kind) }
        } finally {
            unsafe { LibC.free(cUri) }
        }
        if (handle.isNull()) {
            throw Exception("cannot create CoAP client")
        }
        try {
            for ((key, value) in settings) {
                check(unsafe { cc_client_set_uint(handle, key, value) })
            }
            check(withBytes(payload, {p, n => unsafe { cc_client_set_payload(handle, p, n) }}))
            if (let Some(bytes) <- token) {
                check(withBytes(bytes, {p, n => unsafe { cc_client_set_token(handle, p, n) }}))
            }
            for (option in options) {
                let number = option.number
                check(withBytes(option.value, {p, n => unsafe { cc_client_add_option(handle, number, p, n) }}))
            }
            let status = unsafe { cc_client_run(handle) }
            if (status != 0) {
                let message = unsafe { cc_client_error(handle).toString() }
                throw Exception("CoAP request failed (${status}): ${message}")
            }
            let code = unsafe { cc_client_response_code(handle) }
            let bytes = copyBytes(unsafe { cc_client_response_data(handle) }, unsafe { cc_client_response_size(handle) })
            return CoapResponse(code, bytes)
        } finally {
            unsafe { cc_client_free(handle) }
        }
    }
}

public func setNativeLog(enabled: Bool): Unit {
    unsafe { cc_set_log(if (enabled) {
        1
    } else {
        0
    }) }
}

setToken 最大 8 字节;setWaitSeconds 为 1–600 秒;重传次数为 0–8;端口必须非零。setBlockMode 接受 0、1、3,但本文公开响应对象只交付一次完整正文:需要自动重组时保持默认 3。模式 1 下收到不完整块体会报错,模式 0 不提供自动大正文分块。默认等待 10 秒、最大重传 4 次,等待截止可能早于全部重传完成。

协议响应码使用 CoAP 的整数编码。例如 2.05 为 69、2.04 为 68、4.04 为 132、4.05 为 133、5.00 为 160。isSuccess 判断 2.xx;不要用 HTTP 状态码 200/404 比较。

7.3 server.cj:资源回调与可控停止

服务端默认只绑定 127.0.0.1,适合回环检查。要接受局域网请求,可以显式使用设备接口地址或 0.0.0.0。每个实例只运行一次,先注册所有 GET/POST 路由,再 spawn 运行。回调串行执行,避免在回调中做长时间阻塞工作。

package ohos_app_cangjie_coap

import std.collection.{ArrayList, HashMap}
import std.sync.{Mutex, AtomicBool}

public class CoapRequest {
    public let method: Int32
    public let path: String
    public let payload: Array<UInt8>
    public let query: Array<UInt8>
    public let options: Array<CoapOption>
    public init(method: Int32, path: String, payload: Array<UInt8>, query: Array<UInt8>, options: Array<CoapOption>) {
        this.method = method
        this.path = path
        this.payload = payload
        this.query = query
        this.options = options
    }
    public prop text: String {
        get() {
            String.fromUtf8(payload)
        }
    }
    public prop queryText: String {
        get() {
            String.fromUtf8(query)
        }
    }
}

public class CoapReply {
    public let code: Int32
    public let format: UInt16
    public let payload: Array<UInt8>
    public init(code: Int32, payload: Array<UInt8>, format!: UInt16 = 0) {
        this.code = code
        this.format = format
        this.payload = payload
    }
    public init(text: String, code!: Int32 = 69, format!: UInt16 = 0) {
        this.code = code
        this.format = format
        this.payload = text.toArray()
    }
}

let registryLock = Mutex()
let registry = HashMap<Int64, (CoapRequest) -> CoapReply>()
var nextID: Int64 = 1

@C
func dispatch(id: Int64, exchange: CPointer<Unit>): Unit {
    // No Cangjie exception may cross a C callback boundary.
    try {
        let handler = synchronized(registryLock) {
            registry.get(id)
        }
        if (let Some(call) <- handler) {
            let options = ArrayList<CoapOption>()
            var index: UInt32 = 0
            while (true) {
                let number = unsafe { cc_exchange_option_number_at(exchange, index) }
                if (number < 0) {
                    break
                }
                let bytes = copyBytes(unsafe { cc_exchange_option_data_at(exchange, index) },
                    unsafe { cc_exchange_option_size_at(exchange, index) })
                options.add(CoapOption(UInt16(number), bytes));
                index++
            }
            let request = CoapRequest(unsafe { cc_exchange_method(exchange) },
                unsafe { cc_exchange_path(exchange).toString() },
                copyBytes(unsafe { cc_exchange_payload(exchange) }, unsafe { cc_exchange_payload_size(exchange) }),
                copyBytes(unsafe { cc_exchange_query(exchange) }, unsafe { cc_exchange_query_size(exchange) }),
                options.toArray())
            let reply = call(request)
            let code = reply.code
            let format = reply.format
            check(withBytes(reply.payload, {p, n => unsafe { cc_exchange_reply(exchange, code, format, p, n) }}))
        }
    } catch (error: Exception) {
        // C supplies 5.00 when no response was produced.
        println("CoAP handler failed: ${error.message}")
    }
}

public class CoapServer {
    private let address: String
    private let port: UInt16
    private let lock = Mutex()
    private let routes = ArrayList<(String, Int32, (CoapRequest) -> CoapReply)>()
    private let stopping = AtomicBool(false)
    private let running = AtomicBool(false)
    private var started = false

    public init(address!: String = "127.0.0.1", port!: UInt16 = 5683) {
        if (address.contains("\u{0}")) {
            throw Exception("address contains NUL")
        }
        this.address = address
        this.port = port
    }
    public prop isRunning: Bool {
        get() {
            running.load()
        }
    }
    public func register(path: String, method: Int32, handler: (CoapRequest) -> CoapReply): Unit {
        synchronized(lock) {
            if (started || (method != 1 && method != 2) || path.contains("\u{0}")) {
                throw Exception("register GET/POST resources before run")
            }
            routes.add((path, method, handler))
        }
    }
    public func stop(): Unit {
        stopping.store(true)
    }

    // One run per server; caller must stop and join its Future before discarding it.
    public func run(): Unit {
        synchronized(lock) {
            if (started) {
                throw Exception("server was already started")
            }
            started = true
        }
        let cAddress = unsafe { LibC.mallocCString(address) }
        let handle = try {
            unsafe { cc_server_new(cAddress, port) }
        } finally {
            unsafe { LibC.free(cAddress) }
        }
        if (handle.isNull()) {
            throw Exception("cannot bind CoAP server ${address}:${port}")
        }
        let ids = ArrayList<Int64>()
        try {
            for ((path, method, handler) in routes) {
                let id = synchronized(registryLock) {
                    let value = nextID;
                    nextID++;
                    registry.add(value, handler);
                    value
                }
                ids.add(id)
                let cPath = unsafe { LibC.mallocCString(path) }
                try {
                    check(unsafe { cc_server_register(handle, cPath, method, id) })
                } finally {
                    unsafe { LibC.free(cPath) }
                }
            }
            running.store(true)
            while (!stopping.load()) {
                if (unsafe { cc_server_poll(handle, 100, dispatch) } < 0) {
                    throw Exception(unsafe { cc_server_error(handle).toString() })
                }
            }
        } finally {
            running.store(false)
            unsafe { cc_server_free(handle) }
            synchronized(registryLock) {
                for (id in ids) {
                    registry.remove(id)
                }
            }
        }
    }
}

queryText 来自 libcoap 重建的查询字符串,会保留必要的百分号转义。例如本文请求 a=one%20two&b=3 得到相同的编码形式,不会自动变成含空格的字符串。options 保存每一个选项的编号和原始字节值;需要业务解析时自行按照该选项协议解释,不能把所有选项都当 UTF-8 文本。

7.4 demo.cj:可重复的仓颉回环检查

这个函数启动本机服务端,验证根路径、中文 GET、二进制 POST、8 KB Block1/Block2、查询、NON 和协议错误码,最后保证 stop() 与任务等待配对。端口 15684 必须空闲。

package ohos_app_cangjie_coap

func expect(value: Bool, label: String): Unit {
    if (!value) {
        throw Exception("check failed: ${label}")
    }
}

public func runLoopbackChecks(): String {
    let server = CoapServer(port: 15684)
    let large = Array<UInt8>(8192, {i => UInt8(i % 251)})
    server.register("", 1, {_ => CoapReply("root")})
    server.register("hello", 1, {_ => CoapReply("你好,仓颉与 libcoap")})
    server.register("echo", 2, {request => CoapReply(68, request.payload, format: 42)})
    server.register("large", 1, {_ => CoapReply(69, large, format: 42)})
    server.register("query", 1, {request => CoapReply(request.queryText)})
    let task = spawn {=> server.run()}
    try {
        var attempts = 0
        while (!server.isRunning && attempts < 100) {
            sleep(20 * Duration.millisecond);
            attempts++
        }
        expect(server.isRunning, "server ready")
        let base = "coap://127.0.0.1:15684/"
        expect(CoapClient().requestUri(base).text == "root", "root URI")
        expect(CoapClient().requestUri(base + "hello").text == "你好,仓颉与 libcoap", "UTF-8 GET")
        let client = CoapClient()
        client.setFormat(42);
        client.setMID(1234);
        client.setToken([1, 2, 3])
        client.setWaitSeconds(10);
        client.setRetryCount(2);
        client.setBinaryPayload(large)
        let echoed = client.requestUri(base + "echo", method: 2)
        expect(echoed.code == 68 && echoed.payload.size == large.size, "Block1 POST size")
        for (i in 0..large.size) {
            expect(echoed.payload[i] == large[i], "Block1 POST bytes")
        }
        let response = CoapClient().requestUri(base + "large")
        expect(response.payload.size == large.size, "Block2 GET size")
        for (i in 0..large.size) {
            expect(response.payload[i] == large[i], "Block2 GET bytes")
        }
        expect(CoapClient().requestUri(base + "query?a=one%20two&b=3").text == "a=one%20two&b=3", "URI query")
        expect(CoapClient().requestUri(base + "hello", kind: 1).isSuccess, "NON GET")
        expect(CoapClient().requestUri(base + "missing").code == 132, "4.04 response")
        expect(CoapClient().requestUri(base + "hello", method: 3).code == 133, "PUT reaches server, 4.05")
        return "PASS: root, UTF-8 GET, binary POST, Block1/Block2 8192 bytes, query, NON, 4.04, PUT/4.05"
    } finally {
        server.stop();
        task.get()
    }
}

8. 接入构建配置

8.1 HAR 的 cjpm.toml

将 coap/cjpm.toml 核对为下面的内容。最关键的是两段 目标平台自己的 ffi.c。原生库名称写 coap_cj_bridge,CJPM 会查找对应平台的 libcoap_cj_bridge.so,路径相对于库模块。

只在编译参数中手工追加 -L ./libs/... -lcoap_cj_bridge,可能让 HAR 单独构建成功,却在应用编译依赖库时按另一工作目录解析路径而失败。目标级 ffi.c 是这个工程通过两种构建的配置;仓颉 1.1 发布说明 记录了平台隔离的 FFI 配置支持,本文实际使用并验证的是 1.2 SDK。

[package]
  cjc-version = "1.2.0"
  compile-option = "--dy-std --cfg=\"${COMPILE_CONDITION}\""
  override-compile-option = "${OVERRIDE_COMPILE_OPTION}"
  description = "CangjieUI Application"
  name = "ohos_app_cangjie_coap"
  output-type = "dynamic"
  src-dir = "./src/main/cangjie"
  target-dir = ""
  version = "1.0.0"

[profile]
  [profile.build]
    incremental = true
    lto = ""
    [profile.build.combined]
      ohos_app_cangjie_coap = "dynamic"
  [profile.customized-option]
    debug = "-g -Woff all -Won apilevel-check"
    release = "--fast-math -O2 -s -Woff all -Won apilevel-check"
    asan = "--sanitize=address -lclang_rt.asan"
  [profile.test]

[target.aarch64-linux-ohos]
  compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/aarch64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
  [target.aarch64-linux-ohos.ffi.c]
    coap_cj_bridge = { path = "./libs/arm64-v8a" }
  [target.aarch64-linux-ohos.bin-dependencies]
    path-option = ["${AARCH64_LIBS}", "${AARCH64_MACRO_LIBS}", "${AARCH64_KIT_LIBS}"]
    package-option = { }

[target.x86_64-linux-ohos]
  compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/x86_64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
  [target.x86_64-linux-ohos.ffi.c]
    coap_cj_bridge = { path = "./libs/x86_64" }
  [target.x86_64-linux-ohos.bin-dependencies]
    path-option = ["${X86_64_OHOS_LIBS}", "${X86_64_OHOS_MACRO_LIBS}", "${X86_64_OHOS_KIT_LIBS}"]

[target.x86_64-w64-mingw32.bin-dependencies]
  path-option = ["${X86_64_LIBS}", "${X86_64_MACRO_LIBS}", "${X86_64_KIT_LIBS}"]
  package-option = { }

8.2 HAR 构建配置与模块清单

coap/build-profile.json5:

{
  "apiType": "stageMode",
  "buildOption": {
    "cangjieOptions": {
      "path": "./cjpm.toml",
      "abiFilters": ["arm64-v8a", "x86_64"]
    }
  },
  "buildOptionSet": [
  ],
  "targets": [
    {
      "name": "default"
    },
    {
      "name": "ohosTest"
    }
  ]
}

coap/src/main/module.json5:

{
  "module": {
    "name": "coap",
    "type": "har",
    "deviceTypes": [
      "default"
    ]
  }
}

coap/oh-package.json5:

{
  "name": "coap",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "types": "./Index.d.ets",
  "license": "Apache-2.0",
  "dependencies": {
  }
}

8.3 应用依赖和网络权限

entry/oh-package.json5 用本地模块依赖。安装依赖后,IDE/Hvigor 会把仓颉包依赖同步到 entry/cjpm.toml。

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": { "coap": "file:../coap" }
}

同步后的 entry/cjpm.toml 如下。${coap} 是 HarmonyOS 构建集成注入的模块路径;不要在纯 Windows cjpm build 环境中期待它自动存在,也不要重复添加第二个 [dependencies] 表。

[package]
  cjc-version = "1.2.0"
  compile-option = "--dy-std --cfg=\"${COMPILE_CONDITION_ENTRY}\""
  override-compile-option = "${OVERRIDE_COMPILE_OPTION}"
  description = "CangjieUI Application"
  name = "ohos_app_cangjie_entry"
  output-type = "dynamic"
  src-dir = "./src/main/cangjie"
  target-dir = ""
  version = "1.0.0"

[profile]
  [profile.build]
    incremental = true
    lto = ""
    [profile.build.combined]
      ohos_app_cangjie_entry = "dynamic"
  [profile.customized-option]
    debug = "-g -Woff all -Won apilevel-check"
    release = "--fast-math -O2 -s -Woff all -Won apilevel-check"
    asan = "--sanitize=address -lclang_rt.asan"
  [profile.test]

[target]
  [target.aarch64-linux-ohos]
    compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/aarch64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
    [target.aarch64-linux-ohos.bin-dependencies]
      path-option = [ "${AARCH64_LIBS}", "${AARCH64_MACRO_LIBS}", "${AARCH64_KIT_LIBS}" ]
      [target.aarch64-linux-ohos.bin-dependencies.package-option]
  [target.x86_64-linux-ohos]
    compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/x86_64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
    [target.x86_64-linux-ohos.bin-dependencies]
      path-option = [ "${X86_64_OHOS_LIBS}", "${X86_64_OHOS_MACRO_LIBS}", "${X86_64_OHOS_KIT_LIBS}" ]
  [target.x86_64-w64-mingw32]
    [target.x86_64-w64-mingw32.bin-dependencies]
      path-option = [ "${X86_64_LIBS}", "${X86_64_MACRO_LIBS}", "${X86_64_KIT_LIBS}" ]
      [target.x86_64-w64-mingw32.bin-dependencies.package-option]

[dependencies]
  [dependencies.ohos_app_cangjie_coap]
    path = "${coap}"

entry/build-profile.json5 与 HAR 使用同样的架构过滤;nativeLib.filter.enableOverride 处理应用收集库时的同名原生库覆盖。

{
  "apiType": "stageMode",
  "buildOption": {
    "cangjieOptions": {
      "path": "./cjpm.toml",
      "abiFilters": ["arm64-v8a", "x86_64"]
    },
    "nativeLib": {
      "filter": {
        "enableOverride": true
      }
    }
  },
  "buildOptionSet": [
  ],
  "targets": [
    {
      "name": "default"
    }
  ]
}

entry/src/main/module.json5 加入 ohos.permission.INTERNET。srcEntry 的包名必须与后面的 Ability 代码一致。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "requestPermissions": [{ "name": "ohos.permission.INTERNET" }],
    "srcEntry": "ohos_app_cangjie_entry.MyAbilityStage",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "ohos_app_cangjie_entry.MainAbility",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

8.4 根配置

工程根 build-profile.json5 中同时注册 entry 和 coap。下面是未配置个人签名的版本;自己的证书、profile 和密码只在本机配置,不复制进公开工程。

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "targetSdkVersion": "26.0.0",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,
            "useNormalizedOHMUrl": true
          }
        }
      }
    ],
    "buildModeSet": [
      {
        "name": "debug",
      },
      {
        "name": "release"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ]
        }
      ]
    },
    {
      "name": "coap",
      "srcPath": "./coap"
    }
  ]
}

根 oh-package.json5 保留模板依赖:

{
  "modelVersion": "26.0.0",
  "description": "Please describe the basic information.",
  "dependencies": {
  },
  "devDependencies": {
    "@ohos/hypium": "1.0.25",
    "@ohos/hamock": "1.0.0"
  }
}

根 hvigor/hvigor-config.json5 使用向导生成的 modelVersion: "26.0.0"。根 hvigorfile.ts 导出 appTasks;entry/hvigorfile.ts 导出 hapTasks;coap/hvigorfile.ts 导出 harTasks。这些文件由模板生成即可,不需要额外安装上游 @ohos/coap,否则会引入原来的 ArkTS/NAPI 实现。

9. 应用界面与任务调度

覆盖 entry/src/main/cangjie/index.cj。点击界面文字后,在 spawn 中执行网络检查,完成后用 ohos.base.launch 更新 UI 状态。同步网络请求和服务端轮询不能放在 UI 线程中。

package ohos_app_cangjie_entry

import kit.ArkUI.LengthProp
import kit.ArkUI.Column
import kit.ArkUI.Row
import kit.ArkUI.Text
import kit.ArkUI.CustomView
import kit.ArkUI.CJEntry
import kit.ArkUI.loadNativeView
import kit.ArkUI.FontWeight
import kit.ArkUI.SubscriberManager
import kit.ArkUI.ObservedProperty
import kit.ArkUI.LocalStorage
import ohos.arkui.state_macro_manage.Entry
import ohos.arkui.state_macro_manage.Component
import ohos.arkui.state_macro_manage.State
import ohos.base.launch
import ohos_app_cangjie_coap.runLoopbackChecks

@Entry
@Component
class EntryView {
    @State
    var message: String = "点击运行 CoAP 回环检查"
    @State
    var busy: Bool = false

    func runChecks(): Unit {
        if (busy) {
            return
        }
        busy = true
        message = "正在验证仓颉 + libcoap…"
        spawn {
            =>
                let result = try {
                    runLoopbackChecks()
                } catch (error: Exception) {
                    "FAIL: ${error.message}"
                }
                launch(
                    {
                        =>
                        this.message = result
                        this.busy = false
                    }
                )
        }
    }

    func build() {
        Row {
            Column {
                Text(this.message).fontSize(20).fontWeight(FontWeight.Bold).onClick({
                    evt => this.runChecks()
                })
            }.width(100.percent)
        }.height(100.percent)
    }
}

两个 Ability 文件沿用向导模板,完整内容列在这里,便于核对包名和加载的视图名称。

entry/src/main/cangjie/main_ability.cj:

package ohos_app_cangjie_entry

import kit.PerformanceAnalysisKit.Hilog
import kit.AbilityKit.Want
import kit.AbilityKit.UIAbility
import kit.AbilityKit.LaunchParam
import kit.AbilityKit.LaunchReason
import kit.ArkUI.WindowStage

class MainAbility <: UIAbility {
    public init() {
        super()
        registerSelf()
    }

    public override func onCreate(want: Want, launchParam: LaunchParam): Unit {
        Hilog.info(1, "Cangjie", "MainAbility OnCreated.${want.abilityName}")
        match (launchParam.launchReason) {
            case LaunchReason.StartAbility => Hilog.info(1, "Cangjie", "START_ABILITY")
            case _ => ()
        }
    }

    public override func onWindowStageCreate(windowStage: WindowStage): Unit {
        Hilog.info(1, "Cangjie", "MainAbility onWindowStageCreate.")
        windowStage.loadContent("EntryView")
    }
}

entry/src/main/cangjie/ability_stage.cj:

package ohos_app_cangjie_entry

import kit.AbilityKit.AbilityStage
import kit.PerformanceAnalysisKit.Hilog

class MyAbilityStage <: AbilityStage {
    public override func onCreate(): Unit {
        Hilog.info(1, "Cangjie", "MyAbilityStage onCreated.")
    }
}

10. 构建原生库、HAR 和 HAP

10.1 原生构建脚本

保存为 ohos-coap-libcoap-port/Build-Native.ps1。不带参数时交叉编译两种 OHOS 架构,复制桥接库到 HAR 的 libs,并附带原生许可文件。-Windows 用于可选的主机检查。

param(
    [string]$NativeSdk = 'C:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\native',
    [string]$AppDir = (Join-Path $PSScriptRoot '..\ohos-coap-app'),
    [switch]$Windows
)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$source = Join-Path $root 'third_party\libcoap'
if (-not (Test-Path -LiteralPath (Join-Path $source 'CMakeLists.txt'))) {
    throw 'Initialize third_party/libcoap at commit 5fd2f89ef068214130e5d60b7087ef48711fa615 first.'
}
function Invoke-Checked([string]$Program, [string[]]$Arguments) {
    & $Program @Arguments
    if ($LASTEXITCODE -ne 0) { throw "$Program failed with exit code $LASTEXITCODE" }
}
Push-Location $root
try {
    if ($Windows) {
        Invoke-Checked 'cmake' @('-S','native','-B','build/windows','-G','Ninja',"-DLIBCOAP_SOURCE_DIR=$source",'-DCMAKE_C_COMPILER=gcc','-DCMAKE_BUILD_TYPE=Debug')
        Invoke-Checked 'cmake' @('--build','build/windows','-j','4')
    } else {
        $toolchain = Join-Path $NativeSdk 'build\cmake\ohos.toolchain.cmake'
        if (-not (Test-Path -LiteralPath $toolchain)) { throw 'HarmonyOS Native SDK toolchain not found.' }
        foreach ($abi in @('arm64-v8a','x86_64')) {
            $build = "build/ohos-$abi"
            Invoke-Checked 'cmake' @('-S','native','-B',$build,'-G','Ninja',"-DLIBCOAP_SOURCE_DIR=$source", "-DCMAKE_TOOLCHAIN_FILE=$toolchain", "-DOHOS_ARCH=$abi",'-DCMAKE_BUILD_TYPE=Release')
            Invoke-Checked 'cmake' @('--build',$build,'-j','4')
            $destination = Join-Path $AppDir "coap\libs\$abi"
            New-Item -ItemType Directory -Path $destination -Force | Out-Null
            Copy-Item -LiteralPath (Join-Path $build 'libcoap_cj_bridge.so') -Destination $destination
        }
        $licenses = Join-Path $AppDir 'coap\src\main\resources\rawfile'
        New-Item -ItemType Directory -Path $licenses -Force | Out-Null
        Copy-Item -LiteralPath (Join-Path $source 'LICENSE') -Destination (Join-Path $licenses 'libcoap-LICENSE.txt')
    }
} finally { Pop-Location }

在工程根目录执行:

.\ohos-coap-libcoap-port\Build-Native.ps1 -NativeSdk $nativeSdk -AppDir "$projectRoot\ohos-coap-app"

两种 .so 必须分别出现在 coap/libs/arm64-v8a 和 coap/libs/x86_64。不要把 Windows DLL 或一种架构的库复制到另一架构目录。

10.2 应用构建脚本

保存为 ohos-coap-app/Build-App.ps1:

param(
    [string]$DevEco = 'C:\Program Files\Huawei\DevEco Studio',
    [string]$CangjieSdk = "$env:USERPROFILE\.cangjie-sdk\26.0\cangjie"
)
$ErrorActionPreference = 'Stop'
$env:PATH = (Join-Path $DevEco 'tools\node') + ';' + $env:PATH
$env:DEVECO_SDK_HOME = Join-Path $DevEco 'sdk'
$env:DEVECO_CANGJIE_PATH = $CangjieSdk
$env:DEVECO_CANGJIE_HOME = $CangjieSdk
$env:DEVECO_OH_NATIVE_HOME = Join-Path $DevEco 'sdk\default\openharmony\native'
$env:DEVECO_CANGJIE_PLUGIN_ENABLED = 'true'
$node = Join-Path $DevEco 'tools\node\node.exe'
$hvigor = Join-Path $DevEco 'tools\hvigor\bin\hvigorw.js'
function Invoke-Hvigor([string]$Module, [string]$Task) {
    & $node $hvigor --mode module -p "module=$Module@default" -p product=default -p buildMode=debug $Task --no-daemon
    if ($LASTEXITCODE -ne 0) { throw "Hvigor $Task failed ($LASTEXITCODE)" }
}
Push-Location $PSScriptRoot
try {
    & (Join-Path $DevEco 'tools\ohpm\bin\ohpm.bat') install --all
    if ($LASTEXITCODE -ne 0) { throw 'ohpm install failed' }
    Invoke-Hvigor 'entry' 'SyncCangjieResource'
    Invoke-Hvigor 'coap' 'assembleHar'
    Invoke-Hvigor 'entry' 'assembleHap'
} finally { Pop-Location }

调用它:

.\ohos-coap-app\Build-App.ps1 -DevEco $devEco -CangjieSdk $cjSdk

顺序是 ohpm install --all → entry:SyncCangjieResource → coap:assembleHar → entry:assembleHap。这比只在根目录执行一次 ohpm install 更明确地覆盖了两个模块。首次运行需要网络获取模板依赖;已安装后可利用缓存。

成功后得到:

ohos-coap-app/coap/build/default/outputs/default/coap.har
ohos-coap-app/entry/build/default/outputs/default/entry-default-unsigned.hap

HAP 是 ZIP 容器。除了看到 BUILD SUCCESSFUL,还应检查最终 HAP 确实包含这四个文件:

libs/arm64-v8a/libcoap_cj_bridge.so
libs/arm64-v8a/libohos_app_cangjie_coap.so
libs/x86_64/libcoap_cj_bridge.so
libs/x86_64/libohos_app_cangjie_coap.so

PowerShell 中可直接检查,不需要改名或解压整个 HAP:

Add-Type -AssemblyName System.IO.Compression.FileSystem
$hapPath = Join-Path $projectRoot 'ohos-coap-app\entry\build\default\outputs\default\entry-default-unsigned.hap'
$hapArchive = [IO.Compression.ZipFile]::OpenRead($hapPath)
try {
    $hapArchive.Entries | Where-Object { $_.FullName -match '^libs/(arm64-v8a|x86_64)/lib(coap_cj_bridge|ohos_app_cangjie_coap)\.so$' } | Select-Object FullName,Length
} finally { $hapArchive.Dispose() }

HAR 的每种架构中还应有仓颉的 .cjo 接口产物。消费 HAR 的应用仍需使用兼容 SDK、配置网络权限并包含桥接 .so;本文验证的是当前工程内的本地 HAR 依赖,没有测试发布到 ohpm 仓库后的独立消费工程。

11. 不启动模拟器也能完成的检查

11.1 Windows 上运行同一份仓颉封装

这个步骤检查封装与原生核心的行为,不能证明 HarmonyOS UI、加载器或设备网络已经工作。使用 SDK 同包的 Windows 编译器和 MinGW GCC,先让 gcc、cmake、ninja 在当前 PATH 中可用。

$env:PATH = 'C:\Programs\mingw64\bin;' + $env:PATH
Set-Location "$projectRoot\ohos-coap-libcoap-port"
.\Build-Native.ps1 -Windows
cjpm init --name coap_host_check --type executable --path host-check

只初始化一次,已有项目不要再次覆盖。该命令使用当前 1.2 CJPM 的 --path 参数指定目录。

初始化后,host-check/cjpm.toml 使用下面的配置:

[package]
  cjc-version = "1.2.0"
  name = "coap_host_check"
  organization = ""
  description = "nothing here"
  version = "1.0.0"
  target-dir = ""
  script-dir = ""
  output-type = "executable"
  compile-option = ""
  override-compile-option = ""
  link-option = ""
  package-configuration = {}

[dependencies]
[ffi.c]
coap_cj_bridge = { path = "../build/windows" }

复制 HAR 中的四个仓颉文件,仅替换包名;业务代码不做平台分叉。此时当前目录仍为 ohos-coap-libcoap-port:

foreach ($cjFile in @('native.cj','client.cj','server.cj','demo.cj')) {
    $sourceText = Get-Content -LiteralPath "..\ohos-coap-app\coap\src\main\cangjie\$cjFile" -Raw -Encoding utf8
    $hostText = $sourceText.Replace('package ohos_app_cangjie_coap','package coap_host_check')
    [IO.File]::WriteAllText((Join-Path (Get-Location) "host-check\src\$cjFile"), $hostText, [Text.UTF8Encoding]::new($false))
}

host-check/src/main.cj:

package coap_host_check

main(): Int64 {
    println(runLoopbackChecks())
    let client = CoapClient()
    client.setWaitSeconds(1)
    var timedOut = false
    try { client.requestUri("coap://127.0.0.1:15685/no-listener") }
    catch (error: Exception) { timedOut = true; println("PASS: timeout/error: ${error.message}") }
    if (!timedOut) { throw Exception("expected transport failure") }
    return 0
}

构建并运行:

Set-Location host-check
cjpm build --target-dir target-ffi
$env:PATH = (Resolve-Path '..\build\windows').Path + ';' + $env:PATH
& '.\target-ffi\release\bin\main.exe'

本机输出:

PASS: root, UTF-8 GET, binary POST, Block1/Block2 8192 bytes, query, NON, 4.04, PUT/4.05
PASS: timeout/error: CoAP request failed (3): delivery failed or retries exhausted

第二行测试没有监听者的端口,Windows 可能通过 ICMP 提前报告不可达。因此,它证明交付失败会抛出异常,不能单独证明等待截止计时。下面另用“端口开着但不回复”的对端验证真正的超时。

11.2 独立 UDP 对端:PUT、重传和截止超时

下面的 Python 程序仅依赖标准库。它加载 Windows 桥接 DLL,用独立 UDP 对端检查请求头、Token、MID、二进制正文、自定义选项和成功 PUT 响应;再故意丢掉第一包,要求第二包与第一包完全一致;最后保持 UDP 端口绑定却不回应,检查状态 4 与约 1 秒的等待截止。

保存为 ohos-coap-libcoap-port/check-native.py。这不是完整 CoAP 服务端,不能拿它替代 libcoap 或用于业务部署。

"""Independent UDP peer checks for the Windows bridge; Python standard library only."""
import ctypes as c
from pathlib import Path
import socket
import threading
import time

dll = c.CDLL(str(Path(__file__).resolve().parent / "build/windows/libcoap_cj_bridge.dll"))
dll.cc_client_new.argtypes = [c.c_char_p, c.c_int32, c.c_int32]
dll.cc_client_new.restype = c.c_void_p
dll.cc_client_set_uint.argtypes = [c.c_void_p, c.c_int32, c.c_int64]
dll.cc_client_set_payload.argtypes = [c.c_void_p, c.c_void_p, c.c_uint64]
dll.cc_client_add_option.argtypes = [c.c_void_p, c.c_uint16, c.c_void_p, c.c_uint64]
dll.cc_client_set_token.argtypes = [c.c_void_p, c.c_void_p, c.c_uint64]
for name in ("cc_client_run", "cc_client_response_code"):
    getattr(dll, name).argtypes = [c.c_void_p]
    getattr(dll, name).restype = c.c_int32
dll.cc_client_response_data.argtypes = [c.c_void_p]
dll.cc_client_response_data.restype = c.c_void_p
dll.cc_client_response_size.argtypes = [c.c_void_p]
dll.cc_client_response_size.restype = c.c_uint64
dll.cc_client_free.argtypes = [c.c_void_p]
dll.cc_client_free.restype = None


def packet_parts(packet):
    assert packet[0] >> 6 == 1
    token_size = packet[0] & 15
    cursor, number, options = 4 + token_size, 0, []

    def extended(value):
        nonlocal cursor
        if value < 13:
            return value
        count, base = (1, 13) if value == 13 else (2, 269)
        assert value != 15
        result = int.from_bytes(packet[cursor:cursor + count], "big") + base
        cursor += count
        return result

    while cursor < len(packet) and packet[cursor] != 255:
        lead = packet[cursor]
        cursor += 1
        number += extended(lead >> 4)
        length = extended(lead & 15)
        options.append((number, packet[cursor:cursor + length]))
        cursor += length
    payload = packet[cursor + 1:] if cursor < len(packet) else b""
    return packet[4:4 + token_size], options, payload


def client(port, method=1, wait=10, payload=b"", custom=False):
    handle = dll.cc_client_new(f"coap://127.0.0.1:{port}/put".encode(), method, 0)
    assert handle
    try:
        assert dll.cc_client_set_uint(handle, 4, wait * 1000) == 0
        assert dll.cc_client_set_uint(handle, 5, 2) == 0
        assert dll.cc_client_set_uint(handle, 3, 321) == 0
        token = c.create_string_buffer(b"abc")
        assert dll.cc_client_set_token(handle, token, 3) == 0
        data = c.create_string_buffer(payload)
        assert dll.cc_client_set_payload(handle, data, len(payload)) == 0
        if custom:
            option = c.create_string_buffer(b"tag")
            assert dll.cc_client_add_option(handle, 2048, option, 3) == 0
        start = time.monotonic()
        status = dll.cc_client_run(handle)
        elapsed = time.monotonic() - start
        size = dll.cc_client_response_size(handle)
        body = c.string_at(dll.cc_client_response_data(handle), size) if size else b""
        return status, dll.cc_client_response_code(handle), body, elapsed
    finally:
        dll.cc_client_free(handle)


def peer_case(drop_first=False, silent=False):
    errors, seen = [], []
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(("127.0.0.1", 0))
    port = sock.getsockname()[1]
    sock.settimeout(0.1)
    stop = threading.Event()

    def serve():
        try:
            while not stop.is_set():
                try:
                    packet, address = sock.recvfrom(65535)
                except socket.timeout:
                    continue
                seen.append(packet)
                if silent or (drop_first and len(seen) == 1):
                    continue
                token, options, payload = packet_parts(packet)
                assert packet[1] == 3 and payload == b"binary\x00PUT"
                assert (packet[0] >> 4) & 3 == 0
                assert token == b"abc" and packet[2:4] == (321).to_bytes(2, "big")
                assert (11, b"put") in options and (2048, b"tag") in options
                reply = bytes([0x60 | len(token), 68]) + packet[2:4] + token + b"\xffPUT OK"
                sock.sendto(reply, address)
        except BaseException as error:
            errors.append(error)

    thread = threading.Thread(target=serve)
    thread.start()
    try:
        result = client(port, method=3, wait=1 if silent else 10,
                        payload=b"binary\x00PUT", custom=True)
    finally:
        stop.set()
        thread.join(timeout=2)
        sock.close()
    assert not thread.is_alive() and not errors, errors
    status, code, body, elapsed = result
    if silent:
        assert status == 4 and 0.8 <= elapsed <= 3 and seen, result
        print(f"PASS: silent bound peer produces deadline timeout, {elapsed:.2f}s")
    else:
        assert status == 0 and code == 68 and body == b"PUT OK", result
        if drop_first:
            assert len(seen) >= 2 and seen[0] == seen[1], "CON was not retransmitted unchanged"
        print(f"PASS: PUT, token, MID, binary payload, elective option; packets={len(seen)}, {elapsed:.2f}s")


peer_case()
peer_case(drop_first=True)
peer_case(silent=True)

handle = dll.cc_client_new(b"coap://127.0.0.1/", 1, 0)
assert handle
try:
    for option in (0, 3, 6, 7, 11, 12, 15, 23, 27, 28, 60):
        assert dll.cc_client_add_option(handle, option, None, 0) != 0, option
finally:
    dll.cc_client_free(handle)
print("PASS: reserved, Observe and managed URI/body options rejected")

在工程根目录运行:

python -X utf8 .\ohos-coap-libcoap-port\check-native.py

本机一次运行输出如下;重传间隔含随机性,不要求每次都是 2.61 秒。

PASS: PUT, token, MID, binary payload, elective option; packets=1, 0.00s
PASS: PUT, token, MID, binary payload, elective option; packets=2, 2.61s
PASS: silent bound peer produces deadline timeout, 1.01s
PASS: reserved, Observe and managed URI/body options rejected

两组检查覆盖了仓颉封装和独立线上的报文行为,仍不覆盖长时间运行、并发压力、所有异常内存路径、丢包网络中的完整分块组合或设备生命周期。

12. 设备运行:最后一道验收

本次没有执行这一节。准备 API 24 或以上、支持该仓颉 SDK 的 HarmonyOS 设备或模拟器,在 DevEco 登录账号并按需配置应用签名。使用 Project Structure → Signing Configs → Automatically generate signature 的实际可用选项,证书资料留在自己的环境中。

启动设备并执行:

$hdc = Join-Path $devEco 'sdk\default\openharmony\toolchains\hdc.exe'
& $hdc list targets

出现设备标识后,在 DevEco 选择设备、entry 配置并点击 Run。这个流程会生成可部署的签名 HAP。不要把本文的 entry-default-unsigned.hap 当作已经可安装的签名包;安装策略取决于设备和调试环境。

应用启动后,点击 “点击运行 CoAP 回环检查”。验收结果应是第 11 节第一行的 PASS 文本。若界面显示 FAIL,先查看 HiLog 和异常消息,定位端口占用、库加载、权限或 API 不兼容。

回环通过后,再做实际网络测试:服务端绑定可访问的设备接口,客户端 URI 使用服务端设备地址,确认网络路由和 UDP 端口可达。物理设备上的 127.0.0.1 指设备自身,不是开发电脑;模拟器到宿主的寻址也要按实际环境设置。本文没有验证这一步,因此不附上虚构的局域网成功截图。

13. 排错速查

  • cjc 或 cjpm 找不到: 先检查 HarmonyOS 仓颉 SDK 是否安装,在当前 PowerShell 执行其 build-tools/envsetup.ps1,再检查实际命令来源。不要混用通用 1.0.5 SDK 与本文 1.2 工程。
  • 找不到 coap_cj_bridge: 先运行原生构建脚本,确认 HAR 的两个 libs 目录有对应 .so,再检查目标级 ffi.c。不要仅修改临时编译输出目录中的 CJPM 配置。
  • HAR 成功但 HAP 缺少仓颉依赖: 确认 entry/oh-package.json5 的本地 coap 依赖,运行 ohpm install --all 和 SyncCangjieResource,检查 entry/cjpm.toml 已出现 ohos_app_cangjie_coap 依赖。
  • 资源编译提示 Failed to delete directory: 本机已确认过生成资源目录带 ReadOnly 属性。先停止构建,检查报错指向的 entry/build/... 生成目录;仅清除这个已核对目录的只读属性再构建,或把工程放在普通非同步目录。不要递归修改 SDK、项目源码或用户目录的权限。
  • Windows 程序退出码 0xC0000139: 本机曾是 GCC 15 的 clock_gettime64 与 SDK 较旧的 libwinpthread-1.dll 混用。本文 CMake 已为桥接 DLL 静态链接 winpthread 和 libgcc,避免这项运行时依赖冲突;不要随意覆盖 SDK DLL。
  • 4.04 / 4.05: 分别核对路径和已注册方法。它们是收到的 CoAP 错误响应,要读取 code;本文服务端只注册 GET/POST。
  • 超时与重传次数不符合直觉: 等待截止与重传预算是两个限制,先达到的限制结束请求。没有监听者时可能收到 ICMP/NACK,提前得到交付失败;DNS 阶段也不计入发送后的等待时长。
  • 中文字符串异常: C 字符串边界使用 UTF-8,响应二进制数据不能无条件调用 text。PowerShell 读文件显式使用 UTF-8,Python 使用 -X utf8。
  • 停止后仍占用端口: 调用 stop() 后还要等待运行任务完成。不要在其他任务里直接释放正在轮询的原生句柄。
  • 模拟器要求 Hyper-V: 可以暂缓设备验收,继续构建和 Windows 检查;如需完整设备运行验证,之后启用合适的虚拟化环境或连接真机。

14. 验证记录与可交付产物

检查本次结果
Windows 原生桥接编译通过
同一份仓颉 API 的 Windows 回环通过,含 UTF-8 与 8 KB 分块
独立对端的 PUT、Token/MID、二进制和选项通过
丢弃首包后的 CON 重传通过,第二包与第一包一致
保持绑定但不回复的 1 秒截止通过,约 1.01 秒
OHOS arm64 / x86_64 原生交叉编译通过
两种架构的仓颉 HAR构建通过
两种架构的应用 HAP构建通过,未签名
HAP 内桥接与仓颉库检查两种架构均包含
HarmonyOS 设备 / 模拟器运行未执行
实际局域网、独立 HAR 消费工程未执行

到这里,读者拥有了实现客户端与服务端所需的全部新增源码、明确的上游依赖、两套构建脚本和可运行的检查入口。本文所称“完成移植工程”指上述交付范围;它不表示上游所有 API 等价迁移,也不表示已完成设备或生产环境验证。

发布和再分发时保留上游 Apache-2.0 许可、NOTICE,以及 libcoap 源码自带的完整 LICENSE 和源文件声明。本构建脚本将 libcoap LICENSE 放入 HAR 的 resources/rawfile;不要只保留一句“使用 BSD”而丢掉原文。SDK 与插件通过官方渠道安装,不随工程再分发。

此作者没有提供个人介绍。
最后更新于 2026-09-30