Hacking C to its limits

概要

私はlibCello (当時はC+と名づけていた) を、Cにおける多様なオブジェクト指向の実装を楽しむための、面白おかしい実験として始めました。 幾分かの楽しい機能やクールなトリックを実現しましたが、時が経ち、行き詰まりがちになっていました。 アサーションが積み重なってしまったり、文法がひどいものになってしまったり、プログラマが手作業であまりに多くのことをやらなければならなかったり。 各々が、間違いを起こしやすい文字列に大きく依存したメッセージパッシングによるものでした。

ひとつ私が気づいたことは、根本的にCのバックエンドとPythonのようなスクリプト言語が共通点を持ち始めているということでした。

私はこうではないかと思いました。 異なる、または簡素化された推定を実装すれば、より良い文法と簡潔な意味論が実現し、そしてプログラマが手作業で行う工数を減らせるのではないかと。 スクリプト言語のような意味論、しかしながらCの性能とフロントエンドを持つものを創り出すことができるのではないかと。

libCelloは大域での変更と、たくさんのキュートなハックによって成り立っています。 大域での変更は、独創的すぎるものではありません。 しかし、キュートなハックと結びつくことで、とても使いやすいライブラリになりました。

この記事を短くすることで、肝心の詳細について幾分かを省略しています。 興味が湧いた方は、メールで応援したり、ソースコードを覗いてみたりして下さい!

言語の挙動を変えるためには、コンパイラまたはランタイムの変更を行う必要があります。 Cのような言語は、本質的に、ランタイムの仕組みを持っていません。 そういった言語は、何か別の言語のように見えるオブジェクトコードを生成します。 しかし、私たちは、自分たちの手でオブジェクトコードを追加できるのです。 私たちが追加する必要があるたった一つのもの、それは、私たちが欲しているほとんど全てのことを行う、強力な構造体なのです。

例えば、 newdelete を言語、あるいは私たちのシステム (ランタイム) に追加したいと望んだとしましょう。 私たちは割り当てるメモリのサイズを知らなければなりませんでした。 これこそ、 の情報としても知られる メタデータ なのです。 私たちは、プログラムが動作する間、これをどこかへ保管しなければなりません。 それは分離されたテーブルでも可能かもしれませんが、私たちが操作したいと望んでいるオブジェクトそのものに紐付けると便利なことが多いです。

例を示します。構造体の先頭に 又は メタデータ へのポインタを格納した リッチな オブジェクトを作ることができます。

typedef struct {
  type_t* meta_data;
  int other_data;
} some_struct;

これらのオブジェクトを操作したいと望んだ場合、私たちはメタデータへのポインタを追いかけ、必要な情報を抽出するという手があります。 このエントリが常に構造体の先頭のエントリである場合、私たちが出会う構造体がいかなるものであっても、メタデータへのポインタを見つけるための場所を、私たちは常に把握することができます。 これはシンプルな仕組みです。しかし、不運なことに、これによって、私たちは最初から思い込みをしてしまうのです。それは少なからず避けることができません。

全ての リッチな オブジェクトは、このエントリから始まらなければなりません。しかし、プログラマはこの追加作業を慎重に行う必要があります。 プログラマは忘れることがあるのです。

私たちが知っているように、Cには隠れたコストというものが存在しません。これは有名なことです。 あなたは、あなたが使用するもののためだけに、コストを支払えばよいのです。 これが、先ほど言及した不可避の理由です。 Cを使う以上、あなたが前述のエントリをあるプログラマの構造体へ無意識のうちに挿し込む手段はありません。 そのようにできるというのであれば、それは正直なことではないでしょう。そしてCは嘘つきを嫌います。

