環境
- go: 1.12 linux/amd64
問題の概要
C では関数の引数として構造体を渡す場合, 通常ポインタで渡す. この構造体のメンバーの中にポインタ変数が含まれている場合に cgo でこの関数を何も考えずに直接呼び出すと “Go pointer to Go pointer” エラーが発生する.
具体的な例を挙げてみる. 例えば下記のような C のコードがあったとして, cgo から printMyStructs() 関数を呼び出したいとする.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef struct mystruct {
int i;
int* pi;
} mystruct;
/* cgo から呼び出したい関数 */
void printMyStructs(int num, mystruct* strctList) {
int j;
for(j = 0; j < num; j++) {
printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi));
}
}
このとき, GO 側のコードは何も考えずに書くと例えば下記のようになるのだが...
length := 10
strctArray := make([]C.mystruct, length)
// building strctArray
pi_ := C.int(1000)
strctArray[0].i = C.int(10)
strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
....
C.printMyStructs(C.int(length), (*C.mystruct)(unsafe.Pointer(&strctArray[0])))
これをコンパイルすると, 末尾の C.printMyStructs() を呼び出す箇所で下記のような Go pointer to Go pointer エラーとなる.
$ go run cstructarray.go
panic: runtime error: cgo argument has Go pointer to Go pointer
goroutine 1 [running]:
main.main.func1(0x1, 0xc000074010, 0x1, 0x1)
/home/endoh/go/src/test/cstructarray.go:31 +0x52
main.main()
/home/endoh/go/src/test/cstructarray.go:31 +0xa3
exit status 2
構造体単体で渡す場合ならばポインタ渡しを諦めて実体渡しができるようなラッパー関数を C 側で用意するという手もあるが, 構造体の配列を渡す場合はそうもいかない.
解決方法の一例
C 側にヘルパー関数を 2 つ用意して, Go からはこれらを使いつつ最終的に目的の C 関数を呼び出すという手順を取った.
C 側の準備
C 側には次の 2 つの関数を準備する.
(1) 構造体のメモリ確保を行ってポインタを返す関数
mystruct* allocMystruct(int num) {
return malloc(num * sizeof(mystruct));
}
(2) 構造体配列の任意の要素に引数から渡された構造体をコピーする関数
/* offset が配列の何個目かを表す */
void convertMystructData(mystruct* strctArray, int offset, mystruct src) {
memcpy(strctArray + offset, &src, sizeof(mystruct));
}
Go 側の手順
Go 側では次のような手順を踏む.
- make で C.mystruct の配列を作成
- 1 で作成した配列の各要素に値を入れる
- C の「構造体のメモリ確保を行ってポインタを返す関数」を呼び出す
- C の「構造体配列の任意の要素に引数から渡された構造体をコピーする関数」を上記 2 と 3 で得た変数を引数として呼び出す
3 で得たポインタは C ポインタなので構造体のメンバーに Go ポインタが含まれていても Go pointer to Go pointer エラーは発生しない. やっていることは実体渡しと同様, 構造体配列をせっせとコピーしているので全くもってスマートとは言えないが, とりあえずこれで動作する.
- go側のコード
length := 10
strctArray := make([]C.mystruct, length) // これは GO pointer
// building strctArray
pi_ := C.int(1000)
strctArray[0].i = C.int(10)
strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
...
cStrctArray := C.allocMystruct(C.int(length)) // こっちは C pointer
defer C.free(unsafe.Pointer(cStrctArray))
for i, strct := range strctArray {
C.convertMystructData(cStrctArray, C.int(i), strct)
}
C.printMyStructs(C.int(length), cStrctArray)
実行結果は下記の通り. 問題なく動いている模様.
$ go run cstructarray.go
0: i=10, pi=1000
(以下略)
#コード全体
コード全体も載せておきます.
package main
//#include <stdlib.h>
//#include <stdio.h>
//#include <string.h>
//
//typedef struct mystruct {
// int i;
// int* pi;
//} mystruct;
//
//void printMyStructs(int num, mystruct* strctList) {
// int j;
// for(j = 0; j < num; j++) {
// printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi));
// }
//}
//
//mystruct* allocMystruct(int num) {
// return malloc(num * sizeof(mystruct));
//}
//
//void convertMystructData(mystruct* strctArray, int offset, mystruct src) {
// memcpy(strctArray + offset, &src, sizeof(mystruct));
//}
//
import "C"
import (
"unsafe"
)
func main() {
length := 1
strctArray := make([]C.mystruct, length)
pi_ := C.int(1000)
strctArray[0].i = C.int(10)
strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
cStrctArray := C.allocMystruct(C.int(length))
defer C.free(unsafe.Pointer(cStrctArray))
for i, strct := range strctArray {
C.convertMystructData(cStrctArray, C.int(i), strct)
}
C.printMyStructs(C.int(length), cStrctArray)
}
感想
cgo つらい