You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust使用syn为函数添加代码行时写回文件无修改问题

问题根因
  • 对过程宏的执行时机理解完全错误:过程宏是编译期执行的代码,在编译main.rs的阶段,所有gen!()宏调用就已经被展开替换成固定代码了,运行二进制的时候根本不会执行你写在gen.rs里的宏逻辑。
  • 宏输入完全不符合预期:你写的gen::gen!(&token_stream),编译期传给宏的是源码里字面量&token_stream这段表达式的Token流,根本不是你运行时读取文件解析出来的函数语法树。这段输入匹配不到syn::Item::Fn,直接走错误分支返回原输入,展开后的代码等价于let file_contents_updated = &token_stream;,运行时拿到的就是你未修改的原始文件Token流,写出去自然和原文件完全一致。
正确修改方案

你这个需求是运行时读取、修改、生成Rust源码,属于AST操作工具的场景,根本不需要用过程宏,直接把语法树修改逻辑写成普通公共函数,在二进制运行时调用即可。

步骤1:调整Cargo.toml配置

去掉proc-macro属性,补全syn依赖所需的feature:

[package]
name = "gen-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
syn = { version = "2.0", features = ["full", "visit-mut"] }
quote = "1.0"
proc-macro2 = "1.0"

[lib]
name = "gen"
path = "src/gen.rs"

[[bin]]
name = "main"
path = "src/main.rs"

步骤2:重写gen.rs逻辑

把过程宏改成普通语法树处理函数,用syn的可变访问器遍历修改所有函数节点,避免单节点解析无法处理多Item文件的问题:

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::visit_mut::VisitMut;

struct InsertCode;

impl VisitMut for InsertCode {
    fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
        // 先递归处理内部嵌套项,避免漏改
        syn::visit_mut::visit_item_fn_mut(self, node);
        // 在函数块第一行插入目标代码
        node.block.stmts.insert(
            0,
            syn::parse2(quote!(println!("count me in");)).unwrap()
        );
    }
}

pub fn modify_ast(mut file: syn::File) -> TokenStream {
    InsertCode.visit_file_mut(&mut file);
    file.to_token_stream()
}

步骤3:调整main.rs调用逻辑

去掉宏调用,直接在运行时调用普通处理函数:

use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process;

use gen::modify_ast;

#[derive(Debug)]
enum Error {
    ReadFile(std::io::Error),
    ParseFile {
        error: syn::Error,
        filepath: PathBuf,
        source_code: String,
    },
    WriteFile(std::io::Error),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::ReadFile(e) => write!(f, "读取文件失败: {}", e),
            Error::ParseFile { error, filepath, .. } => {
                write!(f, "解析文件{}失败: {}", filepath.display(), error)
            }
            Error::WriteFile(e) => write!(f, "写入文件失败: {}", e),
        }
    }
}

fn main() {
    if let Err(error) = try_main() {
        let _ = writeln!(io::stderr(), "{}", error);
        process::exit(1);
    }
}

fn try_main() -> Result<(), Error> {
    let mut args = env::args_os();
    let _ = args.next();

    let filepath = PathBuf::from("./src/file-to-parse.rs");
    let code = fs::read_to_string(&filepath).map_err(Error::ReadFile)?;
    let syntax = syn::parse_file(&code).map_err(|error| Error::ParseFile {
        error,
        filepath,
        source_code: code,
    })?;

    let updated_content = modify_ast(syntax);
    fs::write("./src/file-updated.rs", updated_content.to_string()).map_err(Error::WriteFile)?;

    Ok(())
}
补充说明

如果你确实要实现过程宏给其他业务代码用(比如给标注了宏的函数自动插入代码),那宏的使用方式应该是直接挂在目标函数定义上,而不是在独立二进制里运行时读文件传入:

// 业务代码里直接使用宏
use gen::gen;

#[gen]
fn init() {
    println!("Hello, world!");
}

这种场景下才需要保留proc-macro = true配置,把宏改成属性宏实现即可。

内容的提问来源于stack exchange,提问作者Mr.Smithyyy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:42:24