cloudwego / cloudwego/pilota

codegen 阶段无法读取 custom option 的值(只保留了 tag id,值被丢弃)

Open
#372 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
167
Forks
25
PR merge metrics
No merged PRs in 30d

Description

# pilota-build 解析 Protobuf custom option 时丢失 option 值

## 问题概述

`pilota-build` 在解析 Protobuf custom option 时,只记录了“使用了哪个扩展”(即 tag ID),却丢弃了对应的 option 值。

例如:

```proto
service UserRegisterDB {
option (srpc.service_option_id) = 0x10016f;
}
```

当前解析结果只能表明 `service_option_id` 被使用,无法获得其值 `0x10016f`。

这导致 `CodegenBackend` 无法将 custom option 中声明的 ID、flag 等元数据写入生成代码,例如:

```rust
const SERVICE_ID: u32 = 0x10016f;
```

依赖这些值的 codegen 后端只能自行重新解析 `.proto` 或 `FileDescriptorSet`,抵消了由 Pilota 统一负责解析的初衷。

---

## 环境

- `pilota-build`: `0.13.10`
- 相关代码:
- `pilota-build/src/ir/ext/pb.rs:86-99`
- `UsedOptions::from_pb_unknown_fields`

---

## 使用场景

Protobuf custom option 常用于在 service、method 或 message 上携带路由、身份标识等元数据。

例如:

```proto
extend google.protobuf.ServiceOptions {
optional uint32 service_option_id = 10001;
}

extend google.protobuf.MethodOptions {
optional uint32 method_option_id = 10002;
}

service UserRegisterDB {
option (srpc.service_option_id) = 0x10016f;

rpc Register(RegisterRequest) returns (RegisterResponse) {
option (srpc.method_option_id) = 0x1;
}
}
```

codegen 后端需要在生成期读取这些值,并生成类似代码:

```rust
const SERVICE_ID: u32 = 0x10016f;
const METHOD_ID: u32 = 0x1;
```

运行时随后按照 `(service_id, method_id)` 进行路由,并与使用相同声明 ID 的 Go/C++ 实现保持 wire 兼容。

因此,这些 option 的值必须能在 codegen 阶段通过 Pilota 的 `Context` 或 `RirDatabase` 获取。

---

## 当前行为

`UsedOptions::from_pb_unknown_fields` 在遍历 unknown fields 时丢弃了值:

```rust
pub fn from_pb_unknown_fields(
extendee_kind: ExtendeeKind,
unknown_fields: &protobuf::UnknownFields,
) -> Self {
Self(
unknown_fields
.iter()
.map(|(k, _)| ExtendeeIndex {
extendee_kind,
tag_id: k,
})
.collect::>(),
)
}
```

`unknown_fields.iter()` 返回 `(tag, UnknownValueRef)`,其中 `UnknownValueRef` 包含实际值,例如:

- `Varint(u64)`
- `Fixed32(u32)`
- `Fixed64(u64)`
- `LengthDelimited(&[u8])`

但当前代码使用 `_` 忽略了该值,仅保留 tag ID。解析阶段结束后,option 的值便无法恢复。

---

## 下游 API 的限制

目前 `Context` / `RirDatabase` 暴露的相关 API 包括:

```rust
pb_ext(index) -> Arc
```

该接口只能获得扩展定义,例如 `field_ty`、`item_ty`,不包含具体使用值。

```rust
pb_ext_used(index) -> bool
```

该接口只能判断某个扩展是否被使用。

```rust
node_contains_tag::()
```

该接口主要支持 Pilota 内置 tag,例如:

- `ClientStreaming`
- `ServerStreaming`

它无法用于读取任意 custom option 的值。

因此,`CodegenBackend` 可以知道:

> 当前 service 使用了 `service_option_id` 扩展。

但无法知道:

> `service_option_id` 的值是 `0x10016f`。

---

## 期望行为

希望 Pilota 在记录扩展 tag ID 的同时保留其实际值,并通过 `Context` 或 `RirDatabase` 向 codegen 后端暴露。

例如,可以将当前结构扩展为:

```rust
pub struct UsedOption {
/// 使用了哪个扩展。
pub index: ExtendeeIndex,

/// custom option 的实际 wire value。
pub value: protobuf::UnknownValue,
}

pub struct UsedOptions(pub Vec);
```

