Shogo's Blog

たぶんプログラミングとかについて書いていくブログ

PerlからGolangを呼び出す

GoのコードをPerlから呼び出せるようにするgo2xsを書いてみました。

使い方

Perlから使いたい関数に以下のようにgo2xsで始まるコメントを付けておきます。

hoge
1
2
3
4
5
6
package main

//go2xs hello
func hello(str string) string {
  return "Hello " + str
}

go2xsをgo getして、xsのグルーコードを作成。 その後通常のPerlモジュールと同じ手順でコンパイルします。 Go 1.5から入ったShared Libraryの機能を使っているのでGo 1.5が必要です。

1
2
3
4
go get https://github.com/shogo82148/go2xs/cli/go2xs
go2xs -name hoge hoge.go
perl Makefile.PL
make

あとは普通に呼び出すだけ。

1
2
perl -Mblib -Mhoge -e 'print hoge::hello("World")'
Hello World

制限事項

今はまだ、整数・浮動小数点型・文字列しか扱えません。

あとGoのShared Libraryを複数回読み込むことができないっぽい? (ref. https://github.com/golang/go/issues/11100 ) ので、go2xsを使ったコードを二つ以上useすると死にます。

FFI::Rawを使う方法

go2xsはGoをShared Libraryとしてコンパイルしているだけなので、go2xsを使わなくても頑張れば呼び出すことができます。 Golang で Shared Library を出力する。で紹介されているこちらのコードで試してみます。

libgofib.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
  "C"
  "log"
)

//export fib
func fib(n int) int {
  if (n < 2) { return n }
  return fib(n - 2) + fib(n - 1)
}

func init() {
  log.Println("Loaded!!")
}

func main() {
}

ビルドしてShared Libraryを作ってみます。

1
build -buildmode=c-shared -o libgofib.so libgofib.go

PerlからShared Libraryを呼び出すにはFFI::Rawを使うのがお手軽のようです。

test.pl
1
2
3
4
5
6
7
8
9
use FFI::Raw;

my $fib = FFI::Raw->new(
    'libfib.so', 'fib',
    FFI::Raw::int, # 戻り値
  FFI::Raw::int, # 引数
);

print $format->call(32);

文字列の受け渡しをしてみる

FFI::Rawを使った方法はお手軽ですが、文字列の受け渡しをしようとすると色々と面倒です。

素朴に実装してみる

GolangのstringはPerlでそのまま扱えないので、C.GoStringC.CStringを使い一度C言語の文字列形式を経由してから相互変換する必要があります。

libgofmt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "C"

import (
  f "go/format"
)

//export format
func format(src *C.char) *C.char {
  gosrc := C.GoString(src)
  dst, _ := f.Source([]byte(gosrc))
  return C.CString(string(dst))
}

func main() {
}
test.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use FFI::Raw;

my $format = FFI::Raw->new(
    'libgofmt.so', 'format',
  FFI::Raw::str, # 戻り値
  FFI::Raw::str,  # 引数1
);

print $format->call(<<"EOF");
package main
import   "fmt"
func main(   )   {
fmt.Println("hogehoge") }
EOF

なんとなく動いてよさ気な感じがしますが、 実はこのコード、メモリーリークしてます。 C.CStringで作ったC言語の文字列はGolangの管理から外れるのでGCで回収されません。 そのため、どこかでfreeを実行して開放する必要があります。

strcpyで頑張る

Golang側でバッファを確保するとPerlに戻った時にバッファ開放ができないので、 Perl側で結果を格納するバッファを確保してみます。

libgofmt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

/*
#include <stdlib.h>
#include <string.h>
*/
import "C"

import (
  f "go/format"
  "unsafe"
)

//export format
func format(dst *C.char, src *C.char) {
  gosrc := C.GoString(src)
  godst, _ := f.Source([]byte(gosrc))
  cs := C.CString(string(godst))
  defer C.free(unsafe.Pointer(cs))
  C.strcpy(dst, cs)
}

func main() {
}
test.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use FFI::Raw;

my $format = FFI::Raw->new(
  'libgofmt.so', 'format',
  FFI::Raw::void, # 戻り値
  FFI::Raw::str,
  FFI::Raw::ptr,
);

while(1) {
  my $hoge;
  my $coderef = sub { $hoge = shift };
  my $callback = FFI::Raw::callback($coderef, FFI::Raw::void, FFI::Raw::str);
  $format->call(<<"EOF", $callback);
package main
import   "fmt"
func main(   )   {
fmt.Println("hogehoge") }
EOF
}

この方式なら作ったShared Libraryを他の言語からも呼びやすいので無難かも? ただし、まだまだ実装が不十分で、このコードはバッファオーバーランの危険があります。 しっかり実装するなら、最初の一回でバッファのサイズだけ計算、次の呼び出しで結果取得・・・のようなフローを踏む必要があります。

コールバック

結果の保存をコールバック関数の呼び出しで行えば、Golang側でfreeを実行するタイミングが分かるので、メモリーリークを防ぐことができます。 ただし、Goからは関数ポインタを呼び出すことはできないので、gcoでグルーコードを書いてあげる必要があります。

libgofmt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

/*
#include <stdlib.h>
typedef void (*callbackFunc) (const char*);

void bridge_callback(callbackFunc f, const char* str);
*/
import "C"

import (
  f "go/format"
  "unsafe"
  )

//export format
func format(src *C.char, dstCallback unsafe.Pointer) {
  gosrc := C.GoString(src)
  godst, _ := f.Source([]byte(gosrc))
  cs := C.CString(string(godst))
  defer C.free(unsafe.Pointer(cs))
  C.bridge_callback(C.callbackFunc(dstCallback), cs)
}

func main() {
}
libgofmt.c
1
2
3
4
5
typedef void (*callbackFunc) (const char*);

void bridge_callback(callbackFunc f, const char* str) {
    f(str);
}
test.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use FFI::Raw;

my $format = FFI::Raw->new(
    'libgofmt.so', 'format',
    FFI::Raw::void, # 戻り値
  FFI::Raw::str, FFI::Raw::ptr,
);

while(1) {
    my $hoge;
  my $coderef = sub { $hoge = shift };
  my $callback = FFI::Raw::callback($coderef, FFI::Raw::void, FFI::Raw::str);
  $format->call(<<"EOF", $callback);
package main
import   "fmt"
func main(   )   {
fmt.Println("hogehoge") }
EOF
}

まとめ

GoのShared Libraryの呼び出しは数値型だけを相手にしていれば比較的簡単ですが、文字列を扱おうとすると少し面倒です。 go2xsを使うとそこら辺が簡単になります。 あんましライブラリ作ったことないですが、文字列の受け渡しをするときには、一般的にはどんな感じのインターフェースにするべきなんですかね?

GoのShared Libraryまだまだ出たばかりで、複数回読み込めなかったりと問題はありますが、 Goの機能を他の言語から呼び出せるのは便利ですね。 今後に期待です。

Comments