Rust代码生成工具tblgen的使用,tblgen提供高效灵活的元编程与自动化代码生成功能

Rust代码生成工具tblgen的使用

tblgen是一个提供高效灵活的元编程与自动化代码生成功能的Rust工具,它提供了对TableGen的原始绑定和安全包装。TableGen是LLVM项目使用的领域特定语言。

主要特性

  • 支持LLVM 16、17、18和19版本(可通过功能标志选择)
  • 允许开发自定义TableGen后端
  • 主要用于从TableGen描述文件生成Rust代码的程序宏

安装

在项目目录中运行以下Cargo命令:

cargo add tblgen

或在Cargo.toml中添加:

tblgen = "0.6.0"

完整使用示例

下面是一个更完整的tblgen使用示例,包含更多细节和功能:

  1. 首先创建TableGen描述文件 person.td:
// 定义基类
class Human {
    string name;
    int age;
}

// 继承Human类
class Person : Human {
    list<string> hobbies;
    string email;
}

// 定义实例
def John: Person {
    let name = "John Doe";
    let age = 30;
    let hobbies = ["reading", "swimming"];
    let email = "john@example.com";
}

def Alice: Person {
    let name = "Alice Smith";
    let age = 28;
    let hobbies = ["hiking", "photography"];
    let email = "alice@example.com";
}
  1. 创建Rust代码生成工具 generate_person.rs:
use tblgen::{Record, RecordKeeper};
use std::path::Path;

fn main() {
    // 初始化记录保持器
    let keeper = RecordKeeper::new();
    
    // 加载TableGen文件
    let input_file = "person.td";
    if !Path::new(input_file).exists() {
        eprintln!("错误: 文件 {} 不存在", input_file);
        std::process::exit(1);
    }
    
    keeper.add_file(input_file)
        .unwrap_or_else(|e| {
            eprintln!("加载TableGen文件失败: {}", e);
            std::process::exit(1);
        });
    
    // 生成Rust代码
    generate_rust_models(&keeper);
}

fn generate_rust_models(keeper: &RecordKeeper) {
    let mut output = String::new();
    
    // 添加文件头
    output.push_str("// 自动生成的人员模型\n");
    output.push_str("// 不要手动编辑此文件\n\n");
    output.push_str("#[allow(unused_imports)]\n");
    output.push_str("use std::collections::HashMap;\n\n");
    
    // 生成枚举类型
    output.push_str("#[derive(Debug, Clone)]\n");
    output.push_str("pub enum PersonValue {\n");
    output.push_str("    String(String),\n");
    output.push_str("    Int(i32),\n");
    output.push_str("    Hobbies(Vec<String>),\n");
    output.push_str("}\n\n");
    
    // 为每个类生成Rust结构体
    for record in keeper.all_records().filter(|r| r.is_class()) {
        // 生成结构体定义
        output.push_str(&format!("#[derive(Debug, Clone)]\n"));
        output.push_str(&format!("pub struct {} {{\n", record.name()));
        
        // 生成字段
        for field in record.fields() {
            output.push_str(&format!("    pub {}: {},\n", 
                field.name(),
                map_tablegen_type_to_rust(field.value())
            ));
        }
        
        output.push_str("}\n\n");
        
        // 为结构体生成实现
        output.push_str(&format!("impl {} {{\n", record.name()));
        output.push_str(&format!("    pub fn new() -> Self {{\n"));
        output.push_str(&format!("        Self {{\n"));
        
        for field in record.fields() {
            output.push_str(&format!("            {}: {},\n",
                field.name(),
                get_default_value(field.value())
            ));
        }
        
        output.push_str(&format!("        }}\n"));
        output.push_str(&format!("    }}\n"));
        output.push_str(&format!("}}\n\n"));
    }
    
    // 生成实例数据
    output.push_str("pub fn load_person_data() -> HashMap<String, HashMap<String, PersonValue>> {\n");
    output.push_str("    let mut persons = HashMap::new();\n\n");
    
    for record in keeper.all_records().filter(|r| r.is_def()) {
        output.push_str(&format!("    let mut {}_data = HashMap::new();\n", record.name()));
        
        for value in record.values() {
            output.push_str(&format!("    {}_data.insert(\"{}\".into(), PersonValue::{}(\"{}\".into()));\n",
                record.name(),
                value.name(),
                match value.value() {
                    "int" => "Int",
                    "list<string>" => "Hobbies",
                    _ => "String"
                },
                value.value()
            ));
        }
        
        output.push_str(&format!("    persons.insert(\"{}\".into(), {}_data);\n\n", record.name(), record.name()));
    }
    
    output.push_str("    persons\n");
    output.push_str("}\n");
    
    // 写入文件
    let output_file = "src/models/person.rs";
    std::fs::create_dir_all("src/models").expect("无法创建目录");
    std::fs::write(output_file, output).unwrap_or_else(|e| {
        eprintln!("写入文件失败: {}", e);
        std::process::exit(1);
    });
    
    println!("成功生成Rust模型到 {}", output_file);
}

