protobuf upb 的 C 语言风格指南:命名规则与 UPB_PRIVATE 私有符号机制
【免费下载链接】protobufProtocol Buffers - Google's data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobuf
upb 是 Protocol Buffers 仓库中一个纯 C 编写的运行时实现,由于 C 语言没有命名空间、没有访问控制关键字,docs/upb/style-guide.md 在 Google C++ 风格指南的基础上补充了一套 C 特有的约束。读透本文后,你将掌握 upb 代码库中"下划线即命名空间分隔符"的命名约定,以及UPB_PRIVATE()宏如何在没有private关键字的情况下,把内部符号从用户可见的 API 中真正屏蔽掉。
定位:为什么 upb 需要一份独立的 C 风格指南
upb 用纯 C 编写,但代码的"精神"完全对齐 Google C++ 风格指南——风格指南原文开宗明义:"Everything written here is intended to follow the spirit of the C++ style guide."
C++ 风格指南中的两条核心机制在 C 中都没有直接对应物:
- C++ 有命名空间(
upb::Arena::New()),C 没有; - C++ 有
private访问控制,C 没有。
这份指南要解决的正是这两个缺口:用下划线前缀 + 大小写规则模拟命名空间语义,用UPB_PRIVATE()宏模拟私有访问控制。文档同时坦承现状:"upb is currently inconsistent about following these conventions",并明确了演进优先级——优先转换公共接口(public interfaces),因为这些接口一旦定型后续更难修改。
命名规则:下划线是命名空间分隔符
函数与类型的命名模式
C 中凡是 C++ 里会写命名空间分隔符::的地方,一律用下划线_替代。以指南中upb::Arena::New()的 C 等价形式为例:
// C equivalent for upb::Arena::New() upb_Arena* upb_Arena_New();这一约定在仓库源码中随处可见,例如 upb/mem/arena.h 中的真实定义:
UPB_NODISCARD UPB_API_INLINE upb_Arena* upb_Arena_New(void) { return upb_Arena_Init(NULL, 0, &upb_alloc_global); } UPB_NODISCARD UPB_API_INLINE upb_Arena* upb_Arena_NewSized(size_t size_hint) { return upb_Arena_Init(NULL, size_hint, &upb_alloc_global); }命名结构一目了然:upb_是顶层"命名空间",Arena是"类"名,New/NewSized是"方法"名,各段之间用_分隔。
禁止用下划线分隔单词
既然_承担了命名空间分隔的职责,就不能再拿它来分隔同一个函数名内的单词,否则符号会产生歧义。指南给出的正反对照是:
// BAD: this would be interpreted as upb::FieldDef::has::default(). bool upb_FieldDef_has_default(const upb_FieldDef* f);// GOOD: this is equivalent to upb::FieldDef::HasDefault(). bool upb_FieldDef_HasDefault(const upb_FieldDef* f);错误的写法会让读者无法区分has_default是一个方法,还是has子命名空间下的default方法。正确写法把方法名整体作为PascalCase词块。仓库中 upb/reflection/field_def.h 就是遵循该规则的实例:
bool upb_FieldDef_HasDefault(const upb_FieldDef* f);同文件中还有upb_FieldDef_Type()(upb/reflection/field_def.h)等大量同一模式的声明,且整个头文件被extern "C"块包裹(见 upb/reflection/field_def.h),保证这套 C API 可以在 C++ 中直接调用。
多词命名空间使用 PascalCase
当"命名空间"本身由多个单词组成时,这部分整体写成PascalCase,与后续的方法名之间仍用_分隔。指南举的例子是 Python 绑定中的PyUpb命名空间:
// `PyUpb` is the namespace. PyObject* PyUpb_CMessage_GetAttr(PyObject* _self, PyObject* attr);这种"前缀即命名空间"的写法在仓库的语言绑定代码中同样得到印证:PHP 绑定头文件 php/ext/google/protobuf/php-upb.h 与 Ruby 绑定头文件 ruby/ext/google/protobuf_c/ruby-upb.h 都在各自 C 入口处独立定义了同款的UPB_PRIVATE()宏,前缀命名让各语言绑定能把自己的符号和 upb 核心库的符号清晰区隔开。
UPB_PRIVATE():没有 private 关键字时的访问控制
基本用法
C 没有private,upb 的做法是用UPB_PRIVATE()宏标记"只允许 upb 内部访问"的函数与结构体成员:
// Internal-only function. int64_t UPB_PRIVATE(upb_Int64_FromLL)(); // Internal-only members. Underscore prefixes are only necessary when the // structure is defined in a header file. typedef struct { const int32_t* UPB_PRIVATE(values); uint64_t UPB_PRIVATE(mask); int UPB_PRIVATE(value_count); } upb_MiniTableEnum; // Using these members in an internal function. int upb_SomeFunction(const upb_MiniTableEnum* e) { return e->UPB_PRIVATE(value_count); }指南特别指出:结构体成员使用下划线前缀只在该结构体定义于头文件时才有必要。这与真实源码一致——upb/mini_table/internal/enum.h 中定义的struct upb_MiniTableEnum几乎与指南示例一一对应:
struct upb_MiniTableEnum { uint32_t UPB_PRIVATE(mask_limit); // Highest that can be tested with mask. uint32_t UPB_PRIVATE(value_count); // Number of values after the bitfield. uint32_t UPB_PRIVATE(data)[]; // Bitmask + enumerated values follow. };该头文件中的内联函数upb_MiniTableEnum_CheckValue()(upb/mini_table/internal/enum.h)在访问这些成员时全部通过e->UPB_PRIVATE(data)、e->UPB_PRIVATE(mask_limit)的形式读取,正是指南中"internal function 使用这些成员"的写法。此外,upb/mem/arena.h 中还展示了头文件里用UPB_PRIVATE()修饰static全局常量的用法:
static const size_t UPB_PRIVATE(kUpbDefaultMaxBlockSize) = UPB_DEFAULT_MAX_BLOCK_SIZE;屏蔽原理:def.inc 与 undef.inc 的配对机制
UPB_PRIVATE()之所以能"防止用户访问这些符号",靠的是宏展开后的符号重命名加上"文本头文件不可访问"两层机制。宏的本体定义在 upb/port/def.inc:
#define UPB_PRIVATE(x) x##_dont_copy_me__upb_internal_use_only也就是说,UPB_PRIVATE(values)在编译期会展开成values_dont_copy_me__upb_internal_use_only——即使有人绕过头文件直接猜字段名,也猜不到这个带后缀的真实符号名。
更关键的是作用域隔离:def.inc文件头的注释明确规定了使用协议——
- 每个
.c文件必须在所有 upb 头文件之后包含upb/port/def.inc; - 每个
.h文件必须在其末尾包含upb/port/undef.inc(对.c文件可以省略); - 这个文本头是private的,用户不允许包含。
upb/port/undef.inc 则会#undef掉def.inc中定义的全部宏,确保这些内部宏不会泄漏到用户代码里。于是对外部用户而言:头文件中的成员名在预处理阶段就已经变成了带_dont_copy_me__upb_internal_use_only后缀的符号,而UPB_PRIVATE这个宏本身也随undef.inc消失,用户既无法拼出正确字段名,也无法自己定义同名宏——私有访问控制在 C 的约束下被最大程度地模拟了出来。
路径说明:风格指南原文写作
port_def.inc/port_undef.inc,当前仓库中对应的实际文件是 upb/port/def.inc 与 upb/port/undef.inc;C++ 运行时一侧另有同名的 src/google/protobuf/port_def.inc / src/google/protobuf/port_undef.inc,二者机制类似但服务于不同代码库。
.c 文件中的私有符号:用 static 就够了
指南最后划定了UPB_PRIVATE()的适用边界:只有定义在头文件里的东西才需要UPB_PRIVATE();对于只出现在.c文件中的符号,用 C 语言原生的static标记私有函数即可,不必额外套宏。这避免了在实现文件里引入无意义的符号重命名。
一致性与演进方向
文档明确承认:upb 代码库目前对上述约定尚未完全一致("upb is currently inconsistent about following these conventions"),但方向是全部代码最终都会向这些规则靠拢。排优先级时以公共接口为最——因为公共 API 是语言绑定(Python、PHP、Ruby 等)和外部嵌入者依赖的稳定面,改动成本最高;内部实现则可以在日常开发中渐进修正。
从仓库现状看,核心模块(如upb/mem、upb/mini_table、upb/reflection)的公共头文件已经比较严格地遵循了"upb_前缀 + PascalCase 类型/方法名 +UPB_PRIVATE()成员"的组合,这套约定实际上已经构成了 upb 公共 API 的"事实接口文档":看到一个upb_FieldDef_HasDefault这样的符号,无需阅读实现即可推断出它对应 C++ 语义下的upb::FieldDef::HasDefault()。
小结
这份 C 风格指南虽然篇幅不长,但它回答了"纯 C 项目如何做 API 设计"的两个核心问题:
- 命名即结构:
upb_Arena_New这种"命名空间_类型_方法"模式让 C 符号自带层级语义,且通过禁止单词级下划线避免了歧义; - 宏即访问控制:
UPB_PRIVATE()配合def.inc/undef.inc的 include 协议,把内部符号在预处理期重命名并限定在 upb 构建上下文内,实现了 C 语言中尽可能接近private的隔离效果。
如果你在为自己的纯 C 库设计公共 API,这套"前缀命名 + 私有符号后缀"的组合值得直接借鉴;继续阅读 docs/upb/design.md 与 docs/upb/arena_fusion.md 可以了解 upb 的运行时设计,进一步理解这些命名约定所服务的整体架构。
【免费下载链接】protobufProtocol Buffers - Google's data interchange format项目地址: https://gitcode.com/GitHub_Trending/pr/protobuf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考