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

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

このコミットは、Go言語のC言語連携ツールであるcgoが生成するGoコードの順序を決定論的にするためのものです。これにより、cgoの実行ごとに異なるコードが生成される問題を解決し、再現可能なビルドを保証します。

コミット

commit f51390b23cb94614ed8ba6b7a89b396c27c80511
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 12:37:50 2012 -0400

    cmd/cgo: make Go code order deterministic
    
    The type declarations were being generated using
    a range over a map, which meant that successive
    runs produced different orders. This will make sure
    successive runs produce the same files.
    
    Fixes #3707.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6300062

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

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

元コミット内容

このコミットは、cmd/cgoツールがGoコードを生成する際に、型宣言の順序が非決定論的になる問題を修正します。以前は、マップのイテレーション順序に依存してコードが生成されていたため、同じ入力に対してもcgoを複数回実行すると異なる出力ファイルが生成される可能性がありました。この変更により、cgoの実行結果が常に同じになるようにし、再現可能なビルドを可能にします。

変更の背景

ソフトウェア開発において、ビルドの「決定性(determinism)」は非常に重要です。決定性ビルドとは、同じソースコードとビルド環境が与えられた場合、常に全く同じバイナリや成果物が生成されることを意味します。これが保証されないと、以下のような問題が発生します。

  1. 再現性の欠如: 特定のバグが特定のビルドでのみ発生し、別のビルドでは再現しないといった問題が生じ、デバッグが困難になります。
  2. CI/CDの不安定化: 継続的インテグレーション/デプロイメント(CI/CD)パイプラインにおいて、ビルドキャッシュの無効化や、不必要な差分(diff)の発生により、パイプラインが不安定になったり、ビルド時間が長くなったりします。
  3. バージョン管理のノイズ: 生成されたコードが非決定論的であると、バージョン管理システム(Gitなど)に不必要な変更がコミットされ、コードレビューが煩雑になります。
  4. セキュリティと信頼性: 監査やセキュリティ検証の際に、ビルドの信頼性が損なわれる可能性があります。

このコミットは、cgoが生成するGoコードの型宣言の順序が、Goのmapのイテレーション順序に依存していたために非決定論的になっていた問題を解決するために行われました。具体的には、Goのマップは設計上、要素の挿入順序や削除順序に関わらず、イテレーション順序が保証されません。これは、マップがハッシュテーブルとして実装されており、内部的なハッシュ値やメモリ配置によってイテレーション順序が変動するためです。この非決定性が、cgoの出力にも影響を与えていました。

この問題は、GoのIssue #3707として報告されており、このコミットはその修正として行われました。

前提知識の解説

cgoとは

cgoは、Go言語のプログラムからC言語のコードを呼び出したり、逆にC言語のプログラムからGo言語の関数を呼び出したりするためのツールです。Go言語の標準ツールチェーンの一部として提供されており、GoとC/C++の相互運用を可能にします。

cgoを使用するGoのソースファイルには、import "C"という特殊なインポート文が含まれます。このインポート文の直前のコメントブロックにC言語のコードを記述することで、GoコードからそのC言語の関数や変数、型にアクセスできるようになります。

cgoは、Goのビルドプロセス中に、GoとCの間のブリッジコードを生成します。このブリッジコードは、Cの関数呼び出しをGoの関数呼び出しに変換したり、Goの型とCの型の間でデータを変換したりする役割を担います。このコミットで問題となっていたのは、このブリッジコードの一部、特に型宣言の生成順序でした。

Go言語におけるmapのイテレーション順序の非決定性

Go言語の組み込み型であるmap(ハッシュマップ、連想配列)は、キーと値のペアを格納するためのデータ構造です。Goの言語仕様では、for rangeループを使ってマップをイテレーションする際の順序は保証されていません。これは意図的な設計であり、マップの実装がハッシュテーブルに基づいているため、イテレーションのたびに異なる順序で要素が返される可能性があります。

この非決定性は、並行処理の効率化や、マップの内部構造の変更(例えば、ハッシュ関数の変更やリサイズ)がイテレーション順序に影響を与えないようにするためです。しかし、ファイル生成やテストの再現性など、順序が重要なコンテキストでは問題となることがあります。

決定性ビルドの重要性

前述の通り、決定性ビルドはソフトウェアの品質、信頼性、および開発プロセスの効率性にとって不可欠です。特に大規模なプロジェクトや、セキュリティが重視されるシステムでは、ビルドの再現性が厳しく求められます。同じソースコードから常に同じバイナリが生成されることで、以下のようなメリットがあります。

  • デバッグの容易性: 特定のビルドで発生したバグを、そのビルドを再現することで確実にデバッグできます。
  • CI/CDの安定性: ビルドキャッシュが効果的に機能し、不必要な再ビルドやテストの実行を防ぎます。
  • 監査とコンプライアンス: 生成されたバイナリが特定のソースコードから正確にビルドされたことを証明できます。
  • バイナリ配布の信頼性: ユーザーがダウンロードしたバイナリが、開発者が意図した通りのものであることを検証できます。

cgoのようなコード生成ツールが非決定論的な出力を生成すると、この決定性ビルドの原則が破られてしまいます。

技術的詳細

このコミットの技術的な核心は、Goのmapの非決定的なイテレーション順序によって引き起こされるコード生成の非決定性を解消することです。