fn map_tablegen_type_to_rust(value: &str) -> &str {
    match value {
        "int" => "i32",
        "string" => "String",
        "list<string>" => "Vec<String>",
        _ => "String",
    }
}

fn get_default_value(value: &str) -> String {
    match value {
        "int" => "0".to_string(),
        "string" => "String::new()".to_string(),
        "list<string>" => "Vec::new()".to_string(),
        _ => "String::new()".to_string(),
    }
}
  1. 使用生成的代码

创建 main.rs 来使用生成的代码:

mod models;

use models::{Person, load_person_data};
use std::collections::HashMap;

fn main() {
    // 使用生成的Person结构体
    let mut person = Person::new();
    person.name = "Bob".to_string();
    person.age = 25;
    person.hobbies = vec!["coding".to_string(), "gaming".to_string()];
    person.email = "bob@example.com".to_string();
    
    println!("{:?}", person);
    
    // 使用加载的数据
    let persons = load_person_data();
    println!("Loaded persons: {:?}", persons);
}

环境配置

可以通过环境变量指定LLVM安装目录:

export TABLEGEN_18_PREFIX=/usr/local/opt/llvm@18

项目信息

  • 许可证: MIT OR Apache-2.0
  • 原始项目: Daan Vanoverloop创建
  • 当前维护者: Yota Toyama, Edgar, Daan Vanoverloop

tblgen特别适合需要将领域特定语言(DSL)转换为Rust代码的场景,如编译器开发、硬件描述语言转换等。它的主要优势在于与LLVM生态系统的紧密集成,为需要处理复杂代码生成任务的开发者提供了强大工具。


1 回复

Rust代码生成工具tblgen使用指南

简介

tblgen是一个Rust代码生成工具,提供了高效灵活的元编程与自动化代码生成功能。它特别适合需要大量重复代码模式或需要根据某些规范自动生成代码的场景。

主要特性

  • 基于模板的代码生成
  • 支持自定义数据源
  • 高效的增量生成
  • 与Rust构建系统无缝集成
  • 可扩展的生成器架构

安装方法

在Cargo.toml中添加依赖:

[dependencies]
tblgen = "0.4"

或者使用cargo命令安装:

cargo add tblgen

基本使用方法

1. 定义模板

创建一个简单的模板文件template.rs.tpl

// 自动生成的代码 - 请勿手动修改
#![allow(unused)]

pub struct {{name}} {
    {% for field in fields %}
    pub {{field.name}}: {{field.type}},
    {% endfor %}
}

impl {{name}} {
    pub fn new({% for field in fields %}{{field.name}}: {{field.type}}{% if !loop.last %}, {% endif %}{% endfor %}) -> Self {
        Self {
            {% for field in fields %}
            {{field.name}},
            {% endfor %}
        }
    }
}

2. 创建生成器脚本

创建build.rs构建脚本:

use tblgen::Generator;
use std::path::Path;

fn main() {
    let generator = Generator::new()
        .template_file("template.rs.tpl")
        .output_file("src/generated.rs");
    
    let data = serde_json::json!({
        "name": "Person",
        "fields": [
            {"name": "name", "type": "String"},
            {"name": "age", "type": "u32"},
            {"name": "email", "type": "Option<String>"}
        ]
    });
    
    generator.generate(&data).unwrap();
}

3. 使用生成的代码

生成的代码会自动放在src/generated.rs中,然后在你的主文件中引入:

mod generated;
use generated::Person;

fn main() {
    let person = Person::new("Alice".to_string(), 30, Some("alice@example.com".to_string()));
    println!("Name: {}, Age: {}", person.name, person.age);
}

