いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-635 (cgoでunsafe.Slice, unsafe.SliceData関数を使ってCの世界とデータをやり取り)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。前回の続きで unsafe.Sliceunsafe.SliceData の使い方について。

unsafeパッケージって、Goの安全性の枠から外れる操作をするパッケージなので、基本的に使わないほうが良いものですが、知ってると便利なパッケージでもあります。

cgoを使う場合はほぼ必須です。特に unsafe.Pointer はめっちゃ使います。

unsafe.Pointer は、C言語でいう (void *) と同じものとなるので、これを利用することで任意の型にキャストできます。

unsafe.Pointer については、以前の記事でサンプル作ったので、そちらご参照ください。

今回は、*byte[]byteの相互変換について。主に低レベルなAPI操作やcgoなどで利用したりします。

unsafe.SliceData は、特定のスライス []T*T に変換してくれます。これはGoのヒープメモリを指すポインタとなります。

unsafe.Slice は、逆を行うものです。つまり、*T[]T にしてくれる関数。ポインタとサイズを渡すとスライスにして返してくれます。 こちらは元のデータが指すポインタをそのまま指した状態で返してくれます。つまり、C側のポインタを渡してスライスにした場合はC側のスタックメモリをそのまま指している状態となります。

元のデータのポインタを直接指しているので、C側から受け取った char * なんかを unsafe.Slice を使ってスライスにして、それを変更などするとC側のスタックメモリを直接変更することになります。非常に危険です。データを見る分には問題ありません。

サンプル

ちょっと複数のファイルに跨ったサンプルとなっているのですが、以下のようなことをしています。

  1. Goのmain関数からcgo経由でCの関数を呼び出し (main.go)
  2. Cの関数からcgoでエクスポートしているGoの関数を呼び出し (c.go)
  3. Goの関数でC側から受け取ったデータを変更して、更にCの関数に渡す (export.go)
  4. 最後に呼ばれるCの関数で変更された値を表示 (c.go)

Go -> C -> Go -> C とやり取りしている感じです。

サンプル自体は、以下にアップしてありまして、そこにあるREADME.mdに詳しく書いていますので、よかったらご参照ください。

try-golang/examples/singleapp/unsafe_slice_with_cgo at main · devlights/try-golang · GitHub

main.go

package main

/*
extern void c_func();
*/
import "C"

func main() {
    C.c_func()
}

c.go

package main

/*
#include <stdio.h>
#include <string.h>

extern void go_func(const char *s, size_t n);

void c_func() {
   char s[] = "helloworld";
   size_t s_size = sizeof(s);
   go_func(s, s_size);
}

void c_func2(const char *s, size_t n) {
   char buf[n];
   {
       memcpy(buf, s, n);
       buf[n-1] = '\0';
   }

   printf("[C ] %s\n", buf);
}
*/
import "C"

export.go

package main

/*
extern void c_func2(const char *s, size_t n);
*/
import "C"
import (
    "fmt"
    "unsafe"
)

//export go_func
func go_func(s *C.char, n C.size_t) {
    var (
        sPtr   = unsafe.Pointer(s)
        cSlice = unsafe.Slice((*byte)(sPtr), n) // cSliceはC側のスタック変数を指している
    )
    fmt.Printf("[Go] %s\n", cSlice)

    // 何らかの変換を行う(例としてデータをリバース)
    //
    // 注意点として、cSliceはC側のスタック変数をそのまま指しているため
    // これを直接変更すると、C側のスタックメモリを書き換えてしまうことになる。
    // 必ず、コピーを取ってから変更処理は行うこと。
    //
    // また、C.GoBytes(), C.CString() を利用せずに直接C側のデータを扱っているので
    // cSliceの中は最後に終端文字が入った状態となっている。
    // この状態でそのままスライスをリバースすると \0 が先頭に来ることになるので除去してから処理する。
    var (
        goSlice []byte   // Go側で扱うスライス
        dataLen = int(n) // 実データのサイズ
    )
    // NULL終端文字がある場合は減算して実データサイズとする
    if dataLen > 0 && cSlice[dataLen-1] == 0 {
        dataLen--
    }

    // 実データ分をコピー
    goSlice = make([]byte, dataLen)
    copy(goSlice, cSlice[:dataLen])

    // リバース
    for i, j := 0, len(goSlice)-1; i < j; i, j = i+1, j-1 {
        goSlice[i], goSlice[j] = goSlice[j], goSlice[i]
    }

    // 終端追加
    goSlice = append(goSlice, 0)

    // C側の関数に渡すための準備
    var (
        bytePtr = unsafe.SliceData(goSlice)          // *byteに変換し
        charPtr = (*C.char)(unsafe.Pointer(bytePtr)) // そこから (char *) に変換
        charLen = C.size_t(len(goSlice))             // サイズはスライスからそのまま取得 ([]byteの場合はこれでOK)
    )
    C.c_func2(charPtr, charLen)
}

実行

   $ task
    task: [default] go run *.go
    [Go] helloworld
    [C ] dlrowolleh

参考情報

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。