ほげほげブログ

TKの日常が語られる、誰向けかさっぱりわからないブログです。

Goで楽しくDLL叩き

こんにちはこんにちは

どうも、いつもはラノベの感想ばかり書き連ねているtkmax777です。

今日は久しぶりにGoのTipsを雑に書きます。

Go言語スキーな皆さんなら、きっとWin32APIを叩いたり、その他機器との接続などでDLLを叩きたいはず。

そこで、今回はGoのDLL呼び出し周りについてまとめます。

もくじ

基本

目的のDLLの関数を呼び出すための関数を、次のように生成することができます。

1: DLL呼び出しスクリプト生成をしてくれる最強プログラムの取得

go get golang.org/x/sys/windows/mkwinsyscall

2: おまじないを書いたGoファイル(ここではwinapi.go)を用意

package main

// NULLが使いたくなったとき用
// 別に0を直で書いても問題ない
const NULL = 0

/*
以下、呪文。-output以降は適宜ファイル名を改める必要がある。
以後、詠唱するときは、//の後に空白を入れてはならない。
*/
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output winapi_generate.go winapi.go


/*
次のように
 //sys <任意の関数名>(<引数...>) (<戻り値>) = <DLL名>.<関数名>
を列挙する。
*/
//sys ShowCursor(state bool) (counter int) = user32.ShowCursor

3: 生成

go generate winapi.go

実際に生成されるものの例:

// Code generated by 'go generate'; DO NOT EDIT.

package main

import (
    "syscall"
    "unsafe"

    "golang.org/x/sys/windows"
)

var _ unsafe.Pointer

// Do the interface allocations only once for common
// Errno values.
const (
    errnoERROR_IO_PENDING = 997
)

var (
    errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
    errERROR_EINVAL     error = syscall.EINVAL
)

// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
    switch e {
    case 0:
        return errERROR_EINVAL
    case errnoERROR_IO_PENDING:
        return errERROR_IO_PENDING
    }
    // TODO: add more here, after collecting data on the common
    // error values see on Windows. (perhaps when running
    // all.bat?)
    return e
}

var (
    moduser32 = windows.NewLazySystemDLL("user32.dll")

    procShowCursor = moduser32.NewProc("ShowCursor")
)

func ShowCursor(state bool) (counter int) {
    var _p0 uint32
    if state {
        _p0 = 1
    }
    r0, _, _ := syscall.Syscall(procShowCursor.Addr(), 1, uintptr(_p0), 0, 0)
    counter = int(r0)
    return
}

4: 呼び出し

ハンドルを保持したりする場合はLockOSThreadを忘れずに。

func main() {
    // 今回は問題ないと思われるが、DLL呼び出しするときは付けておいたほうが無難
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    ShowCursor(false)
}

Easy!!

GetLastError

Win32APIなりで良く見かけるGetLastError関数。残念ながらGoの仕様上同様の手続きで呼び出しても正常に動作しません。

代わりに次のように詠唱すると、GoのError変数の形でエラーを受けとることができます。

//sys ClipCursor(rect uintptr)(ok int, err error) [failretval==0] = user32.ClipCursor

戻り値にerr errorを追加し、その後に[failretval==0]を追記しました。

[failretval==0] が戻り値の値からエラーが発生していると判断する条件で、==0の場合は省略することができます。

これにより、エラーの判定条件に合致している場合にGetLastErrorの値がerrに代入されます。幸せですね。

引数の型

ここからは、Cの型とGoの型の対応が非自明な例について、簡単に紹介します。

構造体

定義

次のように、Cで定義されているのと同様の順番で構造体を定義する必要があります。

例: RECT

・ C

typedef struct tagRECT {
  LONG left;
  LONG top;
  LONG right;
  LONG bottom;
} RECT, *PRECT, *NPRECT, *LPRECT;

・ Go

type RECT struct {
    Left   int32
    Top    int32
    Right  int32
    Bottom int32
}

詠唱

先程のように、uintptr型を指定します。

//sys ClipCursor(rect uintptr)(ok int, err error) = user32.ClipCursor

呼び出し

次のように、先頭要素の番地を渡します。

var rect = RECT{...}

