Edited at
GoDay 18

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


tl;dr

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

ところで Windows では、 DLL からの関数ポインタ取得および syscall.Syscall などの関数を使うことで、一切 Cgo を使わずに C 関数を利用することができます。自分はこの修正を提案し、無事アクセプトされました。現在 PR がレビュー中です。

提案の詳細は Design Doc にまとめています。本記事はこの提案のざっくりとした和訳および補足説明です。


背景

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


  1. Cgo は C コンパイラを必要とする。 1

  2. Cgo はコンパイルを遅くする。 2

  3. Cgo はクロスコンパイルを困難にする。 3

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

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


コードの変更

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


conversions.go

プロトタイプ実装

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


debug.go

プロトタイプ実装

Go の関数を C から呼び出す必要があるため、 syscall.NewCallback を使うという修正をしました。

というか現在そもそもこの関数まわり、全く動いていない気がするんですよね。誰も使ってないのでは??


procaddr.go

プロトタイプ実装

関数名から関数ポインタを取得する実装は Cgo を使わずに syscall.NewLazyDLL などを使えばいけるので、そのように修正します。


package.go

プロトタイプ実装

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

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


Proof of concept

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


後方互換性

ほとんどの関数はそのまま動くことが期待されますが、現時点でどうしても動かない関数が 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 を提案し、実装してしまいました。 Go 1.12 から使えるようになります。よかったですね!


現在の状況

PR を提出しましたが、現在レビュー中です。結構中の人が忙しいようです。早ければ年内にもマージされるでしょう。

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

ちなみに拙作の Ebiten では修正が適用された go-gl を先行して独自に使用しています。よって少なくとも go-gl 部分については Cgo 依存がなくなりました。





  1. 実際に筆者は、 Windows に C コンパイラを入れるのに苦労している人々を何人も見てきました ()。つらい。ちなみに筆者は scoop を使って入れてます。 



  2. 筆者の Windows 環境では、 Cgo を用いた go-gl コンパイルは数分を要しました。なお Cgo を使わないバージョンの go-gl のコンパイルは 1 秒未満です。 



  3. MinGW をいれたりなど、いろいろな工夫をすれば Linux から Windows へのクロスコンパイルは可能。だが、 Pure Go であることに越したことはないでしょう。