27个版本
0.1.26 | 2024年8月11日 |
---|---|
0.1.25 | 2024年6月19日 |
0.1.23 | 2024年3月16日 |
0.1.21 | 2023年10月27日 |
0.1.6 | 2021年5月14日 |
#426 在 编码
4,766 每月下载次数
在 cxxbridge-macro 中使用
105KB
2.5K SLoC
-ast-dump=json
此库为Rust提供了处理Clang的-ast-dump=json
格式的反序列化逻辑。
[dependencies]
clang-ast = "0.1"
格式概述
AST转储是由编译器命令生成的,例如
$ clang++ -Xclang -ast-dump=json -fsyntax-only path/to/source.cc
高级结构是节点树,每个节点都有一个"id"和一个
"kind",根据节点类型,有零个或多个进一步的字段,最后是一个可选的
"inner"
子节点数组。
例如,对于只包含声明class S;
的输入文件,AST将如下所示
{
"id": "0x1fcea38", //<-- root node
"kind": "TranslationUnitDecl",
"inner": [
{
"id": "0xadf3a8", //<-- first child node
"kind": "CXXRecordDecl",
"loc": {
"offset": 6,
"file": "source.cc",
"line": 1,
"col": 7,
"tokLen": 1
},
"range": {
"begin": {
"offset": 0,
"col": 1,
"tokLen": 5
},
"end": {
"offset": 6,
"col": 7,
"tokLen": 1
}
},
"name": "S",
"tagUsed": "class"
}
]
}
库设计
按设计,clang-ast crate 不 提供一个涵盖每个可能的Clang节点类型的所有可能字段的单一大数据结构。有三大原因
-
性能 — 这些AST非常大。对于一个包含几个平台头文件的合理中等规模的翻译单元,你很容易就能得到一个十到几百兆字节的JSON AST。为了保持AST下游工具的性能,你需要仅反序列化你用例直接需要的少数几个字段,并允许Serde的解序列化器高效地忽略其余所有字段。
-
稳定性 — 随着Clang的开发,与每个节点类型相关的特定字段可能会随着时间的推移而以非累加的方式发生变化。这是没有问题的,因为单个节点规模的变动非常小(可能每隔几年才变化一次)。然而,如果存在一种数据结构能够反序列化每个节点中可能包含的每条信息,那么实际上Clang的任何更改都可能成为某些节点某处的破坏性更改,尽管你的工具对那种节点类型根本不在乎。通过只反序列化与你用例直接相关的字段,你可以免受大量语法树更改的影响。
-
编译时间 — 典型用例仅检查可能节点或字段的一小部分,大约为1%。因此,你的代码将比尝试将数据结构中的所有内容都包含在内的代码编译速度快100倍。
数据结构
clang-ast crate的核心数据结构是 Node<T>
。
pub struct Node<T> {
pub id: Id,
pub kind: T,
pub inner: Vec<Node<T>>,
}
调用者必须提供自己的类型T
,它是一个枚举或结构体,如下所述。T
决定了clang-ast crate将从AST转储中反序列化出哪些信息。
按照惯例,你应该将你的T
类型命名为Clang
。
T = 枚举
通常,你希望Clang
是一个枚举。在这种情况下,你的枚举必须为每个你关心的节点类型有一个变体。每个变体的名称与AST中看到的"kind"
条目匹配。
此外,还必须有一个后备变体,其名称必须是Unknown
或Other
,clang-ast会将所有不匹配预期类型的树节点放入其中。
use serde::Deserialize;
pub type Node = clang_ast::Node<Clang>;
#[derive(Deserialize)]
pub enum Clang {
NamespaceDecl { name: Option<String> },
EnumDecl { name: Option<String> },
EnumConstantDecl { name: String },
Other,
}
fn main() {
let json = std::fs::read_to_string("ast.json").unwrap();
let node: Node = serde_json::from_str(&json).unwrap();
}
以下是一个简单示例,具有处理"kind": "NamespaceDecl"
、"kind": "EnumDecl"
和"kind": "EnumConstantDecl"
节点变体的变体。这足以提取翻译单元中每个枚举的变体以及枚举的命名空间(可能是匿名)和枚举名称(可能是匿名)。
新类型变体也是可行的,尤其是如果你将反序列化某些节点的多个字段。
use serde::Deserialize;
pub type Node = clang_ast::Node<Clang>;
#[derive(Deserialize)]
pub enum Clang {
NamespaceDecl(NamespaceDecl),
EnumDecl(EnumDecl),
EnumConstantDecl(EnumConstantDecl),
Other,
}
#[derive(Deserialize, Debug)]
pub struct NamespaceDecl {
pub name: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct EnumDecl {
pub name: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct EnumConstantDecl {
pub name: String,
}
T = 结构体
很少,有时将Node实例化为Clang
作为结构体类型而不是枚举是有意义的。这允许从语法树中的每个节点反序列化一组统一的数据。
以下示例结构体收集了每个节点(如果存在)的"loc"
和"range"
;这些字段提供了节点的文件名/行/列位置。并非每个节点类型都包含此信息,因此我们使用Option
只为具有该信息的节点收集它。
use serde::Deserialize;
pub type Node = clang_ast::Node<Clang>;
#[derive(Deserialize)]
pub struct Clang {
pub kind: String, // or clang_ast::Kind
pub loc: Option<clang_ast::SourceLocation>,
pub range: Option<clang_ast::SourceRange>,
}
如果真的需要,也可以通过一个弱类型的 Map<String, Value>
和 Serde 的 flatten
属性来存储关于每个节点的每个键/值信息。
use serde::Deserialize;
use serde_json::{Map, Value};
#[derive(Deserialize)]
pub struct Clang {
pub kind: String, // or clang_ast::Kind
#[serde(flatten)]
pub data: Map<String, Value>,
}
混合方法
为了反序列化关于一组固定节点类型的特定信息,以及关于其他所有类型节点的一些统一信息,你可以通过给 Other
/ Unknown
回退变体一些字段,使用两种方法的混合。
use serde::Deserialize;
pub type Node = clang_ast::Node<Clang>;
#[derive(Deserialize)]
pub enum Clang {
NamespaceDecl(NamespaceDecl),
EnumDecl(EnumDecl),
Other {
kind: clang_ast::Kind,
},
}
源位置
许多节点类型公开了相应源代码标记的源位置,包括
- 它们所在的文件路径;
- 将文件引入翻译单元的
#include
链; - 源文件中的行/列位置;
- 由 C 预处理器宏的展开构建的标记的宏展开跟踪。
你可以在 JSON 表示中的 "loc"
和/或 "range"
字段中找到这些信息。
{
"id": "0x1251428",
"kind": "NamespaceDecl",
"loc": { //<--
"offset": 7004,
"file": "/usr/include/x86_64-linux-gnu/c++/10/bits/c++config.h",
"line": 258,
"col": 11,
"tokLen": 3,
"includedFrom": {
"file": "/usr/include/c++/10/utility"
}
},
"range": { //<--
"begin": {
"offset": 6994,
"col": 1,
"tokLen": 9
},
"end": {
"offset": 7155,
"line": 266,
"col": 1,
"tokLen": 1
}
},
...
}
由于 Clang 使用省略字段表示“与上一个相同”,因此这些结构的简单反序列化难以处理。所以如果 "loc"
没有在内部打印 "file"
,则表示该位置与序列化顺序中立即前一个位置相同的文件中。
clang-ast crate 提供了反序列化这种源位置信息的类型,以产生可能跨多个源位置共享的文件路径的 Arc<str>
类型。
use serde::Deserialize;
pub type Node = clang_ast::Node<Clang>;
#[derive(Deserialize)]
pub enum Clang {
NamespaceDecl(NamespaceDecl),
Other,
}
#[derive(Deserialize, Debug)]
pub struct NamespaceDecl {
pub name: Option<String>,
pub loc: clang_ast::SourceLocation, //<--
pub range: clang_ast::SourceRange, //<--
}
节点标识符
每个语法树节点都有一个 "id"
。在 JSON 中,它是 Clang 为该节点分配的内部内存地址,序列化为十六进制字符串。
AST 输出使用 id 作为有向无环图中节点的回引用。例如,以下 MemberExpr 节点是 operator bool
转换调用的组成部分,因此其语法树引用了解析的 operator bool
转换函数声明
{
"id": "0x9918b88",
"kind": "MemberExpr",
"valueCategory": "rvalue",
"referencedMemberDecl": "0x12d8330", //<--
...
}
它引用的节点,内存地址为 0x12d8330,可以在语法树中的某个地方找到
{
"id": "0x12d8330", //<--
"kind": "CXXConversionDecl",
"name": "operator bool",
"mangledName": "_ZNKSt17integral_constantIbLb1EEcvbEv",
"type": {
"qualType": "std::integral_constant<bool, true>::value_type () const noexcept"
},
"constexpr": true,
...
}
由于 id 在回引用中的普遍使用,将其反序列化为 64 位整数而不是字符串很有价值。clang-ast crate 提供了一个 Id
类型用于此目的,它具有较低的拷贝成本、可哈希性,并且比字符串更便宜地可比较。你可能会有很多以 Id
为键的散列表。
许可证
根据你的选择,受 Apache 许可证版本 2.0 或 MIT 许可证的许可,可在 Apache License, Version 2.0 或 MIT license 下使用。除非你明确声明,否则根据 Apache-2.0 许可证定义的,任何故意提交以包含在此 crate 中的贡献,都将按上述方式双许可,而无需任何额外的条款或条件。
依赖关系
~135–365KB