C と Go 言語 (Mac OS X)

2012-12-14 (鈴)

1. はじめに

C 言語のコードを Go 言語から呼び出す方法は cgo - The Go Programming Language [golang.org] で説明されている。 しかし,それだけでは実際のプログラミングに十分ではない。 本稿は Mac OS X 10.6.8 (x86) 上の go1.0.3.darwin-386 (Downloads - go [google.com] 参照) で C と Go 言語の組み合わせについて試行したメモである。

しかしながら今,Go 言語のこの部分のつくりについてはクルージ (kluge) だという印象を強く受けています。 プログラム本体の動作がコメントの内容に依存する前例が過去,コンパイラのオプション指定等になかったわけではありませんが,Go 言語の他の部分の整い方と比べるといかにも間に合わせに見えます。 本稿の記述は最新の Mountain Lion を含む他のバージョンの Mac OS X にも通用するはずですが,もし近い将来の Go 言語に対しては通用しないとしても不思議ではない気がします……。

2. C からのコールバック

C 言語の標準ライブラリ関数 qsort を Go から呼び出すにはどうしたらよいだろうか。 もちろん,void qsort(void* base, size_t nel, size_t width, int(*compar)(const void*, const void*))compar 引数には Go 言語で書いた関数を渡したいとする。

残念ながら C 言語から呼び出せる関数のアドレスとして Go の関数を直接表現する方法はない。 しかし,Go の関数を C 言語へ export したときは C 言語のなかからそのアドレスを取ることができる。 したがって,そのための補助的な C 言語関数を定義すればよい。 下記に実装例 qsort.go とその実行例を示す。

package main

/*
 #include <stdlib.h>
 typedef int (*compare_t)(const void* a, const void* b);
 int compareInts(int*, int*);

 static void qsortInts(int* data, int len) {
 qsort(data, len, sizeof(int), (compare_t)compareInts);
 }
*/
import "C"

import . "fmt"

//export compareInts
func compareInts(a, b *C.int) C.int {
    return *a - *b
}

func main() {
    data := [...]C.int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9}
    C.qsortInts(&data[0], C.int(len(data)))
    Println(data)
}
01:~/tmp$ go run qsort.go
[1 1 2 3 3 4 5 5 5 6 7 8 9 9 9]
01:~/tmp$  

import "C" の直前のコメントは C 言語とのインタフェースのために特別扱いされる。 この例では Go 言語の関数 compareInts の C 言語の関数としてのプロトタイプ宣言をし,それを qsortInts 関数から参照することで関数のアドレスを取っている。 Go 言語からは C 言語としての関数のアドレスが取れないから qsort 関数に渡す引数値を用意できない。 そこでかわりに qsortInts 関数を (C.qsortInts として) 呼び出し,それに引数値を用意させている。

関数 compareInts の定義では直前の //export compareInts のコメントによってこの関数を C 言語に export している。

// と export の間にスペースを入れてはいけません。
本来ならば Go 言語から C に export された関数については,Go 言語の中で C の関数としてのアドレスを取得する手段があるべきです。 そうできれば中間の qsortInts 関数をわざわざ設けずに済みます。

今のところ C と Go の int 型が実質的に同じだから,compareInts は次のようにも書ける。

//export compareInts
func compareInts(a, b *int) int {
    return *a - *b
}

同じ理由で配列 dataint の配列として宣言できる。 ただし,そのときは import "unsafe" をして得られる特別な unsafe.Pointer 型へのキャストをなかだちにして配列の先頭アドレスを *C.int 型へ陽にキャストする必要がある。

    data := [...]int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9}
    C.qsortInts((*C.int)(unsafe.Pointer(&data[0])), C.int(len(data)))

実行例を示す。

01:~/tmp$ go run qsort2.go
[1 1 2 3 3 4 5 5 5 6 7 8 9 9 9]
01:~/tmp$  

3. main 関数を主スレッドに

いくつかの C 言語ライブラリは主スレッドで実行する必要がある。 GLUT (OpenGL Utility Toolkit) はその典型的な例である。 しかし,Go の main 関数は必ずしも主スレッドで実行されない。 もしもそこから GLUT の glutMainLoop 関数を呼び出すと,その関数の中でプログラムがフリーズしてしまう。

その解決方法が LockOSThread - go-wiki [google.com] で示されている。 すなわち,init 関数で runtime.LockOSThread() を実行すれば,main 関数を主スレッドに固定させることができる。 init 関数は Go の言語仕様で特別に扱われ,もしあればパッケージの初期化のために先立って実行される。

下記に実装例 glut1.go とその実行例を示す。

package main

/*
 #cgo darwin CFLAGS: -I /System/Library/Frameworks/GLUT.framework/Headers
 #cgo darwin LDFLAGS: -framework GLUT -framework OpenGL

 #include <glut.h>
 void display(void);

 static void foo(void) {
 glutDisplayFunc(display);
 }
*/
import "C"

import . "fmt"
import "runtime"

// Arrange that main.main runs on main thread.
// See http://code.google.com/p/go-wiki/wiki/LockOSThread
func init() {
    runtime.LockOSThread()
}

//export display
func display() {
    Println("hi")
}

