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でも使われます

Goとerror monads

GoCon 2013 autumnの後、yuroyoroなどとと飲んでいて

「Goのエラーを戻り値で引き回すのがどうも冗長」
Haskellのエラーモナドみたいな何かがあればいいのに」
「型パラメータがサポートされればそういうのが書きやすくなるのに」

という話をしていて、クロージャーで関数呼び出しを囲えば今の言語仕様でもいいんじゃないかなと思ってその場で書いてみたものの話。

monad というのがインターフェイスになっていて、次のメソッドを定義している。

  • Do(what func () (interface {}, error)) monad

戻り値であるmonadに対して次にDoもしくはEndが呼ばれたときに呼ぶべき関数を受け取り、その関数への参照を保持するmonadを生成して返す

  • DoIt() (interface{], error)
    • monadの保持している参照先の関数を呼ぶ。内部用のメソッドでDo()から呼び出される
  • End(errorfn func (error)) interface {}
    • モナドの過程のどこかでエラーが発生していたら errorfn を呼び、エラーがなければ最終的な戻り値を返す

そして、これらを実装する just と _error という型がある。

  • just の Do() の中で monad の保持している DoIt() を呼び、DoIt() の戻り値がエラーを示していれば _error を返し、正常ならば新しい just に Do() に渡された引数を入れて返す
  • _error の Do() は中で何もせず、ただ受け取った _error のコピーを返す

しかし

func main() {
    monad := &just {}
    monad.Do(func () (interface {}, error) {
        println("test1")
        return nil, nil
    }).Do(func () (interface {}, error) {
        println("test2")
        // return nil, nil
        return nil, errors.New("1")
    }).Do(func () (interface {}, error) {
        println("test3")
        return nil, nil
    }).Do(func () (interface {}, error) {
        println("test4")
        return nil, nil
    }).End(func (err error) {
        fmt.Printf("I got an error: %s\n", err.Error())
    })
}

こんなメソッドチェイン、誰も書きたいと思わない。

ターミナルで動画を観る試み

Unicode文字セットの一部に、これといって用途がわからないものがある。block elements というものだ。

Block Elements (Range: 2580-259F)

マイコン世代にはおなじみのセミグラフィクス用キャラクターだ。なぜ Unicode 時代にもなってこれが必要だったのだろうという疑問はあるが、何にせよ、ノスタルジーをかき立てる身近な存在には違いない。

今日はこれを使ってターミナルで動画を見てみたいと思ったのでこんなコードを書いた。

出力はこんな感じ。


charfb: semigraphics on a Unicode-capable terminal.

py♨ advent calendar day 12

みなさんこんばんわ。py♨ advent calendar day 12 です。

PySpaに初めて行ったのは第4回、2008年10月24日でしたね。不安と期待が入り交じる中、芳泉閣へと向かったのです。PySpaを知ったのは主催者のVoluntasと知り合ったからで、Voluntasとはビープラウドさんの勉強会 (BPStudy) で出会いました。現地に到着すると、ブログでしか見た事のない有名人が大勢いて。西尾さんとかyoshioriとか怖そうだったのですが杞憂でしたね。ただ予想外に変なメガネかけててわけわからんこと言っている人がいて怖い思いをしたのですが、いつも参考にさせてもらっていた「回転と脱線」というブログを書いてる人だと後で知りました。アメリカ県の人がいたりだとか、サーファーがいたりだとか、なんといろいろなバックグラウンドを持ったギークの集まりなんだろうってかなり触発されましたよね。

ちなみに一番左にいるのが僕です。

第5回 (2009年6月26日) は何を血迷ったか自宅のあった湘南台から来宮まで自転車で行ったのですが、真鶴あたりの坂道で死にそうになりましたね。

寝込みをやられた。

第6回は何らかの事情でキャンセル。

第7回 (2010年6月26日) はどんなんだったかなあ。資料がない。

第9, 10回、何らかの事情でキャンセル。

第11回 (2012年10月26日) ひさしぶりに参加。白い猫がいない。

PySpaにはずっと続いてほしいです。

明日はtakabowです。

同じサブネットに属するIPアドレスを持つ複数のNICを扱う方法

(How to configure routing tables for multiple NICs assigned IP addresses on the same subnet)

なんでそんなことしたかったかというと

  • Amazon VPC を使っていて、複数の Elastic IP アドレスを 1 インスタンスに関連づけるため、複数の private IP addresses を 1 インスタンスに割り当てたかった。
  • しかし、1 つの ENI (Elastic Network Interface) に紐付けることのできる private IP addresses の数には制限があり、それがインスタンス毎に違う。
  • m1.small (1 ENI あたり 4 個)、を使うまでもない案件であり、t1.micro で 2 ENIs - 2 private addresses の合計 4 つで足りる。
  • しかし...

アホな例

ネットワークのことはなんも知らないので、とりあえず昔の記憶を頼りに次のように設定してみた。ネットワークインターフェイスのエイリアスというやつです。

ちなみに以下のような環境となっています。

