はじめに
Golangは良い言語ですね!
Windows にも Static Library が対応し
これからは Native が求められる時は
一部処理をGoで実装して、一緒にNativeにする方法に期待が高まります。
そんな中、実際に Go で作成した Static Libraryを使ってみると
以外な落とし穴に嵌ったので、忘れないようメモメモ。
GoString
今回の主役は タイトルにも書いております string です。
GoをStatic libraryにすると、headerも生成され
それをincludeして使うことになります。
そしてGoで扱うstringは以下のようになっています。
//GoStringの抜粋
typedef struct { const char *p; GoInt n; } GoString;
//exportした関数の宣言を抜粋。
extern void TestFunc_GO(GoString p0);
ポインタに明るい方なら、既に string の落とし穴が見えていますね。
以下サンプルを交えて見てみましょう。
サンプルコードが思ったよりも増えてしまったので要点を書いてしまうと
Goで受け取る string が大きすぎない場合は
コピーしてしまいましょうというのが本記事の内容です。
サンプルコード
ではサンプルを書いていきます。
今回は static library を go で書き、それを使用した shared library を作成し
別のプログラムで更に呼び出すという流れになります。
本記事では Windows でサンプルの確認をしているため dll前提で書いています。
Golang(Static Library)
package main
import (
"C"
"fmt"
)
var g_value string
//export Dump_GO
func Dump_GO() {
// g_valueの中身を出力。
fmt.Println(g_value)
}
//export TestFunc_GO
func TestFunc_GO(s string) {
g_value = s
}
func init() {}
func main() {}
Shared-Library
#ifndef SHARED_H__
#define SHARED_H__
#ifdef __cplusplus
extern "C" {
#endif
void TestFunc(char* s, int length);
void TestDump();
#ifdef __cplusplus
}
#endif
#include "shared.h"
#include "libexample.h"
#include <string>
// char* -> GoString
inline auto convert(const char* s, int length) {
return GoString{ s, static_cast <GoInt> (length) };
}
// string -> GoString
inline auto convert(const std::string& s) {
return GoString{ s.data(), static_cast <GoInt> (s.length()) };
}
void TestFunc(const char* s, int length) {
//Goで作成した関数。(パラメーターの s をGOで保存)
TestFunc_GO(convert(s, length));
}
void TestDump() {
//Goで作成した関数。(GOで保存している文字列を表示)
Dump_GO();
}
LIBRARY shared.dll
EXPORTS
TestFunc = TestFunc
TestDump = TestDump
Main
#include <string>
#include "shared.h"
int main() {
std::string s{"Hello World."};
//sをコピーし
TestFunc(s.data(), s.length());
//画面に表示。
TestDump();
//既にコピーしたので安心。なんとなく文字を上書き。
s[5] = '@';
//再度画面に呼び出し
TestDump();
}
Build
static library -> shared library -> executable の順にビルドしていきます。
# build to static library
go build -buildmode=c-archive -o example.go -o libexample.a
# build to shared library.
g++ -O2 -std=c++1z -o shared.dll -mdll shared.cpp -L./ -lexample shared.def -Wl,--out-implib=shared.lib -lwinmm -lws2_32 -lntdll
# build to executable.
g++ -O2 -std=c++1z main.cpp -L./ -lshared
実行してみると。
main
Hello World.
Hello@World.
想像通りの結果ですね。
文字列をコピーしたと思いきや、実はアドレスをコピーしています。
なのでコピーした側(Main.cpp)が値を変更すると、Goの側も一緒に変わっているのです。
↑の例ではアドレスは存在しているので値が変わるだけで良いのですが
ダングリング化すると目も当てられません。
これは知らないと大嵌りします。
なので、Parameterで受け取った string はコピーしてしまいましょう。
コピーし、Goの世界に値を閉じ込めてしまえば、あとはGCに任せて大丈夫です。
修正版
package main
import (
"C"
"fmt"
)
var g_value string
func convertGoScope(p *string) {
*p = string([]byte(*p))
}
//export Dump_GO
func Dump_GO() {
// g_valueの中身を出力。
fmt.Println(g_value)
}
//export TestFunc_GO
func TestFunc_GO(s string) {
//処理の冒頭で受け取ったパラメーター値は、Goの世界側に変えておく。
convertGoScope(&s)
g_value = s
}
func init() {}
func main() {}
実行
main
Hello World.
Hello World.
さいごに
Goでexportした関数の冒頭でstringをコピーしておくと
以降、受け取ったstringを安心して使いまわせるので不安がなくなります。
当然ですが、コピーするのは最小限にし、exportした関数でのみ実行して下さい。
参考記事
http://qiita.com/yanolab/items/1e0dd7fd27f19f697285
http://qiita.com/yugui/items/cc490d080e0297251090