Edited at

Golang で Static Library を作る際、stringをparameterで受け取るならコピーしよう。

More than 1 year has passed since last update.


はじめに

Golangは良い言語ですね!

Windows にも Static Library が対応し

これからは Native が求められる時は

一部処理をGoで実装して、一緒にNativeにする方法に期待が高まります。

そんな中、実際に Go で作成した Static Libraryを使ってみると

以外な落とし穴に嵌ったので、忘れないようメモメモ。


GoString

今回の主役は タイトルにも書いております string です。

GoをStatic libraryにすると、headerも生成され

それをincludeして使うことになります。

そしてGoで扱うstringは以下のようになっています。


example.h

//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)


example.go

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


shared.h

#ifndef SHARED_H__

#define SHARED_H__

#ifdef __cplusplus
extern "C" {
#endif

void TestFunc(char* s, int length);
void TestDump();

#ifdef __cplusplus
}
#endif



shared.cpp

#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();
}



shared.def

LIBRARY shared.dll

EXPORTS
TestFunc = TestFunc
TestDump = TestDump


Main


main.cpp

#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に任せて大丈夫です。


修正版


example_fix.go

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