ネットワーク: 10.0.0.0/24
ゲートウェイ: 10.0.0.1
ブロードキャストアドレス: 10.0.0.255

ENI #1 primary 10.0.0.128
~ secondary 10.0.0.129
ENI #2 primary 10.0.0.130
~ secondary 10.0.0.131

/etc/network/interfaces:

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
address 10.0.0.128
netmask 255.255.255.0
gateway 10.0.0.1

auto eth0:0
iface eth0:0 inet static
address 10.0.0.129
netmask 255.255.255.0

auto eth1
iface eth1 inet static
address 10.0.0.130
netmask 255.255.255.0
gateway 10.0.0.1

auto eth1:0
iface eth1:0 inet static
address 10.0.0.131
netmask 255.255.255.0

これして、

$ service networking restart

をやってみると、まあ応答がなくなるわけです。

仕方ないので、EBSボリュームを同じリージョンの別のインスタンスに attach してサルベージしました。

何が悪いか、どうすればいいか

同じルーティングテーブル上で、同じネットワークやデフォルトのルートは当然の事ながら 1 つしか許可されません。なので、inbound については区別がつきますが、outbound については区別をしてあげる必要があります。

eth0 の IP address を source とする outbound の場合は、eth0、 eth1 の IP address を source とする outbound の場合は、eth1 を使うように設定します。

結局どうしたか

いろいろ検索してみても、ぴったりの回答が得られなかったのですが、いろいろな情報をもとに試行錯誤した結果です。基本的には、ip コマンドフル活用でpolicy-based routingします。

まず eth1 の IP アドレスですが、エイリアスを使うのはやめて ip コマンドでやってしまいましょう。

# ip addr add 10.0.0.130/24 broadcast 10.0.0.255 dev eth1
# ip addr add 10.0.0.131/24 broadcast 10.0.0.255 dev eth1

eth0 の場合はデフォルトで設定されるので、eth1 の場合のみ別のルーティングテーブルを作り対処します。

まずはルーティングテーブルに手頃な名前をつけるために /etc/iproute2/rt_tables を編集します。

/etc/iproute2/rt_tables:

#
# reserved values
#
255     local
254     main
253     default
0         unspec
#
# local
#
1         sibling_eth1

ルーティングテーブルにはそれぞれ 1〜255 の番号がついていて、253〜255 はシステムで予約されています。1〜252 のうちで使っていない番号を選んで、sibling_eth1 という名称にします。

次に、 sibling_eth1 をクリアします。

# ip route flush table sibling_eth1

sibling_eth1 に、ルートを足します

# ip route add table sibling_eth1 10.0.0.0/24 dev eth1 proto kernel scope link
# ip route add table sibling_eth1 default via 10.0.0.1 dev eth1

main のテーブルに eth1 用のルールが足されているといけないので、これを消します

# ip route del 10.0.0.0/24 dev eth1 

最後に、source が 10.0.0.130 か 10.0.0.131 だった場合にこのルーティングテーブルを代わりに使うようにルールを設定します。

# ip rule add from 10.0.0.130 table sibling_eth1
# ip rule add from 10.0.0.131 table sibling_eth1

これで OK なはず。

リブート後も有効になるようにしたければ、
/etc/network/interfaces で eth1 の設定については

auto eth1
iface eth1 inet manual
up /etc/network/configure_sibling_eth1

として、
/etc/network/configure_sibling_eth1 に適当に上記の内容をシェルスクリプトにして書いておきます。

本当は

down /etc/network/deconfigure_sibling_eth1

として、設定をもとに戻すのを書いておくべきなんだけど、そこは適当に。

pyspaに来ています

Linuxでは一つのネットワークインターフェイスに複数のIPアドレスを紐付ける方法として、

  • エイリアスを使う方法 (バージョン2.0からサポート)
  • ip(8)コマンドのaddr addサブコマンドを使う方法 (バージョン2.2からサポート)

があるのに、残念なことに前者しか今日まで知らなかったのでそれを書く。

この違いは、

  • ioctl(2)によるインターフェイスの操作
  • netlink(7)によるインターフェイスの操作

というところにも表れているので、それぞれの方法でeth0のIPアドレスを取得する方法を調べてみた。

ioctlを使う方法:

非常に単純。この方法だと紐づいているIPアドレスを1つしか取得できないことがわかる。

netlinkソケットを使う方法:

なんでこんな複雑なんだろうって思う。ドキュメントがないので結局カーネルのソース読んだし。

途中で調べた事も書いておく。

Linuxにおけるネットワークデバイスとアドレスの表現

ネットワークデバイスはnet_device構造体(linux/netdevice.h) で表現されている。

struct net_device {
    char			name[IFNAMSIZ];
	struct hlist_node	name_hlist;
	char 			*ifalias;
	unsigned long		mem_end;	
	unsigned long		mem_start;	
	unsigned long		base_addr;	
	unsigned int		irq;		
	unsigned long		state;
	struct list_head	dev_list;
	struct list_head	napi_list;
	struct list_head	unreg_list;
	netdev_features_t	features;
	netdev_features_t	hw_features;
	netdev_features_t	wanted_features;
	netdev_features_t	vlan_features;

