news 2026/10/28 1:48:12

从 24 到 25 字节:compactstr4cj 的 CJMP 鸿蒙适配与 UTF-8 实测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 24 到 25 字节:compactstr4cj 的 CJMP 鸿蒙适配与 UTF-8 实测

前言:给短标签换一种存法

状态码、页面标签、配置项名字,这些字符串通常很短。为了装几个字节,再给内容单独分配一块缓冲区,看起来有些浪费。compact_str 的思路很直接:短内容放进存储体,变长后再切到堆缓冲区。这次 compactstr4cj 的鸿蒙示例就从一个临界值开始看——二十四字节还能内联,二十五字节会发生什么。

CJMP 社区:一起讨论公共逻辑库适配

先说明“紧凑”的对象。这里的原生存储体是二十四字节,仓颉类通过句柄持有它。仓颉对象、原生句柄分配、FFI 字符串转换仍有开销,所以不能把它写成整个仓颉对象只有二十四字节,或整个调用链零堆分配。本文看的是存储表示与接口语义,没有用一组页面操作去推断性能提升比例。

24、25:先对照页面的读数

示例应用 里有两个相邻按钮。二十四个 ASCII 字节显示 INLINE、容量二十四;再加一个感叹号,长度变为二十五、状态变成 HEAP,容量是三十六。这个容量来自原生后端的增长规则,不是 ArkTS 根据长度猜出的标签。页面的长度、容量、内联状态都来自同一个仓颉库结果。

输入 / 操作

字节数

存储状态

24 个 ASCII 字节

24

INLINE

24 个 ASCII 字节再加 !

25

HEAP,容量 36

鸿蒙重复四次

24

INLINE

上面追加一个 emoji

28

HEAP

截到 6 字节

6

先保留 HEAP

显式 shrinkToFit

6

返回 INLINE

上游固定提交 9696af7f7a9451478766b19dedac7371594eee0d,compact_str/Cargo.toml 标记 0.10.0。原始 MIT 许可和源码快照随包保留。适配保留了短串内联、长串堆存储的表示思路,但仓颉 API 和 C 后端是明确范围的重实现,没有把 Rust crate 的全部接口和布局承诺一并搬过来。

compact_str 固定上游源码

原生存储体与最后一个字节

typedef union { unsigned char bytes[24];struct {unsigned char *ptr;uint64_t len;unsigned char cap[7];unsigned char tag;} heap; } Compact; _Static_assert(sizeof(Compact)==24 && sizeof(void*)==8,"64-bit layout required"); #define LIMIT ((uint64_t)64*1024*1024) static int is_heap(const Compact *s){return s->bytes[23]==0xfe;} uint64_t cs_len(const Compact *s){return is_heap(s)?s->heap.len:s->bytes[23]>=0xc0?s->bytes[23]-0xc0:24;} uint64_t cs_capacity(const Compact *s){if(!is_heap(s))return 24;uint64_t n=0;for(int i=0;i<7;i++)n|=(uint64_t)s->heap.cap[i]<<(8*i);return n;} static unsigned char *data(Compact *s){return is_heap(s)?s->heap.ptr:s->bytes;} int32_t cs_inline(const Compact *s){return !is_heap(s);} int32_t cs_sizeof(void){return sizeof(Compact);} void *cs_new(void){Compact *s=calloc(1,sizeof *s);if(s)s->bytes[23]=0xc0;return s;} void cs_free(Compact *s){if(s){if(is_heap(s))free(s->heap.ptr);free(s);}}

在当前两种六十四位小端架构上,内联表示和堆表示共用二十四字节。短于阈值时,最后一个字节记录长度;堆状态使用单独标记。恰好二十四字节时没有空位再存长度,但一个完整 UTF-8 字符串的最后一个字节只能是 ASCII 或续字节,可以与较高的状态标记区分。

这也是不能随便装入任意字节数组的原因。后端先验证 UTF-8,包括续字节、过长编码、代理区间和最大码点约束。输入保持合法,最后字节的判别才成立。库当前限定六十四位小端与六十四 MiB 内容上限,三十二位和大端没有验证,也没有按目录名称把它们列为支持平台。

UTF-8 截断:六字节可以,一字节不行

