*[hatefu:labs.yaneu.com/20111002/] C#をプリプロセッサとして使う ― CsPP
最近、10年ぐらい前に自分が書いたコードを保守することになった。
そのコードはC++でtemplateをバリバリ(死語)に使ってあるコードだ。
きっと当時の私は『Modern C++ Design』を読んでC++ template厨になってたんだろな。
いまさらtemplateなんか駆使したくないし、templateとタイプすらしたくない。
じゃあどうすればtemplateを使わずに済むか。これが今回の発想の原点だった。
* メタ記法を取り入れよう
templateの引き金となるのは、< と > だ。
>>
std::Vector <RGB888> v1;
std::Vector <RGB565> v2;
<<
つまり、この < と > との内側にメタプログラムが書ければいいのだ。
>>
set T = "RGB888";
Vector<T> v1;
set T = "RGB565";
Vector<T> v2;
<<
「やねうらおが、また変な独自仕様の言語を作りやがったんだな、この老害め」とか思われる前に言っておくが、今回はそんな独自仕様の言語は作らない。このメタプログラムを行なう部分はC#で記述する。
* メタプログラムのトリガー
< と >だとparseがしんどいので、通常のプログラムで到底使いそうもない文字列をメタプログラムのトリガーとする。
最初に私がその文字列の候補として思いついたのは「 \(^ 」と「^)/」だった。顔文字だ。Google IMEなら「おわた」と入力するだけで出るのもポイントが高い。
>>
//# var Life = new [] { "学業","会社"};
//# foreach o in Life
\(^o^)/オワタ
↓変数oの部分が置換され、こう展開される。
学業オワタ
会社オワタ
<<
人生が、すごく終わったような感じになる。
他の候補としては「(・」と「・)」なんかも考えた。
変数xをメタプログラム文字列として埋め込むならこういう感じになる。
>>
(・x・)
<<
どう見てもミッフィーである。
などと、大多数の人にとってはどうでもいいことを真剣に悩みながら、結局は、文字【】とか『』とか《》をメタプログラムのトリガーとすることにした。
これらの文字に(parserが)遭遇したとき、メタプログラミングの扉は開かれる。
* C#のソースコードを書くための記法
それからC#のプログラムはC++のソースコードの外側に書く必要があるが、一つのソースプログラム上に混在させたいので、C++のコード的にはコメントとみなされるところに書くことにする。
そのために //# を用いる。
>>
//# var T = "RGB888"; // ← C#のソースコード
Vector【T】 v1; // ← C++のソースコード
//# T = "RGB565"; // ← C#のソースコード
Vector【T】 v2; // ← C++のソースコード
<<
これは単なる文字置換などでは断じてない。【】の部分はC#のプログラムとして処理されている。
例えば、次のようなことも出来る。
>>
//# for(int i=0;i<3;++i)
//# {
Vector【i】 v【i】;
//# }
// これは、次のように出力される。
Vector0 v0;
Vector1 v1;
Vector2 v2;
<<
* 種明かし
もう賢明な読者は、どうやっているか理解できただろう。
先のプログラムは次のようなプログラムを生成し、コンパイルし、実行する。
ただそれだけなのである。
>>
for(int i=0;i<3;++i)
{
Console.WriteLine(
" Vector"+i+" v"+i+";"
);
}
<<
【 を発見するとそこでダブルコーテーションと '+'をつける
】 を発見するとそこで '+'とダブルコーテーション をつける。
簡単に言えばそれだけのプログラムである。
このアイデアこそがメタプログラムの扉を開いたのだ。
# 関連資料
C#をスクリプト言語として使う
http://labs.yaneu.com/20101017/
* どんなことが出来るのか
C#で出来ることなら何でも出来る。
ループのアンロールに使えることはすでに見たが、次のように二種類のコードを生成することも出来る。
>>
//# var Turn = new[]{"Black","White"};
//# foreach(var turn in Turn)
Move【turn】();
//#
// ↓のコードが出力される。
MoveBlack();
MoveWhite();
<<
これは、関数の出力にも応用できる。
似た処理を行なう二つの関数を出力することが出来るし、また次のように片側のコードだけコメントアウトすることも出来る。
>>
//# for(int k=0;k<2;++k)
//# {
//# // k == 0のときはmoveのコードを生成する
//# var move_begin = new[] { "","/*"}[k];
//# var move_end = new[] { "","*/"}[k];
//# // k == 1のときはno_moveのコードを生成する
//# var no_move_begin = new[] { "/*",""}[k];
//# var no_move_end = new[] { "*/",""}[k];
void func【no_move_begin】_no_move【no_move_end】()
{
【no_move_being】no_move用のコード【no_move_end】
【move_begin】move用のコード【move_end】
共通のコード
}
//# }
<<
このプログラムは次のソースコードを出力する。
>>
void func()
{
no_move用のコード
/* move用のコード */
共通のコード
}
void func_no_move()
{
/* no_move用のコード */
move用のコード
共通のコード
}
<<
C#4.0対応なのでラムダ式も使える。
>>
//# var Rev = (Func<string,string>)
( (string s)=> { return s=="Black"?"White":"Black"; }) ;
//# foreach(var turn in Turn)
Move【turn】();
Move【Rev(turn)】();
//#
// ↓次のコードを出力する。
MoveBlack();
MoveWhite();
MoveWhite();
MoveBlack();
<<
次のコードは私が書いているコンピュータ将棋のプログラムの一部で、先手用のコードなら盤面の1から3段目に対する処理を行ない、後手用のコードならば7から9段目の処理を行なうコードを生成する。
>>
//# for(int i=0;i<2;++i) {
//# Func<int,int,string> R = (start,end) =>
//# {
//# if (i==0)
//# return "R"+start+"_"+end;
//# else
//# return "R"+(10-end)+"_"+(10-start);
//# }; // 段を反転
foreach_bitboard_【R(1,3)】( ... );
//# }
// ↓次のように生成される。
foreach_bitboard_R(1,3)( ... );
foreach_bitboard_R(7,9)( ... );
<<
もちろん多重ループや、ループ中の分岐もできる。
>>
//# var Turn = new[]{"Black","White"};
//# foreach(var turn in Turn) {
Move【turn】();
//# // ループ中にifを使う。
//# if (turn=="Black")
printf("Black\n");
//# else
printf("White\n");
//# }
// ↓次のように出力される。
MoveBlack();
printf("Black\n");
MoveWhite();
printf("White\n");
<<
* メタメタプログラミング
さて、メタプログラミングへの扉が開かれたところで、次にメタメタプログラミングへの扉も開こう。
出力したソースプログラム自体を文字列として受け取る記法の導入である。
ここでまた、どんな文字列がいいか私は熟考に熟考を重ね、ああ、たぶんもう誰も聞きたくないだろうから、そのへんの話は今回は割愛し、要するに最終的に私は、 {: :} という記号を用いることにした。
>>
//# var source = {:
//# for(int i=0;i<2;++i)
print(【i】);
print(【i】);
//# :}
//# Console.WriteLine("// これが出力されたよー\n" + source);
// ↓実行結果
これが出力されたよー
print(0);
print(1);
<<
出力されたソースを受け取れると何が便利かというと、このソースをさらに正規表現による置換などによって改変することが出来るし、どこかのファイルに出力することも出来るということだ。
上のプログラムはどんなトリックで実行されているのだろうか?
一度出力したプログラムをさらに受け取っているのか?
実は、そんなことはしていない。プログラムの実行は一度しかされない。一度しかされなくともメタメタプログラミングは可能なのだ。
そもそも途中で一度プログラムを出力してしまうと、次のプログラムが正しく動作しない。
>>
//# for(int j=0;j<2;++j)
//# {
//# var source = {:
//# for(int i=0;i<2;++i)
print(【i】,【j】);
//# :}
//# Console.WriteLine("// これが出力されたよー\n" + source);
//# }
// ↓実行結果はこうなって欲しい。
これが出力されたよー
print(0,0);
print(1,0);
これが出力されたよー
print(0,0);
print(1,1);
<<
つまり、{: :} 記号の内側であっても、【】記法のなかではきちんとその外側のスコープの変数をキャプチャ(束縛)して欲しい。
これは次のようなトリックを用いている。
>>
{: を見つけたら次の文字列に置き換える。
new Func<string>( () => { var sb = StringBuilder();
そのあと Console.WriteLineではなく、sb.AppendLineを用いて変数sbに書き出し
:} を見つけたら次の文字列に置き換える
return sb.ToString(); } ) ();
<<
たったこれだけである。stringを返すlambda式に変形させ、その末尾に () をつけて、そのlambda式をその場で実行している。
こうすることによって、var s = {: :} のような代入を可能とし、かつ、{: :} の内側で、その外側のスコープにある変数をキャプチャすることが出来る。
単純な実装だが、素晴らしいアイデア(←自画自賛)であり、このアイデアこそがメタメタプログラミングの扉を開いたのである。
* CsPP 実装上の問題点
以上で見てきたような二つのhacksと呼ぶにふさわしいアイデア(←自画自賛)によって、CsPP(C#によるプリプロセッサ)の実装は簡単だったよと言いたいところだが、最初のプロトタイプを30分程度で作ったあと、いろいろ問題点が出てきて、現在の形に至るまでかれこれ10時間ぐらいかかった。いや、メタプログラミング用の文字列を「\(^」と「^)/」にするか、「(・」と「・)」にするかを10時間悩んでいたのではなくてな。
どんな問題があるのか、その問題を共有しておくことは有意義だと思うので、以下につらつらと書き殴っておく次第である。
** ネストコメント
C/C++では普通、/* ~ */によるコメントのネストは出来ない。
>>
/*
/* no_moveのときはコメント化される */
*/
<<
ところが、今回のようなプリプロセッサで片側のコードだけを無効にしたいことは多々あり、無効にしたコードの内側で /* ~ */ をコメントとして使いたかったり、あるいは、この無効になるコードのなかにも場合わけがあって、/* ~ */を使いたいというようなこともある。
そこで、ネストコメントを正しく処理するコードを生成して欲しい。
そのためには、上のようなネストコメントは次のように出力されるべきである。
>>
/*
/* no_moveのときはコメント化される *//*
*/
<<
こうしておけば、C/C++コンパイラは正しくネストコメントを処理できる。
このためには、ConsoleではなくStringBuilderにいったんソースを出力していき、それをparseして、ネストコメントを処理するコードが必要になる。もちろん、行コメント( // )中の /* と */ は無視しなければならないし、" …"や@"…"(C#の文字リテラル表現) の内側も無視されなければならない。"…"の内側には ¥マークが出てきたり、@"…"のなかでは "" は単なる文字としての " なのでそこはスキップしたりと、なかなか大変である。
さらに、さきほどの {: :} 記法でStringBuilderに出力するためにダブルコーテーションで囲っていたが、その部分はダブルコーテーションで囲まれているにも関わらず、ネストコメントの処理対象でなければならない。
考えるにつれて問題点がいろいろ出てきて、これらを真面目に実装したところ、大変時間がかかった。たぶん5,6時間費やしたと思う。
当初、"/* ~ */" と "#if 0" ~ "#endif" とでネストさせていたのだが、これが案外うまくいかない。C/C++では"#if"は、#defineマクロの途中では使えないからだ。
いまどき#defineマクロなんか使うなと言われそうだが、templateは避けて通りたいし、1クロックでも速いコードを書かなけばならないし、もはやそれ以外方法が無いのである。1クロックのために命のロウソクを削っている身にもなってもらいたい。
** コメントの除去機能
次のC++のコードがコンパイル通らなくて困った。
>>
foofunc/*_no_move*/_no_hash();
<<
・foofuncの末尾にno_moveがつく、つかない
・foofuncの末尾にno_hashがつく、つかない
の2×2 = 4通りの関数を作って、それを呼び出したかったのだが、関数名の途中をコメントアウトするコードを生成したら、この部分が関数呼び出しだと認識されずC/C++のコンパイルが通らなかった。
つまり、/* */自体を除去する機能が必要になった。
//# SourceStringBuilder.GlobalCommentCut = true;
と書くだけで、一時的に /* */によるコメントを出力ソースから除去できるようにした。もちろん、ネストコメントも適切に除去される。これらのために2時間ぐらい改造に費やした。
** 出力ファイルはread-onlyに
出力されるプログラムファイルをVisual C++ 2010のプロジェクトに追加しているのだが、そうするとVisual C++でコンパイルしたときにエラーが出ると、その出力されたファイルのほうを修正してしまうミスが頻発した。
おまけに修正したのに、そのファイルがまた上書きされて修正した部分が消えてしまう。実に腹立たしい。
そこで出力されるプログラムファイルはread-only属性にすることにした。これなら、誤って修正しても保存するときに書き出せない警告ダイアログが出て気づくので、そのときに元ファイルに反映させればいい。
** コンパイル中のエラーをVisual Studioに反映させたい
このプリプロセッサでのコンパイル中に出たエラーをVisual Studioの出力ウインドゥに反映させたいし、エラーが出たときにコンパイルを停止させたい。
そのためには、Visual C++のビルド前プロセスとしてバッチファイルを登録し、そのバッチファイルに次のようにだらだらと書くことにした。
>>
@echo off
"CsPP\CsPP.exe" -compile foreach_macro_cspp.h foreach_macro.h
@if "%ERRORLEVEL%" == "1" goto fail
"CsPP\CsPP.exe" -compile gen_move_cspp.cpp gen_move.cpp
@if "%ERRORLEVEL%" == "1" goto fail
rem …中略…
:good
goto end
:fail
exit /b 1
:end
<<
あと、CsPPでは、出力前のファイルと出力後のファイルのタイムスタンプを比較しているが、Visual C++でリビルドを行なうときに、CsPPも再度無条件にファイルをコンパイルして欲しいのだが、それをうまく書く方法がない。
後述するカスタムビルドツールはそれに近い動作が出来るのだが、read-only属性をつけているとVisual Studio 2010はリビルド時にそれらのファイルを削除できないようである。
** カスタムビルドツール
CsPP用のソースコードも編集する上でVisual C++のプロジェクトに追加しておきたいのだが、これをC++のコードとしてコンパイルされても困るので、このソースコードは「ビルドから除外」するか、「カスタムビルドツール」を選択しておかなければならない。
ファイルをソリューションエクスプローラーで右クリックして「項目の選択」のなかから「カスタムビルドツール」を選択。
本当は CsPP.exe もカスタムビルドツールとして呼び出せると良いのだが、Visual C++2010では残念ながらUIからそういう設定は出来ないようである。
ビルド時イベントとして次図のように、CsPP用に書いたソースファイルすべてを「追加の依存ファイル」のところに書き、CsPPが出力するファイルを「出力ファイル」のところに書くことによって、「出力ファイル」のうちの一つでも「追加の依存ファイル」より古ければバッチファイルを実行するということは出来る。
[img:project_setting.png]
** エラーが出たときに元ソースのエラー行に飛ぶ機能
人によっては必須機能だと言われるかも知れないが、結構時間がかかりそうだったので今回は見送った。いずれやりたい。
* まとめ
簡単に実現出来るかと思ったら、意外と実装に苦労した。
またC#でメタプログラミングするのは結構楽しく、独自の変な言語を開発しなくて本当に良かった。
また、作ってみてわかったが、これはかなり実用的である。C++でtemplateを駆使して書かなければならないようなプログラムのうち、8割ぐらいは、実際はこの手の強力なプリプロセッサがあればtemplateなんて使わずに済ますことが出来ると思う。あとの2割は…まあ、頑張れ。
あと今回製作したCsPPを以下に置いておく。
ソースもつけてあるので自由に改造して使っていただきたい。
なお、動作には .NET Framework 4.0 が必要である。
* CsPPダウンロード
# CsPP version 1.00
CsPPのバイナリとソース(2011年10月2日バージョン)
http://labs.yaneu.com/20111002/CsPP1.00.zip
* 更新履歴
2011.10.02 公開