    ...

	struct in_device __rcu	*ip_ptr;	
	struct inet6_dev __rcu	*ip6_ptr;

    ...
};

IPv4インターフェイスの情報はip_ptrが参照するin_device構造体(linux/inetdevice.h)に、IPv6インターフェイスの情報はip6_ptrが参照するinet6_dev構造体(include/net/if_inet6.h)に定義されている。

struct in_device:

struct in_device {
    struct net_device	*dev;
	atomic_t		refcnt;
	int			dead;
	struct in_ifaddr	*ifa_list;	/* IP ifaddr chain		*/
	struct ip_mc_list __rcu	*mc_list;	/* IP multicast filter chain    */
	int			mc_count;	/* Number of installed mcasts	*/
	spinlock_t		mc_tomb_lock;
	struct ip_mc_list	*mc_tomb;
	unsigned long		mr_v1_seen;
	unsigned long		mr_v2_seen;
	unsigned long		mr_maxdelay;
	unsigned char		mr_qrv;
	unsigned char		mr_gq_running;
	unsigned char		mr_ifc_count;
	struct timer_list	mr_gq_timer;	/* general query timer */
	struct timer_list	mr_ifc_timer;	/* interface change timer */

	struct neigh_parms	*arp_parms;
	struct ipv4_devconf	cnf;
	struct rcu_head		rcu_head;
};

struct inet6_dev:

struct inet6_dev {
    struct net_device	*dev;

	struct list_head	addr_list;

	struct ifmcaddr6	*mc_list;
	struct ifmcaddr6	*mc_tomb;
	spinlock_t		mc_lock;
	unsigned char		mc_qrv;
	unsigned char		mc_gq_running;
	unsigned char		mc_ifc_count;
	unsigned long		mc_v1_seen;
	unsigned long		mc_maxdelay;
	struct timer_list	mc_gq_timer;	/* general query timer */
	struct timer_list	mc_ifc_timer;	/* interface change timer */

	struct ifacaddr6	*ac_list;
	rwlock_t		lock;
	atomic_t		refcnt;
	__u32			if_flags;
	int			dead;

    ...

	struct neigh_parms	*nd_parms;
	struct inet6_dev	*next;
	struct ipv6_devconf	cnf;
	struct ipv6_devstat	stats;
	unsigned long		tstamp; /* ipv6InterfaceTable update timestamp */
	struct rcu_head		rcu;
};

in_device構造体に紐づいているIPアドレスのリストはifa_listで、inet6_dev構造体に紐づいているIPアドレスのリストはaddr_listで表現されている。

IPv4IPv6で全然構造が違うのが面白い。

エイリアスを使った場合も、そうでない場合もどちらもそれぞれのIPアドレスがリストのエントリとして格納される。

グリー株式会社を退職していました

グリー株式会社を退職していました。2006年12月29日が最終出社日でした。

最終日は例年を上回る寒さでしたが、六本木は晴れ、抜けるような空の青さが印象的でした。有給を消化するはずが、結局最後まで吉田くんが手がけていたお正月特集の仕掛けに時間を費やしていたので、本当にここで出社しなくなるのだろうか、と自問するほどでした。

在職期間は契約社員時代も含め1年半とちょっと短かったのですが、その中でもたくさんのことを経験することができました。サイトの初リニューアル作業に始まり、DBの最適化、出たばかりのSennaを使った全文検索システムや画像サーバクラスタの構築など、責任のある仕事を丸ごと任せてもらえたのは大きかったです。一緒にやっているメンバーには、自分のささいなミスでたくさん迷惑をかけました。ごめんなさい。

一方で、何か大きな成果を残すことができたかというと、謙遜ではなく、あまり胸を張れるところではないなあというところが若干の心残りではあります。

一緒にやってきたチームのメンバーのみんなとの出会いはもちろん、CTO の藤本さんと一緒に仕事できたのは一生の思い出になっただろうと思います。特に、休日出勤中にうっかり本番データをTRUNCATEしたときに、プライベートで忙しいにも関わらず出社してフォローをしてくださるなど面倒見の良さには感銘を受けましたし、何より自分自身、困ったときに何度もサポートしていただき心強かった。後に辞めていったメンバーがみんな藤本さんの名前を口にしながら退職の挨拶をしていると聞いて、今もなお畏敬の念を拭う事ができません。

退職理由は語ran新しいことを始めてみたいというのが理由でした。結果として、Webサービスをそれまでと同じように作っていたらできなかったであろう得難い経験をする事ができたので、今でも退職したことは後悔していません。ただ、もし今も在職していたら、それはそれで別の得難い経験ができていたのではないかと思わせる、そんな組織だったと思います。なお、今はどんな形でも会社に関わっていません。

みなさん、本当にありがとうございました。
なおやさん、テンプレをありがとうございました。