acl4のプログラムのページ0008
(1) プリプロセッサは善か悪か?(この部分はa4_p0007と同じ)
- 私は今はプリプロセッサが大好きです。プリプロセッサは世界を変えるパワーを持っていると思います。うまく使えばこれほど有用なものはないのです。今日はその話をしたいと思います。
(2) プログラム p0008a.c について #0 (プリプロセッサ)(a4_p0007と同じ)
- acl4 ライブラリを使って、プリプロセッサを作りました。acl4 ライブラリにはプリプロセッサ関数があるので、それを呼んでいるだけです(a4_0022)。難しいことは何もありません。
- [p0007b.txt]:
#define Square(x) (x)*(x)
#define One 1
#define Two 2
#define Three 3
#define Four 4
#define Five 5
#define Six 6
#define Seven 7
#define Eight 8
#define Nine 9
#define Ten 10
#define Eleven 11
#define Twelve 12
#if (Square(2+3)!=11)
Square(Three + Seven)
#endif
- これをプリプロセッサにかけるとこうなります。
>p0008a pp:1 src:p0007b.txt
#line 14 "p0007b.txt"
(3 + 7)*(3 + 7)
- もし #line を出力しないでほしければ、こうします。
>p0008a pp:1 src:p0007b.txt mod:2
(3 + 7)*(3 + 7)
(3) プログラム p0008a.c について #1 (コマンドライン計算機)(a4_p0007と同じ)
(4) プログラム p0008a.c について #2 (簡易Cコンパイラ)
- さてここからが本気です(笑)。
- 実は p0008a.c 内で使っている自作プリプロセッサは、普通のプリプロセッサの機能に加えて #typedDef などの拡張機能を持っています(型付きdefine)。
- これは acl4 ライブラリのプリプロセッサ関数が標準で持っている機能なので、別にこの機能を使うためにあれこれと準備しなければいけないということはありません。
- これを使って何ができるかと考えて、今回はCコンパイラを作りました。
- [p0007e.txt]:
int i, j; // 素数を表示するプログラム.
for (i = 2; i < 100; i++) {
for (j = 2; j * j <= i; j++) {
if (i % j == 0) goto skip;
}
printf("%d ", i);
skip: ;
}
printf("\n");
- コンパイルしてみます。
>p0008a cc:1 src:p0007e.txt
ADD(ESP,-4096);
MOV(DWORD [ESP+(23)*4+1024],2);
JMP(L4);
LABEL(L2:);
MOV(DWORD [ESP+(24)*4+1024],2);
JMP(L8);
LABEL(L6:);
MOV(EAX,[ESP+(23)*4+1024]); CDQ(); IDIV(DWORD [ESP+(24)*4+1024]); MOV([ESP+(128)*4+1024],EDX);
MOV(EAX,[ESP+(128)*4+1024]); CMP(EAX,0); SETE(AL); MOVZX(EAX,AL); MOV([ESP+(129)*4+1024],EAX);
MOV(EAX,[ESP+(129)*4+1024]); CMP(EAX,0); JNE(L22);
LABEL(L7:);
(以下略)
- こんな感じの出力が出てきます。全く最適化されていないので、冗長なコードになっていますが、でもちゃんと動きます(後述)。
- ここで強調したいのは、出力コードの質の低さではなく、これだけのものがどれほど簡単にできたのかという話です。
- p0008a.c のコード内で、Cコンパイラの処理は以下のように書かれています。
1: if (strtol(getArg(argc, argv, "cc:", "0"), NULL, 0) != 0) {
2: a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
3: a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
4: VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
5: a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
6: a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_x86-32-0]\n%.*s\n", stdout, "conv_a4vm_asmX86Bit32Typ0");
7: a_convVc_pp(vc1, vc0, 1 + 2 * 16, 1, "%.*s\n", stdout, "conv_asmX86Bit32Typ0_Typ1");
8: }
- 1行目と8行目は「cc:1になったときに以下を実行せよ」と言っているだけなので、実質的にこのCコンパイラは6行で構成されています。
- 2行目では、まず与えられたC言語のソースコードに対してプリプロセッサ処理をします。これで #include や #define などが全部展開された1つのテキストデータが得られます。
- 3行目では、C言語の a=b+c; を Add(a, b, c); みたいな形式に変換しています。こういうプリプロセッサで置換できる形の集まりに変換することで、その後の処理をプリプロセッサだけでやるのです(!)。この最初の形式変換処理はプリプロセッサではできないので、C言語の関数でやります。
- 5行目は、 a_convVc_clang_clout() の出力結果に、 conv_clout_a4vm.h (正味40行程度)をインクルードして、プリプロセッサにかけます。こうすることで a4vm のアセンブラに変換されます。
- 6行目は、5行目の出力結果に、 conv_a4vm_asmX86Bit32Typ0.h (26行)をインクルードしてプリプロセッサにかけます。こうすることで x86 のアセンブラになります。
- 7行目は、6行目の出力結果に、 conv_asmX86Bit32Typ0_Typ1.h (32行)をインクルードしてプリプロセッサにかけます。こうすることでアセンブラの文法がNASMっぽくなって、読みやすくなります。そしてそれを標準出力に出力しています。
- つまりC言語で書かれたの関数(今は170行くらい)を一度呼んで、プリプロセッサを合計4回実行するだけで、Cコンパイラはできてしまうということなんです。
- C言語を作ったことがあればわかるのですが、これは結構衝撃的なことです。
- なお、もし変換の中間結果に関心がなければ、最後の3回のプリプロセッサ処理は、一度に3つをインクルードして、ひとまとめで処理することができますし、たぶんそのほうが速いです。
(5) プログラム p0008a.c について #3 (C言語JITコンパイラ)
- p0008a.c のコード内で、C言語JITコンパイラの処理は以下のように書かれています。
1: if (strtol(getArg(argc, argv, "cjit:", "0"), NULL, 0) != 0) {
2: a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
3: a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
4: VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
5: a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
6: a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_x86-32-0]\n%.*s\n", stdout, "conv_a4vm_asmX86Bit32Typ0");
7: a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_db]\n%.*s\n", stdout, "conv_asmX86Bit32Typ0_asmDb");
8: intptr_t bn = a_conv_asmDb_bin(vc0->p, VecChr_N(vc0), bin, binSz, tbl, sizeof tbl / sizeof (a_AsmDbConstTable), bf);
9: if (bn > 0) {
10: if (dbg != 0) { printf("[binary]\n"); hexDump(bin, bn, stdout); }
11: void (*pf)(); pf = (void (*)()) bin; pf();
12: }
13: }
- 1行目から6行目までは cc:1 の時と全く同じです。
- 7行目ではプリプロセッサを使ってアセンブルをしています。これにより以下のような内容に変換されます。ちなみに conv_asmX86Bit32Typ0_asmDb.h は46行です。
DB(0x81); DB(0xC0+4); DD(-4096);
DB(0xC7); DB(0x84); DB(0x24); DD((23)*4+1024); DD(2);
DB(0xE9); DD_L(4, 1, -4);
LBL(2);
DB(0xC7); DB(0x84); DB(0x24); DD((24)*4+1024); DD(2);
DB(0xE9); DD_L(8, 1, -4);
LBL(6);
(以下略)
- アセンブラが46行で書けるというは、非常にくるっています(笑)。一部を見せるとこんな感じです。こんなのは https://www.felixcloutier.com/x86/ を見れば、テレビを見ながらでも作れます。
#define RET() DB(0xC3)
#define CDQ() DB(0x99)
#define PUSH_R(r) DB(0x50+r)
#define POP_R(r) DB(0x58+r)
#define PUSH_I(i) DB(0x68); DD(i)
#define CALL_I(i) DB(0xE8); DD_R(i-4)
#define ADD_RI(r,i) DB(0x81, 0xC0+r); DD(i)
#define SUB_RI(r,i) DB(0x81, 0xC0+r+5*8); DD(i)
#define CMP_RI(r,i) DB(0x81, 0xC0+r+7*8); DD(i)
#define IMUL_RI(r,i) DB(0x69, 0xC0+r*9); DD(i)
#define MOV_RI(r,i) DB(0xB8+r); DD(i)
#define SHL_RI(r,i) DB(0xC1, 0xC0+r+4*8, i)
#define SAR_RI(r,i) DB(0xC1, 0xC0+r+7*8, i)
#define JMP_T(t) DB(0xE9); DD_L(t, 1, -4)
#define JNE_T(t) DB(0x0F, 0x85); DD_L(t, 1, -4)
(以下略)
- 8行目では、このDB/DW/DDの列を実際のバイナリデータに変換します。そこで式の計算処理が必要になりますが、 calc:1 と同じ計算ルーチンが使えるので、余裕でこなせます。
- 9行目以降で、生成した機械語の関数を呼び出して実行させています。
- 嘘みたいに簡単にC言語のJITコンパイラができました。
- ここでは動作画面などは省略しますが、a4_p0007で例示したサンプルアプリはすべて同じように動きます。しかも高速に動きます。
(6) プログラム p0008a.c について #4 (C言語インタプリタ)(a4_p0007と同じ)
- JITコンパイラが簡単に作れることはわかりました。でもCPUの勉強をしないと新しいCPU上では一切実行できないっていうのもちょっと悲しいので、実CPUの機械語に変換するのではなく、a4vmのバイトコードに変換して、それでインタプリタ実行するというモードも付けました。
- p0008a.c のコード内で、C言語インタプリタの処理は以下のように書かれています。
1: if (strtol(getArg(argc, argv, "clang:", "0"), NULL, 0) != 0) {
2: a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
3: a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
4: VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
5: a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
6: a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_db]\n%.*s\n", stdout, "conv_a4vm_asmDb");
7: intptr_t bn = a_conv_asmDb_bin(vc1->p, a_VecChr_N(vc1), bin, binSz / sizeof(intptr_t), tbl, sizeof tbl / sizeof (a_AsmDbConstTable), bf);
8: intptr_t a4arg[10]; a4arg[0] = 0; A4vm_exec0((intptr_t *) bin, bn, 10, a4arg);
9: }
- ここでは動作画面などは省略しますが、a4_p0007で例示したサンプルアプリはすべて同じように動きます。
(7) 主張のまとめ
- プリプロセッサ関数があれば、プログラミング言語はこんなに簡単に作れるようになるんです。
- C言語コンパイラ・C言語JITコンパイラ・x86アセンブラ・C言語インタプリタを例にして説明しました。
- 今回、 p0007b.txt ~ p0007h.txt が動かせればそれでよいという程度の最小実装なので、とても簡単に書けているという面はあります。しかしそれにしても短くてシンプルになっていると思います。
- 将来C言語以外の言語を作る際も、 a4vm のアセンブラまで変換できればその先は今回と同じものがそのまま使えるので、JITコンパイラもインタプリタもすぐに作れることになります。
- 今回の p0008a.c が全部まぜこぜの all-in-one で実装されているので、個々の機能がどのくらい規模なのかわかりにくいと思います。それで、一部機能だけを有効にして、.exeファイルサイズがどう変わるか試してみました。
| 8.0KB | (参考用) printf("hello, world\n"); のみのプログラム | | 23.5KB | Mode=0: ppのみ | | 28.0KB | Mode=1: pp+cc | | 30.0KB | Mode=2: pp+cc+cjit | | 35.5KB | Mode=3: pp+cc+cjit+clang | | 37.5KB | Mode=4: pp+cc+cjit+clang+calc+graphic(=全部) |
(8) ダウンロード
(9) p0008a.c のソースコード
#define a_Version 1
#include <acl4v1.c>
#include <windows.h>
#include "a4_t0002.c"
#include "a4_t0003.c"
#define Mode 4
// 0:pp-only, 1:pp+cc, 2:pp+cc+cjit, 3:all-graphic 4:all
#if (Mode >= 4)
a_Win *win;
void openWin(intptr_t x, intptr_t y) { win = a_malloc(_a_ sizeof (a_Win)); a_Win_ini(_a_ win, (int) x, (int) y, "graph", 0x000000); }
void setPix(intptr_t x, intptr_t y, intptr_t c) { win->buf[x + y * win->xsz] = (uint32_t) c; }
void flushWin() { a_Win_flushAll0(win); }
void waitInf() { if (win != NULL) { flushWin(); } for (;;) Sleep(1000); }
#endif
int main(int argc, const char **argv)
{
const char *src = getArg(argc, argv, "src:", "");
int dbg = strtol(getArg(argc, argv, "dbg:", "0"), NULL, 0);
VecChr vc0[1], vc1[1]; VecChr_iniArg(_a_ vc0, src, 2); VecChr_ini(_a_ vc1);
#if (Mode >= 2)
static a_AsmDbConstTable tbl[5] = {
{ "printf", 0, 0, (intptr_t) printf },
#if (Mode >= 4)
{ "openWin", 0, 0, (intptr_t) openWin },
{ "setPix", 0, 0, (intptr_t) setPix },
{ "flushWin", 0, 0, (intptr_t) flushWin },
{ "waitInf", 0, 0, (intptr_t) waitInf },
#endif
};
intptr_t binSz = 64 * 1024; char *bin = a_mallocRWX(binSz); BufFree bf[1]; BufFree_ini(_a_ bf);
#endif
if (strtol(getArg(argc, argv, "pp:", "0"), NULL, 0) != 0) {
int mod = strtol(getArg(argc, argv, "mod:", "1"), NULL, 0);
a_convVc_pp(vc0, vc1, 1 + 2 + mod * 16, 1, "%.*s", stdout, src);
}
#if (Mode >= 1)
if (strtol(getArg(argc, argv, "cc:", "0"), NULL, 0) != 0) {
a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_x86-32-0]\n%.*s\n", stdout, "conv_a4vm_asmX86Bit32Typ0");
a_convVc_pp(vc1, vc0, 1 + 2 * 16, 1, "%.*s\n", stdout, "conv_asmX86Bit32Typ0_Typ1");
}
#endif
#if (Mode >= 2)
if (strtol(getArg(argc, argv, "cjit:", "0"), NULL, 0) != 0) {
a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_x86-32-0]\n%.*s\n", stdout, "conv_a4vm_asmX86Bit32Typ0");
a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_db]\n%.*s\n", stdout, "conv_asmX86Bit32Typ0_asmDb");
intptr_t bn = a_conv_asmDb_bin(vc0->p, VecChr_N(vc0), bin, binSz, tbl, sizeof tbl / sizeof (a_AsmDbConstTable), bf);
if (bn > 0) {
if (dbg != 0) { printf("[binary]\n"); hexDump(bin, bn, stdout); }
void (*pf)(); pf = (void (*)()) bin; pf();
}
}
#endif
#if (Mode >= 3)
if (strtol(getArg(argc, argv, "clang:", "0"), NULL, 0) != 0) {
a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
a_convVc_clang_clout(vc1, vc0, 1); if (dbg != 0) printf("[clout]\n%s\n", vc0->p);
VecChr_N(vc1) = 0; VecChr_printf(vc1, "Ent_III(256,256,0);\n%sLev_III(256,256,0);\n", vc0->p);
a_convVc_pp(vc1, vc0, 1 + 2 * 16, dbg, "[asm_a4vm]\n%.*s\n", stdout, "conv_clout_a4vm");
a_convVc_pp(vc0, vc1, 1 + 2 * 16, dbg, "[asm_db]\n%.*s\n", stdout, "conv_a4vm_asmDb");
intptr_t bn = a_conv_asmDb_bin(vc1->p, a_VecChr_N(vc1), bin, binSz / sizeof(intptr_t), tbl, sizeof tbl / sizeof (a_AsmDbConstTable), bf);
intptr_t a4arg[10]; a4arg[0] = 0; A4vm_exec0((intptr_t *) bin, bn, 10, a4arg);
}
#endif
#if (Mode >= 4)
if (strtol(getArg(argc, argv, "calc:", "0"), NULL, 0) != 0) {
a_convVc_pp(vc0, vc1, 1 + 2 + 2 * 16, dbg, "[preprocessed]\n%.*s\n", stdout, src);
Preprocessor_Eval ev[1]; Preprocessor_Eval_ini(_a_ ev); ev->err = 0;
Token0 t0[1]; Token0_ini1(t0); t0->s = vc1->p; t0->s1 = vc1->p + VecChr_N(vc1);
for (;;) {
const char *t = Token1_get(t0);
if (t0->c == ',' || t0->c == ';') { printf("%c ", t0->c); continue; }
if (t0->c == 0) break;
t0->s = t; intptr_t i = Preprocessor_eval(ev, t0, 0x7fff);
if (ev->err != 0) break;
printf("%d", (int) i);
}
printf("\n"); Preprocessor_Eval_din(_a_ ev);
}
#endif
#if (Mode >= 2)
BufFree_flush(_a_ bf); BufFree_din(_a_ bf);
#endif
VecChr_din4(_a_ vc0, vc1, 0, 0);
a_malloc_debugList(_a); a_DbgObjInfTbl_debugList(_a);
return 0;
}
(99) 更新履歴
|