高级用法

从外部文件加载数据

use tblgen::Generator;
use std::fs;

fn main() {
    let generator = Generator::new()
        .template_file("template.rs.tpl")
        .output_file("src/generated.rs");
    
    let data = fs::read_to_string("data.json").unwrap();
    let data: serde_json::Value = serde_json::from_str(&data).unwrap();
    
    generator.generate(&data).unwrap();
}

多模板生成

use tblgen::Generator;

fn main() {
    Generator::new()
        .template_file("struct.tpl")
        .output_file("src/structs.rs")
        .generate(&get_struct_data()).unwrap();
        
    Generator::new()
        .template_file("enum.tpl")
        .output_file("src/enums.rs")
        .generate(&get_enum_data()).unwrap();
}

自定义过滤器

use tblgen::{Generator, Filter};

fn uppercase(input: &str) -> String {
    input.to_uppercase()
}

fn main() {
    let mut generator = Generator::new()
        .template_file("template.tpl")
        .output_file("output.rs");
    
    generator.add_filter("uppercase", Filter::new(uppercase));
    
    generator.generate(&some_data).unwrap();
}

模板语法参考

tblgen使用类似Jinja2的模板语法:

  • {{variable}} - 变量插值
  • {% if condition %} - 条件语句
  • {% for item in list %} - 循环
  • {# comment #} - 注释
  • 过滤器:{{ name | uppercase }}

性能提示

  1. 对于大型项目,考虑将生成器拆分为多个步骤
  2. 使用增量生成只重新生成必要的文件
  3. 缓存解析后的模板以提高性能

完整示例

下面是一个完整的tblgen使用示例,包含所有必要文件和目录结构:

project/
├── Cargo.toml
├── build.rs
├── data.json
├── src/
│   ├── main.rs
│   └── generated.rs (自动生成)
└── templates/
    └── person.tpl
  1. 首先创建模板文件templates/person.tpl:
// 自动生成的Person模块
#![allow(unused)]

pub struct {{name}} {
    {% for field in fields %}
    pub {{field.name}}: {{field.type}},
    {% endfor %}
}

impl {{name}} {
    /// 创建新实例
    pub fn new({% for field in fields %}{{field.name}}: {{field.type}}{% if !loop.last %}, {% endif %}{% endfor %}) -> Self {
        Self {
            {% for field in fields %}
            {{field.name}},
            {% endfor %}
        }
    }
    
    /// 显示信息方法
    pub fn display(&self) {
        println!("{{name}}信息:");
        {% for field in fields %}
        println!("- {{field.name}}: {:?}", self.{{field.name}});
        {% endfor %}
    }
}
  1. 创建数据文件data.json:
{
    "name": "Employee",
    "fields": [
        {"name": "id", "type": "u32"},
        {"name": "name", "type": "String"},
        {"name": "department", "type": "String"},
        {"name": "salary", "type": "f64"}
    ]
}
  1. 创建build.rs构建脚本:
use tblgen::Generator;
use std::{fs, path::Path};

fn main() {
    // 设置生成器
    let generator = Generator::new()
        .template_file("templates/person.tpl")
        .output_file("src/generated.rs");
    
    // 从JSON文件加载数据
    let data = fs::read_to_string("data.json").unwrap();
    let data: serde_json::Value = serde_json::from_str(&data).unwrap();
    
    // 生成代码
    generator.generate(&data).unwrap();
    
    // 告诉Cargo在数据或模板变化时重新运行构建脚本
    println!("cargo:rerun-if-changed=data.json");
    println!("cargo:rerun-if-changed=templates/person.tpl");
}
  1. 主程序src/main.rs:
mod generated;
use generated::Employee;

fn main() {
    let emp = Employee::new(
        1001,
        "张三".to_string(),
        "技术部".to_string(),
        15000.0
    );
    
    emp.display();
}
  1. 运行项目:
cargo run

这个完整示例展示了如何:

  1. 创建模板文件
  2. 使用外部JSON作为数据源
  3. 设置构建脚本自动生成代码
  4. 使用生成的代码
  5. 设置文件监视以支持增量生成

tblgen为Rust开发者提供了强大的代码生成能力,可以显著减少样板代码,提高开发效率。

回到顶部