コードを文字列データとして与え、プログラム中で評価して実行するしくみ(Eval Function)を、多くのスクリプト言語で持っています。Javascript,PHPなどでeval()という関数名で使われます。個人的にはBashやJavaScriptでよく使うのですが、ちょっとアグレッシブなことができたりします。
初めてこのようなことができると知ったのは、Lispがきっかけですが、まだWebもJavaもない時代で、プログラムを作りながら実行できるなんて、コンピュータがプログラムを自動的につくるなんて人工知能みたい、とショックを受けたのを覚えています。
プログラムコードとデータの境目がなくなるしくみは、たいへん興味深いです。
(XMLのXSLTも近いものを感じます)
Schemeで実際にどのように見えるか、ちょっと前にテストしたコードを見てみます。
環境 : DrRacket 6.1.1 / Windows 8.1
pで定義された処理を、後から変更(追加)しているところがポイントです。
式もデータも同じリストとして表現するLispならではの面白い部分です。
そこで、インタプリタでないコンパイラ言語のCでもeval()関数もどきができないか、やってみたらどうなるか、GWの自由研究みたいなのりでやってみました。
仕組みは、文字列で与えられたCコードを、ファイルに書き出し、これを外部コマンドでコンパイルしてダイナミックライブラリにします。これをランタイムで呼び出し実行します。
サンプルコードと、ビルドスクリプト(Pythonプログラム)をGithubにアップしました。
https://github.com/systemsblue/Eval-C-Function
ここでは、ビルドスクリプトとランタイムに生成されるファイルについて説明したいと思います。
環境 : Ubuntu 14.04
evalc_gen.h
int evalc(char *eval){ void (*func)(__PSTRC *); void *so; __PSTRC ps; ps.p1 = p1; ps.p2 = p2; ps.pstr1 = pstr1; FILE *fp; fp = fopen("./sotemp.c", "w"); fprintf(fp, __eval_fmt, eval); fclose(fp); system("cc -w -fPIC --share -o ./sotemp.so ./sotemp.c"); so = dlopen("./sotemp.so", RTLD_LAZY); if(!so){ fprintf(stderr, "%s\n", dlerror()); return -1; } func = dlsym(so, "__feval"); if(!func){ fprintf(stderr, "%s\n", dlerror()); return -2; } (*func)(&ps); dlclose(so); p1 = ps.p1; p2 = ps.p2; return 0; }
ビルドスクリプトを実行すると、このヘッダーファイルが生成されます。
サンプルコードを解析して、evalc関数の最初と最後に、パラメータをI/O部分を記述しています。
サンプルソースの、
sample.c
int /*SO*/ p1; int /*SO*/ p2; char /*SO*/ pstr1[100]; char pstr2[100]; #include /*SO*/ "evalc_gen.h"
/*SO*/とコメントされている部分が処理対象となります。
evalc_gen.hは、生成ヘッダファイル名を指定します。生成された後読み込まれるもので、パラメータより後に記述します。
fprintf(fp, __eval_fmt, eval);
は、テンプレート__eval_fmtに対して、コードを書きこみます。
sotemp.c
#include<stdio.h> #include<string.h> typedef struct{ int p1; int p2; char *pstr1; } __PSTRC; void __feval(__PSTRC *ps){ int p1 = ps->p1; int p2 = ps->p2; char *pstr1; pstr1 = ps->pstr1; strcpy(pstr1, "AAAA"); ps->p1 = p1; ps->p2 = p2; }
ダイナミックライブラリの関数定義です。これはプログラム実行時に生成されます。evalc()の呼び出し分作成されます。(上書き)
sample.c
// Eval code evalc("p1 /= 2; p2 -= 100"); printf("p1 : %d p2 : %d\n", p1, p2); // Eval code evalc("strcpy(pstr1, \"AAAA\");");
制約条件としては、変数はグローバルで、型はポインタを使うのはcharのみです。
実用的かどうかはわかりませんが、自分ルールで作ってみるのは楽しいものです。