Go言語 -buildmode=c-sharedで出来るWindows DLLを試してみた

# 概要

いつの間にか`-buildmode=c-shared`でWindowsのDLLが作成できるようになっていたのでサンプルを作って試してみた。

# 環境

- Windows 10
- go 1.12.5

# サンプル


DLLのソース

```go:goshared.go
package main

import "C"

//export afunc
func afunc() int {
	return 1234
}

func main(){

}
```

`import "C"`は必須で公開したい関数を`//export 関数名`で指定します。

DLLをビルドします。

```
go build -buildmode=c-shared  -o goshared.dll goshared.go
```

このとき`goshared.dll`と`goshared.h`が作成されます。`goshared.h`で`afunc`がexportされています。以下は抜粋です。

```c:goshared.h
#ifdef __cplusplus
extern "C" {
#endif


extern GoInt afunc();

#ifdef __cplusplus
}
#endif
```

テスト用のEXEソース

```go:main.go
package main

import (
	"fmt"
	"syscall"
)

var (
	dll  = syscall.NewLazyDLL("goshared.dll")
	proc = dll.NewProc("afunc")
)

func main() {
	ret, _, _ := proc.Call()
	fmt.Println(ret)
}
```

実行

```
go run main.go
1234

```

## DllMainについて

WindowsのDLLは`DllMain`があればDLLをロードしたときにDllMainを呼び出します。 DllMainをexportとしてみます

```go:goshared.go
package main

import "C"

var num = 12345

//export afunc
func afunc() int {
	return num
}

//export DllMain
func DllMain(arg1,arg2,arg3,arg4 uintptr) int{
	num = 9876
}

func main(){

}
```

これをビルドして実行したところexeはフリーズした状態になりました。`CTRL+C`でも終了させられなかったのでタスクマネジャーで強制終了させました。

ネットで調べたら https://github.com/NaniteFactory/dllmain を見つけました。

ここの`dllmain.c`の３１行目に次ようなコメントがありました。 
`// CreateThread() because otherwise DllMain() is highly likely to deadlock.`
なのでこのソースでは別スレッドにしてGoの関数を呼び出すようにしています。
このサイトのソースをダウンロードして試してみました。

```go:goshared.go
package main
import "C"

import "unsafe"

var num int = 1234

//export afunc
func afunc() int {
	return num
}

//export OnProcessAttach
func OnProcessAttach(
	hinstDLL unsafe.Pointer, // handle to DLL module
	fdwReason uint32, // reason for calling function
	lpReserved unsafe.Pointer, // reserved
) {
	num = 456
}

func main(){

}
```

ダウンロードした`dllmain.go`は次のようになっていますが

```go:dllmain.go
package main

//#include "dllmain.h"
import "C"
```

ビルドは出来ましたが、自分の環境では `//#include "dllmain.c"`　でないと`OnProcessAttach`が実行していないようでした。

ビルドします。

```
go build -buildmode=c-shared  -o goshared.dll goshared.go  dllmain.go
```

EXEの実行

```
go run main.go
1234
```

`OnProcessAttach`が最初に実行されていれば`456`が表示されているはずですが、`DllMain`から別スレッドを作成してから呼び出されているので実行が遅れていると思われます。

これを確かめるため、`main.go`を次のようにします。

```go:main.go
package main

import (
	"fmt"
	"syscall"
)

var (
	dll  = syscall.NewLazyDLL("goshared.dll")
	proc = dll.NewProc("afunc")
)

func main() {
	ret, _, _ := proc.Call()
	fmt.Println(ret)
	var input string
	fmt.Print("enter> ")
	fmt.Scanf("%s", &input)
	ret, _, _ = proc.Call()
	fmt.Println(ret)
}

```

実行します。

```
1234
enter>
456
```
やはり、実行が遅れています。また、`num`は別スレッドで変更しているので、本来はロックが必要でしょう。


### init関数

```go:goshared.go
package main
import "C"

var num int = 1234

//export afunc
func afunc() int {
	return num
}

func init() {
	num = 9876
}

func main(){

}
```

実行します。

```
go run main.go
1234
enter>
9876
```

`init`関数も`DllMain`の実験と同じように遅れるようです。


## Go言語のDLLからDLLのLoad

自分の環境では`afunc`から`syscall.LoadDLL`を実行したら`panic`が起きました。解決策は不明のままです。解決できた方は投稿をお願いします。

