char* を Go の string に変換するテクニック
はじめに
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
そこで登場するのが unsafe.String です。unsafe.String は byte 型のポインタと長さを受け取り string を作る為の物です。
それって reflect.StringHeader でいいじゃん?と思いますよね、実際やってる事は同じなのです。何が違うかというとコンパイル時にポインタの出所や、実行時の nil チェック等、考えられる誤用を沢山チェックしているのです。unsafe.String が入ったコミットを見て頂けると分かりますが、ほぼチェック処理です。
では実際に 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 が起きるというバグがありました。
ちょっと前にこのバグが修正されたので、晴れて 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