架构设计文档

版本: 2.0 更新时间: 2025-12-15 目标: 为 HarmonyOS ArkTS 生成高性能、类型安全的 Protobuf 代码


目录

  1. 核心架构
  2. Visitor 模式设计
  3. 混合序列化策略
  4. 关键设计决策
  5. ArkTS 合规性设计
  6. 性能优化
  7. FAQ

核心架构

系统组件

┌─────────────────────────────────────────────────────────┐
│                     代码生成器                            │
│                  (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."

优势:

  1. 关注点分离: 消息结构(traverse)与格式编码(Visitor)解耦
  2. 可扩展性: 新增格式只需实现新的 Visitor,无需修改消息类
  3. 类型安全: 编译时检查所有字段类型
  4. 性能优化: 避免反射,直接生成代码

核心接口

// 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?

原因:

  1. 未来扩展性: 可能需要支持 TextFormat、Hash、Debug 等多种格式
  2. 代码复用: Visitor 逻辑可以在不同格式间共享
  3. 架构一致性: 遵循 SwiftProtobuf 成熟设计

为什么 JSON 直接生成?

原因:

  1. 性能至上: JSON 是最常用的格式,避免 Visitor 的间接调用开销
  2. 特殊逻辑: JSON 有很多特殊处理(枚举名称映射、bigint→string、Base64 编码)
  3. 代码清晰: 直接生成的代码更易于阅读和调试

生成的 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,类型安全

优势:

  1. 类型安全: 返回具体类型,不需要类型转换
  2. 符合工厂模式: 静态方法作为构造器更自然
  3. 避免先创建实例: 不需要 new Person() 再填充数据
  4. 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--  // ⭐ 确保恢复
    }
  }
}

保护位置:

  1. BinaryEncodingVisitor - 编码时检查
  2. JsonEncodingVisitor - JSON 编码时检查
  3. 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>

原因:

  1. ✅ ArkTS 规范明确支持 Record(规则63)
  2. ✅ 代码更简洁
  3. ✅ 易于传递给其他函数(如 JSON.stringify()
  4. ✅ 运行时类型检查成本低

Q4: 能否添加 TextFormat Visitor?

A: 完全可以!这正是 Visitor 模式的优势。

步骤:

  1. 创建 TextFormatVisitor.ets 实现 Visitor 接口
  2. Message 基类添加 toTextFormat() 方法
  3. 无需修改任何生成的消息类
// 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
}

总结

本项目的架构设计体现了以下原则:

  1. 合规性优先: 100% 符合 ArkTS 规范
  2. 性能与灵活性平衡: 混合使用 Visitor 和直接生成
  3. 类型安全: 充分利用 ArkTS 的静态类型系统
  4. 安全性: 内置递归深度保护和未知字段处理
  5. 可维护性: 清晰的职责分离和模块化设计

架构版本历史:

  • 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