GoでCocoa APIを使う、もしくは他のイベントループをGoに混ぜる方法

Goの cgo という機能を使うと、GoからCの世界のコードを呼んだり、呼ばれたりすることができる。 Perlで言うところのXSというやつだ。

このcgoを使ったGoプログラムは、昔はMakefileを利用してビルドしていたそうだが、 1.0からは go getgo 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の難易度が一番高くなってしまう。

とっつきやすく出来てるだけに、もったいないポイントだ。今後に期待。