競技プログラミングにprocedural macroを持ち込む
前回の記事では競技プログラミングに手続き型マクロを持ち込みたい動機と、それを成すための(あまりスマートとは言えない)方法を紹介しました。そして先日「そういえばrust-analzyer (RA)はどうやって手続き型マクロを扱っているのか」と思いふとGitHub上でRAのソースコードのそれっぽい場所を眺めてみたところ、rpc
, msg
, stdin
, stdout
といった単語が目に入ってきました。もしやと思いarchitecture.mdを読むとこんなことが書かれているではありませんか。
For proc macros, the client-server model are used. We pass an argument
--proc-macro
torust-analyzer
binary to start a separate process (proc_macro_srv
). And the client (proc_macro_api
) provides an interface to talk to that server separately.
(今は--proc-macro
ではなくサブコマンドproc-macro
のようですが)試しにクエリListMacro
のJSONを放り込んだところ普通に結果を返してくれました。
$ echo '{"ListMacro":{"lib":"./target/debug/deps/libfastout-2dbcc333cc21dae5.so"}}' | rust-analyzer proc-macro
{"ListMacro":{"macros":[["fastout","Attr"]]}}
もう一つのクエリExpansionMacro
についてもproc-macro2
から上手くJSONにシリアライズすればいけるはずです。これでwatt
を要求しなくてもよくなります。今回cargo-equipにRAを使った手続き型マクロの展開機能を実装し、v0.10.0としてリリースしました。本記事ではそれにあたり遭遇した問題とその解決方法を書いていこうと思います。
rust-analzyer(.exe)
のダウンロード
JSONの形式についてはドキュメント化されておらず、ユーザが使うことも想定されていません。従ってこれからやることはhackであり、単純に$PATH
内のrust-analzyer(.exe)
を使うとRAの更新により動作しなくなる可能性が付き纏います。なので特定のバージョンのバイナリをGitHub Releasesからツール側でダウンロードして使うような形にしました。
メッセージのスキーマ
やりとりするメッセージのスキーマはproc_macro_api::msg::{Request, Response}
で定義されています。tt::Subtree
を模した型を作り、Serialize
とDeserialize
とproc_macro2::Group
との相互From
を実装すればあとは簡単に扱うことができます。
proc-macro
クレートのDLLの場所
proc-macro
クレートは動的リンクライブラリとしてコンパイルされます。この場所ですが、cargo check --message-format json
の出力をcargo_metadataクレートでパースすれば得ることができます。目当てであるcompiler-artifact
はコンパイルがスキップされていても含まれるため、再コンパイルを試みる必要はありません。
proc-macro
クレートをコンパイルするRustのバージョン
proc-macro
クレートは1.47.0以上のRustでコンパイルされる必要があります。1.42.0でコンパイルしたものをRAに渡すとこのようなことになります。
今回はactive toolchainが1.47.0未満ならrustupにあるツールチェインを探してそれを使うという形にしました。
動作例
proconio-deriveとmemoiseがAtCoder以外で使えるようになりました。
# [macro_use]
extern crate memoise as _;
# [macro_use]
extern crate proconio_derive as _;
# [fastout]
fn main() {
for i in 0..=100 {
println!("{}", fib(i));
}
}
# [memoise(n <= 100)]
fn fib(n: i64) -> i64 {
if n == 0 || n == 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}
↓
Output
//! # Procedural macros
//!
//! - `memoise 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)` licensed under `BSD-3-Clause`
//! - `proconio-derive 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)` licensed under `MIT OR Apache-2.0`
/*#[macro_use]
extern crate memoise as _;*/
/*#[macro_use]
extern crate proconio_derive as _;*/
/*#[fastout]
fn main() {
for i in 0..=100 {
println!("{}", fib(i));
}
}*/
fn main() {
let __proconio_stdout = ::std::io::stdout();
let mut __proconio_stdout = ::std::io::BufWriter::new(__proconio_stdout.lock());
#[allow(unused_macros)]
macro_rules ! print { ($ ($ tt : tt) *) => { { use std :: io :: Write as _ ; :: std :: write ! (__proconio_stdout , $ ($ tt) *) . unwrap () ; } } ; }
#[allow(unused_macros)]
macro_rules ! println { ($ ($ tt : tt) *) => { { use std :: io :: Write as _ ; :: std :: writeln ! (__proconio_stdout , $ ($ tt) *) . unwrap () ; } } ; }
let __proconio_res = {
for i in 0..=100 {
println!("{}", fib(i));
}
};
<::std::io::BufWriter<::std::io::StdoutLock> as ::std::io::Write>::flush(
&mut __proconio_stdout,
)
.unwrap();
return __proconio_res;
}
/*#[memoise(n <= 100)]
fn fib(n: i64) -> i64 {
if n == 0 || n == 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}*/
thread_local ! (static FIB : std :: cell :: RefCell < Vec < Option < i64 > > > = std :: cell :: RefCell :: new (vec ! [None ; 101usize]));
fn fib_reset() {
FIB.with(|cache| {
let mut r = cache.borrow_mut();
for r in r.iter_mut() {
*r = None
}
});
}
fn fib(n: i64) -> i64 {
if let Some(ret) = FIB.with(|cache| {
let mut bm = cache.borrow_mut();
bm[(n) as usize].clone()
}) {
return ret;
}
let ret: i64 = (|| {
if n == 0 || n == 1 {
return n;
}
fib(n - 1) + fib(n - 2)
})();
FIB.with(|cache| {
let mut bm = cache.borrow_mut();
bm[(n) as usize] = Some(ret.clone());
});
ret
}
// The following code was expanded by `cargo-equip`.
# [allow(clippy::deprecated_cfg_attr)]#[cfg_attr(rustfmt,rustfmt::skip)]#[allow(unused)]pub mod memoise{}
# [allow(clippy::deprecated_cfg_attr)]#[cfg_attr(rustfmt,rustfmt::skip)]#[allow(unused)]pub mod proconio_derive{}