[インデックス 17192] ファイルの概要
このコミットは、Go言語のcgo
ツールにおけるC言語の関数ポインタのサポートを追加するものです。具体的には、以下のファイルが変更されています。
misc/cgo/test/cgo_test.go
:cgo
のテストスイートに新しいテストケースTestFpVar
が追加されました。misc/cgo/test/fpvar.go
: C言語の関数ポインタ変数を扱うcgo
のテストケースが新規に作成されました。このファイルには、Cのtypedef
で定義された関数ポインタ型intFunc
、その関数ポインタを呼び出すC関数bridge_int_func
、およびテスト用のC関数fortytwo
が含まれています。Go側からはこれらのC関数を呼び出し、関数ポインタの挙動を確認しています。src/cmd/cgo/doc.go
:cgo
のドキュメントが更新され、C言語の関数ポインタ変数をGoとCの間で受け渡し、Cコードから呼び出す方法に関する説明とサンプルコードが追加されました。src/cmd/cgo/gcc.go
:cgo
の主要な処理ロジックが含まれるファイルで、関数ポインタ変数のサポートのために大幅な変更が加えられました。特に、rewriteRef
関数内で関数ポインタが式として使用される場合の処理が追加され、新しいfpvar
というName
のKind
が導入されました。また、式としてのみ使用される関数に対してはブリッジコードの生成を避けるロジックが追加されています。src/cmd/cgo/main.go
:cgo
ツールのエントリポイントおよび主要なデータ構造が定義されているファイルです。Name
構造体に新しいKind
として"fpvar"
が追加され、関数ポインタ変数を識別できるようになりました。また、IsVar
ヘルパー関数が追加され、"var"
と"fpvar"
の両方を変数として扱えるようになりました。src/cmd/cgo/out.go
:cgo
がGoとCの間のブリッジコードを生成する部分が含まれるファイルです。writeDefs
関数がfpvar
型の変数を適切に処理するように修正され、関数ポインタ変数のGo側での宣言方法が調整されました。
コミット
cmd/cgo: Add support for C function pointers
* Add a new kind of Name, "fpvar" which stands for function pointer variable
* When walking the AST, find functions used as expressions and create a new Name object for them
* Track functions which are only used in expr contexts, and avoid generating bridge code for them
R=golang-dev, minux.ma, fullung, rsc, iant
CC=golang-dev
https://golang.org/cl/9835047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c18dc11ef210ca91b251996ff2a2546d0bcde848
元コミット内容
cmd/cgo: Add support for C function pointers
このコミットは、Go言語のcgo
ツールにC言語の関数ポインタのサポートを追加します。
主な変更点は以下の通りです。
Name
構造体に新しい種類として「fpvar」(関数ポインタ変数)を追加。- AST(抽象構文木)を走査する際に、式として使用される関数を見つけ、それらに対して新しい
Name
オブジェクトを作成。 - 式コンテキストでのみ使用される関数を追跡し、それらに対するブリッジコードの生成を回避。
変更の背景
Go言語は、cgo
ツールを通じてC言語のコードと相互運用する機能を提供しています。しかし、このコミット以前は、C言語の関数ポインタをGoコード内で直接的かつ柔軟に扱うための完全なサポートが不足していました。特に、Cの関数ポインタをGoの変数に代入したり、GoからC関数に引数として渡したりする際に、cgo
が適切に型変換やブリッジコードの生成を行う必要がありました。
従来のcgo
では、Cの関数をGoから呼び出すためのブリッジコードは生成されていましたが、Cの関数ポインタをGoの式の中で利用する(例えば、Cの関数ポインタをGoの変数に代入して、それを別のC関数に渡す)といったシナリオが十分に考慮されていませんでした。このような場合、cgo
は関数ポインタを通常の関数呼び出しと誤解釈し、不必要なブリッジコードを生成したり、コンパイルエラーを引き起こしたりする可能性がありました。
このコミットの目的は、Cの関数ポインタをGoのコードベースでより自然に、かつ安全に扱えるようにすることです。具体的には、Cの関数ポインタをGoの変数として宣言し、GoとCの間で受け渡し、Cコードからその関数ポインタを呼び出すといったユースケースをサポートするために、cgo
の内部処理を強化することが背景にあります。これにより、Goプログラムが既存のCライブラリと連携する際の柔軟性と表現力が向上します。
前提知識の解説
このコミットの理解には、以下の概念に関する前提知識が役立ちます。
1. Go言語のcgo
ツール
cgo
は、GoプログラムがC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Goのソースファイル内に特別なコメントブロック(import "C"
の直前)でCのコードを記述することで、cgo
がGoとCの間の相互運用に必要なブリッジコードを自動生成します。
- GoからCの呼び出し: Goコード内で
C.funcName()
のように記述することで、Cの関数を呼び出すことができます。cgo
は、Goの型とCの型の間で適切な変換を行い、Goの呼び出し規約とCの呼び出し規約の間のギャップを埋めるコードを生成します。 - CからGoの呼び出し(コールバック): Goの関数に
//export FuncName
というディレクティブを付与することで、そのGo関数をCから呼び出せるようにエクスポートできます。cgo
は、Cから呼び出し可能なラッパー関数を生成し、CのコードがGoの関数をコールバックとして利用できるようにします。
2. C言語の関数ポインタ
C言語において、関数ポインタは関数を指すポインタ変数です。これにより、関数を他の関数の引数として渡したり、データ構造に格納したり、実行時に呼び出す関数を動的に決定したりすることが可能になります。
例:
typedef int (*IntFunc)(int, int); // intを2つ引数にとり、intを返す関数へのポインタ型
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
IntFunc op;
op = add; // 関数ポインタにadd関数を代入
int result = op(5, 3); // opを通じてadd関数を呼び出す (結果は8)
op = subtract; // 関数ポインタにsubtract関数を代入
result = op(5, 3); // opを通じてsubtract関数を呼び出す (結果は2)
return 0;
}
3. GoとCの間の相互運用における課題
GoとCは異なるメモリモデル、型システム、呼び出し規約を持っています。cgo
はこれらの違いを吸収しますが、特にポインタやメモリ管理に関しては注意が必要です。
- ポインタの安全性: Goのガベージコレクタはメモリを移動させる可能性がありますが、Cは安定したメモリアドレスを期待します。GoのポインタをCに渡す際には、Goのポインタが他のGoのポインタを含まないこと、C関数がGoのポインタのコピーを保持しないことなど、厳格なルールがあります。
runtime/cgo.Handle
のようなメカニズムは、Goの値を安全にCに渡し、CからGoに返すためのIDとして機能します。 - 型変換: Goの
string
やslice
はCの文字列や配列とは異なる内部表現を持っています。cgo
はこれらの変換を支援しますが、手動でのメモリ解放(C.free
など)が必要になる場合があります。 - 関数ポインタの直接呼び出しの制限: GoからCの関数ポインタを直接呼び出すことは、このコミット以前は困難でした。GoのランタイムとガベージコレクタがGoの関数を管理するため、Goの関数のアドレスは安定しておらず、Cの関数ポインタと直接互換性がありませんでした。このため、Cのラッパー関数を介して呼び出すなどの回避策が必要でした。
このコミットは、特にCの関数ポインタをGoの式として扱う際のcgo
の内部的な処理を改善し、よりシームレスな相互運用を可能にすることを目指しています。
技術的詳細
このコミットの技術的詳細は、cgo
がC言語の関数ポインタをGoのコードベースでどのように扱うかを改善した点に集約されます。
-
新しい
Name
のKind
「fpvar」の導入:cgo
は、Cのシンボル(変数、関数、型など)をGoのコードにマッピングする際に、内部的にName
という構造体を使用します。このName
構造体には、シンボルの種類を示すKind
フィールドがあります。このコミットでは、Cの関数ポインタ変数を識別するために、新たに"fpvar"
というKind
が追加されました。これにより、cgo
は通常の変数("var"
)と関数ポインタ変数("fpvar"
)を区別して処理できるようになります。 -
AST走査時の関数ポインタ変数の識別と
Name
オブジェクトの生成:cgo
はGoのソースコードを解析し、抽象構文木(AST)を構築します。このASTを走査する際に、Cの関数が式(expression)として使用されているケースを検出するロジックがsrc/cmd/cgo/gcc.go
のrewriteRef
関数に追加されました。 以前は、C.funcName
が式として現れた場合、cgo
は「C.funcName
は呼び出す必要があります」というエラーを発生させていました。これは、cgo
がC.funcName
を常にC関数への呼び出しとして解釈していたためです。 しかし、Cの関数ポインタを扱う場合、C.funcName
が関数ポインタ変数として扱われることがあります(例:f := C.intFunc(C.fortytwo)
のように、Cの関数fortytwo
のアドレスをGoの変数f
に代入するケース)。 このコミットでは、C.funcName
が式コンテキストで現れた場合、それが関数ポインタ変数として扱われるべきであると判断し、その関数ポインタ変数に対応する新しいName
オブジェクト(Kind
が"fpvar"
)を動的に作成するようになりました。この新しいName
オブジェクトは、Go側ではunsafe.Pointer
型として表現され、C側ではvoid*
として扱われます。 -
式コンテキストでのみ使用される関数のブリッジコード生成の回避:
cgo
は、GoからC関数を呼び出すために、GoとCの間の呼び出し規約を変換する「ブリッジコード」を生成します。しかし、Cの関数がGoのコード内で関数ポインタ変数としてのみ使用され、直接呼び出されない場合、その関数に対するブリッジコードは不要です。 このコミットでは、rewriteRef
関数内で、各C関数が実際にGoから呼び出されているかどうかを追跡するメカニズムが導入されました。具体的には、functions
というマップを使用して、C関数がcall
コンテキスト(つまり、Goから実際に呼び出されている)で使用された場合にフラグを立てます。rewriteRef
関数の最後に、このfunctions
マップを走査し、call
コンテキストで一度も使用されなかったC関数(つまり、式コンテキストでのみ使用された関数ポインタ変数)については、f.Name
マップからそのName
オブジェクトを削除します。これにより、cgo
は不要なブリッジコードの生成を回避し、生成されるコードの効率性を向上させます。 -
out.go
におけるfpvar
の適切な処理:src/cmd/cgo/out.go
のwriteDefs
関数は、Go側でCの変数や関数ポインタに対応する定義を生成する役割を担っています。この関数が、新しい"fpvar"
型のName
オブジェクトを適切に処理するように修正されました。 具体的には、"var"
型の変数と"fpvar"
型の関数ポインタ変数の両方をn.IsVar()
ヘルパー関数で識別し、それぞれに応じたGoの型("var"
は*n.Type.Go
、"fpvar"
はn.Type.Go
)で宣言されるように調整されました。これにより、Go側でCの関数ポインタ変数がunsafe.Pointer
として正しく表現されるようになります。また、C側でこれらの変数へのポインタを取得する際に、"fpvar"
の場合は&
演算子を付けないように修正され、Cの関数ポインタが直接void*
として扱われるようになりました。
これらの変更により、cgo
はCの関数ポインタをGoのコード内でより柔軟に、かつ効率的に扱えるようになり、GoとCの間の相互運用性が向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルと関数に集中しています。
-
src/cmd/cgo/main.go
:Name
構造体のKind
フィールドのコメントに"fpvar"
が追加されました。- 新しいメソッド
IsVar() bool
がName
構造体に追加されました。これは、Kind
が"var"
または"fpvar"
の場合にtrue
を返します。
// src/cmd/cgo/main.go type Name struct { // ... Kind string // "const", "type", "var", "fpvar", "func", "not-type" // ... } // IsVar returns true if Kind is either "var" or "fpvar" func (n *Name) IsVar() bool { return n.Kind == "var" || n.Kind == "fpvar" }
-
src/cmd/cgo/gcc.go
:rewriteRef
関数内で、C関数が式として使用される場合の処理が大幅に変更されました。functions
マップが導入され、Goから実際に呼び出されたC関数を追跡するようになりました。r.Name.Kind == "func"
かつr.Context == "expr"
の場合、つまりC関数が式として使用されている場合(関数ポインタとして扱われる場合)、新しいName
オブジェクト(Kind: "fpvar"
)が動的に作成され、r.Name
がこの新しいName
オブジェクトに置き換えられます。rewriteRef
関数の最後に、functions
マップを走査し、Goから一度も呼び出されなかったC関数(式としてのみ使用された関数ポインタ変数)のName
オブジェクトをf.Name
から削除するロジックが追加されました。
// src/cmd/cgo/gcc.go - rewriteRef function (抜粋) func (p *Package) rewriteRef(f *File) { // Keep a list of all the functions, to remove the ones // only used as expressions and avoid generating bridge // code for them. functions := make(map[string]bool) // ... (既存のコード) for _, r := range f.Ref { // ... case "call": // ... functions[r.Name.Go] = true // 関数が呼び出されたことを記録 // ... case "expr": if r.Name.Kind == "func" { // Function is being used in an expression, to e.g. pass around a C function pointer. // Create a new Name for this Ref which causes the variable to be declared in Go land. fpName := "fp_" + r.Name.Go name := f.Name[fpName] if name == nil { name = &Name{ Go: fpName, C: r.Name.C, Kind: "fpvar", // 新しいKind "fpvar" を設定 Type: &Type{Size: p.PtrSize, Align: p.PtrSize, C: c("void*"), Go: ast.NewIdent("unsafe.Pointer")}, } p.mangleName(name) f.Name[fpName] = name } r.Name = name expr = ast.NewIdent(name.Mangle) } else if r.Name.Kind == "type" { // ... } else if r.Name.Kind == "var" { // ... } // ... } // Remove functions only used as expressions, so their respective // bridge functions are not generated. for name, used := range functions { if !used { delete(f.Name, name) // 呼び出されなかった関数は削除 } } }
-
src/cmd/cgo/out.go
:writeDefs
関数内で、Name
オブジェクトが変数であるかどうかのチェックにn.IsVar()
が使用されるようになりました。"var"
と"fpvar"
のKind
に応じて、Go側で生成される型宣言が分岐するようになりました。"var"
の場合はポインタ型(&
と*
)が、"fpvar"
の場合は直接の型が使用されます。
// src/cmd/cgo/out.go - writeDefs function (抜粋) func (p *Package) writeDefs() { // ... for _, key := range nameKeys(p.Name) { n := p.Name[key] if !n.IsVar() { // n.IsVar() を使用 continue } // ... var amp string var node ast.Node if n.Kind == "var" { amp = "&" node = &ast.StarExpr{X: n.Type.Go} } else if n.Kind == "fpvar" { node = n.Type.Go } else { panic(fmt.Errorf("invalid var kind %q", n.Kind)) } if *gccgo { fmt.Fprintf(fc, `extern void *%s __asm__("%s.%s");`, n.Mangle, gccgoSymbolPrefix, n.Mangle) fmt.Fprintf(&gccgoInit, "\\t%s = %s%s;\\n", n.Mangle, amp, n.C) // amp を使用 } else { fmt.Fprintf(fc, "void *·%s = %s%s;\\n", n.Mangle, amp, n.C) // amp を使用 } fmt.Fprintf(fc, "\\n") fmt.Fprintf(fgo2, "var %s ", n.Mangle) conf.Fprint(fgo2, fset, node) // node を使用 fmt.Fprintf(fgo2, "\\n") } // ... }
これらの変更は、cgo
がCの関数ポインタをGoの型システムにマッピングし、不要なブリッジコードの生成を回避するための基盤を構築しています。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、cgo
がC言語の関数ポインタをGoのコード内でより適切に扱うための内部的なメカニズムを確立しています。
1. Name
構造体とIsVar()
メソッド (src/cmd/cgo/main.go
)
-
Name
構造体のKind
フィールドへの"fpvar"
追加:cgo
は、Cのシンボル(関数、変数、型など)をGoのコードに変換する際に、それらの情報をName
構造体で管理します。Kind
フィールドは、そのシンボルが何であるかを示します。このコミット以前は、Cの関数ポインタ変数を明確に区別するKind
がありませんでした。"fpvar"
(function pointer variable)の追加により、cgo
はCの関数ポインタ変数を特別な種類として認識し、それに応じた処理を適用できるようになりました。 -
IsVar()
メソッドの導入: このヘルパーメソッドは、Name
オブジェクトが通常の変数("var"
)であるか、関数ポインタ変数("fpvar"
)であるかを簡潔にチェックするために導入されました。これにより、cgo
の他の部分で、これら両方の種類の変数を一括して処理する必要がある場合に、コードの可読性と保守性が向上します。例えば、Go側でCの変数に対応する宣言を生成する際に、通常の変数と関数ポインタ変数の両方を対象とすることができます。
2. rewriteRef
関数における関数ポインタの処理 (src/cmd/cgo/gcc.go
)
rewriteRef
関数は、GoのASTを走査し、C.xxx
形式の参照をGoの等価な表現に書き換えるcgo
の核心部分です。
-
式コンテキストでのC関数の検出と
fpvar
への変換: 最も重要な変更点の一つは、C.funcName
がcall
コンテキスト(関数呼び出し)ではなく、expr
コンテキスト(式)で現れた場合の処理です。 以前は、C.funcName
が式として使われるとエラーになっていました。しかし、Cの関数ポインタをGoの変数に代入するような場合(例:f := C.intFunc(C.fortytwo)
)、C.fortytwo
は関数ポインタとして扱われるべきであり、直接呼び出されるわけではありません。 この変更により、cgo
はC.funcName
がexpr
コンテキストで使用されていることを検出すると、そのC関数に対応する新しいName
オブジェクトを動的に作成し、そのKind
を"fpvar"
に設定します。この新しいName
オブジェクトは、Go側ではunsafe.Pointer
として表現され、C側ではvoid*
として扱われます。これにより、GoのコードがCの関数ポインタをGoの変数として安全に保持し、他のC関数に渡すことが可能になります。 -
不要なブリッジコード生成の回避:
cgo
は、GoからC関数を呼び出すためにブリッジコードを生成します。しかし、Cの関数がGoのコード内で関数ポインタとしてのみ使用され、直接呼び出されない場合、その関数に対するブリッジコードは不要です。functions
マップは、Goから実際に呼び出されたC関数を追跡するために使用されます。rewriteRef
関数の処理が完了した後、このマップをチェックし、一度もcall
コンテキストで使用されなかったC関数(つまり、関数ポインタ変数としてのみ参照された関数)のName
オブジェクトをf.Name
から削除します。これにより、cgo
はこれらの関数に対するブリッジコードの生成をスキップし、生成されるコードのサイズと複雑さを削減します。これは、コンパイル時間と最終的なバイナリサイズの両方に良い影響を与えます。
3. writeDefs
関数におけるfpvar
の出力 (src/cmd/cgo/out.go
)
writeDefs
関数は、cgo
がGoとCの間の相互運用に必要な定義(変数宣言など)を生成する場所です。
"var"
と"fpvar"
の型宣言の区別: この関数は、n.IsVar()
を使用して、処理中のName
オブジェクトが通常の変数か関数ポインタ変数かを判断します。- 通常の変数(
n.Kind == "var"
)の場合、Go側ではそのC変数のポインタ型(例:var _Cvar_myVar *C.int
)として宣言されます。これは、Cの変数のアドレスをGoのポインタとして扱うためです。C側では、その変数のアドレスを取得するために&
演算子が付加されます(例:void *·_Cvar_myVar = &myVar;
)。 - 関数ポインタ変数(
n.Kind == "fpvar"
)の場合、Go側ではunsafe.Pointer
型(例:var _Cfpvar_fortytwo unsafe.Pointer
)として宣言されます。これは、Cの関数ポインタがGoのunsafe.Pointer
にマッピングされるためです。C側では、関数ポインタ自体がアドレスであるため、&
演算子は不要です(例:void *·_Cfpvar_fortytwo = fortytwo;
)。
- 通常の変数(
この変更により、cgo
はCの関数ポインタ変数をGoの型システムに正しくマッピングし、GoとCの間で安全かつ効率的に受け渡しできるようになりました。全体として、このコミットはcgo
のC言語関数ポインタサポートを大幅に強化し、より複雑なCライブラリとの連携を可能にしています。
関連リンク
- Go CL 9835047: https://golang.org/cl/9835047
- GitHubコミットページ: https://github.com/golang/go/commit/c18dc11ef210ca91b251996ff2a2546d0bcde848
参考にした情報源リンク
- Go's cgo tool allows interoperability between Go and C code, enabling Go programs to call C functions and C code to call Go functions. This includes handling C function pointers, though with specific considerations due to the differences in Go's and C's memory models and type systems.
- Go's
cgo
tool and its handling of C function pointers, including workarounds for calling C function pointers from Go and passing Go functions as callbacks to C. - The challenges of interfacing Go with C, particularly regarding memory management and pointer safety, and the role of
cgo.Handle
. - Stack Overflow discussion on passing Go functions as C callbacks, highlighting the use of
//export
andcgo.Handle
. - Official Go documentation on
cgo
and the//export
directive. - Detailed explanation of
cgo
and callbacks, including the use ofcgo.Handle
for safe data passing. - Go 1.6 pointer passing rules for
cgo
. - Further details on
cgo
pointer rules. - YouTube video discussing
cgo
type conversions and memory management.