架构设计文档
版本: 2.0 更新时间: 2025-12-15 目标: 为 HarmonyOS ArkTS 生成高性能、类型安全的 Protobuf 代码
目录
核心架构
系统组件
┌─────────────────────────────────────────────────────────┐
│ 代码生成器 │
│ (arkpb-gen.js) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ protobufjs │───────▶│visitor-codegen│ │
│ │ (解析.proto)│ │ (生成traverse) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 生成 ArkTS 消息类 │ │
│ │ - traverse() 方法 (Visitor模式) │ │
│ │ - toJson/fromJson() 静态方法 │ │
│ │ - decodeFrom() 方法 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 运行时 (runtime/arkpb/) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Message │ │ Visitor │ │
│ │ (抽象基类) │ │ (接口定义) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ │ ├────────────┬─────────────┤
│ ▼ ▼ ▼ ▼
│ ┌──────────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐
│ │ UnknownFields│ │ Binary │ │ JSON │ │ ... │
│ │ (兼容性) │ │ Visitor │ │ Visitor │ │ (扩展) │
│ └──────────────┘ └───────────┘ └──────────┘ └─────────┘
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Writer/Reader│ │ MessageUtils │ │
│ │ (编解码) │ │ (工具函数) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
Visitor 模式设计
为什么选择 Visitor 模式?
参考: SwiftProtobuf 架构设计
"Conceptually, serializers create visitor objects that are then passed recursively to every message and field via generated traverse methods."
优势:
- 关注点分离: 消息结构(
traverse)与格式编码(Visitor)解耦 - 可扩展性: 新增格式只需实现新的 Visitor,无需修改消息类
- 类型安全: 编译时检查所有字段类型
- 性能优化: 避免反射,直接生成代码
核心接口
// Visitor 接口 (runtime/arkpb/Visitor.ets)
export interface Visitor {
// 标量字段
visitInt32(value: number, fieldNumber: number): void
visitInt64(value: bigint, fieldNumber: number): void
visitString(value: string, fieldNumber: number): void
visitBytes(value: Uint8Array, fieldNumber: number): void
// ... 其他类型
// 嵌套消息(自动递归)
visitMessage(value: Message, fieldNumber: number): void
// Repeated 字段
visitRepeatedInt32(value: number[], fieldNumber: number): void
visitRepeatedMessage(value: Message[], fieldNumber: number): void
// ... 其他类型
// Map 字段
visitMapStringInt32(value: Map<string, number>, fieldNumber: number): void
visitMapStringMessage(value: Map<string, Message>, fieldNumber: number): void
// ... 其他类型组合
}
生成的消息类
// 生成的消息类包含 traverse() 方法
export class Person extends Message {
name: string = ''
age: number = 0
emails: string[] = []
// ⭐ 核心方法:遍历所有字段
traverse(v: Visitor): void {
if (this.name !== '') {
v.visitString(this.name, 1)
}
if (this.age !== 0) {
v.visitInt32(this.age, 2)
}
if (this.emails.length > 0) {
v.visitRepeatedString(this.emails, 3)
}
}
// 从基类继承的方法(使用 Visitor)
toBinary(): Uint8Array {
const visitor = new BinaryEncodingVisitor()
this.traverse(visitor) // ⭐ 调用 Visitor
return visitor.finish()
}
}
Visitor 实现示例
// runtime/arkpb/BinaryEncodingVisitor.ets
export class BinaryEncodingVisitor implements Visitor {
private writer: Writer
private recursionDepth: number = 0
private readonly maxRecursionDepth: number = 100
visitString(value: string, fieldNumber: number): void {
this.writer.tag(fieldNumber, 2).string(value)
}
visitMessage(value: Message, fieldNumber: number): void {
// ⭐ 自动处理递归
this.recursionDepth++
if (this.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Nesting depth exceeds limit`)
}
try {
const nestedVisitor = new BinaryEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth
value.traverse(nestedVisitor) // ⭐ 递归遍历
const bytes = nestedVisitor.finish()
this.writer.tag(fieldNumber, 2).bytes(bytes)
} finally {
this.recursionDepth--
}
}
finish(): Uint8Array {
return this.writer.finish()
}
}
混合序列化策略
策略对比
| 特性 | Binary (Visitor) | JSON (直接生成) |
|---|---|---|
| 实现方式 | Visitor 模式 | 静态方法 |
| 扩展性 | ⭐⭐⭐⭐⭐ 优秀 | ⭐⭐⭐ 中等 |
| 性能 | ⭐⭐⭐⭐ 良好 | ⭐⭐⭐⭐⭐ 优秀 |
| 代码清晰度 | ⭐⭐⭐⭐ 良好 | ⭐⭐⭐⭐⭐ 优秀 |
| 维护成本 | ⭐⭐⭐⭐⭐ 低 | ⭐⭐⭐⭐ 低 |
为什么 Binary 使用 Visitor?
原因:
- 未来扩展性: 可能需要支持 TextFormat、Hash、Debug 等多种格式
- 代码复用: Visitor 逻辑可以在不同格式间共享
- 架构一致性: 遵循 SwiftProtobuf 成熟设计
为什么 JSON 直接生成?
原因:
- 性能至上: JSON 是最常用的格式,避免 Visitor 的间接调用开销
- 特殊逻辑: JSON 有很多特殊处理(枚举名称映射、bigint→string、Base64 编码)
- 代码清晰: 直接生成的代码更易于阅读和调试
生成的 JSON 方法示例:
export class Person extends Message {
// ⭐ 静态方法,不使用 Visitor
static toJson(m: Person): PersonJSON {
const o: PersonJSON = {}
if (m.name !== '') {
o.name = m.name
}
if (m.age !== 0) {
o.age = m.age
}
if (m.emails.length > 0) {
o.emails = [...m.emails]
}
return o
}
static fromJson(j: PersonJSON): Person {
const m = new Person()
if (j.name !== undefined) {
m.name = String(j.name)
}
if (j.age !== undefined) {
m.age = Number(j.age)
}
if (j.emails !== undefined) {
m.emails = j.emails.map(v => String(v))
}
return m
}
}
关键设计决策
1. 静态工厂方法 vs 实例方法
问题: fromJson() 应该是静态方法还是实例方法?
决策: 提供静态方法
对比:
// ❌ 实例方法(基类提供)
class Message {
fromJson(json: Record<string, Object>): Message {
// 问题:返回 Message 基类,丢失具体类型信息
}
}
let person = new Person()
person.fromJson(json) // 返回 Message,需要手动转换
// ✅ 静态方法(生成的子类提供)
class Person extends Message {
static fromJson(j: PersonJSON): Person {
// 直接返回 Person 类型
}
}
let person = Person.fromJson(json) // 返回 Person,类型安全
优势:
- ✅ 类型安全: 返回具体类型,不需要类型转换
- ✅ 符合工厂模式: 静态方法作为构造器更自然
- ✅ 避免先创建实例: 不需要
new Person()再填充数据 - ✅ ArkTS 兼容: 不依赖多态或复杂类型推导
基类保留实例方法的原因:
- 为统一接口保留,子类可以覆盖
- 默认抛出异常,强制使用静态方法
2. 递归深度保护
问题: 恶意构造的深层嵌套消息可能导致栈溢出
决策: 所有 Visitor 内置递归深度限制
实现:
export class BinaryEncodingVisitor implements Visitor {
private recursionDepth: number = 0
private readonly maxRecursionDepth: number = 100 // ⭐ 限制100层
visitMessage(value: Message, fieldNumber: number): void {
this.recursionDepth++
if (this.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
try {
// ... 递归处理
} finally {
this.recursionDepth-- // ⭐ 确保恢复
}
}
}
保护位置:
BinaryEncodingVisitor- 编码时检查JsonEncodingVisitor- JSON 编码时检查Reader- 解码时检查(通过incrementRecursionDepth())
3. 未知字段保留
问题: 如何支持前后兼容?
决策: 保存未知字段的原始二进制数据
场景:
1. 服务器升级 proto,新增字段 3
message Person { string name = 1; int32 age = 2; string email = 3; }
2. 旧客户端(只认识字段 1, 2)接收消息
→ 字段 3 的数据保存到 unknownFields
3. 客户端转发消息
→ 字段 3 的数据被完整保留
实现:
// runtime/arkpb/UnknownFields.ets
export class UnknownFields {
private data: Uint8Array = new Uint8Array(0)
append(bytes: Uint8Array): void {
// 追加未知字段数据
}
toBytes(): Uint8Array {
return this.data // 编码时追加到消息末尾
}
}
// 生成的 decodeFrom() 方法
decodeFrom(r: Reader, len?: number): void {
while (r.pos < endPos) {
const tag = r.uint32()
const fieldNum = tag >>> 3
switch (fieldNum) {
case 1: /* 已知字段 */ break
case 2: /* 已知字段 */ break
default: {
// ⭐ 保存未知字段
const wireType = tag & 7
const start = r.pos
r.skipType(wireType)
this.unknownFields.append(r.view(start, r.pos))
}
}
}
}
4. Map 字段的 forEach 遍历
问题: ArkTS 不支持 for (const [k, v] of map) 解构语法
ArkTS 规范:
- ❌ 不支持:
for (const [k, v] of map.entries())(规则33: arkts-no-destruct-assignment) - ✅ 支持:
map.forEach((v, k) => { ... })
实现:
// ❌ TypeScript 常见写法(ArkTS 不支持)
for (const [k, v] of this.items.entries()) {
visitor.visitMapEntry(k, v, fieldNumber)
}
// ✅ ArkTS 兼容写法
this.items.forEach((v, k) => {
const entryWriter = this.writer.fork()
entryWriter.tag(1, 2).string(k) // key
entryWriter.tag(2, 0).int32(v) // value
this.writer.tag(fieldNumber, 2).ldelim(entryWriter)
})
代码位置:
BinaryEncodingVisitor.ets:400-406(visitMapStringInt32)JsonEncodingVisitor.ets:472-475(visitMapStringInt32)
ArkTS 合规性设计
核心原则
本项目严格遵循 ArkTS 规范(2025-12-02 版本),所有设计决策都以合规性为前提。
关键合规点
1. 不使用 any/unknown(规则6)
替代方案:
- 使用
Object作为通用类型 - 使用
Record<string, Object>表示 JSON 对象 - 使用
Partial<T>表示可选字段(ArkTS 支持)
// ❌ 不合规
function process(data: any): any { }
// ✅ 合规
function process(data: Object): Object { }
// ✅ 合规(JSON 场景)
toJson(): Record<string, Object> { }
2. 使用 forEach 而非 for...in(规则37)
// ❌ 不合规
for (let key in obj) { }
// ✅ 合规(数组)
for (let i = 0; i < arr.length; i++) { }
// ✅ 合规(Map)
map.forEach((value, key) => { })
3. 不使用解构赋值(规则33)
// ❌ 不合规
const [x, y] = [1, 2]
for (const [k, v] of map) { }
// ✅ 合规
const arr = [1, 2]
const x = arr[0]
const y = arr[1]
map.forEach((v, k) => { })
4. 点操作符访问属性(规则19)
// ❌ 不合规
const value = obj['fieldName']
const key = 'name'
const value2 = obj[key]
// ✅ 合规
const value = obj.fieldName
// ✅ 例外:Map 和 TypedArray 支持索引访问
map.get(key)
typedArray[index]
5. 对象布局编译时确定(规则2)
// ❌ 不合规
class C { }
let c = new C()
c.newField = 123 // 动态添加字段
// ✅ 合规
class C {
newField: number = 0 // 编译时声明
}
let c = new C()
c.newField = 123
6. 支持的 Utility Types(规则63)
ArkTS 支持:
- ✅
Partial<T>- T 必须是类或接口 - ✅
Required<T> - ✅
Readonly<T> - ✅
Record<K, V>
不支持:
- ❌
Pick<T, K> - ❌
Omit<T, K> - ❌
Exclude<T, U> - ❌
Extract<T, U> - ❌ 其他高级 Utility Types
项目使用:
// MessageConstructor 接口
export interface MessageConstructor<T extends Message> {
create(init?: Partial<T>): T // ✅ Partial 合法
}
// JSON 类型
type PersonJSON = Record<string, Object> // ✅ Record 合法
性能优化
1. JSON 直接生成(避免 Visitor 开销)
性能对比(估算):
Visitor 模式:
Person.toJson() → JsonEncodingVisitor → traverse() → visitXXX() * N
开销:方法调用 * (字段数 * 2)
直接生成:
Person.toJson() → 直接处理字段
开销:方法调用 * 1
性能提升: 约 30-50%(取决于字段数)
2. 递归深度检查仅在必要时进行
visitMessage(value: Message, fieldNumber: number): void {
this.recursionDepth++
if (this.recursionDepth > this.maxRecursionDepth) { // ⭐ 仅在超限时抛出异常
throw new Error(...)
}
// ... 正常情况下无额外开销
}
3. UnknownFields 零拷贝
// Reader.view() 使用 subarray(零拷贝)
view(start: number, end: number): Uint8Array {
return this.b.subarray(start, end) // ⭐ 不创建新数组
}
4. Map 字段编码优化
使用 writer.fork() 避免多次内存分配:
visitMapStringInt32(value: Map<string, number>, fieldNumber: number): void {
value.forEach((v, k) => {
const entryWriter = this.writer.fork() // ⭐ 共享缓冲区
entryWriter.tag(1, 2).string(k)
entryWriter.tag(2, 0).int32(v)
this.writer.tag(fieldNumber, 2).ldelim(entryWriter)
})
}
FAQ
Q1: 为什么 Message 基类的 fromJson() 没有实现?
A: 这是设计决策,不是遗漏。
- 基类实例方法: 保留接口,默认抛出异常
- 子类静态方法: 实际实现,提供类型安全的工厂方法
用户应该使用:
let person = Person.fromJson(json) // ✅ 使用静态方法
而不是:
let person = new Person()
person.fromJson(json) // ❌ 会抛出异常
Q2: Visitor 接口为什么不完整?
A: 当前实现涵盖了最常用的 Map 类型组合(约70%的使用场景)。
已支持:
Map<string, *>- 所有值类型Map<int32, *>- 常见值类型Map<int64, *>- 常见值类型
未支持(按需扩展):
Map<bool, *>Map<uint32, *>Map<uint64, *>- 其他不常见组合
如果你的 proto 使用了这些类型,代码生成器会报错,提示需要扩展 Visitor 接口。
Q3: 为什么使用 Record<string, Object> 而非对象字面量类型?
A: 这是 ArkTS 最佳实践。
对比:
// ❌ 对象字面量类型(代码冗长)
static toJson(m: Person): { name?: string, age?: number } {
// ...
}
// ✅ Record 类型(简洁,合规)
static toJson(m: Person): PersonJSON {
// ...
}
type PersonJSON = Record<string, Object>
原因:
- ✅ ArkTS 规范明确支持
Record(规则63) - ✅ 代码更简洁
- ✅ 易于传递给其他函数(如
JSON.stringify()) - ✅ 运行时类型检查成本低
Q4: 能否添加 TextFormat Visitor?
A: 完全可以!这正是 Visitor 模式的优势。
步骤:
- 创建
TextFormatVisitor.ets实现Visitor接口 - 在
Message基类添加toTextFormat()方法 - 无需修改任何生成的消息类
// runtime/arkpb/TextFormatVisitor.ets
export class TextFormatVisitor implements Visitor {
private lines: string[] = []
visitString(value: string, fieldNumber: number): void {
this.lines.push(`${fieldNumber}: "${value}"`)
}
// ... 实现其他方法
finish(): string {
return this.lines.join('\n')
}
}
// Message.ets
toTextFormat(): string {
const visitor = new TextFormatVisitor()
this.traverse(visitor)
return visitor.finish()
}
Q5: 为什么 String(j.realm) 是安全的?
A: String() 是 JavaScript/ArkTS 的内置类型转换函数,不是动态特性。
ArkTS 规范:
- ✅ 允许:
String(value),Number(value),Boolean(value) - ❌ 禁止:动态的
.toString()调用(在某些场景)
使用示例:
static fromJson(j: PersonJSON): Person {
const m = new Person()
if (j.name !== undefined) {
m.name = String(j.name) // ✅ 安全的类型转换
}
return m
}
总结
本项目的架构设计体现了以下原则:
- 合规性优先: 100% 符合 ArkTS 规范
- 性能与灵活性平衡: 混合使用 Visitor 和直接生成
- 类型安全: 充分利用 ArkTS 的静态类型系统
- 安全性: 内置递归深度保护和未知字段处理
- 可维护性: 清晰的职责分离和模块化设计
架构版本历史:
- v1.0: 初始版本(直接编码)
- v2.0: 引入 Visitor 模式,混合序列化策略(当前版本)
未来规划:
- 支持更多 Map 类型组合
- 添加 TextFormat Visitor
- 性能进一步优化(packed encoding)
- 支持更多 Well-Known Types
维护者: protobuf-arkts-generator contributors 问题反馈: https://gitcode.com/xiaofenger_705/protobuf-arkts-generator/issues