自己紹介
出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。
Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。
環境
新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.39.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell
前回
前回は並列処理について学びました。
Rust勉強中 – その25
まず1.39.0にアップデート
11/7-8にRust 1.39.0がリリースされたようです。
本題に入る前にアップデートしておきます。
$ rustup update stable
$ cargo --version
cargo 1.39.0 (1c6ec66d5 2019-09-30)
$ rustc --version
rustc 1.39.0 (4560ea788 2019-11-04)
$ rustdoc --version
rustdoc 1.39.0 (4560ea788 2019-11-04)
主な更新内容
-
- async/.awaitが安定化
-
- matchのガードで値が移動する場合でも共有参照が取れるようになった
- 関数やクロージャ、関数ポインタのパラメータに属性を付けられるようになった
などなど
詳しくは以下を参照:
-
- https://blog.rust-lang.org/2019/11/07/Rust-1.39.0.html
-
- https://github.com/rust-lang/rust/blob/stable/RELEASES.md#compatibility-notes
-
- https://tech-blog.optim.co.jp/entry/2019/11/08/080000
- https://tech-blog.optim.co.jp/entry/2019/11/08/163000
マクロ
Rustではマクロがサポートされています。関数で表現できない処理や、短縮したい処理などをマクロとして定義して呼び出します。
定義
マクロの定義にはmacro_rules!を使います。
macro_rules! macro_name {
...
}
このほかに、組込みマクロや手続きマクロがあります。
この記事では、macro_rules!を使ったマクロのみに焦点を絞ります。
マクロの呼び出しは以下のように!を付けて呼び出します。
macro_name!(...);
パターンマッチ
マクロは呼び出し時に与えられた引数に対して、上から順番にパターンマッチを行います。マッチすれば定義された処理を実行します。
macro_rules! macro_name {
(pattern1) => { // rule1
...
};
(pattern2) => { // rule2
...
};
...
}
ちなみに、パターンや処理の括弧は、適切に対応していれば()や{}、[]でも良いそうです。
さらに、マクロ呼び出し時も括弧が対応していれば()や{}、[]でも良いです。この時{}のみセミコロンは不要です。
展開
マクロの展開はコンパイルの早い段階で行われます。そして何らかのRustコードに変換されます。
vec!より良い例が浮かばなかったので、vec!を例に説明します。
macro_rules! vec {
($elem:expr ; $n:expr) => { // rule1
::std::vec::from_elem($elem, $n)
};
($($x:expr),*) => { // rule2
<[_]>::into_vec(Box::new([$($x),*]))
};
($($x:expr),+,) => { // rule3
vec![$($x),*]
};
}
\$elemや\$n, \$xはマッチした値を代入する変数のようなものです。パターン中の:exprはフラグメント指定子やフラグメント型と呼ばれてるみたいです(ここでは単にフラグメントと呼びます)。フラグメントは、マッチする値の種類を決めます。exprの場合はRustの式を意味します。フラグメントには以下があります。
itemアイテム
blockブロック
meta属性のボディ部
ident識別子
ttトークンツリー
フラグメントには、直後に来るトークンの制約を受けるものがあります。例えば、exprの場合は、直後には,か;しか来ません。
identとttに関してはRustの構文に属さない値にマッチさせることができます。
identは任意の識別子に、ttは適切に対応した括弧に囲まれている部分あるいは、括弧に囲まれていない単一のトークンにマッチします。
vec!は以下の3パターンで呼び出すことができます。
vec![0; 3]; // rule1
vec![0, 1, 2]; // rule2
vec![0, 1, 2,]; // rule3
まず、vec![0; 3];の引数は0; 3となり、rule1に当てはめます。rule1のパターンは$elem:expr ; $n:exprです。まず0は$elem:exprにマッチします。次に;はそのまま;にマッチします。最後に3も$n:exprにマッチします。結果、rule1にマッチすることになり、その処理を実行します。
パターンマッチ時はコメントやホワイトスペースは無視されます。
繰り返し
次に、vec![0, 1, 2]のパターンマッチをみていきます。この場合引数は0, 1, 2になります。最初にrule1に当てはめますがマッチしません。次にrule2に当てはめてみます。rule2のパターンは$($x:expr),*のようになっています。これの意味は、「$x:exprのカンマ区切りで0回以上の繰り返し」という意味です。このとき、繰り返しの対象は$($$x:expr)となります。正規表現のようにカンマの繰り返し出ないことに注意です。繰り返しの表現は*と+があります。
rule2のパターンマッチに戻ります。0, 1, 2はカンマをセパレータに繰り返されているので、マッチします。結果、rule2の処理を実行します。ちなみに、処理中でも繰り返しが使われています。
再帰
最後にvec![0, 1, 2,]のパターンマッチをみていきます。引数は0, 1, 2,となり、最後にカンマがきた形になっています。これは、rule1にマッチしません。次のrule2にもマッチしません。最後にカンマがこないからです。
最後のrule3に当てはめてみます。rule3のパターンは$($x:expr),+,です。意味は「$x:exprのカンマ区切りで1回以上の繰り返しで最後にカンマ」ですね。これは、引数とマッチしますので、rule3の処理が実行されます。
rule3の処理はvec![$($x),*]となっており最後のカンマを抜いて、自分自身を呼び出しています。つまり、マクロを再帰的に呼び出していることになります。
デバッグ
マクロはコンパイル時にRustコードに展開されます。展開後のコードを確認するには、以下の手順で行います。
-
- Rustをnightlyにします。rustup default nightly
cargo run –verboseでrustcのコマンドを確認。
rustcコマンドのオプションに-Z unstable-options –pretty expandedを追加して実行
vec!の展開結果は以下のようになりました。分かりやすいように改行やインデントを入れています。
macro_rules! vec {
($ elem : expr ; $ n : expr) =>
{ :: std :: vec :: from_elem ($ elem, $ n) } ;
($ ($ x : expr), *) =>
{ < [_] > :: into_vec (Box :: new ([$ ($ x), *])) } ;
($ ($ x : expr), +,) => { vec ! [$ ($ x), *] } ;
}
また、log_syntax!を使う方法や、trace_macros!(true)を使う方法もあります。
インポートとエクスポート
単一クレートでは、
-
- あるモジュールで見えているマクロは、自動的にその子モジュールからもそのマクロが見える
- あるモジュールから親モジュールへエクスポートするには#[macro_use]を使う
複数クレートでは、
-
- ほかのクレートからマクロをインポートするには、extern crate宣言に#[macro_use]を付ける
- クレート内のマクロをエクスポートするには、マクロそれぞれに#[macro_export]を付ける
今回はここまで。