[インデックス 12660] ファイルの概要
このコミットは、Go言語のツールチェインにおいて、cgo (C言語との相互運用を可能にするツール) がgccgo (GCCベースのGoコンパイラ) と連携する際の機能強化とバグ修正を目的としています。具体的には、gccgoでコンパイルされたGo関数をC言語からエクスポートして呼び出す機能の追加と、go testコマンドがpackage mainのテストをgccgoで実行する際の問題を解決しています。
コミット
commit 3211b2cca98936e94dc2e819dbe474337ecdd24e
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Mar 15 23:50:25 2012 +0100
cmd/cgo: add support for function export for gccgo.
A "gccgoprefix" flag is added and used by the go tool,
to mirror the -fgo-prefix flag for gccgo, whose value
is required to know how to access functions from C.
Trying to export Go methods or unexported Go functions
will not work.
Also fix go test on "main" packages.
Updates #2313.
Fixes #3262.
R=mpimenov, rsc, iant
CC=golang-dev
https://golang.org/cl/5797046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3211b2cca98936e94dc2e819dbe474337ecdd24e
元コミット内容
cmd/cgo: gccgo向けにGo関数のエクスポートをサポートする機能を追加。
go toolによって使用される"gccgoprefix"フラグが追加され、gccgoの-fgo-prefixフラグをミラーリングします。この値は、C言語からGo関数にアクセスする方法を知るために必要です。
GoのメソッドやエクスポートされていないGo関数をエクスポートしようとしても機能しません。
また、"main"パッケージに対するgo testの修正も行います。
Issue #2313 を更新。 Issue #3262 を修正。
変更の背景
このコミットは、Go言語のツールチェインにおけるcgoとgccgoの連携を改善するという、より大きな目標の一部です。
-
cgoとgccgoの互換性向上 (Issue #2313): 従来のGoコンパイラ(gc)とcgoの組み合わせでは、Go関数をC言語から呼び出す(エクスポートする)機能が提供されていました。しかし、gccgoは異なるコンパイルモデルとシンボル命名規則を持つため、この機能が直接利用できませんでした。Issue #2313は、cgoがgccgo環境でも同様の機能を提供できるようにするための包括的な課題でした。このコミットは、その課題に対する具体的な解決策の一つとして、gccgoにおけるGo関数のエクスポート機能を追加しています。これにより、gccgoを使用する開発者も、C言語とのより深い相互運用性を享受できるようになります。 -
go testとgccgoのpackage main問題の解決 (Issue #3262):go testコマンドは、テスト対象のパッケージがpackage mainである場合に、特定の挙動を示します。gccgoでpackage mainのテストをコンパイル・実行する際に、内部的なリンケージやシンボル解決の問題が発生していました。Issue #3262は、この特定のシナリオにおけるgo testの不具合を報告しており、このコミットはその問題を修正することで、gccgo環境でのテストの信頼性を向上させています。
これらの変更は、Go言語のツールチェインが多様なコンパイラ環境(特にgccgo)においても一貫した機能と安定性を提供できるようにするための重要なステップです。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
-
Go言語の
cgo:cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoツールです。GoとCの間の橋渡しをします。- Goのソースファイル内に
import "C"という行を記述し、その後にCのコードを直接埋め込むことで、Cの関数をGoから呼び出したり、Goの関数をCから呼び出せるようにエクスポートしたりできます。 cgoは、GoとCの間の型変換やメモリ管理の調整を行います。
-
gcとgccgo:gc: Go言語の公式コンパイラであり、通常go buildコマンドで使用されるデフォルトのコンパイラです。Goのソースコードを直接機械語にコンパイルします。gccgo: GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。gcとは異なるコンパイルバックエンドを使用し、GCCの最適化やリンケージ機能を利用できます。これにより、既存のC/C++ライブラリとの連携が容易になる場合がありますが、gcとは異なる挙動を示すこともあります。
-
シンボルとリンケージ:
- コンパイルされたプログラムでは、関数や変数などの実体は「シンボル」として表現されます。
- 異なるコンパイル単位(例: GoのオブジェクトファイルとCのオブジェクトファイル)間でシンボルを参照する場合、リンカがそれらを解決(リンケージ)する必要があります。
gccgoは、Goの関数シンボルに特定のプレフィックス(例:go_やfake_)を付与することがあり、C言語からこれらの関数を呼び出す際には、このプレフィックスを考慮する必要があります。
-
package mainと実行可能ファイル:- Go言語において、
package mainは実行可能プログラムのエントリポイント(main関数)を含むパッケージを指します。 - 通常、
go buildでpackage mainをビルドすると、単一の実行可能ファイルが生成されます。 - テストにおいては、
go testコマンドはテスト対象のパッケージをコンパイルし、テスト用の実行可能ファイルを生成します。package mainのテストは、通常のライブラリパッケージのテストとは異なるリンケージの考慮が必要になる場合があります。
- Go言語において、
-
Go関数のエクスポート:
- Goの関数をC言語から呼び出すには、その関数がGo側でエクスポートされている必要があります(関数名が大文字で始まる)。
cgoは、エクスポートされたGo関数に対応するCのヘッダファイルとスタブコードを生成し、CコードがGo関数を呼び出せるようにします。
技術的詳細
このコミットの技術的詳細は、主にcgoツールとgoコマンドがgccgo環境でGo関数をCにエクスポートする際の挙動を調整することにあります。
-
gccgoprefixフラグの導入:src/cmd/cgo/main.goに-gccgoprefixという新しいフラグが追加されました。このフラグは、gccgoが生成するGoシンボルのプレフィックスを指定するために使用されます。デフォルト値は"go"です。gccgoは、Goの関数をコンパイルする際に、そのシンボル名に特定のプレフィックスを付与します(例:go_pkgname.FuncName)。C言語からこれらの関数を呼び出すためには、C側でこのプレフィックスを含んだ正しいシンボル名を参照する必要があります。- このフラグは、
goコマンドがcgoを呼び出す際に、gccgoの-fgo-prefixフラグと連携して使用されます。
-
writeGccgoExports関数の実装:src/cmd/cgo/out.goにwriteGccgoExportsという新しい関数が追加されました。この関数は、gccgoを使用する場合にGo関数をCにエクスポートするためのCヘッダファイル(_cgo_export.h)とCソースファイル(_cgo_export.c)を生成します。- この関数は、エクスポートされるGo関数の名前、引数、戻り値の型を解析し、対応するCの関数宣言を生成します。
- 特に重要なのは、生成されるCの関数宣言に
__asm__属性を使用して、gccgoが生成する実際のGoシンボル名(gccgoSymbolPrefix.PackageName.FuncNameの形式)をリンカに伝える点です。これにより、CコードがGo関数を正しいシンボル名で参照できるようになります。 - 複数の戻り値を持つGo関数については、C側で対応する構造体(
struct { ... }_result)を定義し、その構造体を戻り値とするC関数としてエクスポートするロジックが実装されています。 - GoのメソッドやエクスポートされていないGo関数は、
gccgoではエクスポートできないという制約がコードに反映されています(fatalf("cannot export unexported function %s with gccgo", fn.Name))。
-
goコマンドのビルドロジックの変更:src/cmd/go/build.goにおいて、gccgcToolchain(gccgoを使用するツールチェイン)のビルドロジックが更新されました。gc関数(Goソースをコンパイルする部分)では、gccgoPrefixヘルパー関数が導入され、パッケージの種類(mainパッケージかライブラリか、fakeパッケージか)に応じて適切な-fgo-prefixフラグがgccgoに渡されるようになりました。これにより、gccgoが生成するシンボル名が適切に制御されます。cgo関数(cgoツールを呼び出す部分)では、gccgoツールチェインが使用されている場合に、新しく追加された-gccgoフラグと-gccgoprefixフラグがcgoツールに渡されるようになりました。これにより、cgoはgccgo向けの特別なエクスポート処理を実行するようになります。
-
go testのpackage main修正:src/cmd/go/pkg.goにforceLibraryという新しいフィールドがPackage構造体に追加されました。これは、たとえパッケージ名が"main"であっても、そのパッケージをライブラリとして扱うべきかどうかを示すフラグです。src/cmd/go/test.goにおいて、テスト用の合成パッケージ(ptest)を準備する際に、ptest.forceLibrary = trueが設定されるようになりました。これにより、go testがpackage mainのテストをビルドする際に、そのテストパッケージがライブラリとして扱われ、gccgoでのリンケージ問題が回避されます。- また、テストのメイン実行ファイルとなる合成パッケージの
build.PackageのNameフィールドが"main"に設定されるようになりました。これは、go testが最終的に実行可能ファイルを生成する際の整合性を保つためです。
これらの変更により、cgoはgccgo環境でGo関数をCにエクスポートする機能を獲得し、go testはgccgoとpackage mainの組み合わせで安定して動作するようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/cgo/main.go:--- a/src/cmd/cgo/main.go +++ b/src/cmd/cgo/main.go @@ -136,6 +136,7 @@ var cdefs = flag.Bool("cdefs", false, "for bootstrap: write C definitions for C var objDir = flag.String("objdir", "", "object directory") var gccgo = flag.Bool("gccgo", false, "generate files for use with gccgo") +var gccgoprefix = flag.String("gccgoprefix", "go", "prefix of symbols generated by gccgo") var importRuntimeCgo = flag.Bool("import_runtime_cgo", true, "import runtime/cgo in generated code") var goarch, goos stringgccgoprefixという新しいコマンドラインフラグが追加されました。
-
src/cmd/cgo/out.go:--- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -107,7 +107,11 @@ func (p *Package) writeDefs() { } } - p.writeExports(fgo2, fc, fm) + if *gccgo { + p.writeGccgoExports(fgo2, fc, fm) + } else { + p.writeExports(fgo2, fc, fm) + } fgo2.Close() fc.Close() @@ -624,6 +628,83 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) { } } +// Write out the C header allowing C code to call exported gccgo functions. +func (p *Package) writeGccgoExports(fgo2, fc, fm *os.File) { + fgcc := creat(*objDir + "_cgo_export.c") + fgcch := creat(*objDir + "_cgo_export.h") + _ = fgcc + + fmt.Fprintf(fgcch, "/* Created by cgo - DO NOT EDIT. */\\n") + fmt.Fprintf(fgcch, "%s\\n", p.Preamble) + fmt.Fprintf(fgcch, "%s\\n", gccExportHeaderProlog) + fmt.Fprintf(fm, "#include \\\"_cgo_export.h\\\"\\n") + + clean := func(r rune) rune { + switch { + case 'A' <= r && r <= 'Z', 'a' <= r && r <= 'z', + '0' <= r && r <= '9': + return r + } + return '_' + } + gccgoSymbolPrefix := strings.Map(clean, *gccgoprefix) + + for _, exp := range p.ExpFunc { + // TODO: support functions with receivers. + fn := exp.Func + fntype := fn.Type + + if !ast.IsExported(fn.Name.Name) { + fatalf("cannot export unexported function %s with gccgo", fn.Name) + } + + cdeclBuf := new(bytes.Buffer) + resultCount := 0 + forFieldList(fntype.Results, + func(i int, atype ast.Expr) { resultCount++ }) + switch resultCount { + case 0: + fmt.Fprintf(cdeclBuf, "void") + case 1: + forFieldList(fntype.Results, + func(i int, atype ast.Expr) { + t := p.cgoType(atype) + fmt.Fprintf(cdeclBuf, "%s", t.C) + }) + default: + // Declare a result struct. + fmt.Fprintf(fgcch, "struct %s_result {\\n", exp.ExpName) + forFieldList(fntype.Results, + func(i int, atype ast.Expr) { + t := p.cgoType(atype) + fmt.Fprintf(fgcch, "\\t%s r%d;\\n", t.C, i) + }) + fmt.Fprintf(fgcch, "};\\n") + fmt.Fprintf(cdeclBuf, "struct %s_result", exp.ExpName) + } + + // The function name. + fmt.Fprintf(cdeclBuf, " "+exp.ExpName) + gccgoSymbol := fmt.Sprintf("%s.%s.%s", gccgoSymbolPrefix, p.PackageName, exp.Func.Name) + fmt.Fprintf(cdeclBuf, " (") + // Function parameters. + forFieldList(fntype.Params, + func(i int, atype ast.Expr) { + if i > 0 { + fmt.Fprintf(cdeclBuf, ", ") + } + t := p.cgoType(atype) + fmt.Fprintf(cdeclBuf, "%s p%d", t.C, i) + }) + fmt.Fprintf(cdeclBuf, ")") + cdecl := cdeclBuf.String() + + fmt.Fprintf(fgcch, "extern %s __asm__(\\\"%s\\\");\\n", cdecl, gccgoSymbol) + // Dummy declaration for _cgo_main.c + fmt.Fprintf(fm, "%s {}\\n", cdecl) + } +} + // Call a function for each entry in an ast.FieldList, passing the // index into the list and the type. func forFieldList(fl *ast.FieldList, fn func(int, ast.Expr)) {gccgoフラグが有効な場合にwriteGccgoExportsを呼び出す条件分岐が追加されました。writeGccgoExports関数が新規追加され、gccgo向けのエクスポートヘッダとスタブコードを生成します。
-
src/cmd/go/build.go:--- a/src/cmd/go/build.go +++ b/src/cmd/go/build.go @@ -661,7 +661,7 @@ func (b *builder) build(a *action) (err error) { } cgoExe := tool("cgo") - if a.cgo != nil { + if a.cgo != nil && a.cgo.target != "" { cgoExe = a.cgo.target } outGo, outObj, err := b.cgo(a.p, cgoExe, obj, gccfiles) @@ -1239,12 +1239,8 @@ func (gccgcToolchain) gc(b *builder, p *Package, obj string, importArgs []string out := p.Name + ".o" ofile = obj + out gcargs := []string{"-g"} - if p.Name != "main" { - if p.fake { - gcargs = append(gcargs, "-fgo-prefix=fake_"+p.ImportPath) - } else { - gcargs = append(gcargs, "-fgo-prefix=go_"+p.ImportPath) - } + if prefix := gccgoPrefix(p); prefix != "" { + gcargs = append(gcargs, "-fgo-prefix="+gccgoPrefix(p)) } args := stringList("gccgo", importArgs, "-c", gcargs, "-o", ofile, buildGccgoflags) for _, f := range gofiles { @@ -1304,6 +1300,16 @@ func (gccgcToolchain) cc(b *builder, p *Package, objdir, ofile, cfile string) er "-DGOOS_"+goos, "-DGOARCH_"+goarch, "-c", cfile) } +func gccgoPrefix(p *Package) string { + switch { + case p.build.IsCommand() && !p.forceLibrary: + return "" + case p.fake: + return "fake_" + p.ImportPath + } + return "go_" + p.ImportPath +} + // gcc runs the gcc C compiler to create an object from a single C file. func (b *builder) gcc(p *Package, out string, flags []string, cfile string) error { cfile = mkAbs(p.Dir, cfile) @@ -1404,6 +1410,9 @@ func (b *builder) cgo(p *Package, cgoExe, obj string, gccfiles []string) (outGo,\ } if _, ok := buildToolchain.(gccgcToolchain); ok { cgoflags = append(cgoflags, "-gccgo") + if prefix := gccgoPrefix(p); prefix != "" { + cgoflags = append(cgoflags, "-gccgoprefix="+gccgoPrefix(p)) + } } if err := b.run(p.Dir, p.ImportPath, cgoExe, "-objdir", obj, cgoflags, "--", cgoCFLAGS, p.CgoFiles); err != nil { return nil, nil, errgccgoPrefixヘルパー関数が追加され、gccgoの-fgo-prefixを決定します。gc関数内でgccgoPrefixを使用して-fgo-prefixを渡すように変更されました。cgo関数内でgccgoツールチェインの場合に-gccgoと-gccgoprefixフラグをcgoに渡すように変更されました。
-
src/cmd/go/pkg.go:--- a/src/cmd/go/pkg.go +++ b/src/cmd/go/pkg.go @@ -64,16 +64,17 @@ type Package struct { XTestImports []string `json:",omitempty"` // imports from XTestGoFiles // Unexported fields are not part of the public API. - build *build.Package - pkgdir string // overrides build.PkgDir - imports []*Package - deps []*Package - gofiles []string // GoFiles+CgoFiles+TestGoFiles+XTestGoFiles files, absolute paths - target string // installed file for this package (may be executable) - fake bool // synthesized package - forceBuild bool // this package must be rebuilt - local bool // imported via local path (./ or ../) - localPrefix string // interpret ./ and ../ imports relative to this prefix + build *build.Package + pkgdir string // overrides build.PkgDir + imports []*Package + deps *Package + gofiles []string // GoFiles+CgoFiles+TestGoFiles+XTestGoFiles files, absolute paths + target string // installed file for this package (may be executable) + fake bool // synthesized package + forceBuild bool // this package must be rebuilt + forceLibrary bool // this package is a library (even if named "main") + local bool // imported via local path (./ or ../) + localPrefix string // interpret ./ and ../ imports relative to this prefix } func (p *Package) copyBuild(pp *build.Package) {Package構造体にforceLibraryフィールドが追加されました。
-
src/cmd/go/test.go:--- a/src/cmd/go/test.go +++ b/src/cmd/go/test.go @@ -446,6 +446,7 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,\ ptest.imports = append(append([]*Package{}, p.imports...), imports...) ptest.pkgdir = testDir ptest.fake = true + ptest.forceLibrary = true ptest.Stale = true ptest.build = new(build.Package) *ptest.build = *p.build @@ -489,7 +490,7 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,\ ImportPath: "testmain", Root: p.Root, imports: []*Package{ptest}, - build: &build.Package{}, + build: &build.Package{Name: "main"}, fake: true, Stale: true, }- テストパッケージの準備時に
ptest.forceLibrary = trueが設定されるようになりました。 - テストのメイン実行ファイルとなる合成パッケージの
build.PackageのNameが"main"に設定されるようになりました。
- テストパッケージの準備時に
コアとなるコードの解説
src/cmd/cgo/out.go の writeGccgoExports 関数
この関数は、gccgoコンパイラを使用する際に、Goの関数をC言語から呼び出せるようにするためのCヘッダファイル(_cgo_export.h)とCソースファイル(_cgo_export.c)を生成する中心的なロジックを含んでいます。
-
ファイル生成:
_cgo_export.cと_cgo_export.hという2つのファイルを作成します。これらはCgoによって生成される中間ファイルであり、CコードがGo関数を呼び出すためのインターフェースを提供します。
-
シンボルプレフィックスの処理:
gccgoprefixフラグ(src/cmd/cgo/main.goで定義)の値を取得し、strings.Map(clean, *gccgoprefix)を使って、シンボル名として安全な形式に変換します。例えば、"go"というプレフィックスはそのまま"go"として使われます。- このプレフィックスは、
gccgoがGo関数に付与する実際のシンボル名の一部となります。
-
Go関数のエクスポートループ:
p.ExpFunc(エクスポート対象のGo関数リスト)をイテレートします。- エクスポート可能チェック:
!ast.IsExported(fn.Name.Name)で、Goの関数名が大文字で始まっているか(エクスポートされているか)を確認します。エクスポートされていない関数をgccgoでエクスポートしようとすると、fatalfでエラーを発生させます。これは、Goの言語仕様とgccgoの制約に準拠するためです。 - Cの関数宣言生成:
cdeclBufというbytes.Bufferを使って、Cの関数宣言(戻り値の型、関数名、引数リスト)を構築します。- 戻り値の処理:
- 戻り値がない場合 (
resultCount == 0) は、Cのvoid型を使用します。 - 戻り値が1つの場合 (
resultCount == 1) は、そのGoの型に対応するCの型(p.cgoType(atype).C)を使用します。 - 戻り値が複数ある場合 (
resultCount > 1) は、C側で結果を保持するための構造体(例:struct MyFunc_result { ... };)を_cgo_export.hに定義し、その構造体を戻り値とするC関数として宣言します。これは、C言語が複数の戻り値を直接サポートしないためです。
- 戻り値がない場合 (
- 関数名:
exp.ExpName(エクスポートされるGo関数の名前)をCの関数名として使用します。 gccgoシンボル名:fmt.Sprintf("%s.%s.%s", gccgoSymbolPrefix, p.PackageName, exp.Func.Name)を使って、gccgoが生成する実際のGo関数のシンボル名(例:go.mypackage.MyFunction)を構築します。- 引数の処理: Go関数の引数リストをイテレートし、それぞれのGoの型に対応するCの型と仮引数名(例:
int p0,char* p1)を生成します。 __asm__属性:fmt.Fprintf(fgcch, "extern %s __asm__(\\\"%s\\\");\\n", cdecl, gccgoSymbol)という行が非常に重要です。これはGCCの拡張機能である__asm__属性を使用しており、Cの関数宣言(cdecl)が、実際にはgccgoSymbolという名前のシンボルにリンクされるべきであることをリンカに指示します。これにより、Cコードは通常のC関数呼び出し構文でGo関数を呼び出すことができますが、リンカは内部的にgccgoが生成したGoシンボルに解決します。- ダミー宣言:
fmt.Fprintf(fm, "%s {}\\n", cdecl)は、_cgo_main.c(Cgoが生成する別のCソースファイル)内で、エクスポートされたGo関数のダミー宣言を提供します。これは、リンケージの目的で必要となる場合があります。
このwriteGccgoExports関数は、gccgoのシンボル命名規則とC言語のリンケージメカニズムの間のギャップを埋めることで、GoとCのシームレスな相互運用性を実現しています。
src/cmd/go/build.go の gccgoPrefix 関数
このヘルパー関数は、特定のGoパッケージに対してgccgoが使用すべきシンボルプレフィックスを決定します。
func gccgoPrefix(p *Package) string {
switch {
case p.build.IsCommand() && !p.forceLibrary:
return ""
case p.fake:
return "fake_" + p.ImportPath
}
return "go_" + p.ImportPath
}
package mainの扱い:p.build.IsCommand()は、パッケージが実行可能ファイル(package main)であるかどうかをチェックします。!p.forceLibraryは、そのパッケージが強制的にライブラリとして扱われるべきではないことを確認します。これらの条件が真の場合、プレフィックスは空文字列""を返します。これは、実行可能ファイルのメインパッケージのシンボルには特別なプレフィックスが不要な場合があるためです。fakeパッケージの扱い:p.fakeは、パッケージがテストなどで一時的に生成された「偽の」パッケージである場合に真となります。この場合、"fake_"というプレフィックスがImportPathに付与されます。- 通常のライブラリパッケージ: 上記のいずれにも該当しない場合(通常のライブラリパッケージ)、
"go_"というプレフィックスがImportPathに付与されます。
この関数は、goコマンドがgccgoを呼び出す際に、-fgo-prefixフラグに渡す値を動的に決定するために使用されます。これにより、gccgoが生成するGoシンボル名が、パッケージの種類に応じて適切に設定され、リンケージの問題が回避されます。
関連リンク
- Go言語の
cgoドキュメント: https://pkg.go.dev/cmd/cgo - GCCGoプロジェクトページ: https://gcc.gnu.org/onlinedocs/gccgo/
- Go Issue #2313: cmd/cgo: support for gccgo - https://github.com/golang/go/issues/2313
- Go Issue #3262: cmd/go: go test + gccgo + package main - https://github.com/golang/go/issues/3262
参考にした情報源リンク
- GitHub Go言語リポジトリ: https://github.com/golang/go
- Go言語公式ドキュメント: https://go.dev/
- Web検索結果 (Issue #2313, #3262に関する情報)