Skip to content

feat(grpcgen): 分层控制 —— 每一层是下一层的默认值,不是另一条路 - #4

Open
Sunrisepeak wants to merge 5 commits into
mainfrom
feat/grpcgen-layered-control
Open

feat(grpcgen): 分层控制 —— 每一层是下一层的默认值,不是另一条路#4
Sunrisepeak wants to merge 5 commits into
mainfrom
feat/grpcgen-layered-control

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

实现 .agents/docs/2026-08-06-grpcgen-layered-control-design.md

问题不是「不够方便」,是断崖

generate_all() 之外只有两个旋钮(proto_dir / grpc)。需求一超出,用户只能像 examples/helloworld 那样手写六十行 build.mcpp 绕开整个规则 —— 而那六十行要重新实现规则已经解决过的东西:well-known types 目录怎么定位、嵌套 .proto 的输出子目录怎么建、输入集合怎么算。它们会与规则悄悄漂移,且没有任何机制会报出漂移

断崖本身就是设计缺陷:它把「优雅」和「可控」变成了二选一。

四层,不是四条路

L0  generate_all()
L1  generate_all({.extra_dirs=…, .imports=…, .mock=…, .protoc_args=…})
L2  generate_all({.plugins={cpp(), my_plugin}})
L3  auto e = plan_all(); /* 改 */ ; submit(e);

generate_all(opt) 就是 submit(plan_all(opt)),.grpc = true 就是 .plugins = {cpp()}。L0 逐字节不变。

extra_dirsimports 是两件事 —— 写示例时撞出来的

不是设计时想到的。protoc 会把 #include "common/types.pb.h" 写进任何 import 了它的文件,所以只靠 -I 够到的共享树会产出一个没人生成的头:

fatal error: 'common/types.pb.h' file not found

而且报错离原因很远。于是分成两个概念:extra_dirs 既生成又搜索,imports 只搜索。这是 L1 里唯一一个不在原设计里的字段。

可观测性的分工由引擎定死,不由规则发明

核实过(不是推断):mcpp:action= 的边其完整 argv 原样落在 build.ninja 里,ninja -t commands <output> 即可取。所以规则不实现命令行 dump —— 第二个真相来源只会漂移。规则负责的是「哪些旋钮产生了它」,写进每次构建都打印的 description:

GENERATE protoc:orders (+grpc +mock, -Iproto -I../shared-proto)

mock:能生成,但本生态编不了,示例如实反映

--grpc_out=generate_mock_code=true:<dir>插件参数而非 protoc 顶层参数 —— 正是消费者不该被要求知道的拼写,所以做成具名选项。

生成的 mock 头 #include <gmock/gmock.h>,而 compat.gtest 只带 googletest、不带 gmock。因此示例声明并产出它、但不 include;plan 阶段断言它进了输出集,CI 再断言文件真的存在。

