Windows 上の go-gl で Cgo を不要にしようとしている話 - なぜ Syscall18 が生まれたか

# tl;dr

[go-gl](https://github.com/go-gl/gl) は Go の OpenGL バインディングです。 Windows などのデスクトップ環境に対応しています。実装は単純に C の関数を [Cgo](https://golang.org/cmd/cgo/) を使って呼ぶだけです。が、 Cgo には後述するような問題があり、現在のところ必要悪とみなされています。

ところで Windows では、 DLL からの関数ポインタ取得および `syscall.Syscall` などの関数を使うことで、一切 Cgo を使わずに C 関数を利用することができます。自分はこの修正を[提案](https://github.com/go-gl/gl/issues/109#issuecomment-433609120)し、無事アクセプトされました。現在 [PR](https://github.com/go-gl/glow/pull/102) がレビュー中です。

提案の詳細は [Design Doc](https://docs.google.com/document/d/1mqquznil9fR2amtb3eTC2ObCx3A8Af_5INqKjO-pKDg/edit?usp=sharing) にまとめています。本記事はこの提案のざっくりとした和訳および補足説明です。

# 背景

OpenGL は C の関数セットであるため、 Go と OpenGL のバインドに Cgo が使われています。しかしながら Cgo には以下のような問題があります:

1. Cgo は C コンパイラを必要とする。 [^c-compiler]
2. Cgo はコンパイルを遅くする。 [^slow]
3. Cgo はクロスコンパイルを困難にする。 [^cross-compile]

[^c-compiler]: 実際に筆者は、 Windows に C コンパイラを入れるのに苦労している人々を何人も見てきました ([例](https://github.com/hajimehoshi/ebiten/issues/689))。つらい。ちなみに筆者は [scoop](https://scoop.sh/) を使って入れてます。
[^slow]: 筆者の Windows 環境では、 Cgo を用いた go-gl コンパイルは数分を要しました。なお Cgo を使わないバージョンの go-gl のコンパイルは 1 秒未満です。
[^cross-compile]: MinGW をいれたりなど、いろいろな工夫をすれば Linux から Windows へのクロスコンパイルは可能。だが、 Pure Go であることに越したことはないでしょう。

これらは特に Windows ユーザーにとっては負担です。ほかの POSIX な環境とは異なり、 C コンパイラに馴染みがないかもしれないからです。幸運なことに、 Windows では `syscall.Syscall` 関数を使って C 関数を Cgo なしで呼ぶことができます。残念なことにこの手法は[他のプラットフォームでは困難](https://github.com/golang/go/issues/18296)です。

筆者は [glow](https://github.com/go-gl/glow) (バインディングのジェネレータ) を修正し、 Windows では `syscall.Syscall` を使って Cgo に依存しないようにする提案をしました。

# コードの変更

glow は 4 つのファイルを出力するので、説明も 4 パートに分けます。

## `conversions.go`

[プロトタイプ実装](https://github.com/hajimehoshi/glow/commit/09b7832586476b95dfc88886f7055cae107a17ad)

Go と C の値を相互変換する関数など。筆者の見る限り Cgo を一切使わなくても同等のことができるので、そのように修正します。

## `debug.go`

[プロトタイプ実装](https://github.com/hajimehoshi/glow/commit/5e0daf80b8369ded69bd05e8892ca55792e145e6)

Go の関数を C から呼び出す必要があるため、 [`syscall.NewCallback`](https://golang.org/pkg/syscall/?GOOS=windows#NewCallback) を使うという修正をしました。

というか現在そもそもこの関数まわり、[全く動いていない気がする](https://github.com/go-gl/gl/issues/116)んですよね。誰も使ってないのでは??

## `procaddr.go`

[プロトタイプ実装](https://github.com/hajimehoshi/glow/commit/82c14b3c3ee38abff6acb28a7101e6509e6c91f9)

関数名から関数ポインタを取得する実装は Cgo を使わずに [`syscall.NewLazyDLL`](https://golang.org/pkg/syscall/?GOOS=windows#NewLazyDLL) などを使えばいけるので、そのように修正します。

## `package.go`

[プロトタイプ実装](https://github.com/hajimehoshi/glow/commit/11618d0f342c1af1c6662e6a4b205e9c1d2311e1)

一番作業量が多い部分。基本的に `syscall.Syscall` を呼ぶように修正するだけなのですが、引数の数によって `syscall.Syscall6` だったり `syscall.Syscall9` だったり、呼ぶ関数が変わってしまいます。その修正を行います。

`syscall.Syscall` 系の関数は引数をすべて `uintptr` で受け取ります。整数型など、たいていそのままコンバートすればいいのですが、一部例外があります。 bool はそのための `if` 文が必要だったり、浮動小数点型は [`math.Float64bits`](https://golang.org/pkg/math/#Float64bits) なで変換してあげる必要があります。

# Proof of concept

実際提案だけだと絵に描いた餅なので実際に example を動かしてみました。詳細な手順は Design Docs を参照してください。

![screen.png](https://qiita-image-store.s3.amazonaws.com/0/1911/62bf09d9-7d47-7189-56c7-f718ab5868c7.png)

# 後方互換性

ほとんどの関数はそのまま動くことが期待されますが、現時点でどうしても動かない関数が 2 つあります:

* `func LGPUCopyImageSubDataNVX(sourceGpu uint32, destinationGpuMask uint32, srcName uint32, srcTarget uint32, srcLevel int32, srcX int32, srxY int32, srcZ int32, dstName uint32, dstTarget uint32, dstLevel int32, dstX int32, dstY int32, dstZ int32, width int32, height int32, depth int32)`
* `func MulticastCopyImageSubDataNV(srcGpu uint32, dstGpuMask uint32, srcName uint32, srcTarget uint32, srcLevel int32, srcX int32, srcY int32, srcZ int32, dstName uint32, dstTarget uint32, dstLevel int32, dstX int32, dstY int32, dstZ int32, srcWidth int32, srcHeight int32, srcDepth int32)`

`syscall.Syscall` 系の関数は引数の数ごとに関数名が異なりますが、最大が `syscall.Syscall15` であり、 15 個が最大値です。しかしながらこれらの関数は 16 個以上引数を取ります。よって `syscall.Syscall` の手法ではこれらの関数は呼べない、ということになります。

この手法の唯一の懸念点だったのですが、「まあこの関数、誰も使ってないでしょ」ということで、未実装状態で置いとくということで、一件落着しました。

ついでながら [`syscall.Syscall18` を提案し、実装してしまいました](https://github.com/golang/go/issues/28434)。 Go 1.12 から使えるようになります。よかったですね!


# 現在の状況

[PR](https://github.com/go-gl/glow/pull/102) を提出しましたが、現在レビュー中です。結構中の人が忙しいようです。早ければ年内にもマージされるでしょう。

あとは「これを入れるとしてメンテする人いるの?」という問題があって、「じゃあ自分がやります」と立候補したところ、 [go-gl のメンバーに入れていただきました](https://github.com/orgs/go-gl/people)。今後とも宜しくお願いします。

ちなみに拙作の [Ebiten](https://github.com/hajimehoshi/ebiten/) では修正が適用された go-gl を先行して独自に使用しています。よって少なくとも go-gl 部分については Cgo 依存がなくなりました。