八个汉字正好占二十四个 UTF-8 字节,一个常见 emoji 再占四字节。这些数字跟“看见几个字”不是同一个概念。库的 byteLength、reserve 和 truncate 都按字节工作,所以方法名字和 README 会强调这一点,避免调用方拿字符数直接当偏移量。

int32_t cs_truncate(Compact *s,uint64_t n){ uint64_t old=cs_len(s);if(n>=old)return 0; if((data(s)[n]&0xc0)==0x80)return EILSEQ; set_len(s,n);return 0; }

截断位置如果落在续字节上,后端拒绝操作,原内容保持不变。错误边界按钮装入“鸿蒙🙂”,然后尝试截到第一个字节,页面保留完整字符串并给出拒绝提示。截到第六字节则是合法边界,得到“鸿蒙”。大于当前长度的 truncate 是无操作,负数由仓颉门面提前拒绝。

还有一项不容易从普通示例看出来:嵌入 NUL。库的 toString 逐字节取回指定长度,再按 UTF-8 构造仓颉字符串,没有依赖 C 风格的零终止读取。设备自检包含 A、NUL、B 的三字节往返;当前页面没有给 NUL 提供输入框,不把这个库层检查写成一个已展示的界面功能。

public func toString(): String { ensureOpen();let bytes=Array<UInt8>(byteLength,{i => unsafe {cs_byte(handle,UInt64(i))}}) return String.fromUtf8(bytes) }

变短以后,什么时候释放容量

长字符串已经在堆上,截短并不马上搬回内联区。这样后面又追加内容时,可以继续使用已有容量。需要主动收缩时调用 shrinkToFit:短内容复制回二十四字节区域,旧堆缓冲区释放。页面把截断与收缩分成两个按钮,方便观察长度和存储状态分别在哪一步改变。

int32_t cs_shrink(Compact *s){ uint64_t n=cs_len(s);if(!is_heap(s))return 0; if(n<=24){unsigned char tmp[24]={0};memcpy(tmp,data(s),n);free(s->heap.ptr);memcpy(s->bytes,tmp,24);if(n<24)s->bytes[23]=(unsigned char)(0xc0+n);return 0;} if(cs_capacity(s)==n)return 0;unsigned char *p=malloc(n);if(!p)return ENOMEM; memcpy(p,data(s),n);free(s->heap.ptr);s->heap.ptr=p;set_cap(s,n);return 0; }

reserve 接收额外字节数,并不是目标总容量。扩容至少覆盖请求,按约一点五倍当前容量增长;超出上限会拒绝且不破坏现有内容。clone 则复制出独立可变内容,修改副本不会改变原串,但没有承诺复制旧容量。复制内容与复制容量是两件事,使用时应按本版接口约定理解。

public func append(value: String): Unit { ensureOpen();let bytes=unsafe {LibC.mallocCString(value)} let code=unsafe {cs_append(handle,bytes,UInt64(value.size))};unsafe {LibC.free(bytes)};checked(code) }
public func close(): Unit {if(!closed){unsafe {cs_free(handle)};handle=CPointer<Unit>();closed=true}}

仓颉封装实现 Resource,正常使用可显式 close 或放进 try 资源作用域,析构作为兜底。FFI 创建的临时字节串调用后释放,原生指针不交给 ArkTS。接口按拥有线程串行使用;这一版不提供跨线程共享可变字符串的同步承诺。

CJMP 工程落到哪些目录

CPF-RN:基础工具与安装资料

CJMP 开发准备

CJMP 0.2.2 配套说明

项目

本次实际配套

框架

CJMP Tools 0.2.2 / logic-module / native

IDE

DevEco Studio 6.1.1.300

SDK / 编译器

HarmonyOS 6.1.1(24) / 仓颉与 cjpm 1.1.3

模拟器

x86_64,6.1.1.350

真机

MatePad Edge,arm64-v8a,6.1.1.120

环境搭建直接引用 CPF-RN 的基础工具文件,RN 专有依赖不照搬到这个 CJMP 工程。CJMP 0.2.2 文档给出的仓颉配套是 1.1.0-beta.22,本机实际为 1.1.3,构建目录与 C ABI 桥按实际 SDK 调整。活动优先要求 HarmonyOS 7/API 26;目前完成的是 API 24 双端实测,API 26 仍需要换配套后重新验证。