命名:codegen 不改

  • protoc 只命名三者之一,且是 protobuf 那一半;buf 存在,换生成器就成谎话。
  • 与 tonic 的歧义单向:tonic 的 codegen 指生成代码的运行时导出,C++ 里不存在对应物(生成的桩直接 #include <grpcpp/...> 并链同一个库)。
  • L2 之后 codegen 更站得住:它表达「这个包知道 .proto 怎么变成 C++,包括你后来加的插件」。

真正欠的是文档,已补:codegen 是两步(--cpp_out 是 protobuf 的事,--grpc_out 才是 gRPC 的),以及默认关闭的真实理由 —— 官方 Generic API(GenericStub + ByteBuffer)是真实存在的无 codegen 路径,代理/LB 类服务的推荐做法。

本机验证

examples/advanced service = orders.Orders / advanced: OK (messages + stubs, cross-root generation);产出含 common/types.pb.* 与两个 *_mock.grpc.pb.h
examples/greeter(L0) 从零重建,helloworld: OK —— 加法没有改变默认行为
examples/helloworld 从零重建,helloworld: OK

`generate_all()` 之外原本是断崖:两个旋钮(proto_dir / grpc)之后,需求一超出
就只能手写六十行 build.mcpp **绕开整个规则** —— 而那六十行要重新实现规则已经解决
过的东西(well-known types 定位、嵌套 .proto 建子目录、输入集合计算),并且会与
规则悄悄漂移、没有任何机制报出。**断崖本身就是设计缺陷**:它把「优雅」和「可控」
变成了二选一。

    L0  generate_all()
    L1  generate_all({.extra_dirs=…, .imports=…, .mock=…, .protoc_args=…})
    L2  generate_all({.plugins={cpp(), my_plugin}})
    L3  auto e = plan_all(); /* 改 */ ; submit(e);

它们不是并列选项:`generate_all(opt)` **就是** `submit(plan_all(opt))`,
`.grpc = true` **就是** `.plugins = {cpp()}`。L0 逐字节不变。

## extra_dirs 与 imports 是两件事

写示例时撞出来的,不是设计时想到的:protoc 会把 `#include "common/types.pb.h"`
写进任何 import 了它的文件,所以**只靠 -I 够到的共享树会产出一个没人生成的头**,
报错还离原因很远(`fatal error: 'common/types.pb.h' file not found`)。

于是分成两个概念:`extra_dirs` 既生成又搜索,`imports` 只搜索。

## 可观测性的分工由引擎定死

核实过:`mcpp:action=` 的边其完整 argv 原样落在 build.ninja 里
(`rule mcpp_action_N / command = …`),`ninja -t commands <output>` 即可取。
**规则因此不实现命令行 dump** —— 第二个真相来源只会漂移。规则负责的是「哪些旋钮
产生了它」,写进每次构建都打印的 description:

    GENERATE protoc:orders (+grpc +mock, -Iproto -I../shared-proto)

## mock

`--grpc_out=generate_mock_code=true:<dir>` 是**插件参数**而非 protoc 顶层参数 ——
正是消费者不该被要求知道的拼写,所以做成具名选项而不是让人往 protoc_args 里塞。

生成的 mock 头 `#include <gmock/gmock.h>`,而本生态的 compat.gtest 只带
googletest、不带 gmock,所以示例**声明并产出**它、但不 include 它;CI 直接断言
文件存在。

## 示例

新增 examples/advanced(L1 + L3)与 examples/shared-proto(被跨根生成的共享树)。
greeter(L0)与 helloworld(手写展开)从零重建复验,输出不变。

设计:.agents/docs/2026-08-06-grpcgen-layered-control-design.md
* extra_dirs —— 设计里只有 imports(额外 -I),写示例时撞出来:protoc 会把
  `#include "common/types.pb.h"` 写进 import 了它的文件,只靠 -I 够到的共享树
  会产出一个没人生成的头。两个概念必须分开。这条是**示例**发现的,不是设计发现
  的 —— 只写文档不写示例会把它漏掉。
* plan_entries / entry —— L3 的真正底层((root, name) 寻址),plan 与 plan_all
  都汇入它。必须公开:那是「.proto 不在任何已知根下」的项目唯一的出路,否则它们
  又回到自己写 build.mcpp,即本设计要消掉的断崖。
* 并如实记录 gmock 缺失这个生态限制。
L2(自定义插件)实现完就交了 PR、没跑过;写探针跑一次就照出来:传**绝对路径**
时 `lexically_relative` **会成功**,但返回一串比原路径还长的 `..`:

    -I../../../../../../home/speak/workspace/github/mcpplibs/grpc-m/examples/shared-proto

我的回退条件只覆盖「算不出」,没覆盖「算出来更差」。改为:相对形式以 `../..`
开头就用原值。

顺带记下 L2 探针的结论(argv 构造正确,无需改动):

    --foo_out=a=1,b=2:<out>              protoc 的 params:dir 文法
    --plugin=protoc-gen-foo=<binary>
    outputs 含 .foo.cc / .foo.h          后缀进了输出集
    desc 含 +grpc +foo                   与内置 cpp() 共存

这是「只写文档不写示例会漏掉」的第二次印证(第一次是 extra_dirs)。
设计文档 §9 列了这条,实现时漏了:CI 只断言产物存在,没断言 description。

mcpp 已经把完整 argv 写进 build.ninja,所以规则拥有的那一半是「**哪些旋钮**产生
了它」——而那串字符是每次构建都会打印的东西。不断言它,它与实际选项漂移了也不会
有人发现。

    description = GENERATE protoc:orders (+grpc +mock, -Iproto -I../shared-proto)

本地已验证该断言通过。
官方状态页已恢复 operational。此前各轮的失败均为
`Failed to resolve action download info: Service Unavailable` 与无日志的
cancelled,与改动无关。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant