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関数に処理が渡るまでのステップは次のようなものです。
- OSによってバイナリがメモリにロードされる
- OSからエントリポイントとして_rt0_{arch}_{os} (例: _rt0_amd64_linux) が呼び出される (cf. src/cmd/ld/lib.c)
- _rt0_{arch}_{os} がOSから受け取ったコマンドライン引数 (argc, argv) をスタックに積んで _rt0_go を呼び出す
- _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
*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 時代にもなってこれが必要だったのだろうという疑問はあるが、何にせよ、ノスタルジーをかき立てる身近な存在には違いない。
今日はこれを使ってターミナルで動画を見てみたいと思ったのでこんなコードを書いた。
出力はこんな感じ。
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)
なんでそんなことしたかったかというと
アホな例
ネットワークのことはなんも知らないので、とりあえず昔の記憶を頼りに次のように設定してみた。ネットワークインターフェイスのエイリアスというやつです。
ちなみに以下のような環境となっています。
ネットワーク: 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で表現されている。
グリー株式会社を退職していました
グリー株式会社を退職していました。2006年12月29日が最終出社日でした。
最終日は例年を上回る寒さでしたが、六本木は晴れ、抜けるような空の青さが印象的でした。有給を消化するはずが、結局最後まで吉田くんが手がけていたお正月特集の仕掛けに時間を費やしていたので、本当にここで出社しなくなるのだろうか、と自問するほどでした。
在職期間は契約社員時代も含め1年半とちょっと短かったのですが、その中でもたくさんのことを経験することができました。サイトの初リニューアル作業に始まり、DBの最適化、出たばかりのSennaを使った全文検索システムや画像サーバクラスタの構築など、責任のある仕事を丸ごと任せてもらえたのは大きかったです。一緒にやっているメンバーには、自分のささいなミスでたくさん迷惑をかけました。ごめんなさい。
一方で、何か大きな成果を残すことができたかというと、謙遜ではなく、あまり胸を張れるところではないなあというところが若干の心残りではあります。
一緒にやってきたチームのメンバーのみんなとの出会いはもちろん、CTO の藤本さんと一緒に仕事できたのは一生の思い出になっただろうと思います。特に、休日出勤中にうっかり本番データをTRUNCATEしたときに、プライベートで忙しいにも関わらず出社してフォローをしてくださるなど面倒見の良さには感銘を受けましたし、何より自分自身、困ったときに何度もサポートしていただき心強かった。後に辞めていったメンバーがみんな藤本さんの名前を口にしながら退職の挨拶をしていると聞いて、今もなお畏敬の念を拭う事ができません。
退職理由は語ran新しいことを始めてみたいというのが理由でした。結果として、Webサービスをそれまでと同じように作っていたらできなかったであろう得難い経験をする事ができたので、今でも退職したことは後悔していません。ただ、もし今も在職していたら、それはそれで別の得難い経験ができていたのではないかと思わせる、そんな組織だったと思います。なお、今はどんな形でも会社に関わっていません。
みなさん、本当にありがとうございました。
なおやさん、テンプレをありがとうございました。