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

[インデックス 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がサポートする様々なアーキテクチャや最適化を利用できるという利点があります。しかし、gcgccgoでは、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側で取得・設定するためのメカニズムです。

技術的詳細

このコミットの主要な技術的課題は、gccgogcとは異なる方法でCgoのラッパー関数を処理する必要がある点にありました。特に、gccgogcが使用するcgocallのような直接的なメカニズムをサポートしていなかったため、GoとCの間の呼び出し規約やシンボル解決に調整が必要でした。

変更の核心は以下の2点に集約されます。

  1. 変数シンボルのマングル名の調整: gccgoでは、Cコードから参照されるGoの変数がグローバルシンボルとしてエクスポートされる必要があります。このため、_Cvar_のようなプレフィックスではなく、Cvar_のようなプレフィックスを使用するようにマングル名を変更しています。これは、CコンパイラがGoの変数を正しくリンクできるようにするためです。
  2. 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つのファイルを変更しています。

  1. src/cmd/cgo/gcc.go:

    • rewriteRef関数内で、gccgoを使用する場合の変数シンボルのマングル名(n.Mangle)の生成ロジックが変更されました。n.Kind == "var"の場合に、プレフィックスが_CからCに変更されています。
    • これは、gccgoが生成するCコードがGoの変数をグローバルシンボルとして参照できるようにするためです。
  2. 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からのコールバックを適切に処理できるようになります。
    • 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関数を呼び出すためのラッパー関数を生成します。

  1. syscall.Cgocall()syscall.CgocallDone()を呼び出すことで、GoランタイムスケジューラにC関数呼び出しの開始と終了を通知します。これにより、スケジューラはCコードの実行中にゴルーチンの状態を適切に管理できます。
  2. n.AddErrorが真の場合(C関数がerrnoを設定する可能性がある場合)、syscall.SetErrno(0)errnoをクリアし、C関数呼び出し後にsyscall.GetErrno()errnoを取得します。もしerrnoが0でなければ、それをGoのエラーとして返します。
  3. 実際の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ラッパー関数を生成します。

  1. syscall.CgocallBack()syscall.CgocallBackDone()を呼び出すことで、GoランタイムスケジューラにCからのコールバックの開始と終了を通知します。これにより、スケジューラはCコードがGo関数を呼び出している間、Goランタイムの状態を適切に管理できます。
  2. このラッパー関数は、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言語の公式ドキュメント
  • GCCの公式ドキュメント
  • Go言語のソースコード(特にsrc/runtimeディレクトリ)
  • Go言語のメーリングリストやIssueトラッカー(golang.org/cl/6812058など)
  • Go言語のCgoに関する技術記事やブログポスト