|
| 1 | +# これは何? |
| 2 | + |
| 3 | +このサンプルプログラムは、cgoを利用してGo言語とC言語の間でデータを連携させる方法、特にGo 1.17で導入された`unsafe.Slice`とGo 1.20で導入された`unsafe.SliceData`を活用した効率的なメモリアクセス方法を具体的に示すものです。 |
| 4 | + |
| 5 | +Cの関数からGoの関数を呼び出し、Go側で受け取ったデータを処理して、再びCの関数に処理結果を返す、という一連の流れを実装しています。 |
| 6 | + |
| 7 | +## 処理の流れ |
| 8 | + |
| 9 | +このプログラムは、以下の順序で処理が実行されます。 |
| 10 | + |
| 11 | +1. **`main.go:main()`** |
| 12 | + * プログラムのエントリポイント。C言語側で定義された`c_func()`を呼び出します。 |
| 13 | + |
| 14 | +2. **`c.go:c_func()`** |
| 15 | + * スタック上に文字列 `"helloworld"` を確保します。 |
| 16 | + * Go側でエクスポートされている`go_func()`を、文字列のポインタとサイズを引数にして呼び出します。 |
| 17 | + |
| 18 | +3. **`export.go:go_func()`** |
| 19 | + * Cから渡されたポインタ (`*C.char`) とサイズ (`C.size_t`) を `unsafe.Slice` を使ってGoのスライス (`[]byte`) に変換します。この操作はメモリコピーを発生させず、Cのメモリ領域を直接参照します。 |
| 20 | + * 安全のため、Cのメモリを直接変更するのではなく、Goの管理するメモリにデータをコピーします。 |
| 21 | + * コピーしたデータに対して文字列の反転処理を行います。 |
| 22 | + * 処理後のGoスライスを `unsafe.SliceData` を使ってCで扱えるポインタ形式に変換し、C側の`c_func2()`を呼び出します。 |
| 23 | + |
| 24 | +4. **`c.go:c_func2()`** |
| 25 | + * Goから渡されたデータを受け取り、標準出力に表示します。 |
| 26 | + |
| 27 | +## 技術詳細 |
| 28 | + |
| 29 | +### CからGoへのデータ受け渡し: `unsafe.Slice` |
| 30 | + |
| 31 | +Goの関数 (`go_func`) がCからデータを受け取る際、`unsafe.Slice` を利用してパフォーマンスを向上させています。 |
| 32 | + |
| 33 | +```go |
| 34 | +//export go_func |
| 35 | +func go_func(s *C.char, n C.size_t) { |
| 36 | + var ( |
| 37 | + sPtr = unsafe.Pointer(s) |
| 38 | + cSlice = unsafe.Slice((*byte)(sPtr), n) // cSliceはC側のスタック変数を指している |
| 39 | + ) |
| 40 | + fmt.Printf("[Go] %s", cSlice) |
| 41 | + // ... |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +- `*C.char` を `unsafe.Pointer` を経由してGoの `*byte` に変換します。 |
| 46 | +- `unsafe.Slice` は、このポインタとデータ長 `n` を基に、Goのスライスヘッダを生成します。 |
| 47 | +- この結果得られる `cSlice` は、C言語側のメモリ領域を直接指し示すスライスとなり、**余計なメモリコピーが発生しません(ゼロコピー)**。 |
| 48 | + |
| 49 | +**【重要】注意点:** |
| 50 | +`cSlice`が参照しているのはCのスタックメモリです。Goのガベージコレクタの管理外であり、関数を抜けると無効になる可能性があります。Go側でこのデータを永続化したり変更したりする場合は、必ずGoが管理するメモリに`copy()`で複製してから操作する必要があります。 |
| 51 | + |
| 52 | +### GoからCへのデータ受け渡し: `unsafe.SliceData` |
| 53 | + |
| 54 | +Goで処理したデータをCの関数に渡す際には、`unsafe.SliceData` を利用します。 |
| 55 | + |
| 56 | +```go |
| 57 | +// ... |
| 58 | + // C側の関数に渡すための準備 |
| 59 | + var ( |
| 60 | + bytePtr = unsafe.SliceData(goSlice) // *byteに変換し |
| 61 | + charPtr = (*C.char)(unsafe.Pointer(bytePtr)) // そこから (char *) に変換 |
| 62 | + charLen = C.size_t(len(goSlice)) // サイズはスライスからそのまま取得 |
| 63 | + ) |
| 64 | + C.c_func2(charPtr, charLen) |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +- `unsafe.SliceData` は、Goのスライスの先頭要素へのポインタ (`*byte`) を返します。 |
| 69 | +- このポインタを `unsafe.Pointer` を経由してCの `*C.char` 型にキャストすることで、Cの関数に渡せるようになります。 |
| 70 | +- これにより、Goのメモリ領域をC側から直接読み取ることが可能になります。 |
| 71 | + |
| 72 | +### C言語とGo言語間の文字列の扱い |
| 73 | + |
| 74 | +Cの文字列は通常NULL文字で終端されます。`unsafe.Slice`でGoスライスに変換した場合、このNULL文字もスライスの一部として含まれることがあります。 |
| 75 | +このサンプルでは、文字列を反転させる前に、NULL文字を考慮して実際のデータ長を計算しています。 |
| 76 | + |
| 77 | +```go |
| 78 | +// NULL終端文字がある場合は減算して実データサイズとする |
| 79 | +if dataLen > 0 && cSlice[dataLen-1] == 0 { |
| 80 | + dataLen-- |
| 81 | +} |
| 82 | + |
| 83 | +// 実データ分をコピー |
| 84 | +goSlice = make([]byte, dataLen) |
| 85 | +copy(goSlice, cSlice[:dataLen]) |
| 86 | +``` |
| 87 | + |
| 88 | +逆に、GoからCへデータを渡す際は、C側がNULL終端文字列を期待していることを想定し、処理後のスライスにNULL文字を追加しています。 |
| 89 | + |
| 90 | +## 実行方法 |
| 91 | + |
| 92 | +プロジェクトのルートにある`Taskfile.yml`に実行コマンドが定義されています。以下のコマンドでサンプルを実行できます。 |
| 93 | + |
| 94 | +```sh |
| 95 | +go run *.go |
| 96 | +``` |
| 97 | + |
| 98 | +### 実行結果 |
| 99 | + |
| 100 | +``` |
| 101 | +[Go] helloworld |
| 102 | +[C ] dlrowolleh |
| 103 | +``` |
0 commit comments