GoのツールチェインのCコンパイラを使う

(この記事は Go Advent Calendar 2013 の12月24日に公開されるはずの記事でした。関係者の皆様大変申し訳ない。)

はじめに

Goコンパイラをビルドすると、なぜかアセンブラ (5a, 6a, 8a) やCコンパイラ (5c, 6c, 8c) までもがビルドされます。これらはPlan 9のツールチェイン由来のもので、ランタイムライブラリ (src/pkg/runtime) の一部のソースコードのコンパイルに使われます。システムデフォルトのコンパイラを使わない (使えない) 理由は、GoのABIがPlan 9のものをなぞっており、Goランタイム上ではこのツールチェインの生成するコードしか動かせないからです。*1

つまり、このツールチェインを利用することで、Goランタイム上で動くコードをGoではなく全部C言語で書くことができます。

Goのアプリケーション初期化シーケンス

Goアプリケーションのmain関数に処理が渡るまでのステップは次のようなものです。

  1. OSによってバイナリがメモリにロードされる
  2. OSからエントリポイントとして_rt0_{arch}_{os} (例: _rt0_amd64_linux) が呼び出される (cf. src/cmd/ld/lib.c)
  3. _rt0_{arch}_{os} がOSから受け取ったコマンドライン引数 (argc, argv) をスタックに積んで _rt0_go を呼び出す
  4. _rt0_go で main の goroutine の生成、及び main·init() と main·main() の呼び出しが行われる

main·init()

main·init() では動的なグローバル変数の初期化が行われます。また、利用しているパッケージの、{package_name}·init() もここからネストして呼び出されます。この様子は、次のようなコードで確かめることができます。

package main

func test() bool {
    panic("nooo!")
    return false
}

var a = test()

func main() {}
panic: nooo!

goroutine 1 [running]:
runtime.panic(0x21a40, 0x2100d1010)
        /Users/moriyoshi/Sources/go/src/pkg/runtime/panic.c:264 +0xb6
main.test(0x2210254fa0)
        /private/tmp/test.go:4 +0x55
main.init()
        /private/tmp/test.go:8 +0x45
exit status 2

main·main()

メインルーチン。mainパッケージのmain関数がこれに相当します。

Hello World

ここまでの内容を踏まえ、hello worldを書いてみます。

ソースコード

#include <runtime.h>

void main·init() {}

void main·main()
{
    runtime·prints("Hello, world!\n");
}

middle dot

· (U+00B7) は、Mac OS XであればOption + Shift + 9で入力することができます。

  • コンパイル

6cの部分は、アーキテクチャによって変えてください。(ARMであれば5c, x86_64であれば6c, i386であれば8c)

$ go 6c -I $GOROOT/src/pkg/runtime hello.c
  • リンク

6lの部分は、アーキテクチャによって変えてください。(ARMであれば5l, x86_64であれば6l, i386であれば8l)

$ go 6l -o hello hello.c
  • 実行
$ ./hello
Hello, world!

ちゃんと実行されました。

goroutine をCから使う

いきなりハードルを上げて、goroutineをCから使ってみましょう。

#include <runtime.h>


/* runtime.h で宣言されていないので */
extern void runtime·newproc(int32 sz, FuncVal* fn, ...);

void main·init() {}

/* runtimeモジュールには用意されていない */
String itos(intgo i)
{
    byte buf[128];
    byte *e = buf + nelem(buf), *p = e;
    if (i < 0) {
        *--p = '-';
        i = -i;
    }
    while (--p >= buf) {
        *p = "0123456789"[i % 10];
        i /= 10;
        if (i == 0)
            break;
    }
    return runtime·gostringn(p, e - p);
}

/* UTF-8クリーン */
static void ゴ〜(String s)
{
    runtime·printstring(s);
    runtime·prints("\n");
}

void main·main()
{
    FuncVal fv = { (void(*)(void))ゴ〜 };
    intgo i;
    for (i = 0; i < 10; i++) {
        String hello = runtime·gostring((byte*)"Hello from #");
        String is = itos(i);
        String s = runtime·catstring(hello, is);
        /* 第1引数に引数のサイズを与える */
        runtime·newproc(sizeof(String), &fv, s);
    }
    /* goroutineをyieldする前に終了するのを防ぐため適当にwaitする */
    runtime·tsleep(10000000000l, "wait");
}

String構造体がGoのstring型にそのまま対応していて、Goの文字列操作と等価のことをランタイムライブラリの関数呼び出しによって行うことができます。FuncVal構造体は、Goの関数型に相当します。

String構造体はruntime.hで次のように定義されています。

struct String
{
    byte*    str;
    intgo    len;
};


上記のコードの実行結果は次のようになります。

$ ./gohello
Hello from #0
Hello from #1
Hello from #2
Hello from #3
Hello from #4
Hello from #5
Hello from #6
Hello from #7
Hello from #8
Hello from #9

まとめ

  • GoランタイムをCから使うことができました
  • Pythonなど単純な処理系をGoランタイムの上に載せて動かすというようなことができそうで夢が広がります

次は najeira さんです。

*1:ちなみに、このツールチェイン、cgoでも使われます