[インデックス 18080] ファイルの概要
このコミットは、Go言語のWindows向けsyscall
パッケージにおいて、NewCallbackCDecl
関数を再導入するものです。これにより、Goの関数をC言語のcdecl
呼び出し規約に準拠するコールバックとしてWindows APIに渡す機能が再び利用可能になります。この変更は、特にWindows環境での外部CライブラリやシステムAPIとの相互運用性において、柔軟性と互換性を向上させることを目的としています。
コミット
- コミットハッシュ:
7f8a5057dd0f471c49910a3f77ded5edf7a72a08
- 作者: Alex Brainman alex.brainman@gmail.com
- コミット日時: 2013年12月19日 木曜日 14:38:50 +1100
- コミットメッセージ:
syscall: add NewCallbackCDecl again Fixes #6338 R=golang-dev, kin.wilson.za, rsc CC=golang-dev https://golang.org/cl/36180044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f8a5057dd0f471c49910a3f77ded5edf7a72a08
元コミット内容
syscall: add NewCallbackCDecl again
Fixes #6338
R=golang-dev, kin.wilson.za, rsc
CC=golang-dev
https://golang.org/cl/36180044
変更の背景
このコミットの主な背景は、Windows環境におけるGoプログラムとC言語で書かれたライブラリやWindows APIとの相互運用性の向上です。コミットメッセージにある「add NewCallbackCDecl again」という記述から、この機能が以前に存在し、何らかの理由で削除された後、再び必要とされて再導入されたことが示唆されます。
Windows APIの関数は、主にstdcall
という呼び出し規約を使用しますが、一部のCライブラリや特定のAPIではcdecl
呼び出し規約が用いられることがあります。Goのsyscall.NewCallback
関数は、Goの関数をstdcall
規約のコールバックとしてエクスポートするために使用されます。しかし、cdecl
規約のコールバックが必要な場合、既存のNewCallback
だけでは対応できませんでした。
Fixes #6338
という記述がありますが、Goの公式Issueトラッカーで直接この番号のIssueは見つかりませんでした。しかし、このコミットの内容から推測すると、おそらくWindows環境でcdecl
規約のコールバックを必要とするシナリオにおいて、Goからその機能が提供されていないことによる問題や要望が存在したと考えられます。NewCallbackCDecl
の再導入は、このような特定の呼び出し規約を必要とする外部コードとの連携を可能にし、Goプログラムの適用範囲を広げるための重要な変更です。
前提知識の解説
呼び出し規約 (Calling Convention)
呼び出し規約とは、関数が呼び出される際に、引数をどのようにスタックに積むか、誰がスタックをクリーンアップするか、戻り値をどのように返すかなどを定義する取り決めです。異なるプログラミング言語やコンパイラ間で関数を呼び出す(相互運用する)際には、呼び出し規約が一致している必要があります。
Windows環境でよく使われる主要な呼び出し規約には以下の2つがあります。
-
stdcall
(Standard Call):- 呼び出し側が引数を右から左へスタックに積みます。
- 呼び出された関数(Callee)がスタックをクリーンアップします。
- Windows APIのほとんどの関数がこの規約を使用しています。
- スタックのクリーンアップを呼び出された関数が行うため、呼び出し側は引数の数を知る必要がなく、可変長引数には不向きです。
-
cdecl
(C Declaration):- 呼び出し側が引数を右から左へスタックに積みます。
- 呼び出し側(Caller)がスタックをクリーンアップします。
- C言語の標準的な呼び出し規約です。
- スタックのクリーンアップを呼び出し側が行うため、可変長引数に対応できます。
Goのsyscall
パッケージとコールバック
Go言語のsyscall
パッケージは、オペレーティングシステムの低レベルな機能(システムコール)にアクセスするためのインターフェースを提供します。Windowsにおいては、このパッケージを通じてWindows API関数を呼び出すことができます。
コールバック (Callback) とは、ある関数(A)が別の関数(B)を呼び出す際に、関数Bがさらに別の関数(C)を呼び出すように登録するメカニズムです。Windows APIでは、イベントハンドラや列挙関数など、システムが特定の状況でユーザー定義の関数を呼び出す必要がある場合にコールバックが頻繁に利用されます。
GoからWindows APIにコールバック関数を渡す場合、Goの関数をWindowsが理解できる形式(関数ポインタ)に変換し、かつ適切な呼び出し規約に準拠させる必要があります。syscall.NewCallback
は、Goの関数をstdcall
規約の関数ポインタに変換するために使用されます。このコミットで再導入されるsyscall.NewCallbackCDecl
は、Goの関数をcdecl
規約の関数ポインタに変換する役割を担います。
runtime
パッケージ
Goのruntime
パッケージは、Goプログラムの実行時環境を管理するGo言語のコア部分です。ガベージコレクション、スケジューラ、goroutineの管理、そしてシステムコールとの低レベルな連携などが含まれます。syscall
パッケージが提供する高レベルなインターフェースの背後では、runtime
パッケージが実際のOSとのやり取りや、Goの関数をCの呼び出し規約に合わせるためのアセンブリコードの生成などを行っています。
技術的詳細
このコミットは、Goのランタイムとsyscall
パッケージにわたる複数のファイルに影響を与え、cdecl
呼び出し規約のコールバックをサポートするための変更を加えています。
-
src/pkg/runtime/syscall_windows.goc
:- このファイルは、Goの
syscall
パッケージのWindows固有の実装を含んでいます。 - 以前コメントアウトされていた
NewCallbackCDecl
関数の定義がアンコメントされ、有効化されています。 NewCallbackCDecl
は、runtime·compilecallback(fn, false)
を呼び出します。ここでfalse
は、スタックのクリーンアップを呼び出し側が行うcdecl
規約であることを示します。- 既存の
NewCallback
はruntime·compilecallback(fn, true)
を呼び出し、true
は呼び出された関数がスタックをクリーンアップするstdcall
規約であることを示します。
- このファイルは、Goの
-
src/pkg/runtime/callback_windows.c
:- このC言語のファイルは、Goの関数をWindowsが呼び出せるコールバックにコンパイルするロジックを含んでいます。
runtime·compilecallback
関数の内部で、既存のコールバックを再利用する際の条件が変更されています。- 変更前:
if(cbs.ctxt[i]->gobody == fn.data)
- 変更後:
if(cbs.ctxt[i]->gobody == fn.data && cbs.ctxt[i]->cleanstack == cleanstack)
- 変更前:
- この変更により、同じGoの関数であっても、
stdcall
(cleanstack=true
)とcdecl
(cleanstack=false
)で異なるコールバックコンテキストが生成されるようになります。これは、呼び出し規約によってスタックのクリーンアップ方法が異なるため、それぞれ独立したコールバックエントリポイントが必要となるためです。 - 新しいコールバックコンテキストを作成する際に、
c->cleanstack = cleanstack;
という行が追加され、このフラグがコンテキストに保存されるようになりました。これにより、ランタイムがコールバックの呼び出し規約を正しく管理できるようになります。
-
src/pkg/runtime/runtime.h
:WinCallbackContext
構造体にbool cleanstack;
フィールドが追加されました。- このフィールドは、そのコールバックが
stdcall
(true
)かcdecl
(false
)かを示すために使用されます。ランタイムがコールバックを処理する際に、この情報に基づいて適切なスタッククリーンアップ動作を行うために必要です。
-
src/pkg/syscall/syscall_windows.go
:NewCallbackCDecl(fn interface{}) uintptr
という関数宣言が追加されました。- これにより、Goのユーザーコードから
syscall.NewCallbackCDecl
を呼び出して、cdecl
規約のコールバックを作成できるようになります。 - コメントも更新され、
NewCallback
がstdcall
またはcdecl
呼び出し規約に準拠する関数ポインタに変換できることが明記されました。
-
src/pkg/runtime/syscall_windows_test.go
:- このファイルには、
NewCallback
とNewCallbackCDecl
の両方をテストするための大規模なテストコードが追加されています。 cbDLLFunc
構造体と関連するメソッドは、stdcall
とcdecl
の両方の呼び出し規約を持つC言語のDLL関数を動的に生成するためのものです。これにより、GoのコールバックがCのDLLから正しく呼び出されるかを検証できます。cbFuncs
は、様々な引数を持つGoのコールバック関数を定義しています。cbDLL
構造体とcbDLLs
変数は、テスト用のDLLをビルドするための設定を含んでいます。gcc
を使用してDLLをコンパイルしています。cbTest
構造体とcbTests
変数は、テストケースのパラメータ(引数の数、DLL関数へのパラメータ)を定義しています。TestStdcallAndCDeclCallbacks
関数は、一時ディレクトリを作成し、テスト用のDLLをビルドし、各テストケースに対してstdcall
とcdecl
の両方のコールバックを検証します。これにより、Goのコールバックが異なる呼び出し規約で正しく動作することを確認しています。
- このファイルには、
これらの変更により、Goのランタイムは、Goの関数をstdcall
とcdecl
の両方の呼び出し規約に準拠するコールバックとしてWindows APIに渡すための完全なサポートを提供できるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/runtime/callback_windows.c
:runtime·compilecallback
関数内で、既存のコールバックを検索するロジックにcleanstack
フラグの比較が追加されました。- if(cbs.ctxt[i]->gobody == fn.data) { + if(cbs.ctxt[i]->gobody == fn.data && cbs.ctxt[i]->cleanstack == cleanstack) {
- 新しいコールバックコンテキストを作成する際に、
cleanstack
フラグが保存されるようになりました。+ c->cleanstack = cleanstack;
-
src/pkg/runtime/runtime.h
:WinCallbackContext
構造体にcleanstack
フィールドが追加されました。+ bool cleanstack;
-
src/pkg/runtime/syscall_windows.goc
:NewCallbackCDecl
関数のコメントアウトが解除され、有効化されました。-/* - * If this is needed, uncomment here and add a declaration in package syscall - * next to the NewCallback declaration. - * func NewCallbackCDecl(fn Eface) (code uintptr) { code = (uintptr)runtime·compilecallback(fn, false); } - */
-
src/pkg/syscall/syscall_windows.go
:NewCallbackCDecl
関数の宣言が追加されました。+func NewCallbackCDecl(fn interface{}) uintptr
NewCallback
関数のコメントが更新され、stdcall
またはcdecl
の両方をサポートすることが示されました。-// to the stdcall calling convention. This is useful when +// to the stdcall or cdecl calling convention. This is useful when
-
src/pkg/runtime/syscall_windows_test.go
:TestStdcallAndCDeclCallbacks
関数を含む、stdcall
とcdecl
両方のコールバックを検証するための大規模なテストスイートが追加されました。これは、GoのコールバックがCのDLLから正しく呼び出されることを確認するためのものです。
コアとなるコードの解説
runtime/callback_windows.c
の変更
runtime·compilecallback
関数は、Goの関数をWindowsが呼び出せるネイティブなコールバック関数ポインタに変換するGoランタイムの内部関数です。
変更前は、同じGoの関数であれば、常に同じコールバックエントリポイントを再利用していました。しかし、stdcall
とcdecl
ではスタックのクリーンアップの責任が異なるため、同じGoの関数であっても、異なる呼び出し規約でコールバックとして登録される場合は、それぞれ独立したエントリポイントが必要です。
// 変更前: 同じGo関数なら再利用
if(cbs.ctxt[i]->gobody == fn.data) {
// ...
}
// 変更後: Go関数とcleanstackフラグの両方が一致する場合のみ再利用
if(cbs.ctxt[i]->gobody == fn.data && cbs.ctxt[i]->cleanstack == cleanstack) {
// ...
}
この変更により、runtime·compilecallback
は、Goの関数ポインタ (fn.data
) と、そのコールバックがstdcall
(cleanstack=true
) なのかcdecl
(cleanstack=false
) なのかを示すcleanstack
フラグの両方が一致する場合にのみ、既存のコールバックエントリポイントを再利用するようになりました。これにより、異なる呼び出し規約を持つ同じGo関数が、それぞれ正しく動作するようになります。
また、新しいコールバックコンテキストを作成する際に、cleanstack
フラグをWinCallbackContext
構造体に保存するようになりました。
c->gobody = fn.data;
c->argsize = argsize;
c->cleanstack = cleanstack; // 新しく追加された行
このcleanstack
フラグは、コールバックが実際に呼び出された際に、ランタイムがスタックをどのように処理すべきかを判断するために使用されます。
runtime/runtime.h
の変更
WinCallbackContext
構造体は、Goのコールバック関数に関するコンテキスト情報を保持します。この構造体にcleanstack
フィールドが追加されたことで、各コールバックがどの呼び出し規約に属するのかをランタイムが追跡できるようになりました。
struct WinCallbackContext
{
void* gobody; // Go function to call
uintptr argsize; // callback arguments size (in bytes)
uintptr restorestack; // adjust stack on return by (in bytes) (386 only)
bool cleanstack; // 新しく追加されたフィールド
};
runtime/syscall_windows.goc
の変更
このファイルは、Goのsyscall
パッケージのWindows固有の内部実装です。以前コメントアウトされていたNewCallbackCDecl
関数がアンコメントされ、有効化されました。
func NewCallbackCDecl(fn Eface) (code uintptr) {
code = (uintptr)runtime·compilecallback(fn, false);
}
NewCallbackCDecl
は、runtime·compilecallback
をcleanstack
引数にfalse
を渡して呼び出します。これは、生成されるコールバックがcdecl
規約であり、呼び出し側がスタックをクリーンアップすることをランタイムに伝えます。
対照的に、既存のNewCallback
はtrue
を渡します。
func NewCallback(fn Eface) (code uintptr) {
code = (uintptr)runtime·compilecallback(fn, true);
}
これにより、NewCallback
はstdcall
規約のコールバックを生成します。
syscall/syscall_windows.go
の変更
このファイルは、Goのsyscall
パッケージの公開APIです。NewCallbackCDecl
関数の宣言が追加されたことで、Goのユーザーはsyscall.NewCallbackCDecl
を直接呼び出して、cdecl
規約のコールバックを作成できるようになりました。
func NewCallback(fn interface{}) uintptr
func NewCallbackCDecl(fn interface{}) uintptr // 新しく追加された宣言
これにより、GoプログラムはWindows APIやCライブラリがcdecl
規約のコールバックを要求する場合にも対応できるようになります。
runtime/syscall_windows_test.go
の変更
このテストファイルは、NewCallbackCDecl
の機能が正しく動作することを検証するために非常に重要です。
追加されたテストコードは、以下のようなことを行います。
- C言語DLLの動的生成:
cbDLLFunc
とcbDLL
構造体を使用して、stdcall
とcdecl
の両方の呼び出し規約を持つC言語の関数をエクスポートするDLLを動的に生成します。これにより、実際のCコードからGoのコールバックが呼び出されるシナリオをシミュレートします。 - Goコールバック関数の定義:
cbFuncs
マップには、様々な引数の数を持つGoの関数が定義されています。これらの関数は、CのDLLから呼び出されるコールバックとして機能します。 - テストケースの実行:
TestStdcallAndCDeclCallbacks
関数は、生成されたDLLをロードし、syscall.NewCallback
とsyscall.NewCallbackCDecl
を使ってGoの関数をコールバックとして登録します。その後、DLL内のC関数を呼び出し、Goのコールバックが正しい引数で、かつ適切な呼び出し規約で呼び出されることを検証します。
この包括的なテストスイートは、NewCallbackCDecl
の導入が既存のNewCallback
の動作に影響を与えず、両方の呼び出し規約がWindows環境で正しく機能することを保証します。
関連リンク
- Go
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - GoのIssueトラッカー (Issue #6338は直接見つかりませんでしたが、関連する議論があるかもしれません): https://github.com/golang/go/issues
参考にした情報源リンク
- Microsoft Docs: Calling Conventions: https://learn.microsoft.com/en-us/cpp/build/calling-conventions?view=msvc-170
- Wikipedia: X86 calling conventions: https://en.wikipedia.org/wiki/X86_calling_conventions
- Go言語のソースコード (特に
src/pkg/runtime
ディレクトリとsrc/pkg/syscall
ディレクトリ) - GitHubのコミットページ: https://github.com/golang/go/commit/7f8a5057dd0f471c49910a3f77ded5edf7a72a08
- Go CL 36180044: https://golang.org/cl/36180044 (これはコミットメッセージに記載されているGoのコードレビューリンクです)
- Google検索: "golang stdcall cdecl", "windows api calling conventions" など