GoでCocoa APIを使う、もしくは他のイベントループをGoに混ぜる方法
Goの cgo という機能を使うと、GoからCの世界のコードを呼んだり、呼ばれたりすることができる。 Perlで言うところのXSというやつだ。
このcgoを使ったGoプログラムは、昔はMakefileを利用してビルドしていたそうだが、
1.0からは go get
や go build
と言った、go toolがcgoに対応したのでそういうことも必要なくなっている。
cgoの基本的な使い方
まずは
import "C"
として、Cという疑似パッケージをimportする。
このパッケージを使うと、 C.fprintf(...)
というような感じでCの世界にアクセスできるようになる。
また、このimport文の直前のコメントはCのコードとして解釈される。
なので、
package main
/*
#include <stdio.h>
void hello() {
fprintf(stderr, "Hello cgo World\n");
}
*/
import "C"
func main() {
C.hello()
}
というコードは期待通り動く。 この、コメントを利用してるっていうところがだいぶキモイ感じ。
また、これだとエディタのサポートをうけられないので、
package main
//#include "hello.h"
import "C"
func main() {
C.hello()
}
みたいにして、実装は hello.[c|h]
に分けるようにすると良いだろう。
go toolはGoソース上に import "C" を見つけると、同じディレクトリ上の C/C++ ソースも自動的にコンパイルしてくれる。
Objective-C を使うには?
cgoは、 .m ファイルをコンパイルしてくれないので、 .c として保存して、 CFLAGS に -x objective-c
を渡して言語を指定するようにして無理矢理使う。
CFLAGSなんかを設定するには、また例によってコメント中に #cgo
ディレクティブを埋め込むことによって設定する。
こんな感じ
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void hello() {
NSLog(@"Hello objective cgo world!");
}
*/
import "C"
func main() {
C.hello()
}
Cocoaのイベントループ(NSRunLoop)どうするの?
cgoからObjective-Cが使えるだけだと、CocoaのAPIをフルに使うことはできない。
CocoaにはNSRunLoopというイベントループがあり、こいつが回っていないと、コールバックなどのイベントが起きないし、 非同期IO系の機能はまったく動作しない。
また、このイベントループはスレッドセーフではなく、スレッド用のイベントループが作成されるようになっている。 したがって、Goからこのイベントループを回すには、それが実行されるスレッドを固定する必要がある。
Goでスレッドを固定するには、runtimeパッケージで提供される LockOSThread
という関数を使う良い。
この関数が呼ばれると、呼びだしたgoroutineを現在のスレッドに固定し、また他のgoroutineがそのスレッドを使わないようにしてくれる。
ただ、どのスレッドに固定するかは選択することができない。
ググると、メインスレッドに固定する方法(というかハック)がgo-wikiに乗っていた。
LockOSThread - go-wiki - How to works with libraries that must be called from the main OS thread?
というわけで、
package eventloop
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void Run() {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
*/
import "C"
import (
"runtime"
)
var (
mainfunc = make(chan func())
stop = make(chan bool)
)
// https://code.google.com/p/go-wiki/wiki/LockOSThread
func init() {
runtime.LockOSThread()
}
func Run() {
for {
select {
case f := <-mainfunc:
f()
case <-stop:
return
default:
C.Run()
}
}
}
func Stop() {
stop <- true
}
func Do(f func()) {
done := make(chan bool, 1)
mainfunc <- func() {
f()
done <- true
}
<-done
}
こんなのを github.com/typester/go-cocoa-eventloop として作った。
動作としては、main.mainで eventloop.Run()
を呼び出すと、イベントループがメインスレッドで周りだす。
スレッドをロックしているので、他のgoroutineがメインスレッドで動けなくなってしまうので、それを、
eventloop.Do(func () {
// ここに処理
})
みたいにして処理を挟めるようになっている。
Objective-Cプログラマとしては、
dispatch_sync(dispatch_get_main_queue(), ^{
// ここに処理
});
と同じようなものだと思えば分かりやすいかもしれない。
サンプル
github.com/typester/go-bonjour
Bonjour をGoから使おうというライブラリ。
package main
import (
"github.com/typester/go-bonjour"
"github.com/typester/go-cocoa-eventloop"
)
func main() {
service := bonjour.NewService("local.", "_ssh._tcp.", "GoTest", 22)
service.Publish()
eventloop.Run()
}
で、publish。
package main
import (
"github.com/typester/go-bonjour"
"github.com/typester/go-cocoa-eventloop"
"fmt"
)
func main() {
browser := bonjour.NewBrowser()
browser.Search("_ssh._tcp", "local.")
go func() {
for event := range browser.Event {
switch e := event.(type) {
case *bonjour.FindServiceEvent:
fmt.Println("found service: ", e.Service.Name())
}
}
}()
eventloop.Run()
}
で、discover。みたいな感じ。
現状まだ機能が不足していてあまり実用的ではないので、ボチボチアップデートしていきたい。
cgoの感想
Perlとnode.jsのこういうネイティブ拡張は作った経験があるが、とっつきやすさとしてはcgoがダントツでやさしいと思う。 ただ、こみいったことをやろうとすると、cgoはまだまだ機能が足りないかな〜という印象。
いちばん不満なのは、CからGoのちゃんとしたGC対象のオブジェクトを作れないという点。
それが出来ないためにGo側のプログラムが無駄に複雑になる。なので逆にそれが欲しくなるような場面では、cgoの難易度が一番高くなってしまう。
とっつきやすく出来てるだけに、もったいないポイントだ。今後に期待。