[インデックス 14286] ファイルの概要
このコミットは、Go言語のcmd/cgo
ツールにおけるgccgo
サポートの改善を目的としています。具体的には、Goランタイムスケジューラとの連携を強化するためにラッパー関数を使用し、gccgo
環境下でのCgoテストの合格を目指しています。
コミット
- コミットハッシュ:
3b04d23cbfdb1c6a868d4ca4f264a8136376bf13
- Author: Ian Lance Taylor iant@golang.org
- Date: Thu Nov 1 11:21:30 2012 -0700
- Commit Message:
cmd/cgo: improve gccgo support Use wrapper functions to tell scheduler what we are doing. With this patch, and a separate patch to the go tool, all the cgo tests pass with gccgo. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/6812058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3b04d23cbfdb1c6a868d4ca4f264a8136376bf13
元コミット内容
cmd/cgo: improve gccgo support
Use wrapper functions to tell scheduler what we are doing.
With this patch, and a separate patch to the go tool, all the
cgo tests pass with gccgo.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6812058
変更の背景
Go言語は、C言語のコードをGoプログラムから呼び出すためのcgo
ツールを提供しています。Goの公式コンパイラ(gc
)とは別に、GCC(GNU Compiler Collection)をバックエンドとして使用するgccgo
という代替コンパイラが存在します。gccgo
は、既存のGCC最適化やツールチェーンとの統合といった利点を提供しますが、Goランタイムとの連携においてgc
とは異なる課題を抱えることがあります。
このコミットの背景には、gccgo
環境下でcgo
が生成するコードが、Goランタイムスケジューラと適切に連携できていなかったという問題があります。特に、C関数を呼び出す際にGoスケジューラにその状況を通知しないと、デッドロックやパフォーマンスの問題が発生する可能性がありました。このコミットは、gccgo
でCgoテストがすべてパスするように、この連携を改善することを目的としています。コミットメッセージにある「separate patch to the go tool」は、go
コマンド自体にもgccgo
との連携を改善するための変更が必要であったことを示唆しています。
前提知識の解説
cgo
cgo
は、GoプログラムからC言語の関数を呼び出したり、C言語のプログラムからGoの関数を呼び出したりするためのGoツールです。Goのソースコード内にimport "C"
という特殊なインポート文を記述し、その直前のコメントブロックにC言語のコードを記述することで、GoとCの相互運用を可能にします。cgo
は、GoとCの間の型変換や、GoランタイムとCランタイムの間の連携を処理するためのラッパーコードを生成します。
gccgo
gccgo
は、Go言語のプログラムをコンパイルするための代替コンパイラです。Goの公式コンパイラであるgc
とは異なり、GCCのフロントエンドとして実装されています。これにより、GCCがサポートする様々なアーキテクチャや最適化を利用できるという利点があります。しかし、gc
とgccgo
では、Goランタイムの実装やCとの連携方法に細かな違いがあるため、cgo
が生成するコードもそれぞれのコンパイラに合わせて調整する必要があります。
GoランタイムスケジューラとCの相互運用
Go言語は独自のランタイムとスケジューラを持っており、ゴルーチン(軽量スレッド)の並行実行を管理しています。GoのコードがC関数を呼び出す際、GoスケジューラはCコードが実行されている間、そのゴルーチンがブロックされることを認識する必要があります。これにより、スケジューラは他のゴルーチンを効率的に実行したり、必要に応じてOSスレッドを解放したりすることができます。
もしGoスケジューラがC呼び出しを認識しないと、以下のような問題が発生する可能性があります。
- デッドロック: Cコードが長時間実行され、Goスケジューラがそのゴルーチンをブロックされた状態と認識しない場合、他のゴルーチンが実行されず、システム全体が停止する可能性があります。
- パフォーマンスの低下: C呼び出し中にGoスケジューラが不必要にOSスレッドを保持し続けると、リソースの無駄遣いやパフォーマンスの低下につながります。
- リソースリーク: CコードがGoランタイムのリソースを適切に解放しない場合、メモリリークなどの問題が発生する可能性があります。
syscall.Cgocall()
とsyscall.CgocallDone()
は、GoランタイムスケジューラにC関数呼び出しの開始と終了を通知するための内部関数です。これらの関数を適切に呼び出すことで、スケジューラはCコードの実行中にゴルーチンの状態を正しく管理し、効率的な並行処理を維持できます。同様に、syscall.SetErrno()
とsyscall.GetErrno()
は、C関数呼び出しで設定されたerrno
(エラー番号)をGo側で取得・設定するためのメカニズムです。
技術的詳細
このコミットの主要な技術的課題は、gccgo
がgc
とは異なる方法でCgoのラッパー関数を処理する必要がある点にありました。特に、gccgo
はgc
が使用するcgocall
のような直接的なメカニズムをサポートしていなかったため、GoとCの間の呼び出し規約やシンボル解決に調整が必要でした。
変更の核心は以下の2点に集約されます。
- 変数シンボルのマングル名の調整:
gccgo
では、Cコードから参照されるGoの変数がグローバルシンボルとしてエクスポートされる必要があります。このため、_Cvar_
のようなプレフィックスではなく、Cvar_
のようなプレフィックスを使用するようにマングル名を変更しています。これは、CコンパイラがGoの変数を正しくリンクできるようにするためです。 - Cgo呼び出しのラッパー関数の導入:
gccgo
環境下でC関数を呼び出す際に、Goランタイムスケジューラに通知するためのラッパー関数をGo側で生成するように変更されました。これにより、syscall.Cgocall()
とsyscall.CgocallDone()
が適切に呼び出され、スケジューラがCコードの実行を認識できるようになります。また、C関数がerrno
を設定した場合に、Go側でそれを取得し、Goのエラーとして返すための処理もこのラッパー関数内で実装されています。
さらに、GoからCへ、CからGoへの関数呼び出し(エクスポートされたGo関数をCから呼び出す場合)においても、syscall.CgocallBack()
とsyscall.CgocallBackDone()
を呼び出すラッパー関数をGo側に生成することで、GoスケジューラがCからのコールバックを適切に処理できるようにしています。
コアとなるコードの変更箇所
このコミットは主に以下の2つのファイルを変更しています。
-
src/cmd/cgo/gcc.go
:rewriteRef
関数内で、gccgo
を使用する場合の変数シンボルのマングル名(n.Mangle
)の生成ロジックが変更されました。n.Kind == "var"
の場合に、プレフィックスが_C
からC
に変更されています。- これは、
gccgo
が生成するCコードがGoの変数をグローバルシンボルとして参照できるようにするためです。
-
src/cmd/cgo/out.go
:writeDefs
関数内で、gccgo
を使用する場合の変数初期化のためのinit
関数がCコードに生成されるようになりました。これにより、Goの変数がCコードから参照される際に正しく初期化されます。writeDefsFunc
関数内で、gccgo
を使用する場合のC関数呼び出しのGoラッパー関数の生成ロジックが大幅に変更されました。- 以前は
//extern %s
ディレクティブを使ってC関数を直接Goにインポートしていましたが、新しい実装ではGoのラッパー関数を生成し、その中でsyscall.Cgocall()
とsyscall.CgocallDone()
を呼び出すようになりました。 n.AddError
がtrueの場合(C関数がerrno
を設定する可能性がある場合)、syscall.SetErrno(0)
でerrno
をクリアし、C関数呼び出し後にsyscall.GetErrno()
でerrno
を取得し、Goのエラーとして返す処理が追加されました。
- 以前は
writeGccgoExports
関数内で、GoからCへエクスポートされる関数のラッパー生成ロジックが変更されました。- CからGoの関数を呼び出す際に、Go側で
syscall.CgocallBack()
とsyscall.CgocallBackDone()
を呼び出すラッパー関数が生成されるようになりました。これにより、GoスケジューラがCからのコールバックを適切に処理できるようになります。
- CからGoの関数を呼び出す際に、Go側で
gccgoSymbolPrefix
という新しいヘルパー関数が追加され、gccgo
で使用するシンボルプレフィックスを決定するロジックがカプセル化されました。
コアとなるコードの解説
src/cmd/cgo/gcc.go
の変更
// Old code
// if n.Mangle == "" {
// n.Mangle = "_C" + n.Kind + "_" + n.Go
// }
// New code
if n.Mangle == "" {
// When using gccgo variables have to be
// exported so that they become global symbols
// that the C code can refer to.
prefix := "_C"
if *gccgo && n.Kind == "var" {
prefix = "C" // Changed from _C to C for gccgo variables
}
n.Mangle = prefix + n.Kind + "_" + n.Go
}
この変更は、gccgo
コンパイラを使用している場合に、Goの変数のマングル名(Cgoが生成するCコードから参照されるシンボル名)のプレフィックスを_C
からC
に変更しています。これは、gccgo
が生成するCコードがGoの変数をグローバルシンボルとして正しくリンクできるようにするために必要です。
src/cmd/cgo/out.go
の変更 (Cgo呼び出しラッパー)
// Inside func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name)
if *gccgo {
fmt.Fprint(fgo2, "\n")
cname := fmt.Sprintf("_cgo%s%s", cPrefix, n.Mangle)
paramnames := []string(nil)
for i, param := range d.Type.Params.List {
paramName := fmt.Sprintf("p%d", i)
param.Names = []*ast.Ident{ast.NewIdent(paramName)}
paramnames = append(paramnames, paramName)
}
conf.Fprint(fgo2, fset, d) // Original Go function signature
fmt.Fprint(fgo2, " {\n")
fmt.Fprint(fgo2, "\tdefer syscall.CgocallDone()\n") // Notify scheduler of C call end
fmt.Fprint(fgo2, "\tsyscall.Cgocall()\n") // Notify scheduler of C call start
if n.AddError {
fmt.Fprint(fgo2, "\tsyscall.SetErrno(0)\n") // Clear errno before C call
}
fmt.Fprint(fgo2, "\t")
if !void {
fmt.Fprint(fgo2, "r := ")
}
fmt.Fprintf(fgo2, "%s(%s)\n", cname, strings.Join(paramnames, ", ")) // Call the actual C function
if n.AddError {
fmt.Fprint(fgo2, "\te := syscall.GetErrno()\n") // Get errno after C call
fmt.Fprint(fgo2, "\tif e != 0 {\n")
fmt.Fprint(fgo2, "\t\treturn ")
if !void {
fmt.Fprint(fgo2, "r, ")
}
fmt.Fprint(fgo2, "e\n") // Return error if errno is set
fmt.Fprint(fgo2, "\t}\n")
fmt.Fprint(fgo2, "\treturn ")
if !void {
fmt.Fprint(fgo2, "r, ")
}
fmt.Fprint(fgo2, "nil\n") // Return nil error
} else if !void {
fmt.Fprint(fgo2, "\treturn r\n")
}
fmt.Fprint(fgo2, "}\n")
// Declare the C function (extern)
fmt.Fprintf(fgo2, "//extern %s\n", n.C)
d.Name = ast.NewIdent(cname)
if n.AddError {
l := d.Type.Results.List
d.Type.Results.List = l[:len(l)-1] // Remove error return type for the C declaration
}
conf.Fprint(fgo2, fset, d)
fmt.Fprint(fgo2, "\n")
return
}
このコードブロックは、gccgo
を使用している場合に、GoからC関数を呼び出すためのラッパー関数を生成します。
syscall.Cgocall()
とsyscall.CgocallDone()
を呼び出すことで、GoランタイムスケジューラにC関数呼び出しの開始と終了を通知します。これにより、スケジューラはCコードの実行中にゴルーチンの状態を適切に管理できます。n.AddError
が真の場合(C関数がerrno
を設定する可能性がある場合)、syscall.SetErrno(0)
でerrno
をクリアし、C関数呼び出し後にsyscall.GetErrno()
でerrno
を取得します。もしerrno
が0でなければ、それをGoのエラーとして返します。- 実際のC関数呼び出しは、
cname
という内部的な名前で行われます。元のGoの関数シグネチャは、このラッパー関数として機能します。
src/cmd/cgo/out.go
の変更 (Goエクスポート関数ラッパー)
// Inside func (p *Package) writeGccgoExports(fgo2, fc, fm *os.File)
// ...
// For gccgo we use a wrapper function in Go, in order
// to call CgocallBack and CgocallBackDone.
// ...
fmt.Fprint(fgo2, "\n")
fmt.Fprintf(fgo2, "func %s(", goName) // goName is Cgoexp_OriginalGoFunctionName
// ... parameter handling ...
fmt.Fprintf(fgo2, ")")
// ... result handling ...
fmt.Fprint(fgo2, " {\n")
fmt.Fprint(fgo2, "\tsyscall.CgocallBack()\n") // Notify scheduler of C callback start
fmt.Fprint(fgo2, "\tdefer syscall.CgocallBackDone()\n") // Notify scheduler of C callback end
fmt.Fprint(fgo2, "\t")
if resultCount > 0 {
fmt.Fprint(fgo2, "return ")
}
if fn.Recv != nil {
fmt.Fprint(fgo2, "recv.")
}
fmt.Fprintf(fgo2, "%s(", exp.Func.Name) // Call the actual Go function
// ... parameter passing ...
fmt.Fprint(fgo2, ")\n")
fmt.Fprint(fgo2, "}\n")
このコードブロックは、CからGoにエクスポートされた関数を呼び出すためのGoラッパー関数を生成します。
syscall.CgocallBack()
とsyscall.CgocallBackDone()
を呼び出すことで、GoランタイムスケジューラにCからのコールバックの開始と終了を通知します。これにより、スケジューラはCコードがGo関数を呼び出している間、Goランタイムの状態を適切に管理できます。- このラッパー関数は、Cから受け取った引数を元のGo関数に渡し、その結果をCに返します。
src/cmd/cgo/out.go
の追加関数
// Return the package prefix when using gccgo.
func (p *Package) gccgoSymbolPrefix() string {
if !*gccgo {
return ""
}
clean := func(r rune) rune {
switch {
case 'A' <= r && r <= 'Z', 'a' <= r && r <= 'z',
'0' <= r && r <= '9':
return r
}
return '_'
}
if *gccgopkgpath != "" {
return strings.Map(clean, *gccgopkgpath)
}
if *gccgoprefix == "" && p.PackageName == "main" {
return "main"
}
prefix := strings.Map(clean, *gccgoprefix)
if prefix == "" {
prefix = "go"
}
return prefix + "." + p.PackageName
}
この新しいヘルパー関数は、gccgo
が使用するシンボルプレフィックスを決定します。これは、Goパッケージのパスや名前から、CコードがGoのシンボルを参照する際に使用する一意のプレフィックスを生成するために使用されます。これにより、シンボル名の衝突を防ぎ、CとGoの間の正しいリンクを保証します。
関連リンク
- Go言語のCgoドキュメント: https://pkg.go.dev/cmd/cgo
- gccgoプロジェクトページ: https://gcc.gnu.org/onlinedocs/gccgo/
- Goランタイムスケジューラに関する一般的な情報: https://go.dev/doc/effective_go#concurrency
参考にした情報源リンク
- Go言語の公式ドキュメント
- GCCの公式ドキュメント
- Go言語のソースコード(特に
src/runtime
ディレクトリ) - Go言語のメーリングリストやIssueトラッカー(
golang.org/cl/6812058
など) - Go言語のCgoに関する技術記事やブログポスト