cgoツールは、Cの型や関数に対応するGoの型宣言や変数宣言を生成する際に、内部的にmap[string]*Nameのようなマップを使用して、Goの識別子とCの識別子のマッピングを管理していました。このマップをfor rangeループで直接イテレーションしてコードを生成していたため、cgoを実行するたびにマップのイテレーション順序が変わり、結果として生成されるGoコード内の宣言の順序も変わってしまっていました。

この問題を解決するために採用されたアプローチは、マップのキーを抽出し、それらをソートしてから、ソートされたキーの順序でマップの要素にアクセスするというものです。これにより、マップのイテレーション順序の非決定性を回避し、常に同じ順序でコードが生成されるようになります。

具体的には、以下のステップが実装されました。

  1. マップのすべてのキーをスライス([]string)に抽出します。
  2. 抽出したキーのスライスを辞書順(アルファベット順)にソートします。
  3. ソートされたキーのスライスをイテレーションし、それぞれのキーを使ってマップから対応する値を取得し、その値に基づいてコードを生成します。

この方法により、cgoの出力は入力に対して常に決定論的となり、再現可能なビルドが実現されます。

また、このコミットではPackageおよびFile構造体からTypedef map[string]ast.Exprフィールドが削除されています。これは、型定義の処理方法が変更されたか、あるいはこのフィールドがもはや不要になったことを示唆しています。この変更も、コード生成の決定性に関連している可能性があります。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/cmd/cgo/main.go
  2. src/cmd/cgo/out.go

src/cmd/cgo/main.go の変更点

  • import "sort" が追加されました。これは、マップのキーをソートするために必要です。

  • Package構造体から Typedef map[string]ast.Expr フィールドが削除されました。

  • File構造体から Typedef map[string]ast.Expr フィールドが削除されました。

  • 新しいヘルパー関数 nameKeys が追加されました。この関数は、map[string]*Nameを受け取り、そのキーを抽出してソートし、ソートされた文字列スライスとして返します。

    func nameKeys(m map[string]*Name) []string {
    	var ks []string
    	for k := range m {
    		ks = append(ks, k)
    	}
    	sort.Strings(ks)
    	return ks
    }
    

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

  • Package.Nameマップをイテレーションして変数、定数、関数を生成する複数のforループが変更されました。
  • 変更前は for _, n := range p.Name のように直接マップをイテレーションしていましたが、変更後は for _, key := range nameKeys(p.Name) のように、nameKeys関数で取得したソート済みのキーを使ってイテレーションするようになりました。これにより、常に決定論的な順序で要素が処理され、生成されるコードの順序が保証されます。
  • 同様に、File.Nameマップをイテレーションする箇所も、nameKeys関数を使用するように変更されました。

コアとなるコードの解説

このコミットの核心は、src/cmd/cgo/main.goに追加されたnameKeys関数と、それがsrc/cmd/cgo/out.goでどのように利用されているかです。

nameKeys関数の役割

func nameKeys(m map[string]*Name) []string {
	var ks []string
	for k := range m {
		ks = append(ks, k)
	}
	sort.Strings(ks) // ここでキーをソート
	return ks
}

このnameKeys関数は、cgo内部でGoの識別子(Name構造体で表現される)を管理するために使用されるmap[string]*Name型のマップを受け取ります。

  1. for k := range m: まず、マップmのすべてのキーをイテレーションし、それぞれのキーkを新しい文字列スライスksに追加します。この時点では、ks内のキーの順序はマップのイテレーション順序に依存するため、非決定論的です。
  2. sort.Strings(ks): 次に、Go標準ライブラリのsortパッケージにあるStrings関数を使用して、ksスライス内の文字列(マップのキー)を辞書順にソートします。これにより、キーの順序が常に一定になります。
  3. return ks: ソートされたキーのスライスを返します。

out.goでのnameKeysの利用

src/cmd/cgo/out.goファイルは、cgoが最終的なGoコードを生成するロジックを含んでいます。このファイル内で、変数、定数、および関数の宣言を生成する際に、Package.Nameマップ(またはFile.Nameマップ)をイテレーションしていました。

変更前は、以下のようなコードでした。

// 変更前: マップを直接イテレーション
for _, n := range p.Name {
    // ... コード生成ロジック ...
}

このコードでは、p.Nameマップのイテレーション順序が非決定論的であるため、n*Name型の値)が処理される順序も非決定論的でした。

変更後は、nameKeys関数を利用して、ソートされたキーの順序でマップの要素にアクセスするように修正されました。

// 変更後: nameKeysでソートされたキーを使ってイテレーション
for _, key := range nameKeys(p.Name) {
    n := p.Name[key] // ソートされたキーでマップから値を取得
    // ... コード生成ロジック ...
}

この変更により、keyが常に辞書順で提供されるため、nが処理される順序も常に一定になります。結果として、cgoが生成するGoコード内の宣言の順序が決定論的になり、再現可能なビルドが実現されます。

PackageおよびFile構造体からTypedef map[string]ast.Exprフィールドが削除されたことについては、このコミットの直接的な目的である「コード生成の順序決定性」とは異なる側面ですが、型定義の処理方法が変更され、もはやこのマップが不要になったことを示しています。これは、コードベースの整理や、より効率的・決定論的な型処理メカニズムへの移行の一環である可能性があります。

関連リンク

参考にした情報源リンク

  • Go言語のmapのイテレーション順序に関する公式ドキュメントやブログ記事
  • cgoツールの公式ドキュメント
  • 決定性ビルドに関する一般的な情報源(例: Reproducible Buildsプロジェクトなど)