[インデックス 10282] ファイルの概要
このコミットは、Go言語のランタイムにおけるWindowsコールバック機能のテストを追加するものです。主に、misc/cgo/test
に存在するCgoコールバックテストをWindows環境向けに移植し、GoランタイムがWindows APIからのコールバックを適切に処理できることを検証しています。
コミット
commit b776b9e724f3edbe4f52d0c1b8dd3ee532a897a3
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Nov 8 16:53:31 2011 +1100
runtime: add windows callback tests
Just a copy of cgo callback tests from misc/cgo/test.
R=rsc
CC=golang-dev, hectorchu
https://golang.org/cl/5331062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b776b9e724f3edbe4f52d0c1b8dd3ee532a897a3
元コミット内容
このコミットの目的は、GoランタイムにWindowsコールバックのテストを追加することです。これは、既存のmisc/cgo/test
にあるCgoコールバックテストをWindows環境に合わせた形でコピーしたものです。
変更の背景
Go言語はクロスプラットフォーム対応を目指しており、Windows環境においてもOSのネイティブAPI(Win32 APIなど)との連携が重要です。特に、Windows APIには、アプリケーションが特定のイベント発生時や列挙処理中に呼び出される「コールバック関数」を登録する仕組みが多数存在します。GoプログラムがこれらのAPIを介してWindowsシステムと円滑に連携するためには、Goの関数がWindowsからのコールバックとして正しく機能することが不可欠です。
このコミット以前にもCgoを介したコールバックのテストは存在していましたが、それは一般的なC言語との連携を想定したものでした。Windows固有のコールバックメカニズム(特にsyscall.NewCallback
で生成されるコールバック)がGoランタイム内でどのように振る舞うか、特にガベージコレクション(GC)やパニック発生時、あるいはOSスレッドのロック状態といったGoランタイムの内部動作との相互作用を検証する必要がありました。
このテストの追加は、GoプログラムがWindows上でより堅牢かつ予測可能な形で動作することを保証し、開発者がWindowsネイティブ機能を利用したアプリケーションを安心して構築できるようにするための重要なステップです。
前提知識の解説
1. Go言語のランタイム (Runtime)
Go言語のランタイムは、Goプログラムの実行を管理するシステムです。これには、スケジューラ(ゴルーチンの管理)、ガベージコレクタ(メモリ管理)、システムコールインターフェースなどが含まれます。Goプログラムは、OSのネイティブスレッド上で動作しますが、GoランタイムがゴルーチンをこれらのOSスレッドにマッピングし、効率的な並行処理を実現します。
2. Cgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、Goは既存のCライブラリやOSのネイティブAPI(Windows APIなど)と連携できます。Cgoを使用すると、GoのコードとCのコードの間でデータを受け渡し、関数を呼び出すことができます。
3. Windows APIとコールバック関数
Windows API(Application Programming Interface)は、Windowsオペレーティングシステムが提供する機能を利用するための関数の集合です。多くのWindows API関数は、特定のイベントが発生したときにシステムが呼び出す「コールバック関数」をアプリケーションが登録することを要求します。例えば、EnumWindows
関数は、システム上のすべてのトップレベルウィンドウを列挙し、見つかった各ウィンドウに対してアプリケーションが提供するコールバック関数を呼び出します。
コールバック関数は、通常、C言語の関数ポインタとして渡されます。GoプログラムがWindows APIにGoの関数をコールバックとして登録する場合、GoランタイムはGoの関数をC言語から呼び出し可能な形式に変換する必要があります。Goのsyscall
パッケージのNewCallback
関数は、この変換を行い、Goの関数をWindows APIが期待する形式の関数ポインタ(uintptr
)として提供します。
4. runtime.LockOSThread()
と runtime.UnlockOSThread()
Goのゴルーチンは、GoランタイムによってOSスレッドにスケジューリングされます。通常、どのゴルーチンがどのOSスレッドで実行されるかはGoランタイムが自由に決定します。しかし、特定のOS API(例えば、COMオブジェクトの初期化や特定のGUI操作など)は、そのAPIを呼び出したスレッドと同じスレッドで後続の操作を行うことを要求する場合があります。このような場合、Goのruntime.LockOSThread()
関数を使用すると、現在のゴルーチンを特定のOSスレッドに「ロック」し、そのゴルーチンが常に同じOSスレッドで実行されるように保証できます。runtime.UnlockOSThread()
はそのロックを解除します。
5. パニック (Panic) とリカバリ (Recover)
Goのパニックは、プログラムの異常終了を示すメカニズムです。通常、回復不可能なエラーが発生した場合にパニックが起こります。defer
文とrecover()
関数を組み合わせることで、パニックを捕捉し、プログラムのクラッシュを防ぎ、回復処理を行うことができます。コールバック関数内でパニックが発生した場合、Goランタイムがそのパニックを適切に処理し、コールバックの呼び出し元(この場合はWindows API)に影響を与えないようにすることが重要です。
技術的詳細
このコミットで追加されたテストは、GoランタイムがWindowsコールバックを処理する際の様々なシナリオを網羅しています。
syscall.NewCallback
の利用: Goの関数をWindows APIが呼び出し可能な形式に変換するために、syscall.NewCallback
が使用されます。この関数は、Goの関数をラップし、C言語の呼び出し規約に準拠したエントリポイントを提供します。EnumWindows
APIの利用: テストでは、WindowsのEnumWindows
APIが利用されています。このAPIは、システム上のすべてのトップレベルウィンドウを列挙し、各ウィンドウに対して指定されたコールバック関数を呼び出します。これにより、Goのコールバック関数が実際にWindowsシステムから呼び出されることをシミュレートします。- ネストされたコールバック呼び出し (
nestedCall
):nestedCall
関数は、Windows APIを呼び出し、そこからGoのコールバック関数が呼び出され、さらにそのGoのコールバック関数が別のGoの関数f
を呼び出すという、ネストされた呼び出しパスをシミュレートします。これは、Goランタイムがコールバックチェーンを正しく処理できるかを検証します。 - ガベージコレクション (
TestCallbackGC
): コールバック関数内でruntime.GC()
を呼び出すテストが含まれています。これは、コールバック実行中にガベージコレクションがトリガーされた場合に、ランタイムが安定して動作するかを確認します。 - パニック処理 (
TestCallbackPanic
,TestCallbackPanicLoop
,TestCallbackPanicLocked
):TestCallbackPanic
: コールバック関数内でパニックが発生した場合に、Goランタイムがそのパニックを適切に捕捉し、recover()
によって回復できることをテストします。また、パニック発生後もOSスレッドのロック状態が正しく維持されるかどうかも検証します。TestCallbackPanicLoop
: パニックを発生させるコールバックをループ内で繰り返し呼び出すことで、Goランタイムのスタック(特にm->g0
スタック)が枯渇しないことを確認します。これは、多数のコールバックが連続して呼び出されるシナリオでの安定性を保証します。TestCallbackPanicLocked
:runtime.LockOSThread()
でOSスレッドをロックした状態でコールバック内でパニックが発生した場合の挙動をテストします。パニック後もOSスレッドのロックが解除されないことを確認し、特定のOSスレッドに依存するAPI呼び出しの安全性を保証します。
- ブロッキングコールバック (
TestBlockingCallback
): コールバック関数内でチャネル操作(ブロッキング操作)を行うテストが含まれています。これは、コールバック関数がGoの並行処理機能(ゴルーチン、チャネル)を安全に利用できることを確認します。コールバックがOSスレッドをブロックしても、Goランタイムのスケジューラが他のゴルーチンを適切に実行できるかどうかが検証されます。 runtime·golockedOSThread
の追加:src/pkg/runtime/proc.c
にruntime·golockedOSThread
というテスト用の関数が追加されています。これは、Goのテストコードからruntime·lockedOSThread
(現在のゴルーチンがOSスレッドにロックされているかどうかを返す内部関数)の値を参照できるようにするためのものです。これにより、テスト内でOSスレッドのロック状態を正確に検証できます。export_test.go
の変更:src/pkg/runtime/export_test.go
は、Goランタイムの内部関数をテストパッケージからアクセスできるようにするためのファイルです。このコミットでは、golockedOSThread
関数がテストから利用できるようにエクスポートされています。
これらのテストは、GoランタイムがWindowsのコールバックメカニズムと深く連携する際に発生しうる複雑なシナリオ(パニック、GC、スレッドロックなど)を網羅的に検証し、Goプログラムの堅牢性と信頼性を向上させることを目的としています。
コアとなるコードの変更箇所
src/pkg/runtime/export_test.go
--- a/src/pkg/runtime/export_test.go
+++ b/src/pkg/runtime/export_test.go
@@ -18,6 +18,8 @@ var F64toint = f64toint
func entersyscall()
func exitsyscall()
+func golockedOSThread() bool
var Entersyscall = entersyscall
var Exitsyscall = exitsyscall
+var LockedOSThread = golockedOSThread
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1586,6 +1586,14 @@ runtime·lockedOSThread(void)
return g->lockedm != nil && m->lockedg != nil;
}
+// for testing of callbacks
+void
+runtime·golockedOSThread(bool ret)
+{
+ ret = runtime·lockedOSThread();
+ FLUSH(&ret);
+}
+
// for testing of wire, unwire
void
runtime·mid(uint32 ret)
src/pkg/runtime/syscall_windows_test.go
--- a/src/pkg/runtime/syscall_windows_test.go
+++ b/src/pkg/runtime/syscall_windows_test.go
@@ -5,6 +5,7 @@
package runtime_test
import (
+ "runtime"
"syscall"
"testing"
"unsafe"
@@ -120,7 +121,7 @@ func TestCDecl(t *testing.T) {
}\n}\n \n-func TestCallback(t *testing.T) {\n+func TestEnumWindows(t *testing.T) {\n \td := GetDLL(t, "user32.dll")\n \tisWindows := d.Proc("IsWindow")\n \tcounter := 0\n@@ -144,6 +145,99 @@ func TestCallback(t *testing.T) {\n \t}\n }\n \n+func callback(hwnd syscall.Handle, lparam uintptr) uintptr {\n+\t(*(*func())(unsafe.Pointer(&lparam)))()\n+\treturn 0 // stop enumeration\n+}\n+\n+// nestedCall calls into Windows, back into Go, and finally to f.\n+func nestedCall(t *testing.T, f func()) {\n+\tc := syscall.NewCallback(callback)\n+\td := GetDLL(t, "user32.dll")\n+\tdefer d.Release()\n+\td.Proc("EnumWindows").Call(c, uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f))))\n+}\n+\n+func TestCallback(t *testing.T) {\n+\tvar x = false\n+\tnestedCall(t, func() { x = true })\n+\tif !x {\n+\t\tt.Fatal("nestedCall did not call func")\n+\t}\n+}\n+\n+func TestCallbackGC(t *testing.T) {\n+\tnestedCall(t, runtime.GC)\n+}\n+\n+func TestCallbackPanic(t *testing.T) {\n+\t// Make sure panic during callback unwinds properly.\n+\tif runtime.LockedOSThread() {\n+\t\tt.Fatal("locked OS thread on entry to TestCallbackPanic")\n+\t}\n+\tdefer func() {\n+\t\ts := recover()\n+\t\tif s == nil {\n+\t\t\tt.Fatal("did not panic")\n+\t\t}\n+\t\tif s.(string) != "callback panic" {\n+\t\t\tt.Fatal("wrong panic:", s)\n+\t\t}\n+\t\tif runtime.LockedOSThread() {\n+\t\t\tt.Fatal("locked OS thread on exit from TestCallbackPanic")\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() { panic("callback panic") })\n+\tpanic("nestedCall returned")\n+}\n+\n+func TestCallbackPanicLoop(t *testing.T) {\n+\t// Make sure we don't blow out m->g0 stack.\n+\tfor i := 0; i < 100000; i++ {\n+\t\tTestCallbackPanic(t)\n+\t}\n+}\n+\n+func TestCallbackPanicLocked(t *testing.T) {\n+\truntime.LockOSThread()\n+\tdefer runtime.UnlockOSThread()\n+\n+\tif !runtime.LockedOSThread() {\n+\t\tt.Fatal("runtime.LockOSThread didn't")\n+\t}\n+\tdefer func() {\n+\t\ts := recover()\n+\t\tif s == nil {\n+\t\t\tt.Fatal("did not panic")\n+\t\t}\n+\t\tif s.(string) != "callback panic" {\n+\t\t\tt.Fatal("wrong panic:", s)\n+\t\t}\n+\t\tif !runtime.LockedOSThread() {\n+\t\t\tt.Fatal("lost lock on OS thread after panic")\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() { panic("callback panic") })\n+\tpanic("nestedCall returned")\n+}\n+\n+func TestBlockingCallback(t *testing.T) {\n+\tc := make(chan int)\n+\tgo func() {\n+\t\tfor i := 0; i < 10; i++ {\n+\t\t\tc <- <-c\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() {\n+\t\tfor i := 0; i < 10; i++ {\n+\t\t\tc <- i\n+\t\t\tif j := <-c; j != i {\n+\t\t\t\tt.Errorf("out of sync %d != %d", j, i)\n+\t\t\t}\n+\t\t}\n+\t})\n+}\n+\n func TestCallbackInAnotherThread(t *testing.T) {\n \t// TODO: test a function which calls back in another thread: QueueUserAPC() or CreateThread()\n }\n```
## コアとなるコードの解説
### `src/pkg/runtime/export_test.go`
このファイルは、Goランタイムの内部関数をテスト目的で外部に公開するために使用されます。
* `func golockedOSThread() bool` の宣言と、`var LockedOSThread = golockedOSThread` の追加により、`runtime`パッケージのテストコードから、現在のゴルーチンがOSスレッドにロックされているかどうかをチェックする内部関数`runtime·lockedOSThread`にアクセスできるようになります。これは、特に`TestCallbackPanicLocked`のようなテストで、パニック発生後もOSスレッドのロック状態が維持されていることを検証するために不可欠です。
### `src/pkg/runtime/proc.c`
このCファイルはGoランタイムのプロセス管理に関連する部分です。
* `runtime·golockedOSThread` 関数が追加されています。これは、Goのテストから呼び出されることを想定したC関数で、内部的に`runtime·lockedOSThread()`を呼び出し、その結果を`ret`引数に格納します。`FLUSH(&ret)`は、コンパイラの最適化によって`ret`が削除されないようにするためのものです。この関数は、GoのテストがCgoを介さずにランタイムの内部状態を直接確認できるようにするためのブリッジとして機能します。
### `src/pkg/runtime/syscall_windows_test.go`
このファイルは、Windowsシステムコールに関連するテストを定義しています。このコミットの主要な変更点であり、Windowsコールバックのテストケースが多数追加されています。
* **`import "runtime"` の追加**: 新しいテストで`runtime`パッケージの関数(`runtime.GC`, `runtime.LockOSThread`, `runtime.UnlockOSThread`, `runtime.LockedOSThread`)を使用するためにインポートされています。
* **`TestCallback` から `TestEnumWindows` へのリネーム**: 既存の`TestCallback`関数が`TestEnumWindows`にリネームされています。これは、新しい`TestCallback`関数との名前の衝突を避けるためと、そのテストが具体的に`EnumWindows` APIを使用していることを明確にするためと考えられます。
* **`callback` 関数**:
```go
func callback(hwnd syscall.Handle, lparam uintptr) uintptr {
(*(*func())(unsafe.Pointer(&lparam)))()
return 0 // stop enumeration
}
```
この関数は、Windowsの`EnumWindows` APIに渡されるコールバック関数として機能します。`lparam`には、Goの関数ポインタが`uintptr`として渡されており、`unsafe.Pointer`と型アサーションを使って元のGoの関数に変換し、実行しています。`return 0`は、`EnumWindows`の列挙を停止させるための戻り値です。
* **`nestedCall` 関数**:
```go
func nestedCall(t *testing.T, f func()) {
c := syscall.NewCallback(callback)
d := GetDLL(t, "user32.dll")
defer d.Release()
d.Proc("EnumWindows").Call(c, uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f))))
}
```
このヘルパー関数は、Goの関数`f`をWindows APIのコールバックとして実行するための共通のロジックを提供します。`syscall.NewCallback(callback)`でGoの`callback`関数をWindowsが呼び出し可能な形式に変換し、`EnumWindows`を呼び出して`f`を実行させます。`uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f)))`の部分は、Goの関数`f`のアドレスを`uintptr`として`lparam`に渡すためのGoの関数ポインタを`uintptr`に変換するトリッキーなコードです。
* **`TestCallback` (新しいもの)**:
```go
func TestCallback(t *testing.T) {
var x = false
nestedCall(t, func() { x = true })
if !x {
t.Fatal("nestedCall did not call func")
}
}
```
これは、`nestedCall`が実際に渡されたGoの関数を呼び出すことを検証する基本的なテストです。
* **`TestCallbackGC`**:
```go
func TestCallbackGC(t *testing.T) {
nestedCall(t, runtime.GC)
}
```
コールバック内で`runtime.GC()`を呼び出し、ガベージコレクションがコールバックの実行に影響を与えないことを確認します。
* **`TestCallbackPanic`**:
```go
func TestCallbackPanic(t *testing.T) {
// ... (defer recover logic) ...
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}
```
コールバック内でパニックを発生させ、そのパニックが`defer`と`recover`によって適切に捕捉され、Goランタイムがクラッシュしないことを検証します。また、パニック発生前後の`runtime.LockedOSThread()`の状態もチェックし、OSスレッドのロック状態が正しく維持されることを確認します。
* **`TestCallbackPanicLoop`**:
```go
func TestCallbackPanicLoop(t *testing.T) {
// Make sure we don't blow out m->g0 stack.
for i := 0; i < 100000; i++ {
TestCallbackPanic(t)
}
}
```
`TestCallbackPanic`を10万回繰り返すことで、多数のコールバック呼び出しとパニック処理が連続して行われた場合に、Goランタイムの内部スタック(特に`m->g0`スタック)がオーバーフローしないことを確認します。これは、システムの安定性にとって重要です。
* **`TestCallbackPanicLocked`**:
```go
func TestCallbackPanicLocked(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// ... (defer recover logic) ...
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}
```
`runtime.LockOSThread()`で現在のゴルーチンをOSスレッドにロックした状態で、コールバック内でパニックを発生させます。パニックが回復した後もOSスレッドのロックが解除されていないことを検証し、特定のOSスレッドに依存するAPI呼び出しの安全性を保証します。
* **`TestBlockingCallback`**:
```go
func TestBlockingCallback(t *testing.T) {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- <-c
}
}()
nestedCall(t, func() {
for i := 0; i < 10; i++ {
c <- i
if j := <-c; j != i {
t.Errorf("out of sync %d != %d", j, i)
}
}
})
}
```
このテストは、コールバック関数内でチャネルを介したブロッキング操作が行われた場合のGoランタイムの挙動を検証します。コールバックがOSスレッドをブロックしても、Goランタイムのスケジューラが他のゴルーチン(この場合はチャネルの送受信を行うゴルーチン)を適切に実行できることを確認します。これは、Goの並行処理モデルとWindowsコールバックの統合の堅牢性を示します。
これらのテストは、GoランタイムがWindowsのコールバックメカニズムと深く連携する際に発生しうる複雑なシナリオ(パニック、GC、スレッドロック、ブロッキング操作など)を網羅的に検証し、Goプログラムの堅牢性と信頼性を向上させることを目的としています。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Go言語の`syscall`パッケージ: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
* Go言語の`runtime`パッケージ: [https://pkg.go.dev/runtime](https://pkg.go.dev/runtime)
* Windows API `EnumWindows` 関数: [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-enumwindows](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-enumwindows)
## 参考にした情報源リンク
* Go言語のソースコード (特に`src/pkg/runtime`ディレクトリ)
* Go言語のIssueトラッカーやChange List (CL) (例: `https://golang.org/cl/5331062`)
* Microsoft Learn (Windows APIに関する公式ドキュメント)
* Go言語に関する技術ブログやフォーラム(GoのCgoやランタイムに関する議論)
* Go言語のテストコードの慣習とパターン# [インデックス 10282] ファイルの概要
このコミットは、Go言語のランタイムにおけるWindowsコールバック機能のテストを追加するものです。主に、`misc/cgo/test` に存在するCgoコールバックテストをWindows環境向けに移植し、GoランタイムがWindows APIからのコールバックを適切に処理できることを検証しています。
## コミット
commit b776b9e724f3edbe4f52d0c1b8dd3ee532a897a3 Author: Alex Brainman alex.brainman@gmail.com Date: Tue Nov 8 16:53:31 2011 +1100
runtime: add windows callback tests
Just a copy of cgo callback tests from misc/cgo/test.
R=rsc
CC=golang-dev, hectorchu
https://golang.org/cl/5331062
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/b776b9e724f3edbe4f52d0c1b8dd3ee532a897a3](https://github.com/golang/go/commit/b776b9e724f3edbe4f52d0c1b8dd3ee532a897a3)
## 元コミット内容
このコミットの目的は、GoランタイムにWindowsコールバックのテストを追加することです。これは、既存の`misc/cgo/test`にあるCgoコールバックテストをWindows環境に合わせた形でコピーしたものです。
## 変更の背景
Go言語はクロスプラットフォーム対応を目指しており、Windows環境においてもOSのネイティブAPI(Win32 APIなど)との連携が重要です。特に、Windows APIには、アプリケーションが特定のイベント発生時や列挙処理中に呼び出される「コールバック関数」を登録する仕組みが多数存在します。GoプログラムがこれらのAPIを介してWindowsシステムと円滑に連携するためには、Goの関数がWindowsからのコールバックとして正しく機能することが不可欠です。
このコミット以前にもCgoを介したコールバックのテストは存在していましたが、それは一般的なC言語との連携を想定したものでした。Windows固有のコールバックメカニズム(特に`syscall.NewCallback`で生成されるコールバック)がGoランタイム内でどのように振る舞うか、特にガベージコレクション(GC)やパニック発生時、あるいはOSスレッドのロック状態といったGoランタイムの内部動作との相互作用を検証する必要がありました。
このテストの追加は、GoプログラムがWindows上でより堅牢かつ予測可能な形で動作することを保証し、開発者がWindowsネイティブ機能を利用したアプリケーションを安心して構築できるようにするための重要なステップです。
## 前提知識の解説
### 1. Go言語のランタイム (Runtime)
Go言語のランタイムは、Goプログラムの実行を管理するシステムです。これには、スケジューラ(ゴルーチンの管理)、ガベージコレクタ(メモリ管理)、システムコールインターフェースなどが含まれます。Goプログラムは、OSのネイティブスレッド上で動作しますが、GoランタイムがゴルーチンをこれらのOSスレッドにマッピングし、効率的な並行処理を実現します。
### 2. Cgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、Goは既存のCライブラリやOSのネイティブAPI(Windows APIなど)と連携できます。Cgoを使用すると、GoのコードとCのコードの間でデータを受け渡し、関数を呼び出すことができます。
### 3. Windows APIとコールバック関数
Windows API(Application Programming Interface)は、Windowsオペレーティングシステムが提供する機能を利用するための関数の集合です。多くのWindows API関数は、特定のイベントが発生したときにシステムが呼び出す「コールバック関数」をアプリケーションが登録することを要求します。例えば、`EnumWindows`関数は、システム上のすべてのトップレベルウィンドウを列挙し、見つかった各ウィンドウに対してアプリケーションが提供するコールバック関数を呼び出します。
コールバック関数は、通常、C言語の関数ポインタとして渡されます。GoプログラムがWindows APIにGoの関数をコールバックとして登録する場合、GoランタイムはGoの関数をC言語から呼び出し可能な形式に変換する必要があります。Goの`syscall`パッケージの`NewCallback`関数は、この変換を行い、Goの関数をWindows APIが期待する形式の関数ポインタ(`uintptr`)として提供します。
### 4. `runtime.LockOSThread()` と `runtime.UnlockOSThread()`
Goのゴルーチンは、GoランタイムによってOSスレッドにスケジューリングされます。通常、どのゴルーチンがどのOSスレッドで実行されるかはGoランタイムが自由に決定します。しかし、特定のOS API(例えば、COMオブジェクトの初期化や特定のGUI操作など)は、そのAPIを呼び出したスレッドと同じスレッドで後続の操作を行うことを要求する場合があります。このような場合、Goの`runtime.LockOSThread()`関数を使用すると、現在のゴルーチンを特定のOSスレッドに「ロック」し、そのゴルーチンが常に同じOSスレッドで実行されるように保証できます。`runtime.UnlockOSThread()`はそのロックを解除します。
### 5. パニック (Panic) とリカバリ (Recover)
Goのパニックは、プログラムの異常終了を示すメカニズムです。通常、回復不可能なエラーが発生した場合にパニックが起こります。`defer`文と`recover()`関数を組み合わせることで、パニックを捕捉し、プログラムのクラッシュを防ぎ、回復処理を行うことができます。コールバック関数内でパニックが発生した場合、Goランタイムがそのパニックを適切に処理し、コールバックの呼び出し元(この場合はWindows API)に影響を与えないようにすることが重要です。
## 技術的詳細
このコミットで追加されたテストは、GoランタイムがWindowsコールバックを処理する際の様々なシナリオを網羅しています。
1. **`syscall.NewCallback`の利用**: Goの関数をWindows APIが呼び出し可能な形式に変換するために、`syscall.NewCallback`が使用されます。この関数は、Goの関数をラップし、C言語の呼び出し規約に準拠したエントリポイントを提供します。
2. **`EnumWindows` APIの利用**: テストでは、Windowsの`EnumWindows` APIが利用されています。このAPIは、システム上のすべてのトップレベルウィンドウを列挙し、見つかった各ウィンドウに対して指定されたコールバック関数を呼び出します。これにより、Goのコールバック関数が実際にWindowsシステムから呼び出されることをシミュレートします。
3. **ネストされたコールバック呼び出し (`nestedCall`)**: `nestedCall`関数は、Windows APIを呼び出し、そこからGoのコールバック関数が呼び出され、さらにそのGoのコールバック関数が別のGoの関数`f`を呼び出すという、ネストされた呼び出しパスをシミュレートします。これは、Goランタイムがコールバックチェーンを正しく処理できるかを検証します。
4. **ガベージコレクション (`TestCallbackGC`)**: コールバック関数内で`runtime.GC()`を呼び出すテストが含まれています。これは、コールバック実行中にガベージコレクションがトリガーされた場合に、ランタイムが安定して動作するかを確認します。
5. **パニック処理 (`TestCallbackPanic`, `TestCallbackPanicLoop`, `TestCallbackPanicLocked`)**:
* `TestCallbackPanic`: コールバック関数内でパニックが発生した場合に、Goランタイムがそのパニックを適切に捕捉し、`recover()`によって回復できることをテストします。また、パニック発生後もOSスレッドのロック状態が正しく維持されるかどうかも検証します。
* `TestCallbackPanicLoop`: パニックを発生させるコールバックをループ内で繰り返し呼び出すことで、Goランタイムのスタック(特に`m->g0`スタック)が枯渇しないことを確認します。これは、多数のコールバックが連続して呼び出されるシナリオでの安定性を保証します。
* `TestCallbackPanicLocked`: `runtime.LockOSThread()`でOSスレッドをロックした状態でコールバック内でパニックが発生した場合の挙動をテストします。パニック後もOSスレッドのロックが解除されないことを確認し、特定のOSスレッドに依存するAPI呼び出しの安全性を保証します。
6. **ブロッキングコールバック (`TestBlockingCallback`)**: コールバック関数内でチャネル操作(ブロッキング操作)を行うテストが含まれています。これは、コールバック関数がGoの並行処理機能(ゴルーチン、チャネル)を安全に利用できることを確認します。コールバックがOSスレッドをブロックしても、Goランタイムのスケジューラが他のゴルーチンを適切に実行できるかどうかが検証されます。
7. **`runtime·golockedOSThread`の追加**: `src/pkg/runtime/proc.c`に`runtime·golockedOSThread`というテスト用の関数が追加されています。これは、Goのテストコードから`runtime·lockedOSThread`(現在のゴルーチンがOSスレッドにロックされているかどうかを返す内部関数)の値を参照できるようにするためのものです。これにより、テスト内でOSスレッドのロック状態を正確に検証できます。
8. **`export_test.go`の変更**: `src/pkg/runtime/export_test.go`は、Goランタイムの内部関数をテストパッケージからアクセスできるようにするためのファイルです。このコミットでは、`golockedOSThread`関数がテストから利用できるようにエクスポートされています。
これらのテストは、GoランタイムがWindowsのコールバックメカニズムと深く連携する際に発生しうる複雑なシナリオ(パニック、GC、スレッドロックなど)を網羅的に検証し、Goプログラムの堅牢性と信頼性を向上させることを目的としています。
## コアとなるコードの変更箇所
### `src/pkg/runtime/export_test.go`
```diff
--- a/src/pkg/runtime/export_test.go
+++ b/src/pkg/runtime/export_test.go
@@ -18,6 +18,8 @@ var F64toint = f64toint
func entersyscall()
func exitsyscall()
+func golockedOSThread() bool
var Entersyscall = entersyscall
var Exitsyscall = exitsyscall
+var LockedOSThread = golockedOSThread
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1586,6 +1586,14 @@ runtime·lockedOSThread(void)
return g->lockedm != nil && m->lockedg != nil;
}
+// for testing of callbacks
+void
+runtime·golockedOSThread(bool ret)
+{
+ ret = runtime·lockedOSThread();
+ FLUSH(&ret);
+}
+
// for testing of wire, unwire
void
runtime·mid(uint32 ret)
src/pkg/runtime/syscall_windows_test.go
--- a/src/pkg/runtime/syscall_windows_test.go
+++ b/src/pkg/runtime/syscall_windows_test.go
@@ -5,6 +5,7 @@
package runtime_test
import (
+ "runtime"
"syscall"
"testing"
"unsafe"
@@ -120,7 +121,7 @@ func TestCDecl(t *testing.T) {
}\n}\n \n-func TestCallback(t *testing.T) {\n+func TestEnumWindows(t *testing.T) {\n \td := GetDLL(t, "user32.dll")\n \tisWindows := d.Proc("IsWindow")\n \tcounter := 0\n@@ -144,6 +145,99 @@ func TestCallback(t *testing.T) {\n \t}\n }\n \n+func callback(hwnd syscall.Handle, lparam uintptr) uintptr {\n+\t(*(*func())(unsafe.Pointer(&lparam)))()\n+\treturn 0 // stop enumeration\n+}\n+\n+// nestedCall calls into Windows, back into Go, and finally to f.\n+func nestedCall(t *testing.T, f func()) {\n+\tc := syscall.NewCallback(callback)\n+\td := GetDLL(t, "user32.dll")\n+\tdefer d.Release()\n+\td.Proc("EnumWindows").Call(c, uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f))))\n+}\n+\n+func TestCallback(t *testing.T) {\n+\tvar x = false\n+\tnestedCall(t, func() { x = true })\n+\tif !x {\n+\t\tt.Fatal("nestedCall did not call func")\n+\t}\n+}\n+\n+func TestCallbackGC(t *testing.T) {\n+\tnestedCall(t, runtime.GC)\n+}\n+\n+func TestCallbackPanic(t *testing.T) {\n+\t// Make sure panic during callback unwinds properly.\n+\tif runtime.LockedOSThread() {\n+\t\tt.Fatal("locked OS thread on entry to TestCallbackPanic")\n+\t}\n+\tdefer func() {\n+\t\ts := recover()\n+\t\tif s == nil {\n+\t\t\tt.Fatal("did not panic")\n+\t\t}\n+\t\tif s.(string) != "callback panic" {\n+\t\t\tt.Fatal("wrong panic:", s)\n+\t\t}\n+\t\tif runtime.LockedOSThread() {\n+\t\t\tt.Fatal("locked OS thread on exit from TestCallbackPanic")\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() { panic("callback panic") })\n+\tpanic("nestedCall returned")\n+}\n+\n+func TestCallbackPanicLoop(t *testing.T) {\n+\t// Make sure we don't blow out m->g0 stack.\n+\tfor i := 0; i < 100000; i++ {\n+\t\tTestCallbackPanic(t)\n+\t}\n+}\n+\n+func TestCallbackPanicLocked(t *testing.T) {\n+\truntime.LockOSThread()\n+\tdefer runtime.UnlockOSThread()\n+\n+\tif !runtime.LockedOSThread() {\n+\t\tt.Fatal("runtime.LockOSThread didn't")\n+\t}\n+\tdefer func() {\n+\t\ts := recover()\n+\t\tif s == nil {\n+\t\t\tt.Fatal("did not panic")\n+\t\t}\n+\t\tif s.(string) != "callback panic" {\n+\t\t\tt.Fatal("wrong panic:", s)\n+\t\t}\n+\t\tif !runtime.LockedOSThread() {\n+\t\t\tt.Fatal("lost lock on OS thread after panic")\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() { panic("callback panic") })\n+\tpanic("nestedCall returned")\n+}\n+\n+func TestBlockingCallback(t *testing.T) {\n+\tc := make(chan int)\n+\tgo func() {\n+\t\tfor i := 0; i < 10; i++ {\n+\t\t\tc <- <-c\n+\t\t}\n+\t}()\n+\tnestedCall(t, func() {\n+\t\tfor i := 0; i < 10; i++ {\n+\t\t\tc <- i\n+\t\t\tif j := <-c; j != i {\n+\t\t\t\tt.Errorf("out of sync %d != %d", j, i)\n+\t\t\t}\n+\t\t}\n+\t})\n+}\n+\n func TestCallbackInAnotherThread(t *testing.T) {\n \t// TODO: test a function which calls back in another thread: QueueUserAPC() or CreateThread()\n }\n```
## コアとなるコードの解説
### `src/pkg/runtime/export_test.go`
このファイルは、Goランタイムの内部関数をテスト目的で外部に公開するために使用されます。
* `func golockedOSThread() bool` の宣言と、`var LockedOSThread = golockedOSThread` の追加により、`runtime`パッケージのテストコードから、現在のゴルーチンがOSスレッドにロックされているかどうかをチェックする内部関数`runtime·lockedOSThread`にアクセスできるようになります。これは、特に`TestCallbackPanicLocked`のようなテストで、パニック発生後もOSスレッドのロック状態が維持されていることを検証するために不可欠です。
### `src/pkg/runtime/proc.c`
このCファイルはGoランタイムのプロセス管理に関連する部分です。
* `runtime·golockedOSThread` 関数が追加されています。これは、Goのテストから呼び出されることを想定したC関数で、内部的に`runtime·lockedOSThread()`を呼び出し、その結果を`ret`引数に格納します。`FLUSH(&ret)`は、コンパイラの最適化によって`ret`が削除されないようにするためのものです。この関数は、GoのテストがCgoを介さずにランタイムの内部状態を直接確認できるようにするためのブリッジとして機能します。
### `src/pkg/runtime/syscall_windows_test.go`
このファイルは、Windowsシステムコールに関連するテストを定義しています。このコミットの主要な変更点であり、Windowsコールバックのテストケースが多数追加されています。
* **`import "runtime"` の追加**: 新しいテストで`runtime`パッケージの関数(`runtime.GC`, `runtime.LockOSThread`, `runtime.UnlockOSThread`, `runtime.LockedOSThread`)を使用するためにインポートされています。
* **`TestCallback` から `TestEnumWindows` へのリネーム**: 既存の`TestCallback`関数が`TestEnumWindows`にリネームされています。これは、新しい`TestCallback`関数との名前の衝突を避けるためと、そのテストが具体的に`EnumWindows` APIを使用していることを明確にするためと考えられます。
* **`callback` 関数**:
```go
func callback(hwnd syscall.Handle, lparam uintptr) uintptr {
(*(*func())(unsafe.Pointer(&lparam)))()
return 0 // stop enumeration
}
```
この関数は、Windowsの`EnumWindows` APIに渡されるコールバック関数として機能します。`lparam`には、Goの関数ポインタが`uintptr`として渡されており、`unsafe.Pointer`と型アサーションを使って元のGoの関数に変換し、実行しています。`return 0`は、`EnumWindows`の列挙を停止させるための戻り値です。
* **`nestedCall` 関数**:
```go
func nestedCall(t *testing.T, f func()) {
c := syscall.NewCallback(callback)
d := GetDLL(t, "user32.dll")
defer d.Release()
d.Proc("EnumWindows").Call(c, uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f))))
}
```
このヘルパー関数は、Goの関数`f`をWindows APIのコールバックとして実行するための共通のロジックを提供します。`syscall.NewCallback(callback)`でGoの`callback`関数をWindowsが呼び出し可能な形式に変換し、`EnumWindows`を呼び出して`f`を実行させます。`uintptr(*(*unsafe.Pointer)(unsafe.Pointer(&f)))`の部分は、Goの関数`f`のアドレスを`uintptr`として`lparam`に渡すためのGoの関数ポインタを`uintptr`に変換するトリッキーなコードです。
* **`TestCallback` (新しいもの)**:
```go
func TestCallback(t *testing.T) {
var x = false
nestedCall(t, func() { x = true })
if !x {
t.Fatal("nestedCall did not call func")
}
}
```
これは、`nestedCall`が実際に渡されたGoの関数を呼び出すことを検証する基本的なテストです。
* **`TestCallbackGC`**:
```go
func TestCallbackGC(t *testing.T) {
nestedCall(t, runtime.GC)
}
```
コールバック内で`runtime.GC()`を呼び出し、ガベージコレクションがコールバックの実行に影響を与えないことを確認します。
* **`TestCallbackPanic`**:
```go
func TestCallbackPanic(t *testing.T) {
// ... (defer recover logic) ...
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}
```
コールバック内でパニックを発生させ、そのパニックが`defer`と`recover`によって適切に捕捉され、Goランタイムがクラッシュしないことを検証します。また、パニック発生前後の`runtime.LockedOSThread()`の状態もチェックし、OSスレッドのロック状態が正しく維持されることを確認します。
* **`TestCallbackPanicLoop`**:
```go
func TestCallbackPanicLoop(t *testing.T) {
// Make sure we don't blow out m->g0 stack.
for i := 0; i < 100000; i++ {
TestCallbackPanic(t)
}
}
```
`TestCallbackPanic`を10万回繰り返すことで、多数のコールバック呼び出しとパニック処理が連続して行われた場合に、Goランタイムの内部スタック(特に`m->g0`スタック)がオーバーフローしないことを確認します。これは、システムの安定性にとって重要です。
* **`TestCallbackPanicLocked`**:
```go
func TestCallbackPanicLocked(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// ... (defer recover logic) ...
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}
```
`runtime.LockOSThread()`で現在のゴルーチンをOSスレッドにロックした状態で、コールバック内でパニックを発生させます。パニックが回復した後もOSスレッドのロックが解除されていないことを検証し、特定のOSスレッドに依存するAPI呼び出しの安全性を保証します。
* **`TestBlockingCallback`**:
```go
func TestBlockingCallback(t *testing.T) {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- <-c
}
}()
nestedCall(t, func() {
for i := 0; i < 10; i++ {
c <- i
if j := <-c; j != i {
t.Errorf("out of sync %d != %d", j, i)
}
}
})
}
```
このテストは、コールバック関数内でチャネルを介したブロッキング操作が行われた場合のGoランタイムの挙動を検証します。コールバックがOSスレッドをブロックしても、Goランタイムのスケジューラが他のゴルーチン(この場合はチャネルの送受信を行うゴルーチン)を適切に実行できることを確認します。これは、Goの並行処理モデルとWindowsコールバックの統合の堅牢性を示します。
これらのテストは、GoランタイムがWindowsのコールバックメカニズムと深く連携する際に発生しうる複雑なシナリオ(パニック、GC、スレッドロック、ブロッキング操作など)を網羅的に検証し、Goプログラムの堅牢性と信頼性を向上させることを目的としています。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Go言語の`syscall`パッケージ: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
* Go言語の`runtime`パッケージ: [https://pkg.go.dev/runtime](https://pkg.go.dev/runtime)
* Windows API `EnumWindows` 関数: [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-enumwindows](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-enumwindows)
## 参考にした情報源リンク
* Go言語のソースコード (特に`src/pkg/runtime`ディレクトリ)
* Go言語のIssueトラッカーやChange List (CL) (例: `https://golang.org/cl/5331062`)
* Microsoft Learn (Windows APIに関する公式ドキュメント)
* Go言語に関する技術ブログやフォーラム(GoのCgoやランタイムに関する議論)
* Go言語のテストコードの慣習とパターン