🏌️

char* を Go の string に変換するテクニック

2022/12/25に公開

はじめに

Go には cgo と呼ばれる、Go からC言語を扱う為の機能があります。go build を実行すると、内部でC言語のコンパイラが実行され、ソースの一部が Go に取り込まれます。

cgo とは

まずは以下を見て下さい。

//go:build ignore
// +build ignore

package main

/*
int add(int a, int b) {
  return a + b;
}
*/
import "C"

func main() {
	println(C.add(1, 2))
}

go-sqlite3 の様な、ライブラリのバインディングはこの機能を使って実現されています。

LoadDLL

これとは別に、Windows では Win32 API を使い、DLL をロードする事ができます。

func terminateProc(pid uint64) error {
	dll, err := syscall.LoadDLL("kernel32.dll")
	if err != nil {
		return err
	}
	defer dll.Release()

	f, err := dll.FindProc("SetConsoleCtrlHandler")
	if err != nil {
		return err
	}
	r1, _, err := f.Call(0, 1)
	if r1 == 0 {
		return err
	}
	f, err = dll.FindProc("GenerateConsoleCtrlEvent")
	if err != nil {
		return err
	}
	r1, _, err = f.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid))
	if r1 == 0 {
		return err
	}
	return nil
}

このプログラムは、kernel32.dll の SetConsoleCtrlHandler を呼び出し、自分のプロセスに対する CTRL-C や CTRL-Break イベントを無視しつつ、GenerateConsoleCtrlEvent によりプロセスグループに対して CTRL-Break イベントを送信し、子プロセスを終了します。

C言語を使わないのでコンパイルも速く、また移植性も高いです。

char*

さて例えば文字列を返す DLL から得た char* を Go の string に変換したいとします。これまでは、reflect.StringHeader を使うのが通例でした。例えば

type StringHeader struct {
	Data uintptr
	Len  int
}
package main

import (
	"reflect"
	"syscall"
	"unsafe"
)

func main() {
	proc := syscall.NewLazyDLL("msvcrt.dll").NewProc("getenv")
	ptr, _ := syscall.BytePtrFromString("OS")
	r1, _, _ := proc.Call(uintptr(unsafe.Pointer(ptr)))

	n := 0
	for ptr := *(**byte)(unsafe.Pointer(&r1)); *ptr != 0; n++ {
		ptr = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + 1))
	}

	s := *(*string)(unsafe.Pointer(&reflect.StringHeader{
		Data: r1,
		Len:  n,
	}))
	println(s)
}

途中にある for ループはバイト列の中から文字列の終端を探しています。

もっと昔はこんなコードを書いていました。(今はもう動きません)

b := *(*[65535]byte)(unsafe.Pointer(r1))
n := 0
for n = 0; n < 65535 && b[n] != 0; n++ {
}
println(string(b[:n]))

このイディオムが無くなったのは、string にした時に無駄な cap が残ってしまうからです。ゴミデータとは言え、string をコピーする際にゴミもコピーしてしまいます。

reflect.(Slice|String)Header は deprecated

さて、(ここ大事なんですが) Go 1.20 からこの reflect.SliceHeader と reflect.StringHeader が Deprecated になります。Go1 である間は使い続ける事ができますが将来的には廃止されます。困りましたね。どうやって DLL から返された char* から string を得たら良いのでしょうか。

これまで使えていたこのイディオムが使えません。ちなみに reflect.SliceHeader とreflect.StringHeader が deprecated になったのは、扱い方を間違える人が多く、それによって Go の安全性が低くなるからです。

unsafe.String

https://mattn.kaoriya.net/software/lang/go/20220907112622.htm

そこで登場するのが unsafe.String です。unsafe.String は byte 型のポインタと長さを受け取り string を作る為の物です。

それって reflect.StringHeader でいいじゃん?と思いますよね、実際やってる事は同じなのです。何が違うかというとコンパイル時にポインタの出所や、実行時の nil チェック等、考えられる誤用を沢山チェックしているのです。unsafe.String が入ったコミットを見て頂けると分かりますが、ほぼチェック処理です。

https://github.com/cuiweixie/go/commit/59e4ff889f3b59085650f8cf6c0f3ee1fd337dad

では実際に unsafe.String を使った例ですが byte ポインタと長さを渡すだけです。

package main

import (
	"syscall"
	"unsafe"
)

func main() {
	proc := syscall.NewLazyDLL("msvcrt.dll").NewProc("getenv")
	ptr, _ := syscall.BytePtrFromString("OS")
	r1, _, _ := proc.Call(uintptr(unsafe.Pointer(ptr)))

	n := 0
	head := *(**byte)(unsafe.Pointer(&r1))
	for ptr := head; *ptr != 0; n++ {
		ptr = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + 1))
	}

	println(unsafe.String(head, n))
}

簡単ですね。

おまけ

Go は DLL を作る事もできます。例えば以下の様な Go らしい処理を DLL にする事もできます。

package main

import (
	"C"
	"fmt"
)

var (
	c chan string
)

func init() {
	c = make(chan string)
	go func() {
		n := 1
		for {
			switch {
			case n%15 == 0:
				c <- "FizzBuzz"
			case n%3 == 0:
				c <- "Fizz"
			case n%5 == 0:
				c <- "Buzz"
			default:
				c <- fmt.Sprint(n)
			}
			n++
		}
	}()
}

//export fizzbuzz
func fizzbuzz(n int) *C.char {
	return C.CString(<-c)
}

func main() {
}

呼び出し側はこうなります。

package main

import (
	"fmt"
	"log"
	"syscall"
	"unsafe"
)

func bytePtrToString(p uintptr) string {
	n := 0
	head := *(**byte)(unsafe.Pointer(&p))
	for ptr := head; *ptr != 0; n++ {
		ptr = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + 1))
	}
	return unsafe.String(head, n)
}

func main() {
	fizzbuzz := syscall.NewLazyDLL("libfizzbuzz.dll").NewProc("fizzbuzz")
	free := syscall.NewLazyDLL("msvcrt.dll").NewProc("free")

	for i := 0; i < 100; i++ {
		if r0, r1, err := fizzbuzz.Call(); r1 == 0 {
			fmt.Println(bytePtrToString(r0))
			free.Call(r0)
		} else {
			log.Fatal(err)
		}
	}
}

※ C.CString で返されたポインタは呼び出し元が free しなければなりません。

実はこの DLL、Go から呼び出す事ができませんでした。実行モジュールの中に exe と DLL の2つのランタイムが共存してしまい、動作できない状態となって panic が起きるというバグがありました。

https://github.com/golang/go/issues/22192

ちょっと前にこのバグが修正されたので、晴れて Go で DLL を作り、Go で呼び出す事ができる様になったという訳です。

おわりに

Windows で Go で書かれた DLL を Go から呼び出す際の注意点をご紹介しました。もちろんC言語からも呼び出せますし、Vim からも呼び出せます。

function! s:test() abort
  let l:dll = 'c:/dev/go-sandbox/dll/libfizzbuzz.dll'
  for l:i in range(0, 100)
    echo libcall(l:dll, 'fizzbuzz', 0)
  endfor
endfunction

call s:test()

ぜひいろいろなプログラミング言語から呼び出して遊んでみて下さい。

Discussion