はじめに
C言語のようなコンパイル言語でもLispにおけるREPLのような対話的な実行環境があれば良いのにと思ったことがある人は少なくないと思います.実際にC/C++でREPL的なものを実装しようという試みは結構古くからあって,代表格ではCERNのROOTプロジェクトのために開発されているCling(あるいはその前身的な位置づけのCINT)が有名でしょうか.ClingはLLVM(とClang)をベースに動いているようです.私はわりとGNU好きでGNUツールチェインベースで(かつC++ではなくCを対象にした)同様のものがあればなと思うのですがあんまりちゃんとしたのはなさそうです.この記事ではGCCベースでREPL的なものを実現する方法を(雑ですが)思いついたのでそのことについてまとめたいと思います.なおLibgccjitについての記事ではありませんのでそちらを期待された方は申し訳ありません.
いかにしてEvalるか
REPLを実現するにはいかにしてEval相当の機能を実現するかというのが問題になります.Cのようなコンパイルされる言語でこれを実現するには結局のところコンパイラをまるまる実装することとなります.GCCはLLVMのようにライブラリ的な使い方を想定した設計ではないのでClingのようなやり方は困難です.Libgccjitというのもありますが結局のところ自前でCのソースをパースする必要があるので相当ヘビーです.ということで結局のところ入力をすべて保持しておいて毎回まるまるコンパイラに渡すというのが最善と思われます.
例えば以下のような入力があったとします.
[0] int a = 0;
[1] printf("a = %d\n", a);
これをもとに以下のようなソースを生成して毎回コンパイルします.
#include <stdio.h>
int main (void)
{
int a = 0; // [0]
printf("a = %d\n", a); // [1]
return 0;
}
ただしこれでは2つ大きな問題が存在します.
まず第一に入力を評価した結果がPrint
できません.REPLにおけるPrint
部分をどのように実装するかはひとまずおいておいて一連のコードを評価した結果をどうにか取得する必要があります.上記の例だとprintf
の返り値として6
が得られないといけません.
もう一つの問題として変数を再度定義できないという問題があります.例えば続く入力が以下のように与えられたらどうでしょうか.
[3] double a = 0.5;
このような入力を弾くということも考えられますがREPLのように使用することを考えると不便極まりないです.これらの問題を解決するために複文の式化と呼ばれるGNU拡張を用います.どんな型の変数でも値を表示できる万能プリント関数を仮にprint
とします.(これについては後述)このとき先程の例に代えて以下のようなコードを生成すれば良さそうです.
#include <stdio.h>
/* printの定義 */
int main (void)
{
print(
({ int a = 0; // [0]
({ printf("a = %d\n", a); }); // [1]
})
);
return 0;
}
これに続けて先程の[3]
の入力があった場合でもスコープが毎回新たに区切られるので定義のコンフリクトも起きません.
ただこのままだと毎回printf
が実行されてしまうので最後の式以外では標準出力を捨てるようにします.
#include <stdio.h>
/* printの定義 */
int main (void)
{
freopen("/dev/null", "w", stdout); // stdoutを/dev/nullに
print(
({
int a = 0; // [0]
({
freopen("/dev/tty", "w", stdout); // stdoutを/dev/ttyに
printf("a = %d\n", a); // [1]
});
})
);
return 0;
}
残る問題として途中にやたら時間がかかる処理があった場合と途中でユーザー入力をうける場合がありますがこれらはREPL的な用途とは離れるのでバッサリ切り捨てることとします.したがってあとは万能プリント関数を実装すれば適当にGNU Readline的なものでラップすればREPL(的なもの)の完成です.なお関数の定義についてはGCCでは関数内で関数を定義できるので問題ではありません.
いかにしてPrintするか
C11の_Generic
を使います.雑な実装としては以下のような感じです.
#define __fmt_string(v) _Generic((v),\
int: "%d\n",\
double: "%lf\n",\
...以下同様... \
default: "%p\n")
#define print(v) printf(__fmt_string(v), v)
_Generic
マクロはいわゆる型オーバーロードを実現するもので引数の型に応じて処理を振り分けることができます.(こちらの記事を参考にしています.)
ただしこれだけでは問題があってint a = 0
のような入力に対応できません.({int a = 0;})
の値はvoid
になってしまうからです.したがってマクロを使って以下のように振り分けます.
#include <stdio.h>
/* printの定義 */
int main (void)
{
#ifdef PRINT
print(実行するコード);
#else
実行するコード
#endif
return 0;
}
まずgcc -DPRINT...
のようにコンパイルを試みてコンパイルできなかった場合はgcc ...
としてコンパイルします.それでも失敗した場合は入力を弾けばよいわけです.
実装してみた
以上のアイデアをもとにbashスクリプトとして実装してみました.なぜbashで書いているのかというとある程度ポータブルな方が良いというのとシェルスクリプトを華麗に書けるようになりたいからです.ただ私のbash力は見ての通りお察しですのでちょいちょい壊れてると思います.興味があったら長い目で見ていただけると幸いです.継続的に開発を続けるかはわかりませんが.現状以下のような感じで使えます.
終わりに
記事は以上です.お読みいただきありがとうございました.