Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つがあります。

  1. stdcall (Standard Call):

    • 呼び出し側が引数を右から左へスタックに積みます。
    • 呼び出された関数(Callee)がスタックをクリーンアップします。
    • Windows APIのほとんどの関数がこの規約を使用しています。
    • スタックのクリーンアップを呼び出された関数が行うため、呼び出し側は引数の数を知る必要がなく、可変長引数には不向きです。
  2. 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呼び出し規約のコールバックをサポートするための変更を加えています。

  1. src/pkg/runtime/syscall_windows.goc:

    • このファイルは、GoのsyscallパッケージのWindows固有の実装を含んでいます。
    • 以前コメントアウトされていたNewCallbackCDecl関数の定義がアンコメントされ、有効化されています。
    • NewCallbackCDeclは、runtime·compilecallback(fn, false)を呼び出します。ここでfalseは、スタックのクリーンアップを呼び出し側が行うcdecl規約であることを示します。
    • 既存のNewCallbackruntime·compilecallback(fn, true)を呼び出し、trueは呼び出された関数がスタックをクリーンアップするstdcall規約であることを示します。
  2. 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の関数であっても、stdcallcleanstack=true)とcdeclcleanstack=false)で異なるコールバックコンテキストが生成されるようになります。これは、呼び出し規約によってスタックのクリーンアップ方法が異なるため、それぞれ独立したコールバックエントリポイントが必要となるためです。
    • 新しいコールバックコンテキストを作成する際に、c->cleanstack = cleanstack;という行が追加され、このフラグがコンテキストに保存されるようになりました。これにより、ランタイムがコールバックの呼び出し規約を正しく管理できるようになります。
  3. src/pkg/runtime/runtime.h:

    • WinCallbackContext構造体にbool cleanstack;フィールドが追加されました。
    • このフィールドは、そのコールバックがstdcalltrue)かcdeclfalse)かを示すために使用されます。ランタイムがコールバックを処理する際に、この情報に基づいて適切なスタッククリーンアップ動作を行うために必要です。
  4. src/pkg/syscall/syscall_windows.go:

    • NewCallbackCDecl(fn interface{}) uintptrという関数宣言が追加されました。
    • これにより、Goのユーザーコードからsyscall.NewCallbackCDeclを呼び出して、cdecl規約のコールバックを作成できるようになります。
    • コメントも更新され、NewCallbackstdcallまたはcdecl呼び出し規約に準拠する関数ポインタに変換できることが明記されました。
  5. src/pkg/runtime/syscall_windows_test.go:

    • このファイルには、NewCallbackNewCallbackCDeclの両方をテストするための大規模なテストコードが追加されています。
    • cbDLLFunc構造体と関連するメソッドは、stdcallcdeclの両方の呼び出し規約を持つC言語のDLL関数を動的に生成するためのものです。これにより、GoのコールバックがCのDLLから正しく呼び出されるかを検証できます。
    • cbFuncsは、様々な引数を持つGoのコールバック関数を定義しています。
    • cbDLL構造体とcbDLLs変数は、テスト用のDLLをビルドするための設定を含んでいます。gccを使用してDLLをコンパイルしています。
    • cbTest構造体とcbTests変数は、テストケースのパラメータ(引数の数、DLL関数へのパラメータ)を定義しています。
    • TestStdcallAndCDeclCallbacks関数は、一時ディレクトリを作成し、テスト用のDLLをビルドし、各テストケースに対してstdcallcdeclの両方のコールバックを検証します。これにより、Goのコールバックが異なる呼び出し規約で正しく動作することを確認しています。

これらの変更により、Goのランタイムは、Goの関数をstdcallcdeclの両方の呼び出し規約に準拠するコールバックとして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関数を含む、stdcallcdecl両方のコールバックを検証するための大規模なテストスイートが追加されました。これは、GoのコールバックがCのDLLから正しく呼び出されることを確認するためのものです。

コアとなるコードの解説

runtime/callback_windows.c の変更

runtime·compilecallback関数は、Goの関数をWindowsが呼び出せるネイティブなコールバック関数ポインタに変換するGoランタイムの内部関数です。

変更前は、同じGoの関数であれば、常に同じコールバックエントリポイントを再利用していました。しかし、stdcallcdeclではスタックのクリーンアップの責任が異なるため、同じ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·compilecallbackcleanstack引数にfalseを渡して呼び出します。これは、生成されるコールバックがcdecl規約であり、呼び出し側がスタックをクリーンアップすることをランタイムに伝えます。

対照的に、既存のNewCallbacktrueを渡します。

func NewCallback(fn Eface) (code uintptr) {
	code = (uintptr)runtime·compilecallback(fn, true);
}

これにより、NewCallbackstdcall規約のコールバックを生成します。

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の機能が正しく動作することを検証するために非常に重要です。 追加されたテストコードは、以下のようなことを行います。

  1. C言語DLLの動的生成: cbDLLFunccbDLL構造体を使用して、stdcallcdeclの両方の呼び出し規約を持つC言語の関数をエクスポートするDLLを動的に生成します。これにより、実際のCコードからGoのコールバックが呼び出されるシナリオをシミュレートします。
  2. Goコールバック関数の定義: cbFuncsマップには、様々な引数の数を持つGoの関数が定義されています。これらの関数は、CのDLLから呼び出されるコールバックとして機能します。
  3. テストケースの実行: TestStdcallAndCDeclCallbacks関数は、生成されたDLLをロードし、syscall.NewCallbacksyscall.NewCallbackCDeclを使ってGoの関数をコールバックとして登録します。その後、DLL内のC関数を呼び出し、Goのコールバックが正しい引数で、かつ適切な呼び出し規約で呼び出されることを検証します。

この包括的なテストスイートは、NewCallbackCDeclの導入が既存のNewCallbackの動作に影響を与えず、両方の呼び出し規約がWindows環境で正しく機能することを保証します。

関連リンク

参考にした情報源リンク