Правильный способ обработки текстового файла, относящегося ко времени компиляции, переданного в процедурный макрос - PullRequest
1 голос
/ 08 ноября 2019

У меня есть требование передать процедурному макросу либо текстовый файл, либо содержимое текстового файла, чтобы процедурный макрос действовал на основе содержимого этого текстового файла во время компиляции. То есть текстовый файл настраивает вывод макроса. Вариант использования для этого - файл, определяющий карту регистров, которую макрос создает в библиотеке.

Второе требование заключается в том, что текстовый файл правильно обрабатывается Cargo, так что изменения в триггере текстового файлаперекомпиляция так же, как изменения исходного файла, вызывают перекомпиляцию.

Первоначально я думал создать строку static с использованием макроса include_str!. Это решает второе требование, но я не вижу, как передать , что в макрос - в этот момент у меня есть только идентификатор строки для передачи:

use my_macro_lib::my_macro;
static MYSTRING: &'static str = include_str!("myfile");
my_macro!(MYSTRING); // Not the string itself!

Iможет передать строку в макрос с именем файла в строковом литерале и открыть файл внутри макроса:

my_macro!("myfile");

В этот момент у меня возникли две проблемы:

  1. Не очевидно, как получить путь к вызывающей функции, чтобы получить путь к файлу. Первоначально я думал, что это будет выставлено через токен Span, но в целом, похоже, нет (возможно, я что-то упустил?).
  2. Не очевидно, как заставить файл Cargo вызватьперекомпилировать по изменениям. Одна из идей, которую мне пришлось заставить это, заключалась в добавлении include_str!("myfile") к выводу макроса, что, как мы надеемся, привело бы к тому, что компиляция узнала о «myfile», но это немного грязно.

Есть ли способ сделать то, что я пытаюсь сделать? Возможно, либо каким-либо образом получая содержимое строки внутри макроса, который был создан снаружи, либо надежно получая путь к вызывающему файлу ржавчины (затем заставляя Cargo правильно обрабатывать изменения).

В качестве отступления, яЯ читал различные места, в которых говорится, что я не могу получить доступ к содержимому переменных внутри макроса, но мне кажется, что это именно то, что макрос quote делает с #variables. Как это работает?

1 Ответ

0 голосов
/ 12 ноября 2019

Таким образом, оказывается, что это возможно в сущности так, как я надеялся с помощью стабильного компилятора.

Если мы примем, что нам нужно работать относительно корня ящика, мы можем определить наши пути как таковые.

Полезно, что внутри кода макроса std::env::current_dir() вернет текущий рабочий каталог в качестве корня ящика, содержащего сайт вызова. Это означает, что даже если вызов макроса находится внутри некоторой иерархии ящиков, он все равно будет возвращать путь, который имеет смысл в месте вызова макроса.

Следующий пример макроса выполняет по существу то, что мне нужно. Для краткости, он не предназначен для правильной обработки ошибок:

extern crate proc_macro;

use quote::quote;
use proc_macro::TokenStream;
use syn::parse::{Parse, ParseStream, Result};
use syn;
use std;
use std::fs::File;
use std::io::Read;

#[derive(Debug)]
struct FileName {
    filename: String,
}

impl Parse for FileName {

    fn parse(input: ParseStream) -> Result<Self> {
        let lit_file: syn::LitStr = input.parse()?;
        Ok(Self { filename: lit_file.value() })
    }
}

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as FileName);

    let cwd = std::env::current_dir().unwrap();

    let file_path = cwd.join(&input.filename);
    let file_path_str = format!("{}", file_path.display());

    println!("path: {}", file_path.display());

    let mut file = File::open(file_path).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();

    println!("contents: {:?}", contents);

    let result = quote!(

        const FILE_STR: &'static str = include_str!(#file_path_str);
        pub fn foo() -> bool {
            println!("Hello");
            true
        }
    );

    TokenStream::from(result)
}

Который может быть вызван с помощью

my_macro!("mydir/myfile");

, где mydir - каталог в корне вызывающего ящика.

Это использует хак с использованием include_str!() в выводе макроса, чтобы вызвать перестройку при изменениях myfile. Это необходимо и делает то, что ожидается. Я ожидал бы, что это будет оптимизировано, если оно никогда не будет использовано.

Мне было бы интересно узнать, не подходит ли этот подход в любой ситуации.

Относится к моему первоначальному вопросу, текущему ночьюреализует метод source_file() для Span. Это может быть лучший способ реализовать вышеизложенное, но я бы предпочел использовать стабильный. Проблема отслеживания для этого здесь .

Редактировать: вышеприведенная реализация завершается неудачно, когда пакет находится в рабочей области, и в этом месте текущий рабочий каталог является корнем рабочей области, а не корнем ящика,Это легко обойти с помощью чего-то подобного (вставляется между объявлениями cwd и file_path).

    let mut cwd = std::env::current_dir().unwrap();

    let cargo_path = cwd.join("Cargo.toml");
    let mut cargo_file = File::open(cargo_path).unwrap();
    let mut cargo_contents = String::new();
    cargo_file.read_to_string(&mut cargo_contents).unwrap();

    // Use a simple regex to detect the suitable tag in the toml file. Much 
    // simpler than using the toml crate and probably good enough according to
    // the workspace RFC.
    let cargo_re = regex::Regex::new(r"(?m)^\[workspace\][ \t]*$").unwrap();

    let workspace_path = match cargo_re.find(&cargo_contents) {
        Some(val) => std::env::var("CARGO_PKG_NAME"),
        None => "".to_string()
    };

    let file_path = cwd.join(workspace_path).join(input.filename);
    let file_path_str = format!("{}", file_path.display());
...