acl4の開発ログ #11
2026.04.11(土) #0
- 今後の予定:
- ソースコードを少しだけ整えたあとで、以下のことをやりたいです。
- [1] tt0003b.c (2026.03.17(火)#2→a4_log08) で作ったミニコンパイラと今回作った typedDef 付きのプリプロセッサ関数で、簡易Cインタプリタを試作したい。
- [2] 同様にミニコンパイラと適当な定義ファイルで、x86用の簡易Cコンパイラを試作したい。
- [3] 同様にミニコンパイラと適当な定義ファイルで、簡易JITコンパイラを試作したい。
2026.04.11(土) #1
- 今ふと思ったんですが、プリプロセッサのマクロって面白いですよね。
- 普通のプログラムだと、変数への代入順序はとても重要です。
a=1;
b=2;
c=a+b;
printf("%d\n", c);
- これがプリプロセッサだと逆順に書いてもうまくいってしまうのです。
#define c a+b
#define b 2
#define a 1
printf("%d\n", c);
- 当たり前なんだけど、なんだかおもしろいです。
- (ちなみに逆順に限らず、3行のdefineはどんな順序で書いても問題なく動きます。)
2026.04.11(土) #2
- acl4をたくさん使ってだんだんわかってきたので、もっと使いやすくするために、全面的に書き換えたいなという気分になってきました。
- 上記の簡易Cインタプリタ・簡易Cコンパイラ・簡易C-JITコンパイラができたら、acl4v1に移行しようかなと検討中です。
- メモリリーク発見機能が大活躍なので、もうちょっと強化したい。
- プログラムの部品化の粒度を見直したい。
2026.04.12(日) #0
- なんか気分が乗ってきたので、全面書き直しのほうを先にやってみました。・・・ってまだ半分くらいしかできていませんが。
- 旧acl4では、メモリリークの一覧が簡単に見えてとても便利でしたが、acl4v1では、それに加えて init はしたけど deinit はしてないオブジェクトの一覧が出ます。これがもう「うきょー」って言いたくなるくらい便利です(笑)。
- 例えば以下は、a4_0005に書いたt0005b.cを改造したものです。
// tt0005a.c
#define a_Version 1
#include <acl4v1.c>
class_(Data) { SetElm elm[2]; int age; };
int main()
{
Set0 index[2]; char *k;
Set0_ini(_a_ &index[0], NULL);
Set0_ini(_a_ &index[1], NULL);
// nameだけではなく、phoneでもデータを検索できるようにした例.
Data data[4], *p; int i;
data[0].elm[0].k = "Kawai"; data[0].age = 20; data[0].elm[1].k = "090-8765-4321";
data[1].elm[0].k = "Suzuki"; data[1].age = 22; data[1].elm[1].k = "0X0-XXXX-XXXX";
data[2].elm[0].k = "Tanaka"; data[2].age = 25; data[2].elm[1].k = "0Y0-YYYY-YYYY";
data[3].elm[0].k = "Yamamoto"; data[3].age = 30; data[3].elm[1].k = "0Z0-ZZZZ-ZZZZ";
for (i = 0; i < 4; i++) {
data[i].elm[0].n = strlen(data[i].elm[0].k);
data[i].elm[1].n = strlen(data[i].elm[1].k);
Set0_add(&index[0], &data[i].elm[0]);
Set0_add(&index[1], &data[i].elm[1]);
}
k = "Tanaka"; p = Set0_findKn(&index[0], k, strlen(k));
if (p == NULL) printf("name(key)=%s: not found\n", k);
if (p != NULL) printf("name=%s age=%d phone=%s\n", (char *) p->elm[0].k, p->age, (char *) p->elm[1].k);
k = "0X0-XXXX-XXXX"; p = ptrSub(Set0_findKn(&index[1], k, strlen(k)), sizeof (SetElm));
if (p == NULL) printf("name(key)=%s: not found\n", k);
if (p != NULL) printf("name=%s age=%d phone=%s\n", (char *) p->elm[0].k, p->age, (char *) p->elm[1].k);
Set0_din(_a_ &index[0]);
// Set0_din(_a_ &index[1]); // わざとdeinitを忘れてみる.
a_malloc_debugList(_a);
a_DbgObjInfTbl_debugList(_a);
return 0;
}
// 実行結果.
>tt0005a
name=Tanaka age=25 phone=0Y0-YYYY-YYYY
name=Suzuki age=22 phone=0X0-XXXX-XXXX
tt0005a.c(35): malloc_debugList()
[p:00afccf0 sz:24 / a4_0016.c(345)] ← [註]従来からのメモリリーク情報表示
tt0005a.c(36): DbgObjInfTbl_debugList()
[VecChr / a4_0016.c(402)] ← [註]acl4v1のオブジェクトリーク情報表示(new)
[Set0 / tt0005a.c(10)] ← [註]acl4v1のオブジェクトリーク情報表示(new)
- 今までは、 malloc_debugList しかありませんでした。だから「24バイトのメモリリークがあって、それは a4_0016.c の345行目に由来する」ということしかわかりませんでした。
- ちなみに a4_0016.c の345行目は VecChr_reserve() の中です。だからまあ、 VecChr に関係ありそうですが、 tt0005a.c の中には VecChr を明示的に使っている部分はないので、やっぱりよくわからないわけです。
- 一方で、 acl4v1 から見えるようになった DbgObjInfTbl_debugList を見ると、 VecChr が deinit されていないという情報だけではなく、 Set0 が deinit されていないことが分かります。しかも行番号で10行目だとわかるので index[1] のほうだとすぐにわかります。
- これはデバッグがはかどります!
- (ちなみに、 Set0 が VecChr を内部で使っているのでした。)
- 「うきょー」って言いたくなる気持ちが伝わったでしょうか・・・。
2026.04.13(月) #0
- ちなみに今は「どのオブジェクトなのかを特定するために、initを呼んだ行番号を表示」という方式を採用していますが、かつてはこの方法ではありませんでした。
- メモリリークやオブジェクトに関するエラー表示について、最初に考えた方法は「malloc/initするときに名前を付ける」という方法でした。5年くらい前のことです。
- まず、この方法は基本的にはうまくいきました。どのオブジェクトが悪いのかを簡単に見分けられました。
- しかし問題もありました。それは名前を付けるのが面倒ということです。そもそも名前なんてなくてもエラーさえなければ問題はないのです。
- ・・・それで名前を省略したり、同じ名前を適当に連発したりすることになります。しかしそうするとエラーが出たときに結局どのオブジェクトのことなのかよくわからなくなるのです。
- そうなってから名前を付けなおしてデバッグするわけですが、なんかあまり便利な感じではなく、printfデバッグと大差ないような使用感でした。
- __LINE__とか__FILE__を書けば行番号表示に必要な情報が取れることは前から知っていましたが、それがオブジェクト名の代わりにできるということにはずっと気づきませんでした。
- また、このマクロ変数は目立つし長いので、ソースコードに書くのは嫌だとも思っていました。
- 2026年になって、メモリリークを検出する機構をacl4用にまた1から作ろうと思ったとき、名前を付ける方式はもうやめようと思いました。名前がなくてもサイズが分かればそれでいいかなと思ったのです。
- 一方で実行時エラーを表示するために、関数を呼び出した行番号は欲しいと思いました。でも__LINE__とかを関数の引数として書くのは嫌だったので、マクロでうまく隠す方法を採用しました。
- やってみてわかったこと: サイズだけではよくわからない。でもmalloc時の行番号が分かれば十分にデバッグの役に立つ。・・・それでmalloc/freeだけではなくinit/deinitに対しても同様のチェック機構をつけることにしました。それがacl4v1です。
2026.04.13(月) #1
- 私は、VecChrで作った可変長配列の中にオブジェクトを並べていくことが時々あります。
- たとえば、"apple", "banana", "grape", "lemon", "melon", "orange", "peach" みたいなサイズが不ぞろいの単語の配列を動的に作る場合、とりあえずこうします(動的ではないなら普通の配列にすればいいのですが)。
VecChr array[1], *p; VecChr_ini(_a_ array);
p = VecChr_stkAdd(array, sizeof (VecChr)); VecChr_iniCpy0(_a_ p, "apple", -1); // 長さが-1の場合、strlenして長さを設定してくれる.
p = VecChr_stkAdd(array, sizeof (VecChr)); VecChr_iniCpy0(_a_ p, "banana", -1);
p = VecChr_stkAdd(array, sizeof (VecChr)); VecChr_iniCpy0(_a_ p, "grape", -1);
// stkAdd()は、指定されたサイズだけarrayを拡張し、その先頭アドレスを返してくれます.
// そこを VecChr で init して、 文字列をコピーしているのです。
- 一方で、VecChrはサイズの拡大のために realloc() を使っています。だから array の中のVecChrオブジェクトは、メモリアドレスが変わることがあり得るわけです。
- もしもメモリアドレスが変わらないなら、オブジェクトをリンクリストでつないだり外したりするだけで、deinitしていないオブジェクトの一覧を作ることができます。実際mallocのメモリブロックはそうやって管理しています。これならメモリブロック1つ当たりの管理コストは O(1) です。
- でもメモリアドレスが変わるなら、リンクリストでつなぐ方式は使えません。ここが苦労ポイントでした。でも頑張って O(1) でできるようになりました。
- オブジェクトのアドレスは変わってもいいようにします。
- 上記例でたとえばarrayの中身をdeinitせず、親のarrayのみをdeinitしてしまうバグもあり得ます。その場合、メモリが解放されてしまうので、もう個々のVecChrにはアクセスできませんしするべきではありません。それでもdeinitし忘れたことを行番号とともにレポートできるべきです。
- デバッグモードとはいえ、速いほうが望ましいのでできれば O(1) で(1オブジェクトあたりの管理コストが)。
2026.04.15(水) #0
- リファクタリングをすると、そういえばここも直したいって思うところが見つかって、気分がいいです。
- 大半の記述は直す必要はないので、コピー&ペーストしてきて、気になるところ直して、エラーやリークが無くなれば次に進む、みたいな感じなので、さくさく進みます。
2026.04.15(水) #1
2026.04.16(木) #0
- [Q] 結局 acl4 って何ができますか?
- [A1] デバッグレベルの提供
- 一般にCコンパイラには、最適化レベルとデバッグ情報(シンボル情報)の有無程度しか選択肢がありません。最適化レベルを最低にしても、それは単に工夫のないコード生成をしてコードサイズが冗長になり、実行速度が下がってコンパイル時間が短くなる程度で、それ以上のメリットはありません。
- acl4は違います。デバッグレベル2以上にすれば(=これをデバッグモードと言っています)、ライブラリは不適切な引数で呼び出されてはいないかとチェックするようになり、デバッグがやりやすくなります。具体的なチェック内容については以下で説明します。
- なお、デバッグモードではなくても「ファイルオープンのエラー」や「メモリ不足でmallocに失敗した」などのエラーチェックは省略せずに行います。これらはプログラムにバグがなくても起こりうるエラーだからです。デバッグレベルで制御するのは、プログラム内のバグに起因するエラーチェックのみです。
- デバッグモードはプリプロセッサ制御で実現しています。リリースモード時にはデバッグチェックのためのコードはソースコードから消された状態でコンパイラに渡されるので、コンパイラの最適化能力によらず、デバッグチェックのためのコードが実行ファイルに残ることはありません。
- このようにデバッグチェックがリリースモード時の実行速度や実行ファイルサイズに悪影響を与えることがないと保証されているので、チェックは遠慮なく念入りに行うことができています。
- [A2] デバッグモードで提供される機能(メモリ管理支援など)
- mallocしたけどfreeしてないメモリを一覧形式で表示する関数があります。メモリリークのバグを見つけるのにとても重宝します(その際には、プログラムの何行目のmallocで確保したメモリなのかも教えてくれます)。
- mallocしてないメモリをfreeしてしまった(もしくは二重にfreeしてしまった)場合は、エラーメッセージを表示してすぐに終了します。問題のfreeがどこにあるのかもわかります。
- freeしたりreallocでサイズを小さくすると、解放された部分はゼロクリアされます。これでfreeしたメモリを誤って読みだそうとするようなバグはすぐに見つかることになります。
- initしたけどdeinitはしていないオブジェクトの一覧も見られます。メモリリークの原因はたいていこれでわかります。
- また同じオブジェクトに対してinitを2度してしまったとか、initしてないのにdeinitを呼び出したなど、明らかにおかしいバグがあった場合も、エラーメッセージを表示してすぐに終了します。
- これらのサポート機能のおかげで、私は自分の作ったプログラムにバグは残ってないと確信することができるようになりました。
- [A3] 動的可変長配列のクラス VecChr の提供
- C++には vector<T> という動的可変長配列があります。これと同じではないものの、近いものがあれば便利だと思って、 VecChr というクラスを作りました(vector<char>を意識して命名しました)。
- 動的可変長配列があれば事前にサイズを決めておく必要はなくなるので、 acl4 内のいろんなところで多用されています。
- [A4] プリプロセッサを作るための一通りの機能を提供
- プリプロセッサで処理したいソースコードをC言語の文字列で用意すれば(当然VecChrでも可)、プリプロセッサで処理した後の結果をC言語の文字列で返すプリプロセッサ関数があります。
- 基本的にすべてオンメモリで処理するので高速ですし、一時ファイルなどを作らないのでディスクを汚しません。オンメモリで処理するとは言っても、includeでファイルを読み込ませる処理がある場合は、ちゃんとファイルアクセスもします。
- プリプロセッサ関数はより細分化された細かい関数で構成されているので、カスタマイズしたプリプロセッサ関数を作ることも可能です。
- [A5] Cコンパイラ・Cインタプリタを作るための機能を提供(予定)
- Cコンパイラなどの言語を作ることを考えると、結局はソースコードを次々と別の形式に変換していって、最終的にアセンブラか中間コードか機械語に変換することだとわかります。
- この「変換」は実はプリプロセッサが得意とするところなので、プリプロセッサで言語機能の大半を記述し、プリプロセッサで書けない部分だけをC言語で書くようにしたら、従来よりもはるかに少ない手間で言語が作れるのではないかと考えました。
- acl4 では既にプリプロセッサ処理は好きなだけ利用できる状態になっているので、これを活用して、実際に言語を作ってみて、どんな感じになるか確かめています。
2026.04.17(金) #0
- プリプロセッサが、文字列リテラルに対してstrlenできるようになったら(=結果を定数で返せたら)、いろいろ便利になりそうな気がしてきました。
2026.04.17(金) #1
- acl4 → acl4v1 での主な改善点:
- DbgObjInf が入ったので、init/deinit に関するバグを簡単に見つけられます。
- VecChr の sizeof が「ポインタ1つ分」になりました。 VecChr の配列とかを作ってもアクセスが速そうです。 acl4 では「ポインタ3つ分」でした。
- parseArgs() を使いやすくするために結果の渡し方を改良しました。
- Preprocessor_Eval で、演算子や特殊な変数や関数の追加などのカスタマイズの余地を入れました。
- プリプロセッサの機能が密結合になっていた部分があったので、そこを直してより改造しやすくします(予定)。
- typedDef と普通の define の処理ルーチンを統合します(予定)。
2026.04.18(土) #0
- やっと、acl4v1でもプリプロセッサが動きましたー。でもまだdefineは動きません。それ以外は動きます。
- そのテストの時に、デバッグモードにするとやたらと低速になる現象が起きました。「まあデバッグモードだから遅くなってもしょうがないかな」と最初は思いましたが、でも原因くらいは確認するべきだろうと思って調べたら、init/deinitの管理部分のバグでした。直したらデバッグモードでも十分に速くなりました。よかったー。
こめんと欄