ClipCursor(uintptr(unsafe.Pointer(&rect.Left))

unsafeと仲良くなりましょう!!!

配列やスライス

詠唱

プリミティブな型なら、自動生成くんを信じてそのまま書きましょう

//sys Function(chars []byte) = dllname.Function

構造体とかの配列の場合は、先頭要素の番地を指定します。

//sys wtsEnumerateSessionsEx(hServer uintptr, pLevel *uint32, Filter uint32, ppSessionInfo uintptr, pCount *uint32) (ok bool) = Wtsapi32.WTSEnumerateSessionsExW

呼び出し

var chars = make([]byte, 100)
Function(chars)

var sessionInfo = make([]wts_SESSION_INFO_1, 1000)
wtsEnumerateSessionsEx(uintptr(hServer), pLevel, Filter, uintptr(unsafe.Pointer(&sessionInfo[0])), &pCount)

Float

意外な落とし穴はfloat32やfloat64だったりします。

そのままやろうとすると、uintptrに変換されてしまい、正しく渡されません。

詠唱

次のようにuint32やuint64で詠唱します。

//sys Function(number32 uint32, number64 uint64) = dllname.Function

呼び出し

次のように、float型をuint型に変換します。

var Number32 float32
var Number64 float64

Function(math.Float32bits(Number32), math.Float64bits(Number64))

関数ポインタ

コールバック関数などを渡すときは、次のように唱えると良いです。

詠唱

//sys Function(callback uintptr) = dllname.Function

呼び出し

重要事項
コールバック関数には戻り値が必ず必要です。もしvoidを戻り値とするような関数が求められてもreturn 0をしてください!

func callback() uintptr { ... }

~~~

Function(syscall.NewCallback(callback))

文字列の入出力

結構厄介です。

char[]の書き込み

詠唱

*byteとして詠唱します。

//sys Function(c *byte) = dllname.Function

呼び出し

var c = "abc"
p, err := syscall.BytePtrFromString(c)
if err != nil {
   ...
}

Function(p)

char[]の読み込み

詠唱

*byteとして詠唱します。

//sys Function() (pByte *byte) = dllname.Function

呼び出し

func UTF8PtrToString(p *byte) string {
    if p == nil {
        return ""
    }

    var char byte
    var chars = []byte{}

    for i := 0; ; i++ {
        char = *(*byte)(unsafe.Pointer(unsafe.Add(unsafe.Pointer(p), unsafe.Sizeof(byte(0))*uintptr(i))))
        // null char
        if char == 0 {
            break
        }
        chars = append(chars, char)
    }

    return string(chars)
}

func main() {
    var pByte = new(byte)
    Function(pByte)

    UTF8PtrToString(pByte)
}

UTF-16の書き込み

詠唱

[]uint16として詠唱します。

//sys Function(c []uint16) = dllname.Function

呼び出し

var c = "abc"
arr, err := syscall.UTF16FromString(c)
if err != nil {
   ...
}

Function(arr)

UTF-16ポインタの書き込み

詠唱

*uint16として詠唱します。

//sys Function(c *uint16) = dllname.Function

呼び出し

var c = "abc"
arr, err := syscall.UTF16PtrFromString(c)
if err != nil {
   ...
}

Function(arr)

UTF-16の 読み込み

詠唱

[]uint16として詠唱します。

//sys Function() (c []uint16) = dllname.Function

呼び出し

var c = Function(arr)
var cstring = syscall.UTF16ToString(c)

UTF-16ポインタの読み込み

詠唱

*uint16として詠唱します。

//sys Function(c *uint16) = dllname.Function

呼び出し

func UTF16PtrToString(p *uint16) string {
    if p == nil {
        return ""
    }

    var char uint16
    var chars = []uint16{}

    for i := 0; ; i++ {
        char = *(*uint16)(unsafe.Pointer(unsafe.Add(unsafe.Pointer(p), unsafe.Sizeof(uint16(0))*uintptr(i))))
        chars = append(chars, char)
        // null char
        if char == 0 {
            break
        }
    }
    return syscall.UTF16ToString(chars)
}

func main() {
    var pByte = new(byte)
    Function(pByte)

    UTF16PtrToString(pByte)
}

まとめ

これだけわかれば、大概困らないのではなかろうかという気がします。

これで今日から君も、Goマスターだ!!!!