Rust C/C++代码解析库clang-ast的使用:基于Clang的AST工具,实现跨语言源码分析与转换
Rust C/C++代码解析库clang-ast的使用:基于Clang的AST工具,实现跨语言源码分析与转换
该库提供了从Rust高效处理Clang的-ast-dump=json
格式的反序列化逻辑。
[dependencies]
clang-ast = "0.1"
格式概述
AST dump可以通过如下编译器命令生成:
$ clang++ -Xclang -ast-dump=json -fsyntax-only path/to/source.cc
高级结构是一个节点树,每个节点都有一个"id"
和"kind"
,根据节点种类有零个或多个字段,最后可能有一个"inner"
数组包含子节点。
例如,对于仅包含声明class S;
的输入文件,AST如下:
{
"id": "0x1fcea38", //<-- 根节点
"kind": "TranslationUnitDecl",
"inner": [
{
"id": "0xadf3a8", //<-- 第一个子节点
"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可能非常大。对于一个中等规模的翻译单元,包括几个平台头文件,很容易产生几十到几百MB的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>>,
}
调用者必须提供自己的kind类型T
,这是一个枚举或结构体,如下所述。T
决定了clang-ast crate将从AST dump中反序列化的确切信息。
按照惯例,你应该将你的T
类型命名为Clang
。
T = enum
大多数情况下,你会希望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 = struct
很少情况下,用Clang
作为结构体类型而不是枚举来实例化Node可能有意义。这允许从语法树中的每个节点反序列化一组统一的数据。
下面的示例结构体收集每个节点如果存在的"loc"
和"range"
;这些字段提供节点在文件中的文件名/行/列位置。不是每个节点类型都包含此信息,所以我们使用Option
来仅收集那些有它的节点。
use serde::Deserialize;
pub type Node = clang-ast::Node<Clang>;
#[derive(Deserialize)]
pub struct Clang {
pub kind: String, // 或 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, // 或 clang-ast::Kind
#[serde(flatten)]
pub data: Map<String, Value>,
}
混合方法
要反序列化你关心的一组固定节点类型的种类特定信息,以及关于所有其他节点类型的一些统一信息,你可以通过给你的Other
/ Unknown
后备变体一些字段来使用两种方法的混合。
use ser极地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"
在里面,意味着loc与序列化顺序中紧接的前一个loc在同一个文件中。
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 dump在有向无环图性质的节点中使用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
为键的哈希表。
完整示例
下面是一个完整的使用clang-ast解析C++代码并提取函数信息的示例:
use serde::Deserialize;
use clang-ast::Node;
// 定义我们关心的节点类型
#[derive(Deserialize, Debug)]
pub enum Clang {
FunctionDecl(FunctionDecl),
Other,
}
// 函数声明结构
#[derive(Deserialize, Debug)]
pub struct FunctionDecl {
pub name: String,
pub loc: clang-ast::SourceLocation,
pub return_type: Type,
pub params: Option<Vec<ParamDecl>>,
}
// 参数声明结构
#[derive(Deserialize, Debug)]
pub struct ParamDecl {
pub name: String,
pub type_: Type,
}
// 类型信息
#[derive(Deserialize, Debug)]
pub struct Type {
pub qualType: String,
}
pub type AstNode = Node<Clang>;
fn main() {
// 读取AST JSON文件
let json = std::fs::read_to_string("example.ast.json").unwrap();
// 反序列化AST
let root: AstNode = serde_json::from_str(&json).unwrap();
// 遍历AST查找函数
find_functions(&root);
}
fn find_functions(node: &AstNode) {
match &node.kind {
Clang::FunctionDecl(func) => {
println!("Found function: {}", func.name);
println!(" Return type: {}", func.return_type.qualType);
println!(" Location: {}:{}", func.loc.file, func.loc.line);
if let Some(params) = &func.params {
println!(" Parameters:");
for param in params {
println!(" {}: {}", param.name, param.type_.qualType);
}
}
},
Clang::Other => {}
}
// 递归处理子节点
for child in &node.inner {
find_functions(child);
}
}
完整示例代码
以下是基于上述内容的一个更完整的示例,展示如何使用clang-ast分析C++代码中的类定义:
use serde::Deserialize;
use clang_ast::{Node, SourceLocation};
// 定义我们关心的节点类型
#[derive(Deserialize, Debug)]
#[serde(tag = "kind")]
pub enum Clang {
CXXRecordDecl {
name: Option<String>,
loc: SourceLocation,
tagUsed: String,
},
FieldDecl {
name: String,
type_: Type,
loc: SourceLocation,
},
Other,
}
// 类型信息结构
#[derive(Deserialize, Debug)]
pub struct Type {
pub qualType: String,
}
pub type AstNode = Node<Clang>;
fn main() {
// 生成一个示例AST文件
let ast_json = r#"{
"id": "0x1fcea38",
"kind": "TranslationUnitDecl",
"inner": [
{
"id": "0xadf3a8",
"kind": "CXXRecordDecl",
"loc": {
"offset": 6,
"file": "example.cpp",
"line": 3,
"col": 7
},
"name": "MyClass",
"tagUsed": "class",
"inner": [
{
"id": "0xae45f8",
"kind": "FieldDecl",
"loc": {
"offset": 24,
"file": "example.cpp",
"line": 5,
"col": 9
},
"name": "value",
"type": {
"qualType": "int"
}
}
]
}
]
}"#;
// 解析AST
let root: AstNode = serde_json::from_str(ast_json).unwrap();
// 分析类定义
analyze_classes(&root);
}
fn analyze_classes(node: &AstNode) {
match &node.kind {
Clang::CXXRecordDecl { name, loc, tagUsed } => {
if let Some(name) = name {
println!("Found {} {} at {}:{}", tagUsed, name, loc.file, loc.line);
// 查找类的成员变量
for child in &node.inner {
if let Clang::FieldDecl { name, type_, loc } = &child.kind {
println!(" Field: {} of type {} at {}:{}",
name, type_.qualType, loc.file, loc.line);
}
}
}
},
_ => {}
}
// 递归处理子节点
for child in &node.inner {
analyze_classes(child);
}
}
许可证
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Rust C/C++代码解析库clang-ast使用指南
概述
clang-ast是一个基于Clang的Rust库,用于解析和分析C/C++代码的抽象语法树(AST)。它提供了强大的跨语言源码分析和转换能力,使开发者能够在Rust中处理C/C++代码。
主要特性
- 完整的C/C++ AST表示
- 跨语言源码分析能力
- 源码转换功能
- 与Rust生态系统的良好集成
- 基于成熟的Clang前端
安装方法
在Cargo.toml中添加依赖:
[dependencies]
clang-ast = "0.4"
完整示例代码
// 演示clang-ast的完整使用流程:解析C代码、遍历AST、提取信息、生成绑定
use clang_ast::{Clang, EntityKind, TranslationUnit};
fn main() {
// 1. 初始化Clang并解析C文件
let clang = Clang::new().expect("Failed to initialize Clang");
let index = clang.create_index(false, false);
let tu = index
.parser("example.c") // 准备解析的C文件
.parse()
.expect("Failed to parse translation unit");
println!("=== 开始分析C代码 ===");
// 2. 遍历AST并打印基本信息
println!("\n--- AST遍历结果 ---");
visit_ast(tu.get_entity());
// 3. 提取函数信息
println!("\n--- 函数信息提取 ---");
extract_functions(&tu);
// 4. 生成Rust绑定
println!("\n--- Rust FFI绑定生成 ---");
generate_rust_bindings(&tu);
}
// 递归遍历AST节点
fn visit_ast(entity: clang_ast::Entity) {
match entity.get_kind() {
EntityKind::FunctionDecl => {
println!("函数声明: {}", entity.get_name());
}
EntityKind::VarDecl => {
println!("变量声明: {} (类型: {})",
entity.get_name(),
entity.get_type().get_display_name());
}
EntityKind::TypedefDecl => {
println!("类型定义: {}", entity.get_name());
}
_ => {}
}
// 递归访问子节点
for child in entity.get_children() {
visit_ast(child);
}
}
// 提取函数详细信息
fn extract_functions(tu: &TranslationUnit) {
let root = tu.get_entity();
for entity in root.get_children() {
if let EntityKind::FunctionDecl = entity.get_kind() {
let name = entity.get_name();
let return_type = entity.get_type().get_result_type().get_display_name();
println!("\n函数: {} -> {}", name, return_type);
// 打印参数
println!(" 参数:");
for param in entity.get_children().filter(|e| e.get_kind() == EntityKind::ParmVarDecl) {
println!(" {}: {}",
param.get_name(),
param.get_type().get_display_name());
}
// 打印函数体中的变量
println!(" 局部变量:");
for child in entity.get_children() {
if let EntityKind::VarDecl = child.get_kind() {
println!(" {}: {}",
child.get_name(),
child.get_type().get_display_name());
}
}
}
}
}
// 生成Rust FFI绑定
fn generate_rust_bindings(tu: &TranslationUnit) {
let root = tu.get_entity();
println!("// 自动生成的Rust FFI绑定");
println!("use libc::{{c_int, c_float, c_void}};\n");
for entity in root.get_children() {
if let EntityKind::FunctionDecl = entity.get_kind() {
let name = entity.get_name();
let return_type = map_type_to_rust(entity.get_type().get_result_type());
print!("#[no_mangle]\npub extern \"C\" fn {}(", name);
// 处理参数
let params: Vec<String> = entity.get_children()
.filter(|e| e.get_kind() == EntityKind::ParmVarDecl)
.map(|param| {
format!("{}: {}",
param.get_name(),
map_type_to_rust(param.get_type()))
})
.collect();
println!("{}) -> {} {{\n // TODO: 实现函数体\n unimplemented!()\n}}\n",
params.join(", "),
return_type);
}
}
}
// C类型到Rust类型的映射
fn map_type_to_rust(ty: clang_ast::Type) -> String {
let type_name = ty.get_display_name();
match type_name.as_str() {
"int" => "c_int".to_string(),
"float" => "c_float".to_string(),
"void" => "c_void".to_string(),
_ if type_name.ends_with('*') => {
let pointee = map_type_to_rust(ty.get_pointee_type());
format!("*mut {}", pointee)
}
_ => format!("/* 未映射类型: {} */", type_name)
}
}
实际应用场景
- 代码迁移工具:将C/C++代码转换为其他语言
- 静态分析工具:检测代码中的潜在问题
- 文档生成器:从源代码生成API文档
- 语言互操作工具:自动生成FFI绑定
- 代码重构工具:自动化大规模代码修改
注意事项
- 需要系统安装Clang/LLVM
- 处理大型代码库时注意内存使用
- AST遍历是递归的,深度嵌套结构可能导致栈溢出
- 某些C++特性可能不完全支持
clang-ast为Rust开发者提供了强大的C/C++代码处理能力,是构建跨语言工具链的理想选择。