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

[インデックス 16874] ファイルの概要

このコミットは、Go言語のcmd/cgoツールにおけるgccgoコンパイラ関連の修正を目的としています。具体的には、C言語の関数をGoから呼び出す際の挙動、特に特殊なプロローグ関数(CString, GoStringなど)の扱い、import "C"コメント内で定義された静的関数のサポート、そしてgcコンパイラに依存するテストの無効化が含まれています。

コミット

  • コミットハッシュ: d9d3debee5fef63e2cec74937c452001c9e1bbcc
  • Author: Ian Lance Taylor iant@golang.org
  • Date: Thu Jul 25 09:53:57 2013 -0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d9d3debee5fef63e2cec74937c452001c9e1bbcc

元コミット内容

cmd/cgo: gccgo fixes
    
Don't require a full-scale callback for calls to the special
prologue functions.

Always use a simple wrapper function for C functions, so that
we can handle static functions defined in the import "C"
comment.

Disable a test that relies on gc-specific function names.

Fixes #5905.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11406047

変更の背景

このコミットは、cgoツールがgccgoコンパイラと連携する際のいくつかの問題に対処するために行われました。主な背景は以下の通りです。

  1. 特殊なプロローグ関数の最適化: cgoが生成するコードには、Goの文字列をCの文字列に変換するCStringや、Cの文字列をGoの文字列に変換するGoStringなどの特殊なヘルパー関数(プロローグ関数)が含まれています。これらの関数は内部的なものであり、通常のC関数呼び出しとは異なる軽量な処理が望まれます。以前の実装では、これらの関数呼び出しに対してもsyscall.Cgocall()syscall.CgocallDone()といったフルスケールのコールバックメカニズムが適用されており、オーバーヘッドが生じていました。このコミットは、これらの特殊関数に対してはより効率的なパスを使用するように変更しています。

  2. import "C"内の静的関数のサポート: cgoでは、Goのソースコード内でimport "C"ブロックを使用することで、C言語のコードを直接記述し、Goから呼び出すことができます。このCコード内でstaticキーワードを使って定義された関数は、そのファイル内でのみ可視となります。しかし、cgoがGoとCの間の橋渡しをする際に、これらの静的関数を直接参照することが困難な場合がありました。特にgccgoのような異なるコンパイルモデルを持つ環境では、この問題が顕著でした。このコミットは、すべてのC関数(静的関数を含む)に対してシンプルなラッパー関数を生成することで、この問題を解決し、import "C"コメント内の静的関数もGoから適切に呼び出せるようにしています。

  3. コンパイラ固有のテストの調整: Goには主に2つのコンパイラ実装があります。公式のgc(Go Compiler)と、GCCをバックエンドとするgccgoです。一部のテストは、特定のコンパイラが生成する関数名やスタックトレースの形式に依存している場合があります。misc/cgo/test/callback.go内のtestCallbackCallersテストがまさにその一例で、gcコンパイラが生成する正確な関数名に依存していました。gccgoでは異なる関数名が生成されるため、このテストが失敗する可能性がありました。このコミットでは、gccgo環境ではこのテストをスキップするように変更し、コンパイラ間の差異によるテストの失敗を回避しています。

  4. Issue #5905の修正: コミットメッセージにFixes #5905と記載されていますが、Goの公開Issueトラッカーではこの番号のIssueは見つかりませんでした。これは内部的なIssue番号であるか、あるいは別のプロジェクトのIssueを参照している可能性があります。しかし、コミット内容から判断すると、上記のgccgo関連の挙動の修正がこのIssueの解決に繋がったと考えられます。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. cgo:

    • cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。
    • Goのソースファイルにimport "C"という特別なインポート文を記述し、その直前のコメントブロックにC言語のコードを記述することで、GoとCの相互運用が可能になります。
    • cgoは、GoとCの間の型変換や関数呼び出しの橋渡しをするためのグルーコード(スタブ関数やラッパー関数)を生成します。
  2. Goコンパイラ (gc vs. gccgo):

    • gc (Go Compiler): Go言語の公式かつ主要なコンパイラです。Go言語で書かれており、Goのランタイムと密接に連携しています。高速なコンパイルと最適化が特徴です。
    • gccgo: GCC (GNU Compiler Collection) のフロントエンドとしてGo言語をサポートするコンパイラです。GCCの最適化パスを利用できるため、生成されるバイナリのパフォーマンスが異なる場合があります。gcとは異なるコード生成戦略やランタイムの統合方法を持つため、cgoのような低レベルな相互運用においては、両者で挙動の差異が生じることがあります。
  3. import "C"コメントと静的関数:

    • Goのソースファイル内でimport "C"の直前に記述されたCコードは、cgoによってコンパイルされ、Goプログラムにリンクされます。
    • このCコード内でstaticキーワードを使って関数を定義すると、その関数はそのCファイル内でのみ可視となり、外部からは直接参照できません。
    • cgoがGoからこれらの静的関数を呼び出すためのコードを生成する際、直接参照できないために問題が発生することがありました。
  4. syscall.Cgocall()syscall.CgocallDone():

    • これらはGoのsyscallパッケージに含まれる内部的な関数で、GoからC関数を呼び出す際、またはCからGo関数を呼び出す際に、Goランタイムのスケジューラと連携し、Goルーチンの状態を適切に管理するために使用されます。
    • 特に、CコードがGoルーチンをブロックする可能性がある場合や、GoルーチンがCコードからコールバックされるような複雑なシナリオで、Goランタイムがデッドロックやスケジューリングの問題を回避するために重要な役割を果たします。
    • これらの関数は、GoルーチンがCコードに移行する前とCコードから戻った後に呼び出され、GoスケジューラにGoルーチンの状態変化を通知します。
  5. ラッパー関数 (Wrapper Function):

    • ある関数を呼び出す際に、その関数を直接呼び出すのではなく、間に別の小さな関数を挟むことがあります。この挟む関数をラッパー関数と呼びます。
    • ラッパー関数は、元の関数呼び出しの前後に特定の処理(ログ記録、エラーハンドリング、引数の変換など)を追加したり、元の関数のインターフェースを調整したりするために使用されます。
    • cgoの文脈では、GoからC関数を呼び出す際に、C側でGoランタイムとの連携や引数の調整を行うための小さなC関数がラッパーとして生成されることがあります。

