[インデックス 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 string
gccgoprefix
という新しいコマンドラインフラグが追加されました。
-
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, err
gccgoPrefix
ヘルパー関数が追加され、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に関する情報)