对应的解析逻辑可以类似:

```rust
pub fn from_pb_unknown_fields(
extendee_kind: ExtendeeKind,
unknown_fields: &protobuf::UnknownFields,
) -> Self {
Self(
unknown_fields
.iter()
.map(|(tag_id, value)| UsedOption {
index: ExtendeeIndex {
extendee_kind,
tag_id,
},
value: value.to_owned(),
})
.collect(),
)
}
```

具体的 owned value 类型和转换方式可以根据当前 `protobuf` crate 的 API 调整。

---

## 建议的查询 API

由于同一个扩展可以出现在不同的 service、method 或 message 上,查询接口最好同时包含节点信息和扩展信息,例如:

```rust
fn pb_ext_value(
&self,
def_id: DefId,
index: &ExtendeeIndex,
) -> Option<&protobuf::UnknownValue>;
```

也可以提供类型化 helper:

```rust
fn service_option_u64(
&self,
service_id: DefId,
ext: &ExtendeeIndex,
) -> Option;

fn method_option_u64(
&self,
method_id: DefId,
ext: &ExtendeeIndex,
) -> Option;
```

或者直接从节点的 `UsedOptions` 查询:

```rust
impl UsedOptions {
pub fn get(
&self,
index: &ExtendeeIndex,
) -> Option<&protobuf::UnknownValue>;
}
```

如果需要支持 repeated option,或同一个 tag 对应多个 wire value,也可以提供复数形式的接口:

```rust
fn pb_ext_values(
&self,
def_id: DefId,
index: &ExtendeeIndex,
) -> &[protobuf::UnknownValue];
```

---

## 当前变通方案

目前,依赖 custom option 值的 codegen 后端只能采用以下方案。

### 1. 重新解析 `.proto` 或 `FileDescriptorSet`

缺点:

- 与 Pilota 的解析流程重复;
- 增加实现和维护成本;
- 容易产生两套解析结果不一致的问题。

### 2. 扫描 `.proto` 源码文本

缺点:

- 依赖具体文本格式;
- 容易受注释、换行和数字进制影响;
- 难以正确处理聚合语法等复杂情况;
- 整体实现较为脆弱。

### 3. Fork Pilota

在 fork 中修改 `UsedOptions` 以保留值。

缺点:

- 需要长期同步上游;
- 增加维护成本;
- 不利于通用 codegen 后端复用。

这些方案都不适合作为长期实现。

---

## 影响

`volo-grpc` 当前主要按 path 路由,并使用 Pilota 内置的 `ClientStreaming` / `ServerStreaming` tag,因此可能不会触发这个问题。

但对于需要将 proto 中声明的 ID、flag 或其他元数据写入生成代码的 RPC 框架,这项能力是必需的。

特别是需要与已有 Go/C++ 实现保持 wire 兼容的内部 RPC 框架,通常会通过 custom option 声明:

- service ID;
- method ID;
- 路由信息;
- 权限标记;
- 协议特性开关;
- 其他需要在编译期写入桩代码的元数据。

由于 option 值在解析阶段被丢弃,这类后端目前无法完全依赖 Pilota,只能额外维护一套并行解析逻辑。

---

## 预期收益

保留并暴露 custom option 的值后,可以带来以下收益:

- codegen 后端不再需要重复解析 `.proto`;
- Pilota 可以真正作为统一的 Protobuf 解析层;
- 自定义 RPC 框架可以直接基于 Pilota 实现 codegen;
- 可以支持 service ID、method ID、路由标记和权限标记等元数据;
- 更容易与已有 Go/C++ RPC 实现保持兼容;
- 降低下游 codegen 后端的实现和维护成本。

希望 Pilota 能在 `UsedOptions` 或对应的 RIR 节点中保留 custom option 的实际值,并提供稳定的查询 API。

Contributor guide

Open the contributing guide

Research direction

Start in pilota-build/src/ir/ext/pb.rs at UsedOptions::from_pb_unknown_fields, then trace how UsedOptions is stored and exposed through Context/RirDatabase. Done means the custom option wire value survives parsing and is queryable for the owning service, method, or message so a codegen backend can retrieve it without reparsing the .proto or FileDescriptorSet; validate with the supplied service and method option examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, tooling
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.