codegen 阶段无法读取 custom option 的值(只保留了 tag id,值被丢弃)
- 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
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