harmony/cjmp/compactstr4cj/ project.conf logic-module/cjpm.toml logic-module/common/compact_string.cj logic-module/hos/native/compact_core.c logic-module/hos/native/napi_bridge.c logic-module/hos/test_suite.cj hos/compactstr4cj/ scripts/build-logic.ps1 example/harmony/app/

位置

本次改动

common/compact_string.cj

仓颉公共类型、资源关闭、字节接口及 FFI 转换

native/compact_core.c

24 字节表示、UTF-8 验证、扩容、收缩和深克隆

cjpm.toml / scripts

公共包与原生后端的目标架构编译、运行依赖收集

hos/compactstr4cj

HAR 门面、类型定义、外部 common 目录的构建钩子

hos/demo.cj / test_suite.cj

返回实际状态,执行模型与边界检查

example/harmony/app

真实依赖 HAR 的按钮演示和字节读数

harmony 目录有实际适配实现,example 是能导入 compactstr4cj 的鸿蒙应用。项目类型使用 CJMP 官方 logic-module/native 模板,仓颉公共包由 cjpm 编译,原生 C 后端只放平台相关存储实现。HAR 编译前先构建外部 common 源码,避免改了库文件,增量构建却继续复用旧结果。

设备演示与测试记录

模拟器和 MatePad Edge 都从同一版源码构建的 HAP 安装运行。GIF 按实际操作顺序记录二十四字节、二十五字节、汉字、追加 emoji、截断、收缩、深克隆、错误边界及设备自检。每次按钮操作后,还读取页面真实读数核对长度、状态与内容;下面这些画面不是网页预览或设计稿。

测试包先检查零到八十字节的 ASCII 内容与阈值,再做一千次构造、追加、预留、收缩、截断和清空的模型对照。模型使用仓颉标准 String,逐次检查内容、字节长度与容量不变量;另外覆盖合法与非法 UTF-8 边界、克隆独立性、嵌入 NUL、超限预留和关闭后调用。两端各返回二千五百七十七项检查通过。

PASS | 2577 checks | 1000 model operations | UTF-8 and 24/25-byte boundaries

画面复核时也修正了正文颜色:系统深色主题会使未设置颜色的字符串在白卡片上难以辨认。最终显式指定颜色后重新构建和录制,测试数据与最终包哈希一起保存。正确读到字符串只是其中一步,还需要让用户在实际界面上看清它。

小结

这版实现的是一组可复现的短字符串接口,关注字节阈值、内容正确性和资源释放。Rust 的 Option 布局优化、const_new、String 零拷贝转换、format 宏、serde 和数据库集成等能力尚未移植。Android/iOS 没有实测,运行速度与内存收益也没有做系统基准,因此这里不写性能提升百分比。

cjmp build har --platform ohos-arm64

项目开源地址:compactstr4cj(AtomGit)。

加入 CJMP 社区,讨论公共逻辑库与鸿蒙接入

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 1:48:11

5【矢量图设置】

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/28 1:47:02

Umi-OCR快速教程:5分钟完成OCR识别

Umi-OCR快速教程&#xff1a;5分钟完成OCR识别 【免费下载链接】Umi-OCR OCR software, free and offline. 开源、免费的离线OCR软件。支持截屏/批量导入图片&#xff0c;PDF文档识别&#xff0c;排除水印/页眉页脚&#xff0c;扫描/生成二维码。内置多国语言库。 项目地址: …

作者头像 李华
网站建设 2026/10/28 1:42:42

25 行 Python,下一首歌

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/28 1:40:02

从兆欧表到 P-Q 曲线:一文读懂电气工程师的“硬核知识库“

在电气工程领域&#xff0c;基础知识的稳固是通往成功的关键阶梯。即便是资深的技术人员&#xff0c;面对一些基础概念时也可能存在认知盲点 。本文整理了电气工程师日常工作中最常接触、却最容易被误读的几个核心技术点&#xff0c;供行业内读者温故知新&#xff0c;也供感兴趣…

作者头像 李华