1 个不稳定版本
0.0.0 | 2021年4月20日 |
---|
#25 在 #clang
每月 124 次下载
用于 clang-ast
2KB
-ast-dump=json
这个库为 Rust 提供了处理 Clang 的 -ast-dump=json
格式的反序列化逻辑。
[dependencies]
clang-ast = "0.1"
格式概述
编译器命令如
$ 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 License,Version 2.0或MIT许可下许可。Apache License, Version 2.0或MIT license。除非您明确说明,否则您提交给此crate的任何有意包含的贡献,根据Apache-2.0许可定义,将按上述方式双许可,不附加任何其他条款或条件。