Skip to content

Commit 3f73864

Browse files
authored
Merge pull request #951 from devlights/add-unsafe-example
Add unsafe example
2 parents 61ae027 + 3689594 commit 3f73864

File tree

8 files changed

+240
-0
lines changed

8 files changed

+240
-0
lines changed

examples/basic/unsafes/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
| unsafe_stringdata.go | unsafe_stringdata | unsafe.StringData() のサンプルです. |
1010
| unsafe_pointer_cast.go | unsafe_pointer_cast | unsafeパッケージを用いてポインタを任意の型にキャストするサンプルです |
1111
| unsafe_add.go | usnafe_add | unsafe.Add関数を利用してポインタ演算するサンプルです |
12+
| unsafe_slice.go | usnafe_slice | unsafe.SliceData() と unsafe.Slice() のサンプルです |

examples/basic/unsafes/examples.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ func (r *register) Regist(m mapping.ExampleMapping) {
1818
m["unsafe_stringdata"] = UnsafeStringData
1919
m["unsafe_pointer_cast"] = PointerCast
2020
m["unsafe_add"] = Add
21+
m["unsafe_slice"] = Slice
2122
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package unsafes
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"unsafe"
7+
)
8+
9+
// Slice は、unsafe.SliceData() と unsafe.Slice() のサンプルです。
10+
//
11+
// unsafe.SliceData() は、特定のスライスを *T に変換する関数。
12+
// 逆を行ってくれるのが unsafe.Slice() となる。
13+
//
14+
// REFERENCES:
15+
// - https://pkg.go.dev/unsafe@go1.25.3#SliceData
16+
func Slice() error {
17+
var (
18+
original = []byte("helloworld")
19+
result []byte
20+
ptr *byte
21+
)
22+
ptr = unsafe.SliceData(original) // []byteを*byteに変換
23+
result = unsafe.Slice(ptr, len(original)) // *byteを[]byteに変換
24+
25+
fmt.Printf("b1 equals b2: %v\n", bytes.Equal(original, result))
26+
27+
return nil
28+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# https://taskfile.dev
2+
3+
version: '3'
4+
5+
tasks:
6+
default:
7+
cmds:
8+
- go run *.go
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
/*
4+
#include <stdio.h>
5+
#include <string.h>
6+
7+
extern void go_func(const char *s, size_t n);
8+
9+
void c_func() {
10+
char s[] = "helloworld";
11+
size_t s_size = sizeof(s);
12+
go_func(s, s_size);
13+
}
14+
15+
void c_func2(const char *s, size_t n) {
16+
char buf[n];
17+
{
18+
memcpy(buf, s, n);
19+
buf[n-1] = '\0';
20+
}
21+
22+
printf("[C ] %s\n", buf);
23+
}
24+
*/
25+
import "C"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package main
2+
3+
/*
4+
extern void c_func2(const char *s, size_t n);
5+
*/
6+
import "C"
7+
import (
8+
"fmt"
9+
"unsafe"
10+
)
11+
12+
//export go_func
13+
func go_func(s *C.char, n C.size_t) {
14+
var (
15+
sPtr = unsafe.Pointer(s)
16+
cSlice = unsafe.Slice((*byte)(sPtr), n) // cSliceはC側のスタック変数を指している
17+
)
18+
fmt.Printf("[Go] %s\n", cSlice)
19+
20+
// 何らかの変換を行う(例としてデータをリバース)
21+
//
22+
// 注意点として、cSliceはC側のスタック変数をそのまま指しているため
23+
// これを直接変更すると、C側のスタックメモリを書き換えてしまうことになる。
24+
// 必ず、コピーを取ってから変更処理は行うこと。
25+
//
26+
// また、C.GoBytes(), C.CString() を利用せずに直接C側のデータを扱っているので
27+
// cSliceの中は最後に終端文字が入った状態となっている。
28+
// この状態でそのままスライスをリバースすると \0 が先頭に来ることになるので除去してから処理する。
29+
var (
30+
goSlice []byte // Go側で扱うスライス
31+
dataLen = int(n) // 実データのサイズ
32+
)
33+
// NULL終端文字がある場合は減算して実データサイズとする
34+
if dataLen > 0 && cSlice[dataLen-1] == 0 {
35+
dataLen--
36+
}
37+
38+
// 実データ分をコピー
39+
goSlice = make([]byte, dataLen)
40+
copy(goSlice, cSlice[:dataLen])
41+
42+
// リバース
43+
for i, j := 0, len(goSlice)-1; i < j; i, j = i+1, j-1 {
44+
goSlice[i], goSlice[j] = goSlice[j], goSlice[i]
45+
}
46+
47+
// 終端追加
48+
goSlice = append(goSlice, 0)
49+
50+
// C側の関数に渡すための準備
51+
var (
52+
bytePtr = unsafe.SliceData(goSlice) // *byteに変換し
53+
charPtr = (*C.char)(unsafe.Pointer(bytePtr)) // そこから (char *) に変換
54+
charLen = C.size_t(len(goSlice)) // サイズはスライスからそのまま取得 ([]byteの場合はこれでOK)
55+
)
56+
C.c_func2(charPtr, charLen)
57+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
/*
4+
extern void c_func();
5+
*/
6+
import "C"
7+
8+
func main() {
9+
C.c_func()
10+
}
11+
12+
/*
13+
$ task
14+
task: [default] go run *.go
15+
[Go] helloworld
16+
[C ] dlrowolleh
17+
*/

0 commit comments

Comments
 (0)