技術的詳細

このコミットにおける技術的な変更は、主にsrc/cmd/cgo/out.gomisc/cgo/test/callback.goの2つのファイルに集中しています。

src/cmd/cgo/out.goの変更

このファイルはcgoツールの中核であり、GoとCの間の相互運用に必要なグルーコードを生成する役割を担っています。

  1. プロローグ関数のコールバック抑制:

    • CString, GoString, GoStringN, GoBytesといった特殊な組み込み関数(プロローグ関数)に対して、inPrologというフラグが導入されました。
    • これらの関数をGoから呼び出す際に生成されるGo側のラッパー関数において、if !inPrologという条件が追加され、defer syscall.CgocallDone()syscall.Cgocall()の呼び出しがスキップされるようになりました。
    • これにより、これらの軽量なプロローグ関数呼び出しにおいて、Goランタイムのスケジューラとの連携のためのフルスケールなコールバックメカニズムが不要になり、オーバーヘッドが削減されます。
  2. C関数のラッパー生成の統一:

    • 以前は、gccgoを使用している場合、C関数に対するラッパーは生成されないというロジックがありました (if *gccgo { // we don't use wrappers with gccgo. return })。
    • このコミットにより、このロジックが変更され、gccgoの場合でもp.writeGccgoOutputFunc(fgcc, n)が呼び出されるようになりました。これは、gccgoでもC関数に対するラッパーが常に生成されることを意味します。
    • この変更の目的は、import "C"コメント内で定義されたstatic関数をGoから呼び出せるようにすることです。static関数は外部から直接参照できないため、cgoが生成するC側のラッパー関数を介して呼び出すことで、この問題を回避します。ラッパー関数は、Goから呼び出される公開されたシンボルとして機能し、その内部でstatic関数を呼び出します。
    • また、Go側からC関数を呼び出す際の//extern宣言も変更されました。プロローグ関数以外では、//extern _cgo%s%sという形式で、cgoが生成するラッパー関数名を参照するように変更されています。これにより、Goは直接C関数を呼び出すのではなく、cgoが生成したラッパーを介して呼び出すことになります。
  3. writeGccgoOutputFuncの追加:

    • この新しい関数は、gccgoコンパイラ向けにC言語のラッパー関数を生成します。
    • 生成されるラッパー関数は非常にシンプルで、引数を受け取り、元のC関数を呼び出し、その戻り値を返します。
    • ポインタ型の場合、警告を避けるためにvoid*へのキャストが追加されています。
    • このラッパーの存在により、gccgo環境下でもimport "C"コメント内の静的関数がGoから呼び出し可能になります。

misc/cgo/test/callback.goの変更

このファイルはcgoのコールバックメカニズムをテストするためのものです。

  1. testCallbackCallersテストのスキップ:
    • testCallbackCallers関数に、if runtime.Compiler != "gc" { t.Skip("skipping for non-gc toolchain") }という行が追加されました。
    • このテストは、スタックトレース内の正確な関数名に依存しており、これはgcコンパイラとgccgoコンパイラで異なる可能性があります。
    • runtime.Compilerは現在のGoプログラムがどのコンパイラでビルドされたかを示す文字列(例: "gc", "gccgo")を返します。
    • この変更により、gccgoなどのgc以外のコンパイラでビルドされた場合、このテストはスキップされ、コンパイラ間の差異によるテストの失敗が回避されます。

これらの変更は、cgoが異なるGoコンパイラ(特にgccgo)環境下でより堅牢かつ効率的に動作するようにするための重要な改善です。

コアとなるコードの変更箇所

misc/cgo/test/callback.go

--- a/misc/cgo/test/callback.go
+++ b/misc/cgo/test/callback.go
@@ -143,6 +143,10 @@ func testBlocking(t *testing.T) {
 // Test that the stack can be unwound through a call out and call back
 // into Go.
 func testCallbackCallers(t *testing.T) {
+	if runtime.Compiler != "gc" {
+		// The exact function names are not going to be the same.
+		t.Skip("skipping for non-gc toolchain")
+	}
 	pc := make([]uintptr, 100)
 	n := 0
 	name := []string{

src/cmd/cgo/out.go

--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -321,6 +321,9 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 		Type: gtype,
 	}
 
+	// Builtins defined in the C prolog.
+	inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes"
+
 	if *gccgo {
 		// Gccgo style hooks.
 		fmt.Fprint(fgo2, "\n")
@@ -334,8 +337,10 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 
 		conf.Fprint(fgo2, fset, d)
 		fmt.Fprint(fgo2, " {\n")
-		fmt.Fprint(fgo2, "\tdefer syscall.CgocallDone()\n")
-		fmt.Fprint(fgo2, "\tsyscall.Cgocall()\n")
+		if !inProlog {
+			fmt.Fprint(fgo2, "\tdefer syscall.CgocallDone()\n")
+			fmt.Fprint(fgo2, "\tsyscall.Cgocall()\n")
+		}
 		if n.AddError {
 			fmt.Fprint(fgo2, "\tsyscall.SetErrno(0)\n")
 		}
@@ -366,7 +371,11 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 		fmt.Fprint(fgo2, "}\n")
 
 		// declare the C function.
-		fmt.Fprintf(fgo2, "//extern %s\n", n.C)
+		if inProlog {
+			fmt.Fprintf(fgo2, "//extern %s\n", n.C)
+		} else {
+			fmt.Fprintf(fgo2, "//extern _cgo%s%s\n", cPrefix, n.Mangle)
+		}
 	d.Name = ast.NewIdent(cname)
 	if n.AddError {
 		l := d.Type.Results.List
@@ -380,8 +389,7 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 	conf.Fprint(fgo2, fset, d)
 	fmt.Fprint(fgo2, "\n")
 
-	if name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes" {
-		// The builtins are already defined in the C prolog.
+	if inProlog {
 		return
 	}
 
@@ -469,7 +477,7 @@ func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) {
 	p.Written[name] = true
 
 	if *gccgo {
-		// we don't use wrappers with gccgo.
+		// we don't use wrappers with gccgo.
+		p.writeGccgoOutputFunc(fgcc, n)
 		return
 	}
 
@@ -526,6 +534,54 @@ func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) {
 	fmt.Fprintf(fgcc, "\n")
 }
 
+// Write out a wrapper for a function when using gccgo.  This is a
+// simple wrapper that just calls the real function.  We only need a
+// wrapper to support static functions in the prologue--without a
+// wrapper, we can't refer to the function, since the reference is in
+// a different file.
+func (p *Package) writeGccgoOutputFunc(fgcc *os.File, n *Name) {
+	if t := n.FuncType.Result; t != nil {
+		fmt.Fprintf(fgcc, "%s\n", t.C.String())
+	} else {
+		fmt.Fprintf(fgcc, "void\n")
+	}
+	fmt.Fprintf(fgcc, "_cgo%s%s(", cPrefix, n.Mangle)
+	for i, t := range n.FuncType.Params {
+		if i > 0 {
+			fmt.Fprintf(fgcc, ", ")
+		}
+		c := t.Typedef
+		if c == "" {
+			c = t.C.String()
+		}
+		fmt.Fprintf(fgcc, "%s p%d", c, i)
+	}
+	fmt.Fprintf(fgcc, ")\n")
+	fmt.Fprintf(fgcc, "{\n")
+	fmt.Fprintf(fgcc, "\t")
+	if t := n.FuncType.Result; t != nil {
+		fmt.Fprintf(fgcc, "return ")
+		// Cast to void* to avoid warnings due to omitted qualifiers.
+		if c := t.C.String(); c[len(c)-1] == '*' {
+			fmt.Fprintf(fgcc, "(void*)")
+		}
+	}
+	fmt.Fprintf(fgcc, "%s(", n.C)
+	for i, t := range n.FuncType.Params {
+		if i > 0 {
+			fmt.Fprintf(fgcc, ", ")
+		}
+		// Cast to void* to avoid warnings due to omitted qualifiers.
+		if c := t.C.String(); c[len(c)-1] == '*' {
+			fmt.Fprintf(fgcc, "(void*)")
+		}
+		fmt.Fprintf(fgcc, "p%d", i)
+	}
+	fmt.Fprintf(fgcc, ");\n")
+	fmt.Fprintf(fgcc, "}\n")
+	fmt.Fprintf(fgcc, "\n")
+}
+
 // Write out the various stubs we need to support functions exported
 // from Go so that they are callable from C.
 func (p *Package) writeExports(fgo2, fc, fm *os.File) {

コアとなるコードの解説

misc/cgo/test/callback.go

  • testCallbackCallers関数の変更:
    • if runtime.Compiler != "gc" { t.Skip("skipping for non-gc toolchain") }という条件分岐が追加されました。
    • これは、runtime.Compiler変数の値が"gc"(公式Goコンパイラ)でない場合、つまりgccgoなどの他のコンパイラが使用されている場合に、現在のテストケースをスキップすることを意味します。
    • コメントにあるように、このテストはスタックトレース内の正確な関数名に依存しており、コンパイラによって生成される関数名が異なるため、gc以外の環境ではテストが失敗する可能性がありました。この変更により、テストの安定性が向上します。

src/cmd/cgo/out.go

  • inProlog変数の導入と条件付きコールバック:

    • inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes"という行で、Goの組み込みプロローグ関数(C文字列とGo文字列の変換など)を識別するためのブール変数inPrologが定義されました。
    • GoからC関数を呼び出す際に生成されるGo側のラッパー関数内で、if !inProlog { ... }という条件が追加され、defer syscall.CgocallDone()syscall.Cgocall()の呼び出しがこの条件の下でのみ実行されるようになりました。
    • これにより、CStringなどの特殊なプロローグ関数を呼び出す際には、Goランタイムのスケジューラとの連携のためのオーバーヘッドの高いコールバック処理がスキップされ、より効率的な呼び出しが可能になります。
  • C関数宣言の変更とラッパーの利用:

    • C関数をGoから呼び出す際の//extern宣言が変更されました。
    • inPrologtrueの場合(プロローグ関数)、以前と同様に//extern %s(元のC関数名)が使用されます。
    • しかし、inPrologfalseの場合(通常のC関数)、//extern _cgo%s%sという形式が使用されるようになりました。これは、cgoが生成するC側のラッパー関数(_cgoプレフィックスを持つ)を参照することを意味します。
    • この変更により、Goは直接C関数を呼び出すのではなく、cgoが生成したラッパー関数を介してC関数を呼び出すようになります。これにより、import "C"コメント内で定義されたstatic関数など、直接参照できないC関数もGoから呼び出すことが可能になります。
  • gccgoにおけるラッパー生成の強制:

    • writeOutputFunc関数内で、if *gccgo { ... }ブロックの処理が変更されました。
    • 以前はgccgoの場合、ラッパーを生成せずにreturnしていましたが、このコミットではp.writeGccgoOutputFunc(fgcc, n)を呼び出すように変更されました。
    • これは、gccgoコンパイラを使用している場合でも、すべてのC関数に対してラッパー関数を生成することを強制します。これにより、gccgo環境下でのimport "C"コメント内の静的関数のサポートが実現されます。
  • writeGccgoOutputFunc関数の追加:

    • この新しい関数は、gccgo向けにC言語のラッパーコードを生成します。
    • 生成されるC関数は、_cgoプレフィックスとマングルされた名前を持ち、引数を受け取り、元のC関数(n.Cで指定される)を呼び出し、その結果を返します。
    • 戻り値や引数がポインタ型の場合、void*への明示的なキャストが追加されており、これはコンパイラの警告を避けるための一般的なC言語の慣習です。
    • このラッパー関数が、GoとstaticなC関数の間の橋渡し役となります。

これらの変更は、cgoの内部的な動作を最適化し、特にgccgoコンパイラとの互換性と機能性を向上させるためのものです。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分(diff)
  • Go言語のcgoに関する一般的な知識
  • Go言語のコンパイラ(gcgccgo)に関する一般的な知識
  • Goのsyscallパッケージに関する一般的な知識
  • Go Issue 5905については、公開されているGoのIssueトラッカーでは見つかりませんでした。