```
fatal error: unexpected signal during runtime execution
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x62ecdad8]

runtime stack:
runtime.throw(0x62f042a9, 0x2a)
        C:/Go/src/runtime/panic.go:617 +0x79
runtime.sigpanic()
        C:/Go/src/runtime/signal_windows.go:227 +0x272
runtime.memmove(0x0, 0xc000069b90, 0x1)
        C:/Go/src/runtime/memmove_amd64.s:146 +0x108
runtime.heapBitsSetType(0xc000069b90, 0x30, 0x30, 0x62ef9620)
        C:/Go/src/runtime/mbitmap.go:1400 +0x4ae
runtime.mallocgc(0x30, 0x62ef9620, 0x1, 0x7ff9bdde83d0)
        C:/Go/src/runtime/malloc.go:969 +0x55b
runtime.newdefer.func2()
        C:/Go/src/runtime/panic.go:242 +0xaa
runtime.systemstack(0x6fff10)
        C:/Go/src/runtime/asm_amd64.s:351 +0x6b
runtime.mstart()
        C:/Go/src/runtime/proc.go:1153

goroutine 1 [running, locked to thread]:
runtime.systemstack_switch()
        C:/Go/src/runtime/asm_amd64.s:311 fp=0xc00004daa8 sp=0xc00004daa0 pc=0x62eca6f0
runtime.newdefer(0x0, 0x0)
        C:/Go/src/runtime/panic.go:240 +0x183 fp=0xc00004db18 sp=0xc00004daa8 pc=0x62ea6b53
runtime.deferproc(0x0, 0x62f05380)
        C:/Go/src/runtime/panic.go:107 +0x45 fp=0xc00004db48 sp=0xc00004db18 pc=0x62ea6605
syscall.loadlibrary(0xc0000720e0, 0x0, 0x0)
        C:/Go/src/runtime/syscall_windows.go:145 +0x7f fp=0xc00004db78 sp=0xc00004db48 pc=0x62ebe34f
syscall.LoadDLL(0x62efff55, 0xc, 0xc000046000, 0xc000070000, 0xc00004dcd0)
        C:/Go/src/syscall/dll_windows.go:82 +0x3b6 fp=0xc00004dc30 sp=0xc00004db78 pc=0x62ed3d56
main.afunc(0x0)
        C:/ 内緒    /goshared.go:18 +0x4a fp=0xc00004dc88 sp=0xc00004dc30 pc=0x62eda36a
main._cgoexpwrap_14c397c31a8d_afunc(0xc000074010)
        _cgo_gotypes.go:45 +0x29 fp=0xc00004dca0 sp=0xc00004dc88 pc=0x62eda2f9
runtime.call32(0x0, 0x6ffcb0, 0x6ffe28, 0x8)
        C:/Go/src/runtime/asm_amd64.s:519 +0x42 fp=0xc00004dcd0 sp=0xc00004dca0 pc=0x62ecaab2
runtime.cgocallbackg1(0x0)
        C:/Go/src/runtime/cgocall.go:314 +0x185 fp=0xc00004dd48 sp=0xc00004dcd0 pc=0x62e83e15
runtime.cgocallbackg(0x0)
        C:/Go/src/runtime/cgocall.go:191 +0xda fp=0xc00004ddb8 sp=0xc00004dd48 pc=0x62e83bda
runtime: unexpected return pc for runtime.cgocallback_gofunc called from 0x44b3f7
```

# まとめ

簡単にWindows DLLができるようになったのは良かったですが`DllMain`,`init`は使わないで初期化関数を定義して最初に呼び出したほうが良さそうです。
また、`syscall.LoadDLL`で`panic`が起きることも含め、このようなGo言語DLLを使うことはちょっと不安になります。



# Appendix

## dumpbin /exports goshared.dll の抜粋
 
19番目に`afunc`が載っています。最初の方に出るように先頭を`a`にしました。また48番目に`init`,54番目に`num`があります。全部で1595個ありました。多すぎる。

```text
    ordinal hint RVA      name

          1    0 00058980 _cgo_get_context_function = ___w64_mingwthr_remove_key_dtor
          2    1 0005BB28 _cgo_init
          3    2 00058820 _cgo_is_runtime_initialized
          4    3 00058760 _cgo_maybe_run_preinit
          5    4 0005BB30 _cgo_notify_runtime_init_done
          6    5 00052350 _cgo_panic
          7    6 00058700 _cgo_preinit_init
          8    7 000586D0 _cgo_release_context
          9    8 0005BB38 _cgo_sys_thread_create
         10    9 00058A70 _cgo_sys_thread_start
         11    A 0005BB40 _cgo_thread_start
         12    B 0004C2D0 _cgo_topofstack
         13    C 00058850 _cgo_wait_runtime_init_done
         14    D 000D20D0 _cgo_yield
         15    E 00058520 _cgoexp_a74ddd773b6e_afunc
         16    F 0004DAE0 _rt0_amd64_windows_lib
         17   10 0005BB08 _rt0_amd64_windows_lib.ptr
         18   11 0004DB40 _rt0_amd64_windows_lib_go
         19   12 00058690 afunc
         20   13 000523A0 crosscall2
         21   14 00058AC0 crosscall_amd64
         22   15 000013C0 go.buildid
         23   16 0008D860 go.itab.*syscall.DLLError,error
         24   17 0008D880 go.itab.runtime.errorString,error
         25   18 0008D8A0 go.itab.syscall.Errno,error
         26   19 000028A0 internal/bytealg.IndexByteString
         27   1A 000EEB90 internal/bytealg.MaxLen
         28   1B 00002430 internal/bytealg.init
         29   1C 00002400 internal/bytealg.init.0
         30   1D 000EEB20 internal/bytealg.initdone·
         31   1E 000EF000 internal/cpu.ARM64
         32   1F 0005B038 internal/cpu.CacheLineSize
         33   20 00001440 internal/cpu.Initialize
         34   21 000EEF60 internal/cpu.X86
         35   22 00001F90 internal/cpu.cpuid
         36   23 00001B80 internal/cpu.doinit
         37   24 00001B40 internal/cpu.indexByte
         38   25 000D5660 internal/cpu.options
         39   26 000014A0 internal/cpu.processOptions
         40   27 0008E220 internal/cpu.statictmp_0
         41   28 00001FB0 internal/cpu.xgetbv
         42   29 000524D0 internal/syscall/windows/sysdll.Add
         43   2A 000D5020 internal/syscall/windows/sysdll.IsSystemDLL
         44   2B 000525C0 internal/syscall/windows/sysdll.init
         45   2C 00052560 internal/syscall/windows/sysdll.init.ializers
         46   2D 000EEB21 internal/syscall/windows/sysdll.initdone·
         47   2E 00058580 main._cgoexpwrap_a74ddd773b6e_afunc
         48   2F 00058620 main.init
         49   30 00058590 main.init.0
         50   31 000585B0 main.init.1
         51   32 000EEB22 main.initdone·
         52   33 0005BC50 main.keepalive_modinfo
         53   34 000585A0 main.main
         54   35 0005B040 main.num
```