如何编写生成模块的 Rust 编译器插件?
How to write a Rust compiler plugin that generates a module?
我正在编写一个扩展
的 Rust 编译器插件
choose! {
test_a
test_b
}
到
#[cfg(feature = "a")]
mod test_a;
#[cfg(feature = "b")]
mod test_b;
差不多大功告成了,但是最后展开的代码中模块什么都没有。我想原因是跨度没有覆盖模块文件。
use syntax::ast;
use syntax::ptr::P;
use syntax::codemap;
use syntax::parse::token;
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder;
use syntax_pos::Span;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;
// Ideally, it will expand
//
// ```rust
// choose! {
// test_a
// test_b
// }
// ```
// to
// ```rust
// #[cfg(feature = "a")]
// mod test_a;
// #[cfg(feature = "b")]
// mod test_b;
// ```
//
// but the modules contain nothing in the expanded code at present
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods: SmallVector<P<ast::Item>> = SmallVector::many(vec![]);
for arg in args {
let mut attrs = vec![];
let text = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
let cfg_str = token::InternedString::new("cfg");
let feat_str = token::InternedString::new("feature");
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
cfg_str,
vec![cx.meta_name_value(sp,
feat_str,
ast::LitKind::Str(token::intern_and_get_ident(text.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
test_mods.push(P(ast::Item {
ident: cx.ident_of(text.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
// === How to include the specified module file here? ===
ast::Mod {
inner: codemap::DUMMY_SP,
items: vec![],
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("choose", choose);
}
(Gist)
2016-08-25更新:使用libsyntax::parse::new_parser_from_source_str
避免手动设置模块路径。new_parser_from_source_str
只会在CWD定位模块,这是意想不到的.
正如@Francis 所指出的,模块文件的真实路径可能类似于 foo/mod.rs,
我发现了一个名为 new_parser_from_source_str
的函数,它能够创建来自源字符串的新解析器,在 libsyntax::parse
中,所以我决定让编译器为我处理这种情况 所以我必须手动处理这种情况。更新后的代码:
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods = SmallVector::zero();
let cfg_str = intern("cfg");
let ftre_str = intern("feature");
for arg in args {
let mut attrs = vec![];
let mod_name = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
// simply increase the reference counter
cfg_str.clone(),
vec![cx.meta_name_value(sp,
ftre_str.clone(),
ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
let dir = mod_path.parent().expect("no parent directory").to_owned();
let default_path = dir.join(format!("{}.rs", mod_name.as_str()));
let secondary_path = dir.join(format!("{}/mod.rs", mod_name.as_str()));
match (default_path.exists(), secondary_path.exists()) {
(false, false) => {
cx.span_err(sp, &format!("file not found for module `{}`", mod_name.as_str()));
return DummyResult::any(sp);
}
(true, true) => {
cx.span_err(sp, &format!("file for module `{}` found at both {} and {}", mod_name.as_str(), default_path.display(), secondary_path.display()));
return DummyResult::any(sp);
}
(true, false) => mod_path = default_path,
(false, true) => mod_path = secondary_path,
}
test_mods.push(P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
终于找到解决办法了! \o/
Rust 处理模块文件的过程类似于 include!
。结果,我查看了宏include!
的实现,可以找到here,并根据需要重写了它:
use ::std::path::Path;
use ::std::path::PathBuf;
use syntax::parse::{self, token};
use syntax::errors::FatalError;
macro_rules! panictry {
($e:expr) => ({
match $e {
Ok(e) => e,
Err(mut e) => {
e.emit();
panic!(FatalError);
}
}
})
}
pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
let mut ret = vec![];
while p.token != token::Eof {
match panictry!(p.parse_item()) {
Some(item) => ret.push(item),
None => {
panic!(p.diagnostic().span_fatal(p.span,
&format!("expected item, found `{}`", p.this_token_to_string())))
}
}
}
ret
}
要从模块文件中获取项目,我们必须找到真正的模块路径:
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
mod_path.set_file_name(mod_name.as_str());
mod_path.set_extension("rs");
然后我们可以这样构造我们的模块节点:
P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}),
vis: ast::Visibility::Inherited,
span: sp,
})
综上,插件改写如下:
#![feature(plugin_registrar, rustc_private)]
extern crate syntax;
extern crate rustc_plugin;
use syntax::ast;
use syntax::ptr::P;
use syntax::codemap::Span;
use syntax::parse::{self, token};
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::errors::FatalError;
use syntax::ext::build::AstBuilder;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;
use ::std::path::Path;
use ::std::path::PathBuf;
macro_rules! panictry {
($e:expr) => ({
match $e {
Ok(e) => e,
Err(mut e) => {
e.emit();
panic!(FatalError);
}
}
})
}
pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
let mut ret = vec![];
while p.token != token::Eof {
match panictry!(p.parse_item()) {
Some(item) => ret.push(item),
None => {
panic!(p.diagnostic().span_fatal(p.span,
&format!("expected item, found `{}`", p.this_token_to_string())))
}
}
}
ret
}
fn intern(s: &str) -> token::InternedString {
token::intern_and_get_ident(s)
}
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods = SmallVector::zero();
let cfg_str = intern("cfg");
let feat_str = intern("feature");
for arg in args {
let mut attrs = vec![];
let mod_name = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
// simply increase the reference counter
cfg_str.clone(),
vec![cx.meta_name_value(sp,
feat_str.clone(),
ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
mod_path.set_file_name(mod_name.as_str());
mod_path.set_extension("rs");
test_mods.push(P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("choose", choose);
}
我正在编写一个扩展
的 Rust 编译器插件choose! {
test_a
test_b
}
到
#[cfg(feature = "a")]
mod test_a;
#[cfg(feature = "b")]
mod test_b;
差不多大功告成了,但是最后展开的代码中模块什么都没有。我想原因是跨度没有覆盖模块文件。
use syntax::ast;
use syntax::ptr::P;
use syntax::codemap;
use syntax::parse::token;
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder;
use syntax_pos::Span;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;
// Ideally, it will expand
//
// ```rust
// choose! {
// test_a
// test_b
// }
// ```
// to
// ```rust
// #[cfg(feature = "a")]
// mod test_a;
// #[cfg(feature = "b")]
// mod test_b;
// ```
//
// but the modules contain nothing in the expanded code at present
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods: SmallVector<P<ast::Item>> = SmallVector::many(vec![]);
for arg in args {
let mut attrs = vec![];
let text = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
let cfg_str = token::InternedString::new("cfg");
let feat_str = token::InternedString::new("feature");
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
cfg_str,
vec![cx.meta_name_value(sp,
feat_str,
ast::LitKind::Str(token::intern_and_get_ident(text.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
test_mods.push(P(ast::Item {
ident: cx.ident_of(text.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
// === How to include the specified module file here? ===
ast::Mod {
inner: codemap::DUMMY_SP,
items: vec![],
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("choose", choose);
}
(Gist)
2016-08-25更新:使用libsyntax::parse::new_parser_from_source_str
避免手动设置模块路径。new_parser_from_source_str
只会在CWD定位模块,这是意想不到的.
正如@Francis 所指出的,模块文件的真实路径可能类似于 foo/mod.rs,
我发现了一个名为 所以我必须手动处理这种情况。更新后的代码:new_parser_from_source_str
的函数,它能够创建来自源字符串的新解析器,在 libsyntax::parse
中,所以我决定让编译器为我处理这种情况
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods = SmallVector::zero();
let cfg_str = intern("cfg");
let ftre_str = intern("feature");
for arg in args {
let mut attrs = vec![];
let mod_name = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
// simply increase the reference counter
cfg_str.clone(),
vec![cx.meta_name_value(sp,
ftre_str.clone(),
ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
let dir = mod_path.parent().expect("no parent directory").to_owned();
let default_path = dir.join(format!("{}.rs", mod_name.as_str()));
let secondary_path = dir.join(format!("{}/mod.rs", mod_name.as_str()));
match (default_path.exists(), secondary_path.exists()) {
(false, false) => {
cx.span_err(sp, &format!("file not found for module `{}`", mod_name.as_str()));
return DummyResult::any(sp);
}
(true, true) => {
cx.span_err(sp, &format!("file for module `{}` found at both {} and {}", mod_name.as_str(), default_path.display(), secondary_path.display()));
return DummyResult::any(sp);
}
(true, false) => mod_path = default_path,
(false, true) => mod_path = secondary_path,
}
test_mods.push(P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
终于找到解决办法了! \o/
Rust 处理模块文件的过程类似于 include!
。结果,我查看了宏include!
的实现,可以找到here,并根据需要重写了它:
use ::std::path::Path;
use ::std::path::PathBuf;
use syntax::parse::{self, token};
use syntax::errors::FatalError;
macro_rules! panictry {
($e:expr) => ({
match $e {
Ok(e) => e,
Err(mut e) => {
e.emit();
panic!(FatalError);
}
}
})
}
pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
let mut ret = vec![];
while p.token != token::Eof {
match panictry!(p.parse_item()) {
Some(item) => ret.push(item),
None => {
panic!(p.diagnostic().span_fatal(p.span,
&format!("expected item, found `{}`", p.this_token_to_string())))
}
}
}
ret
}
要从模块文件中获取项目,我们必须找到真正的模块路径:
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
mod_path.set_file_name(mod_name.as_str());
mod_path.set_extension("rs");
然后我们可以这样构造我们的模块节点:
P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}),
vis: ast::Visibility::Inherited,
span: sp,
})
综上,插件改写如下:
#![feature(plugin_registrar, rustc_private)]
extern crate syntax;
extern crate rustc_plugin;
use syntax::ast;
use syntax::ptr::P;
use syntax::codemap::Span;
use syntax::parse::{self, token};
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::errors::FatalError;
use syntax::ext::build::AstBuilder;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;
use ::std::path::Path;
use ::std::path::PathBuf;
macro_rules! panictry {
($e:expr) => ({
match $e {
Ok(e) => e,
Err(mut e) => {
e.emit();
panic!(FatalError);
}
}
})
}
pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
let mut ret = vec![];
while p.token != token::Eof {
match panictry!(p.parse_item()) {
Some(item) => ret.push(item),
None => {
panic!(p.diagnostic().span_fatal(p.span,
&format!("expected item, found `{}`", p.this_token_to_string())))
}
}
}
ret
}
fn intern(s: &str) -> token::InternedString {
token::intern_and_get_ident(s)
}
fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
let mut test_mods = SmallVector::zero();
let cfg_str = intern("cfg");
let feat_str = intern("feature");
for arg in args {
let mut attrs = vec![];
let mod_name = match arg {
&TokenTree::Token(_, token::Ident(s)) => s.to_string(),
_ => {
return DummyResult::any(sp);
}
};
attrs.push(cx.attribute(sp,
cx.meta_list(sp,
// simply increase the reference counter
cfg_str.clone(),
vec![cx.meta_name_value(sp,
feat_str.clone(),
ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
mod_path.set_file_name(mod_name.as_str());
mod_path.set_extension("rs");
test_mods.push(P(ast::Item {
ident: cx.ident_of(mod_name.as_str()),
attrs: attrs,
id: ast::DUMMY_NODE_ID,
node: ast::ItemKind::Mod(
ast::Mod {
inner: sp,
items: expand_include(cx, sp, &mod_path),
}
),
vis: ast::Visibility::Inherited,
span: sp,
}))
}
MacEager::items(test_mods)
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("choose", choose);
}