では、型情報を、構造体のメモリの場所から生成したインデックスを持つ、プログラムとは分離したテーブルへ保持するというのはどうでしょう? これは実に上手く機能します。しかし、これの意味するところは、いかなる構造体であっても、それを生成する際には、私たちが定義した関数 (これにより、型情報をテーブルへと追加する) を経由しなければならないということでもあるのです。 多くの場合において、これは良い方法です。しかし、この関数の外部から与えられる有効なデータを保持しておくことが、より便利であることもあります。例えば、それはスタックの上部に生成します ( ドル記号 の項目を見て下さい)。

ともかく、このコストを受け入れるのであれば、私たちは外の世界から見たら魔術のように見えるたくさんの新しい振る舞いを言語に追加することになるでしょう。 次のステージでは、 メタデータ オブジェクトを設計します!

メタデータ

メタデータ・オブジェクトを設計するための方法はたくさんあります。しかし、私たちは注意深くならなければなりません。 すでに私たちには一つの制約があるのです。 メタデータは リッチな オブジェクトであるべきなのです。 第一のエントリは その 型へのポインタでなければならないという意味です。 しかし、Typeオブジェクトの型とは何でしょう? そう、その型はまさしく Type であり、その型もまた Type なのです。

頭がこんがらがってしまいましたか? また別の問題が、定数のイニシャライザにあります。 Cにおいて、多くのデータ型は、コンパイル時におけるメタデータ構造の初期化無しには利用することができません。 しかし、ランタイム時に全てのデータ型を宣言しなければならないというのは、誰もやりたくありません。 当該エントリは、定数リテラルでなければなりません。 その第一のエントリはその型でなければなりません。 そうであるにもかかわらず、その第一のエントリは、あるオブジェクトについて、私たちが必要とする全てを表現しなければならないのです。

libCelloにおいて、私はNULL終端したペアのリストを使用しています。 それぞれのペアは、何かへのポインタと、文字列の識別子によって構成されています。

var MyType = {
  { Type,       "Type" },
  { MyTypeName, "Name" },
  { MyTypeNew,  "New"  },
  { MyTypeOrd,  "Ord"  },
  { NULL,       NULL   }
};

より特別なこととしては、それぞれのエントリは、Typeclass (又は名前のような別の情報) へのポインタとなっていますが、詳しいことは後で述べます。

定数イニシャライザの問題は、今でもあちこちで突然発生します。しかし、ワークアラウンドはあり得ます。 より大きく、より良くなるにあたり、定数イニシャライザを実践的に使うようにしましょう!

ジェネリック関数

もし、例えば、私たちが言語に対して printhash のような関数を追加したいと望むとします。私たちはそれらに対して、それらが理解するあらゆる型について機能できることを望むでしょう。 言い換えれば、それらは ジェネリック であるべきなのです。 Cにおいて、このことはある問題提起となります。型チェッカがこの種の振る舞いを許容しません。 どの事物がどの型の意味を成すかは、わかりません。 しかしながら、私たちは、 void* を取る関数を宣言することが できる のです。 これは型チェックを無視し、 全ての ポインタ型に対して、関数を機能させるものです。

したがって、二番目に主要な推定を、私たちはすることになります。 ジェネリック関数を許容するため、私たちはコンパイラの型チェック無しに済ませるのです。 私たちは、ランタイムにおける型チェックを、新規に宣言したランタイム型を用いることでも、行うことが できはします 。 これは、リッチなオブジェクトのメタデータへのアクセスによるものです。 しかし、それは痛みを伴う妥協案です。

これに対する現実的な解はありません。 今もなお、です。 動的言語が流行っています。 void*var にリネームし、私たちはモダンなスクリプト言語に似た何かを手に入れました。 もし、ランタイム時の型チェックがモダンなスクリプト言語にとって十分に良いものなのであれば、それは私たちにとっても十分に良いものです。

型クラス

特定の操作 (順序付けのような) が特定の型に対して意味が通るかどうかを知るため、私たちはプログラマから教えてもらう必要があります。 これが、型クラスの由来です。 インタフェースとしても知られています。 これらによって、プログラマはある型に対してある特定の操作の下における振る舞いを定義することになります。 もしかしたら、驚くべきことに、それらはほぼ全て、より高次の概念として表現することが可能かもしれません。

