[インデックス 16160] ファイルの概要
このコミットは、Go言語のcgoツールとgoコマンドにおける#cgoディレクティブの処理方法に関する重要な変更を導入しています。具体的には、#cgoディレクティブの解析とフラグの処理の責任をcmd/cgoからcmd/goへ移行することで、ビルドシステムの柔軟性と堅牢性を向上させています。
コミット
commit d06be395cc8094b3cb132be238d745b668dbfa04
Author: Andrew Wilkins <axwalk@gmail.com>
Date: Wed Apr 10 21:41:54 2013 -0700
cmd/cgo, cmd/go: remove #cgo directive parsing from cmd/cgo
This change removes processing of #cgo directives from cmd/cgo,
pushing the onus back on cmd/go to pass all necessary flags.
Fixes #5224. See comments for rationale.
R=golang-dev, iant, r
CC=golang-dev
https://golang.org/cl/8610044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d06be395cc8094b3cb132be238d745b668dbfa04
元コミット内容
cmd/cgo, cmd/go: remove #cgo directive parsing from cmd/cgo
この変更は、#cgoディレクティブの処理をcmd/cgoから削除し、必要なすべてのフラグを渡す責任をcmd/goに戻します。
Issue #5224 を修正します。理由についてはコメントを参照してください。
変更の背景
Go言語のcgoは、GoのコードからC言語のコードを呼び出すためのメカニズムを提供します。#cgoディレクティブは、Cコンパイラやリンカに渡すフラグ(例: CFLAGS, LDFLAGS)を指定するために使用されます。
このコミット以前は、cmd/cgoツールがGoソースファイル内の#cgoディレクティブを解析し、それらのフラグをCコンパイラ(通常はGCC)に渡していました。しかし、このアプローチにはいくつかの問題がありました。
- 責任の分散:
cgoディレクティブの解析とフラグの管理がcmd/cgoとcmd/goの間で分散しており、どちらが最終的なフラグを決定するのかが不明確になる可能性がありました。 - 複雑性:
cmd/cgoがpkg-configのような外部ツールを呼び出してフラグを解決するロジックを持っており、これがcmd/cgoのコードベースを複雑にしていました。 - ビルドプロセスの制御:
cmd/go(Goのビルドコマンド)がビルドプロセス全体を統括する立場にあるにもかかわらず、cgo関連のフラグの最終的な制御がcmd/cgoに委ねられていることで、ビルドの柔軟性やデバッグの難易度が増していました。
これらの問題を解決し、ビルドプロセスの制御を一元化するために、#cgoディレクティブの解析とフラグの処理の責任をcmd/cgoからcmd/goへ完全に移行するという決定がなされました。これにより、cmd/goがすべてのビルドフラグを把握し、必要に応じてcmd/cgoに渡すという、よりクリーンで管理しやすいアーキテクチャが実現されます。
前提知識の解説
- cgo: Go言語の機能の一つで、GoのコードからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりすることを可能にします。Goのソースファイル内で
import "C"と記述することで有効になります。 #cgoディレクティブ:cgoを使用するGoソースファイルのimport "C"ブロックの直前にあるCコメント内に記述される特殊なディレクティブです。Cコンパイラやリンカに渡すオプション(例:CFLAGS,LDFLAGS)を指定するために使われます。#cgo CFLAGS: -I/path/to/include: Cコンパイラに渡す追加のコンパイルフラグを指定します。#cgo LDFLAGS: -L/path/to/lib -lfoo: リンカに渡す追加のリンカフラグを指定します。#cgo pkg-config: foo bar:pkg-configツールを使用して、fooとbarライブラリのCフラグとLフラグを自動的に取得します。
cmd/cgo: Goのツールチェーンの一部で、cgoを使用するGoソースファイルをC言語のソースファイルとGo言語のソースファイルに変換する役割を担います。このツールは、Goのビルドプロセス中にcmd/goによって呼び出されます。cmd/go: Go言語のビルドコマンド(go build,go installなど)を実行する主要なツールです。ソースファイルのコンパイル、リンク、パッケージ管理など、Goプロジェクトのビルドプロセス全体を管理します。- 環境変数 (Environment Variables): プロセスに渡されるキーと値のペアのリストです。プログラムの動作に影響を与える設定やパスなどを指定するために使用されます。このコミットでは、
CGO_CFLAGSやCGO_LDFLAGSといった環境変数が重要な役割を果たします。 pkg-config: ライブラリのコンパイル時およびリンク時に必要なフラグ(インクルードパス、ライブラリパス、ライブラリ名など)を自動的に取得するためのコマンドラインツールです。
技術的詳細
このコミットの核心は、#cgoディレクティブの処理ロジックをcmd/cgoからcmd/goへ移動することです。
cmd/cgo側の変更点:
src/cmd/cgo/gcc.go:Package.ParseFlags関数が削除されました。この関数は、Goソースファイルのプリアンブルから#cgo CFLAGSやLDFLAGSなどのディレクティブを解析し、Package構造体のCgoFlagsに格納する役割を担っていました。pkgConfig関数も削除されました。この関数はpkg-configコマンドを実行し、その出力を解析してCフラグとLフラグを取得していました。- 代わりに、
File.DiscardCgoDirectives関数が導入されました。この関数は、import "C"プリアンブル内の#cgoディレクティブを単に破棄し、_cgo_export.hなどの生成ファイルにそれらが含まれないようにします。これは、cmd/cgoがもはやこれらのディレクティブを解釈する必要がないためです。
src/cmd/cgo/main.go:main関数内で、各Goソースファイルを処理する際にp.ParseFlags(f, input)の呼び出しがf.DiscardCgoDirectives()に置き換えられました。newPackage関数から、gccOptionsをコピーするロジックが削除されました。代わりに、p.addToFlag("CFLAGS", args)が直接呼び出され、cmd/goから渡された初期のCフラグがPackage構造体に追加されるようになりました。
cmd/go側の変更点:
src/cmd/go/build.go:builder.runおよびbuilder.runOut関数が変更され、新たにenv []string引数を受け取るようになりました。これにより、実行するコマンドに特定の環境変数を渡すことができるようになります。builder.cgo関数が大幅に変更されました。pkg-configの呼び出しロジックがcmd/goに移されました。pkg-config --cflagsとpkg-config --libsを実行し、その結果をcgoCFLAGSとcgoLDFLAGSに格納します。CGO_LDFLAGS環境変数を構築するロジックが追加されました。p.CgoLDFLAGS(#cgo LDFLAGSやpkg-configから収集されたもの)を引用符で囲み、CGO_LDFLAGS環境変数として設定します。cgoExe(cmd/cgoの実行ファイル)を呼び出す際に、この新しく構築されたcgoenv(CGO_LDFLAGSを含む)がb.run関数に渡されるようになりました。これにより、cmd/cgoは環境変数を通じて必要なリンカフラグを受け取ります。
src/cmd/go/main.go:mergeEnvListsという新しいヘルパー関数が追加されました。この関数は、2つの環境変数リストをマージし、同じ名前の変数がinリストでoutリストのものを上書きするようにします。envForDir関数がmergeEnvListsを使用するように変更されました。これにより、PWD環境変数を設定する際に、既存の環境変数リストと適切にマージされるようになります。
これらの変更により、cmd/goは#cgoディレクティブの解析、pkg-configの実行、そして最終的なC/Lフラグの決定と、それらをcmd/cgoに環境変数として渡すという、一元的な制御を担うことになります。cmd/cgoは、GoとCの間の橋渡しに特化し、フラグの解析や外部ツールの呼び出しといった複雑なロジックから解放されます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
src/cmd/cgo/gcc.go:ParseFlags関数の削除。pkgConfig関数の削除。File.DiscardCgoDirectives関数の追加。
src/cmd/cgo/main.go:main関数内でp.ParseFlagsの呼び出しをf.DiscardCgoDirectivesに置き換え。newPackage関数でのgccOptionsの扱い変更。
src/cmd/go/build.go:builder.runおよびbuilder.runOut関数のシグネチャ変更(env []string引数の追加)。builder.cgo関数内でのpkg-configの呼び出しとCGO_LDFLAGS環境変数の構築ロジックの追加。
src/cmd/go/main.go:mergeEnvLists関数の追加。envForDir関数でのmergeEnvListsの使用。
コアとなるコードの解説
src/cmd/cgo/gcc.go (変更前)
// ParseFlags extracts #cgo CFLAGS and LDFLAGS options from the file
// preamble. Multiple occurrences are concatenated with a separating space,
// even across files.
func (p *Package) ParseFlags(f *File, srcfile string) {
// ... #cgo ディレクティブを解析し、p.addToFlagを呼び出すロジック ...
}
// pkgConfig runs pkg-config and extracts --libs and --cflags information
// for packages.
func pkgConfig(packages []string) (cflags, ldflags []string, err error) {
// ... pkg-config コマンドを実行し、出力を解析するロジック ...
}
src/cmd/cgo/gcc.go (変更後)
// DiscardCgoDirectives processes the import C preamble, and discards
// all #cgo CFLAGS and LDFLAGS directives, so they don't make their
// way into _cgo_export.h.
func (f *File) DiscardCgoDirectives() {
linesIn := strings.Split(f.Preamble, "\n")
linesOut := make([]string, 0, len(linesIn))
for _, line := range linesIn {
l := strings.TrimSpace(line)
// #cgo ディレクティブでなければそのまま残す
if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
linesOut = append(linesOut, line)
}
// #cgo ディレクティブであれば破棄(linesOutに追加しない)
}
f.Preamble = strings.Join(linesOut, "\n")
}
変更後、ParseFlagsとpkgConfigは完全に削除され、DiscardCgoDirectivesが導入されました。これは、cmd/cgoが#cgoディレクティブの解析やpkg-configの実行に関与しなくなったことを明確に示しています。
src/cmd/cgo/main.go (変更前)
func main() {
// ...
for i, input := range goFiles {
// Parse flags for all files before translating due to CFLAGS.
f := new(File)
f.ReadGo(input)
p.ParseFlags(f, input) // ここでParseFlagsが呼び出されていた
fs[i] = f
}
// ...
}
func newPackage(args []string) *Package {
// Copy the gcc options to a new slice so the list
// can grow without overwriting the slice that args is in.
gccOptions := make([]string, len(args))
copy(gccOptions, args)
// ...
p := &Package{
// ...
GccOptions: gccOptions, // ここでgccOptionsが設定されていた
// ...
}
return p
}
src/cmd/cgo/main.go (変更後)
func main() {
// ...
for i, input := range goFiles {
f := new(File)
f.ReadGo(input)
f.DiscardCgoDirectives() // DiscardCgoDirectivesが呼び出される
fs[i] = f
}
// ...
}
func newPackage(args []string) *Package {
// gccOptionsのコピーロジックが削除された
// ...
p := &Package{
// ...
// GccOptionsフィールドは削除されたか、別の方法で初期化される
// ...
}
p.addToFlag("CFLAGS", args) // CFLAGSが直接追加される
return p
}
main関数では、p.ParseFlagsの代わりにf.DiscardCgoDirectivesが呼び出され、#cgoディレクティブの解析がcmd/cgoから完全に排除されました。newPackageでは、cmd/goから渡された初期のargsが直接CFLAGSとして追加されるようになりました。
src/cmd/go/build.go (変更前)
func (b *builder) runOut(dir string, desc string, cmdargs ...interface{}) ([]byte, error) {
// ...
cmd.Env = envForDir(cmd.Dir) // 環境変数はenvForDirで設定
// ...
}
func (b *builder) cgo(p *Package, cgoExe, obj string, gccfiles []string) (outGo, outC []string, err error) {
// ...
if pkgs := p.CgoPkgConfig; len(pkgs) > 0 {
out, err := b.runOut(p.Dir, p.ImportPath, "pkg-config", "--cflags", pkgs) // pkg-configをcmd/goから呼び出し
// ...
out, err = b.runOut(p.Dir, p.ImportPath, "pkg-config", "--libs", pkgs) // pkg-configをcmd/goから呼び出し
// ...
}
// ...
if err := b.run(p.Dir, p.ImportPath, cgoExe, "-objdir", obj, cgoflags, "--", cgoCFLAGS, p.CgoFiles); err != nil {
return nil, nil, err
}
// ...
}
src/cmd/go/build.go (変更後)
func (b *builder) runOut(dir string, desc string, env []string, cmdargs ...interface{}) ([]byte, error) {
// ...
cmd.Env = mergeEnvLists(env, envForDir(cmd.Dir)) // 新たにenv引数を受け取り、マージする
// ...
}
func (b *builder) cgo(p *Package, cgoExe, obj string, gccfiles []string) (outGo, outC []string, err error) {
// ...
if pkgs := p.CgoPkgConfig; len(pkgs) > 0 {
out, err := b.runOut(p.Dir, p.ImportPath, nil, "pkg-config", "--cflags", pkgs) // env引数にnilを渡す
// ...
out, err = b.runOut(p.Dir, p.ImportPath, nil, "pkg-config", "--libs", pkgs) // env引数にnilを渡す
// ...
}
// Update $CGO_LDFLAGS with p.CgoLDFLAGS.
var cgoenv []string
if len(cgoLDFLAGS) > 0 {
flags := make([]string, len(cgoLDFLAGS))
for i, f := range cgoLDFLAGS {
flags[i] = strconv.Quote(f)
}
cgoenv = []string{"CGO_LDFLAGS=" + strings.Join(flags, " ")} // CGO_LDFLAGS環境変数を構築
}
if err := b.run(p.Dir, p.ImportPath, cgoenv, cgoExe, "-objdir", obj, cgoflags, "--", cgoCFLAGS, p.CgoFiles); err != nil {
return nil, nil, err
}
// ...
}
runOut関数はenv引数を受け取るようになり、cmd.Envを設定する際にこの引数と既存の環境変数をマージするようになりました。cgo関数では、pkg-configの呼び出しは引き続きcmd/goが行いますが、その結果得られたリンカフラグはCGO_LDFLAGS環境変数として構築され、cmd/cgoを呼び出す際にb.runを通じて渡されるようになりました。
src/cmd/go/main.go (変更前)
func envForDir(dir string) []string {
env := os.Environ()
for i, kv := range env {
if strings.HasPrefix(kv, "PWD=") {
env[i] = "PWD=" + dir
return env
}
}
env = append(env, "PWD="+dir)
return env
}
src/cmd/go/main.go (変更後)
// mergeEnvLists merges the two environment lists such that
// variables with the same name in "in" replace those in "out".
func mergeEnvLists(in, out []string) []string {
NextVar:
for _, inkv := range in {
k := strings.SplitAfterN(inkv, "=", 2)[0]
for i, outkv := range out {
if strings.HasPrefix(outkv, k) {
out[i] = inkv
continue NextVar
}
}
out = append(out, inkv)
}
return out
}
func envForDir(dir string) []string {
// Internally we only use rooted paths, so dir is rooted.
// Even if dir is not rooted, no harm done.
return mergeEnvLists([]string{"PWD=" + dir}, os.Environ())
}
mergeEnvLists関数が追加され、環境変数リストを効率的にマージできるようになりました。envForDirはこの新しい関数を利用して、PWD環境変数を設定する際に既存の環境変数と適切に結合します。
これらの変更により、#cgoディレクティブの解析とフラグの処理に関する責任がcmd/goに集約され、cmd/cgoはよりシンプルな役割を担うようになりました。
関連リンク
- Go言語の
cgoに関する公式ドキュメント: https://pkg.go.dev/cmd/cgo - Go言語のビルドコマンドに関する公式ドキュメント: https://pkg.go.dev/cmd/go
pkg-configのmanページ (一般的な情報):man pkg-config(Linux/macOSのターミナルで実行)
参考にした情報源リンク
- コミットハッシュ:
d06be395cc8094b3cb132be238d745b668dbfa04 - GitHub上のコミットページ: https://github.com/golang/go/commit/d06be395cc8094b3cb132be238d745b668dbfa04
- Go CL (Change List) 8610044: https://golang.org/cl/8610044 (これは古いGoのコードレビューシステムへのリンクであり、現在はアクセスできない可能性がありますが、コミットメッセージに記載されているため含めました。)
- Go Issue 5224: コミットメッセージに記載されていますが、公開されているGoのIssueトラッカーでは見つかりませんでした。これは内部的な追跡番号であるか、非常に古いIssueである可能性があります。