API 注解宏和编译期契约支持
这一阶段属于幕后支撑,不是系统真正干活的主循环,而是在编译时先把规矩立好。“宏”可以理解成代码生成器:开发者给某个字段或选项贴上“实验性接口”的标签,lib.rs 里的编译期宏就会自动补出检查代码。这样别的模块收到请求时,不用手写一堆重复判断,也能知道里面有没有用到还不稳定的功能,方便统一登记、提醒或限制。
API 注解宏和编译期契约支持
codex-experimental-api-macros/src/lib.rs源码 ↗
这个文件像一个“自动贴警示牌的工人”。项目里有些 API 字段还在实验阶段,不能悄悄被使用;但如果每个类型都手写检查代码,很容易漏掉。这里定义了 ExperimentalApi 派生宏,也就是 Rust 的过程宏(一段在编译时运行、自动生成代码的程序)。当某个结构体或枚举写了 #[derive(ExperimentalApi)] 后,它会读取字段或枚举项上的 #[experimental("原因")] 标记,生成 experimental_reason 方法:如果实际数据里用了这些实验性内容,就返回原因文字;否则返回空。对结构体字段,它还会根据类型判断“有没有真的使用”:比如 Option 要有值,列表和映射要非空,布尔值要为真。它还支持 #[experimental(nested)],意思是这个字段里面还有一层对象,也要继续往下查。除此之外,它会把实验字段登记到 inventory 里,方便系统集中列出所有实验字段。
derive_experimental_api17–28 ↗
fn derive_experimental_api(input: TokenStream) -> TokenStream
作用:这是整个宏的入口。别人写 #[derive(ExperimentalApi)] 时,编译器会先调用它,它负责看被标记的是结构体、枚举,还是不支持的联合体。
数据流:进去的是编译器交来的代码片段 → 它把代码解析成 Rust 类型说明 → 如果是结构体就交给 derive_for_struct,如果是枚举就交给 derive_for_enum,如果是联合体就生成编译错误 → 出来的是一段新生成的 Rust 代码,或一条编译时报错。
调用关系:它站在最外层,像前台分诊。真正生成结构体检查代码的活交给 derive_for_struct,生成枚举检查代码的活交给 derive_for_enum;遇到不支持的 union,则直接用错误信息打断编译。
调用图:调用 2 个内部函数(derive_for_enum, derive_for_struct);外部调用 2 个(new_spanned, parse_macro_input!)。
derive_for_struct30–158 ↗
fn derive_for_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream
作用:这个函数专门给结构体生成实验性字段检查代码。它会看每个字段有没有实验标记,或者是不是需要继续检查的嵌套对象。
数据流:进去的是结构体的名字和字段清单 → 它逐个字段查看属性:有 #[experimental("原因")] 就生成“这个字段被使用时返回原因”的代码;有 #[experimental(nested)] 就生成“去字段内部继续问”的代码;同时为可登记的字段准备元数据 → 出来的是实现 ExperimentalApi 的代码,以及实验字段登记代码和 EXPERIMENTAL_FIELDS 常量。
调用关系:它由 derive_experimental_api 在发现目标是结构体时调用。它会借助 experimental_reason 读标记文字,用 field_serialized_name 算对外字段名,用 has_nested_experimental 判断是否要深入检查,再用 experimental_presence_expr 或 index_presence_expr 生成“字段算不算被用了”的判断。
调用图:调用 5 个内部函数(experimental_presence_expr, experimental_reason, field_serialized_name, has_nested_experimental, index_presence_expr);被 1 处调用(derive_experimental_api);外部调用 5 个(new, call_site, new, quote!, from)。
derive_for_enum160–193 ↗
fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream
作用:这个函数专门给枚举生成实验性检查代码。它判断当前枚举值是不是某个被标成实验性的选项。
数据流:进去的是枚举的名字和所有枚举项 → 它为每个枚举项生成一个匹配分支:如果该项有 #[experimental("原因")],匹配到它就返回原因;否则返回空 → 出来的是这个枚举的 ExperimentalApi 实现代码。
调用关系:它由 derive_experimental_api 在发现目标是枚举时调用。它主要依赖 experimental_reason 读取每个枚举项上的实验说明,然后把结果拼成一个 match,也就是按当前是哪一项来分情况处理。
调用图:调用 1 个内部函数(experimental_reason);被 1 处调用(derive_experimental_api);外部调用 2 个(new, quote!)。
experimental_reason195–197 ↗
fn experimental_reason(attrs: &[Attribute]) -> Option<LitStr>
作用:这个函数从一组属性里找实验性说明文字。简单说,它负责回答:“这里有没有写 #[experimental("原因")]?”
数据流:进去的是某个字段或枚举项上的属性列表 → 它从头到尾查找名叫 experimental 且参数是字符串的属性 → 找到就返回这段字符串,找不到就返回空。
调用关系:它被 derive_for_struct 和 derive_for_enum 使用。上层函数不直接解析属性细节,而是问它要不要把某个字段或枚举项当成实验性内容。
调用图:被 2 处调用(derive_for_enum, derive_for_struct);外部调用 1 个(iter)。
experimental_reason_attr199–205 ↗
fn experimental_reason_attr(attr: &Attribute) -> Option<LitStr>
作用:这个函数检查单个属性是不是 #[experimental("原因")] 这种形式。它只认字符串说明,不认别的写法。
数据流:进去的是一个属性 → 它先看属性名是不是 experimental,不是就直接返回空;是的话再尝试把括号里的内容解析成字符串 → 成功就返回字符串,失败就返回空。
调用关系:它是 experimental_reason 背后的细小检查员。experimental_reason 遍历属性时,会靠它判断每一条属性是不是实验说明。
调用图:外部调用 1 个(path)。
has_nested_experimental207–209 ↗
fn has_nested_experimental(field: &Field) -> bool
作用:这个函数判断一个字段是否写了“里面还有实验性内容,要继续往下查”的标记。也就是识别 #[experimental(nested)]。
数据流:进去的是一个字段 → 它查看字段上的所有属性 → 只要其中有一个符合 nested 写法,就返回 true;否则返回 false。
调用关系:它被 derive_for_struct 使用。当字段本身没有实验原因文字时,结构体生成逻辑会再问它:这个字段是不是一个需要递归检查的子对象。
调用图:被 1 处调用(derive_for_struct)。
experimental_nested_attr211–218 ↗
fn experimental_nested_attr(attr: &Attribute) -> bool
作用:这个函数检查单个属性是不是 #[experimental(nested)]。它用来区分“这个字段本身实验性”和“这个字段里面要继续查”。
数据流:进去的是一个属性 → 它先确认属性名是不是 experimental,再尝试把括号里的内容解析成标识符 nested → 符合就返回 true,不符合就返回 false。
调用关系:它是 has_nested_experimental 的底层判断工具。上层函数遍历字段属性时,会用它逐条确认有没有 nested 标记。
调用图:外部调用 1 个(path)。
field_serialized_name220–224 ↗
fn field_serialized_name(field: &Field) -> Option<String>
作用:这个函数把 Rust 结构体字段名转换成对外 API 常用的字段名。比如把 model_name 变成 modelName。
数据流:进去的是一个字段 → 它先拿字段自己的名字;如果这个字段没有名字,就返回空 → 有名字时,把下划线风格转换成小驼峰风格 → 出来的是可用于登记的字段名字符串。
调用关系:它被 derive_for_struct 调用,用在登记实验字段时。转换的具体动作交给 snake_to_camel,这样登记出来的字段名更接近 JSON/API 里看到的名字。
调用图:调用 1 个内部函数(snake_to_camel);被 1 处调用(derive_for_struct)。
snake_to_camel226–242 ↗
fn snake_to_camel(s: &str) -> String
作用:这个函数把 snake_case 名字转换成 camelCase 名字。通俗说,就是去掉下划线,并把下划线后面的字母变大写。
数据流:进去的是一个字符串 → 它逐个字符扫描:遇到下划线就记住“下个字母要大写”,普通字符就照常放入结果 → 出来的是转换后的字符串。
调用关系:它被 field_serialized_name 使用。它不懂宏、不懂字段,只做一个小而通用的名字格式转换。
调用图:被 1 处调用(field_serialized_name);外部调用 1 个(with_capacity)。
experimental_presence_expr244–253 ↗
fn experimental_presence_expr(
field: &Field,
tuple_struct: bool,
) -> Option<proc_macro2::TokenStream>
作用:这个函数给普通具名字段生成“这个实验字段是否真的被使用了”的判断代码。比如字段是 Option,就生成检查它是不是 Some 的代码。
数据流:进去的是字段信息,以及一个是否是元组结构体的标志 → 如果是元组结构体,它这里不处理,返回空;否则拿字段名拼出 self.字段名 的访问方式 → 再交给 presence_expr_for_access 按字段类型生成判断表达式 → 出来的是一段可插进生成代码里的判断代码。
调用关系:它被 derive_for_struct 用在具名结构体字段上。它负责把“访问哪个字段”这件事准备好,真正决定怎么判断是否存在,则交给 presence_expr_for_access。
调用图:调用 1 个内部函数(presence_expr_for_access);被 1 处调用(derive_for_struct);外部调用 1 个(quote!)。
index_presence_expr255–258 ↗
fn index_presence_expr(index: usize, ty: &Type) -> proc_macro2::TokenStream
作用:这个函数给元组结构体里的字段生成“是否使用了实验字段”的判断代码。元组结构体没有字段名,所以它用位置编号访问字段。
数据流:进去的是字段的位置编号和字段类型 → 它把编号变成 self.0、self.1 这种访问方式 → 再按类型生成合适的存在性判断 → 出来的是一段判断代码。
调用关系:它被 derive_for_struct 用在没有字段名、只有位置的结构体字段上。它和 experimental_presence_expr 做的是同类事情,只是一个按名字访问,一个按编号访问。
调用图:调用 1 个内部函数(presence_expr_for_access);被 1 处调用(derive_for_struct);外部调用 2 个(quote!, from)。
presence_expr_for_access260–274 ↗
fn presence_expr_for_access(
access: proc_macro2::TokenStream,
ty: &Type,
) -> proc_macro2::TokenStream
作用:这个函数决定一个字段怎样才算“真的被用了”。不同类型判断标准不同,它把这些规则集中放在一起。
数据流:进去的是字段访问代码,比如 self.foo,以及字段类型 → 如果类型是 Option,生成 is_some();如果像列表或映射,生成“不是空的”;如果是布尔值,直接看它是不是 true;其他类型只要字段存在就算使用 → 出来的是一段布尔判断代码。
调用关系:它被 experimental_presence_expr 和 index_presence_expr 调用,是字段使用判断的核心。为了识别类型,它会继续询问 option_inner、is_vec_like、is_map_like 和 is_bool。
调用图:调用 4 个内部函数(is_bool, is_map_like, is_vec_like, option_inner);被 2 处调用(experimental_presence_expr, index_presence_expr);外部调用 1 个(quote!)。
option_inner276–291 ↗
fn option_inner(ty: &Type) -> Option<&Type>
作用:这个函数判断一个类型是不是 Option<T>。Option 可以理解成“可能有值,也可能没有值”的盒子。
数据流:进去的是一个 Rust 类型 → 它检查这个类型是不是路径形式,并且最后一段名字是不是 Option,再看看尖括号里有没有内部类型 T → 如果是,就返回内部类型;不是就返回空。
调用关系:它被 presence_expr_for_access 使用。上层需要知道字段是不是 Option,因为 Option 字段只有在有值时才算用了实验性接口。
调用图:被 1 处调用(presence_expr_for_access)。
is_vec_like293–295 ↗
fn is_vec_like(ty: &Type) -> bool
作用:这个函数判断一个类型是不是 Vec。Vec 可以理解成一串可变长度的列表。
数据流:进去的是一个类型 → 它取出类型路径最后的名字 → 如果最后名字是 Vec 就返回 true,否则返回 false。
调用关系:它被 presence_expr_for_access 使用。上层用它决定列表字段要用“是否为空”来判断有没有使用实验性内容。
调用图:调用 1 个内部函数(type_last_ident);被 1 处调用(presence_expr_for_access)。
is_map_like297–299 ↗
fn is_map_like(ty: &Type) -> bool
作用:这个函数判断一个类型是不是常见的映射表,也就是 HashMap 或 BTreeMap。映射表可以理解成一张“键和值”对应的表。
数据流:进去的是一个类型 → 它取出类型路径最后的名字 → 如果是 HashMap 或 BTreeMap 就返回 true,否则返回 false。
调用关系:它被 presence_expr_for_access 使用。上层用它决定字典/映射字段要用“是否为空”来判断有没有用到实验性内容。
调用图:调用 1 个内部函数(type_last_ident);被 1 处调用(presence_expr_for_access)。
is_bool301–303 ↗
fn is_bool(ty: &Type) -> bool
作用:这个函数判断一个类型是不是布尔值 bool。布尔值就是只有 true 或 false 的开关。
数据流:进去的是一个类型 → 它取出类型路径最后的名字 → 如果最后名字是 bool 就返回 true,否则返回 false。
调用关系:它被 presence_expr_for_access 使用。上层用它决定布尔字段只有为 true 时,才算启用了实验性功能。
调用图:调用 1 个内部函数(type_last_ident);被 1 处调用(presence_expr_for_access)。
type_last_ident305–310 ↗
fn type_last_ident(ty: &Type) -> Option<Ident>
作用:这个函数取出一个类型名字的最后一段。比如 std::vec::Vec 的最后一段就是 Vec。
数据流:进去的是一个 Rust 类型 → 它确认这个类型是路径形式 → 如果能找到路径最后一段,就把这个名字返回;否则返回空。
调用关系:它被 is_vec_like、is_map_like 和 is_bool 共用。那些函数只关心类型最后叫什么,所以把取名字这件小事交给它。
调用图:被 3 处调用(is_bool, is_map_like, is_vec_like)。