libCelloにおいて、型クラスは構造体にすぎません。これは大抵、一通りの関数ポインタを格納しています。 型は、これらのインスタンスを生成します。そして、メタデータ・エントリにおいて、それらを指し示します。

typedef struct {
  var (*iter_start)(var);
  var (*iter_end)(var);
  var (*iter_next)(var, var);
} Iter;

static Iter ListIter = { List_Iter_Start, List_Iter_End, List_Iter_Next };

初めの頃に私が決めたのですが、型クラスは型の内部に何があるかということについての中核となる予定でした。 これにより、入り組んだメタデータ構造をもつ、Cにおける多くの メッセージ・パッシング 又は オブジェクト指向の 仕組みと比較して、例外的にも物事を簡素化することができました。

これですべて?

libCelloと多くの近しいプロジェクトの背景にあるグローバルなアイディアは、本当のところはこれでおしまいです。 私たちがランタイム・システムに構築するリッチでパワフルな仕組み、そこから楽しさはやってくるのです。

トリック

ドル記号

$ 、又は私が考えるところの音符記号は、プログラマがスタック上にシンプルで リッチな オブジェクトを宣言することを可能にするものです。 これは、libCelloを使う上での精神的なオーバーヘッドを減らすために超重要なものです。それは、割り当てられた数百ものヒープ・オブジェクトを手作業で迅速に掃除するという作業を手放すためです。

マクロはとてもシンプルで、Cプログラマが数十年にも渡って使ってきた構造体の初期化というトリックを使用します。 それはまさに、リテラルな構造体の宣言を、単一要素の配列へとラッピングするものです。これは、そのポインタを取得するためのものです。

#define $(T, ...) (T##Data[]){{T, __VA_ARGS__}}

デストラクタやコンストラクタは呼ばれません。そして、構造体の名前を得るために、型が IntData のような名前を付けられているという事実を、私たちは利用しなければなりません。しかし、単なる箱詰めされたオブジェクトにとっては、それは機能的という域を超えています。

私が最初に $ を選んだ時、私が探していたのは、変数を意味する傾向のある記号でした。 私が @ を試した直後なのですが、 $ がプリプロセッサのトークンとしては技術上不正なものであることに気がついたのです。 全てのプリプロセッサのトークンは、変数と全く同じルールに従う正しい識別子でなければなりません。 すると、GCCがドル記号を通す理由なんて、あるわけがないですよね?

GCCが $ をあるシステム、例えばVMSなどとの互換性のための文字として扱うということが分かり、そこでは $ はシステムで定義された関数やオブジェクト名に共通して使われています。 移植性の理由から、最高に賢明な決定というわけではないのですが、これは取り得る中では非常に素晴らしいハックなのです!

Foreach

型クラスを使えば、 foreach を実装するのはほんの瑣末なことでした。私たちは、 iter_start, iter_end, iter_next といった関数を持ち、これらを実装可能なあらゆるオブジェクトがforeachを通じてイテレーションをサポートする、 Iter 型クラスを作ります。

#define foreach(x, xs) \
  for(var x = iter_start(xs); x != iter_end(xs); x = iter_next(xs))

With

with の背景にあるアイディアは、 foreach とほぼ一致しています。私たちには、 with 型クラスによって実装された enter_withexit_with 関数があり、それらを利用するために、一つの for ループを使っています。

#define with(x, y) \
  for(var x = enter_for(y); x isnt Undefined; x = exit_for(x))

enter_for 関数は、 enter_with をコールし、 y を返します。これは exit_for 関数が exit_with をコールし、 Undefined を返すのとは対照的です。 単一のブロックをスコープとする変数を表現するために for ループを使うのは非常にナイスなハックで、これにより、特別な挙動を取る一連のブロック全体を生成することができるのです。

Lambda

例外

True/False