func main() {
    var argc C.int = 0
    C.glutInit(&argc, nil)
    C.glutCreateWindow(C.CString("ho"))
    C.foo()
    C.glutMainLoop()
}

試しにコードから init 関数と import "runtime" を外して実行してみてください。 白抜きのウィンドウのかたちだけが表示され,レインボーがくるくる回ります。

他の注意点として

  1. Mac OS X で GLUT や OpenGL とリンクするには -framework オプションを使う。
      #cgo darwin LDFLAGS: -framework GLUT -framework OpenGL
    
  2. Go 言語の文字列から C 言語の文字列を得るには C.CString 関数を使う。 戻り値として得られた C 言語の文字列は Go 言語の自動的なメモリ管理の範囲外にあるから,陽に C.free (または C 言語の free) 関数で解放する必要がある。 下記ではその解放を怠っているが,プロセスの寿命中に1回どおり実行するだけだから特に問題はない。
        C.glutCreateWindow(C.CString("ho"))
    

4. 簡単なフライト・シミュレータ

より本格的な応用を考えよう。 OpenGL GLUT にデモとして付属する簡単なフライト・シミュレータ skyfly (§5 参照) を Go から制御するとしたらどうなるだろうか?

実装例を skyfly.go に示す。 これは skyfly のソース・ファイルのうち main 関数を含む gm_main.c に相当する処理 (の今生きている部分) を Go 言語に置き換えたものである。 残りのプログラムは共有ライブラリ libskyfly.so にまとめ,skyfly.go から -L. -lskyfly としてリンクする。

以下,その要点を説明する。

  1. 共有ライブラリにまとめたプログラムから参照される go_main.c 内の関数が二つある。 その代替として下記の go_bridge.c を用意し,共有ライブラリに含めた。
    int (*XgetbuttonPtr)(int button);
    int (*XgetvaluatorPtr)(int val);
    
    
    int Xgetbutton(int button) {
        return (*XgetbuttonPtr)(button);
    }
    
    int Xgetvaluator(int val) {
        return (*XgetvaluatorPtr)(val);
    }
    
    skyfly.go では Go 言語による実装関数を export し,その関数のアドレスを上記の変数に次のように代入する。
     XgetbuttonPtr = myXgetbutton;
     XgetvaluatorPtr = myXgetvaluator;
    
  2. コンパイル時に評価される定数式に C から import したマクロ定数を使うことができる。
    var buttons [C.BUTCOUNT]bool
    
  3. C 言語と組み合わせたとき,整数型が要求される場所で整数式の論理値を反転させたい場合がよくあるが,Go は整数型に対して論理否定演算 ! を用意しない。 このとき,もしも整数値として必ず 01 をとるならば ^ 1 として 1 と排他的論理和をとればよい。
    //export keyboard
    func keyboard(c uint8, x, y int) {
        switch c {
        case 0x1B:
            os.Exit(0)
        case 'r':
            buttons[C.RKEY] = true
        case ' ':
            buttons[C.SPACEKEY] = true
        case 'f':
            C.set_fog(C.fog ^ 1)
        case 'd':
            C.set_dither(C.dither ^ 1)
        }
    }
    
  4. 現在のマシンでは skyfly は高速に動きすぎる。 ここではカウンタを設けて速度を調整した。
    var count = 0
    var gameWait = 500
    
    //export gameLogic
    func gameLogic() {
        count++
        if count > gameWait {
            count = 0
            C.sim_singlechannel()
            draw()
        }
    }
    
  5. 速度調整の量をコマンド行オプションで -w 100 などと設定できるようにした。 また -f オプションでフルスクリーン描画になるようにした。
    func main() {
        fullScreen := flag.Bool("f", false, "full screen")
        waiting := flag.Int("w", gameWait, "waiting")
        flag.Parse()
        gameWait = *waiting
    

下記にコンパイル例と実行例を示す。

01:~/tmp/skyfly-with-go$ make clobber
rm -f *.o
rm -f skyfly *.so *~
01:~/tmp/skyfly-with-go$ make
cc -I. -c go_bridge.c database.c fly.c image.c perfdraw.c skyfly.c
libtool -dynamic -o libskyfly.so \
	  go_bridge.o database.o fly.o image.o perfdraw.o skyfly.o \
	  -framework GLUT -framework OpenGL -lc
go build skyfly.go
01:~/tmp/skyfly-with-go$ ./skyfly
 

5. 謝辞

§4 で使った C 言語プログラム skyflyGLUT - The OpenGL Utility Toolkit [opengl.org] で配布されている Mark J. Kilgard 氏の GLUT 3.7 のソースに glut-3.7/progs/demos/skyfly/ として含まれているものです。 同梱されている glut-3.7/NOTICE の全文を下記に示します。

NOTICE:  The OpenGL Utility Toolkit (GLUT) distribution contains source
code published in a book titled "Programming OpenGL for the X Window
System" (ISBN: 0-201-48359-9) published by Addison-Wesley.  The
programs and associated files contained in the distribution were
developed by Mark J. Kilgard and are Copyright 1994, 1995, 1996 by Mark
J. Kilgard (unless otherwise noted).  The programs are not in the
public domain, but they are freely distributable without licensing
fees.  These programs are provided without guarantee or warrantee
expressed or implied.

Copyright (c) 2012 OKI Software Co., Ltd.