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

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

このコミットは、Go言語のcgoツールに関する実装詳細を記述したドキュメントファイルsrc/cmd/cgo/doc.goに、包括的なコメントを追加するものです。この追加されたコメントは、cgoがGoプログラムとCコード間の相互運用性をどのように実現しているかについて、その内部動作を詳細に解説しています。

コミット

commit 062a239046974229a03d13d7e1a7bdedbb247292
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 27 20:55:01 2013 -0800

    cmd/cgo: add implementation comment
    
    R=golang-dev, r, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/7407050

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

https://github.com/golang/go/commit/062a239046974229a03d13d7e1a7bdedbb247292

元コミット内容

cmd/cgo: add implementation comment

変更の背景

cgoはGoプログラムからCコードを呼び出すための重要なツールですが、その内部動作は非常に複雑です。このコミットの背景には、cgoの設計思想、GoとCの間のデータ変換、リンキングプロセス、およびランタイムの相互作用といった、技術的な詳細を開発者や興味を持つユーザーがより深く理解できるようにするという目的があります。特に、cgoが完全なCパーサーを持たずにCの情報を取得する巧妙な方法や、Goのリンカー(6l)の制約をどのように克服しているかといった点は、ドキュメントとして明文化する価値が高いと判断されたと考えられます。これにより、cgoのデバッグや改善、あるいはcgoを利用する際のより深い理解に役立つことが期待されます。

前提知識の解説

cgoとは

cgoは、Go言語のプログラムからC言語のコードを呼び出したり、逆にC言語のコードからGo言語の関数を呼び出したりするためのGoツールです。import "C"という特殊なインポート文を使用することで、Goコード内でCの型、変数、関数などを参照できるようになります。これにより、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。

GoとCの相互運用性の課題

GoとCは異なるメモリ管理モデル(Goはガベージコレクション、Cは手動管理)と型システムを持っています。また、関数呼び出し規約やスタックフレームの構造も異なります。これらの違いを吸収し、安全かつ効率的に相互運用を実現することがcgoの主要な課題です。特に、GoのランタイムとCのランタイムが同じプロセス空間で協調して動作する必要があるため、スレッド管理やシグナル処理などにおいて複雑な調整が必要となります。

GCC (GNU Compiler Collection)

GCCは、C、C++、Objective-C、Fortran、Ada、Goなどの多くのプログラミング言語をサポートするコンパイラ群です。cgoは、CコードのコンパイルやCの型情報の取得にGCCを積極的に利用します。特に、GCCの-E -dMオプション(プリプロセッサの定義済みマクロをダンプ)や、生成されるオブジェクトファイルのDWARFデバッグ情報からCのシンボル情報を解析するのに使われます。

DWARFデバッグ情報

DWARF (Debugging With Arbitrary Record Formats) は、ソースコードとコンパイルされたバイナリコード間の関係を記述するための標準的なデバッグファイル形式です。コンパイラは、変数名、型情報、関数名、ソースファイルの行番号などのデバッグ情報をDWARF形式でオブジェクトファイルや実行ファイルに埋め込みます。cgoは、このDWARF情報を解析して、Cの型や変数の詳細な情報を取得します。

ELF (Executable and Linkable Format) / Mach-O

ELFは、Unix系システムで広く使われている実行ファイル、オブジェクトファイル、共有ライブラリの標準ファイル形式です。Mach-Oは、macOSやiOSで使われる同様のファイル形式です。これらの形式は、プログラムのセクション(コード、データなど)、シンボルテーブル、リロケーション情報などを定義します。cgoは、生成されたCのオブジェクトファイルをこれらの形式で扱い、Goのリンカーが処理できるように調整します。

動的リンクと静的リンク

  • 静的リンク: プログラムが必要とするすべてのライブラリコードが、コンパイル時に最終的な実行ファイルに直接組み込まれる方式です。実行ファイルは自己完結型となり、他のライブラリファイルに依存しません。
  • 動的リンク: プログラムが必要とするライブラリコードが、実行時に共有ライブラリ(WindowsではDLL、Unix系では.soファイル)としてロードされる方式です。実行ファイルのサイズは小さくなり、複数のプログラムで同じライブラリを共有できます。cgoは、Goのリンカーの制約を回避するために、Cライブラリとのリンクに動的リンクを多用します。

Goのツールチェイン (6g, 6c, 6l)

Goの初期のコンパイラとリンカーは、ターゲットアーキテクチャに応じて命名されていました(例: 6gはamd64用Goコンパイラ、6cはamd64用Cコンパイラ、6lはamd64用リンカー)。このコミットのコメントでは、これらの古い命名が使われていますが、概念は現在のGoツールチェインにも引き継がれています。

  • Goコンパイラ (例: 6g): Goソースコードをオブジェクトファイルにコンパイルします。
  • GoのCコンパイラ (例: 6c): Goランタイムの一部や、cgoが生成する一部のCコードをコンパイルします。これはGCCとは異なり、Goツールチェインの一部です。
  • Goリンカー (例: 6l): Goのオブジェクトファイルと、cgoが生成するCのオブジェクトファイルをリンクして、最終的な実行ファイルを生成します。このリンカーは、一般的なCのリンカー(ldなど)ほど複雑なELF/Mach-Oの機能を完全にサポートしているわけではありません。

技術的詳細

このコミットで追加されたコメントは、cgoの内部動作を以下の主要なフェーズに分けて詳細に解説しています。

1. Cの理解 (Understanding C)

cgoは、Goソースファイル内のimport "C"文をスキャンし、C.xxx形式で参照されているすべての識別子(xxx)を収集します。これらの識別子がCの型、関数、定数、グローバル変数のいずれであるかを判断する必要があります。

cgoは、完全なCパーサーを持つ代わりに、システムCコンパイラ(GCC)を巧妙に利用します。

  • 定数の検出: まず、gcc -E -dMをプリアンブル(import "C"のドキュメントコメント内のCコード)に対して実行し、#defineされた定数などの情報を取得します。
  • 識別子の種類の推論: cgoは、収集した識別子(例: C.foo, C.bar)を含むCプログラムを生成し、GCCでコンパイルします。このプログラムは意図的にコンパイルエラーを発生させるように作られています。
    <preamble>
    void __cgo__f__(void) {
    #line 1 "cgo-test"
        foo;
        enum { _cgo_enum_0 = foo };
        bar;
        enum { _cgo_enum_1 = bar };
    }
    
    GCCの出力するエラーメッセージ(例: "unexpected type name", "statement with no effect", "not an integer constant")と、エラーが発生した行番号を解析することで、cgofoobarが型、関数、変数、定数のいずれであるかを推論します。
  • 詳細情報の取得: 識別子の種類が判明した後、cgoはさらに詳細な情報を得るために別のCプログラムを生成し、GCCでコンパイルしてオブジェクトファイルを生成します。
    <preamble>
    typeof(t1) *__cgo__1; // t1は型
    typeof(v2) *__cgo__2; // v2は変数または関数
    // ...
    long long __cgo_debug_data[] = {
        0, // t1
        0, // v2
        0, // v3
        c4, // c4は定数
        c5,
        c6,
        1
    };
    
    cgoは、このオブジェクトファイルに含まれるDWARFデバッグ情報を解析することで、各識別子の正確な型(例: int, char*)や、関数とグローバル変数の区別、定数の値などを取得します。OS X上のLLVMベースのGCCのように、DWARF情報が不完全な場合は、__cgo_debug_dataセグメントから定数値を読み取るといった工夫も行われます。

この段階で、cgoはGoコードから参照されるCの識別子について、翻訳に必要なすべての情報を把握します。

2. Goの翻訳 (Translating Go)

cgoは、入力されたGoソースファイル(例: x.go, y.go)とCの情報を基に、複数のGoおよびCのソースファイルを生成します。

  • x.cgo1.go, y.cgo1.go: 元のGoファイルからimport "C"を削除し、C.xxx形式の参照を_Cfunc_xxx_Ctype_xxxといった内部的な名前に置き換えたGoソースファイルです。これらはGoコンパイラ(6g)によってコンパイルされます。
  • _cgo_gotypes.go: _Cfunc_xxx_Ctype_xxxといった内部的な識別子の定義を含むGoソースファイルです。これには、Cの型に対応するGoの型定義(例: type _Ctype_char int8)や、C関数に対応するGoの外部関数宣言(例: func _Cfunc_puts(*_Ctype_char) _Ctype_int)が含まれます。これもGoコンパイラ(6g)によってコンパイルされます。
  • _cgo_defun.c: GoのCコンパイラ(6c)によってコンパイルされるCソースファイルです。ここには、GoからC関数を呼び出すためのラッパー関数が定義されています。これらのラッパーは、runtime·cgocallを呼び出してGoランタイムからCランタイムへのコンテキストスイッチを行います。 例: _Cfunc_putsの定義
    void _cgo_be59f0f25121_Cfunc_puts(void*); // GCCでコンパイルされる実際のC関数
    void
    ·_Cfunc_puts(struct{uint8 x[1];}p) // Goから呼び出されるラッパー
    {
        runtime·cgocall(_cgo_be59f0f25121_Cfunc_puts, &p);
    }
    
    _cgo_be59f0f25121のような16進数は、cgoの入力のハッシュ値であり、衝突を避けるためのものです。
  • x.cgo2.c, y.cgo2.c: GCCによってコンパイルされるCソースファイルです。ここには、Goから呼び出される実際のC関数(例: _cgo_be59f0f25121_Cfunc_puts)の実装が含まれます。これらの関数は、Goから渡された引数フレームから引数を抽出し、実際のシステムC関数(例: puts)を呼び出し、結果をフレームに格納して返します。
  • _cgo_export.c: GCCによってコンパイルされるCソースファイルです。GoからCにエクスポートされる関数(CからGoを呼び出す場合)の定義が含まれます。
  • _cgo_main.c: GCCによってコンパイルされるCソースファイルです。リンキングプロセスで使用されるスタブ関数(main, crosscall2など)が含まれます。

3. リンキング (Linking)

cgoを使用するプログラムのリンキングは、Goのリンカー(6l)が現代のCライブラリの複雑なELF/Mach-O機能を完全に理解できないという制約があるため、特別なアプローチが取られます。

  • 動的リンクの利用: _cgo_export.c*.cgo2.cなどのGCCでコンパイルされたCのオブジェクトファイルは、最終的なバイナリにリンクされる必要があります。しかし、6lはシステムライブラリへの直接参照を一般的に処理できません。
  • 中間実行ファイルの生成: 代わりに、ビルドプロセスは、_cgo_main.c(スタブ関数を含む)、_cgo_export.c*.cgo2.cをGCCでコンパイルし、動的リンクを使用して一時的な実行ファイルを生成します。
  • _cgo_import.cの生成: cgoはこの一時的な実行ファイルを検査し、必要な共有ライブラリのリストと解決されたシンボル名(例: puts#GLIBC_2.2.5 "libc.so.6")を記録します。これらの情報は、_cgo_import.cという新しいファイルに#pragma dynlinker#pragma dynimportディレクティブとして書き込まれます。 例: _cgo_import.cの内容
    #pragma dynlinker "/lib64/ld-linux-x86-64.so.2"
    #pragma dynimport puts puts#GLIBC_2.2.5 "libc.so.6"
    // ...
    
    この_cgo_import.cはGoのCコンパイラ(6c)によってコンパイルされ、_cgo_import.6というオブジェクトファイルになります。
  • 最終的なGoパッケージの構成: 最終的に、Goのリンカー(6l)に渡されるGoパッケージは、以下のオブジェクトファイルを含みます。
    • _go_.6: _cgo_gotypes.go*.cgo1.go6gでコンパイルしたもの。
    • _cgo_defun.6: _cgo_defun.c6cでコンパイルしたもの。
    • _all.o: _cgo_export.c*.cgo2.cをGCCでコンパイルしたもの。
    • _cgo_import.6: _cgo_import.c6cでコンパイルしたもの。

この複雑なリンキングプロセスにより、6lは比較的シンプルに保たれ(完全なELF/Mach-Oリンカーを実装する必要がない)、また、パッケージがコンパイルされた後はGCCが不要になるという利点があります。例えば、netパッケージはlibcの関数にアクセスするためにcgoを使用しますが、netパッケージをインポートするプログラムをリンクする際にはGCCは必要ありません。

4. ランタイム (Runtime)

cgoを使用する場合、Goランタイムはプロセス内のすべてを完全に制御しているわけではないという前提で動作する必要があります。特に、スレッドやスレッドローカルストレージの使用においてCランタイムと協調する必要があります。

Goのランタイムパッケージは、cgo関連のいくつかの未初期化変数(runtime·iscgo, libcgo_thread_start, initcgo)を宣言しています。cgoを使用するパッケージは"runtime/cgo"をインポートし、これらの変数を初期化します。

  • runtime·iscgo: cgoが使用されていることを示すフラグ。
  • initcgo: プログラム起動の早い段階で呼び出されるGCCコンパイル済みの関数。
  • libcgo_thread_start: Goランタイムが通常直接システムコールでスレッドを作成する代わりに、新しいスレッドを作成するために使用できるGCCコンパイル済みの関数。

これにより、GoとCの両方のランタイムが同じプロセス空間内で適切に協調し、スレッドの作成や管理、リソースの共有などを安全に行うことが可能になります。

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

このコミットのコアとなる変更は、src/cmd/cgo/doc.goファイルに以下の形式で追加された263行のコメントブロック全体です。

/*
Implementation details.

Cgo provides a way for Go programs to call C code linked into the same
address space. This comment explains the operation of cgo.

... (中略:上記「技術的詳細」で解説した内容が続く) ...

*/

このコメントブロックは、Goのドキュメンテーションツールによって解析され、cgoの公式ドキュメントの一部として公開されることを意図しています。

コアとなるコードの解説

追加されたコメントは、cgoの内部実装を「Cの理解 (Understanding C)」、「Goの翻訳 (Translating Go)」、「リンキング (Linking)」、「ランタイム (Runtime)」の4つの主要なセクションに分けて詳細に解説しています。

  1. Cの理解: cgoがCのヘッダーファイルを直接パースするのではなく、GCCを「オラクル」として利用し、コンパイルエラーメッセージやDWARFデバッグ情報を解析することで、Cの型、関数、変数、定数の情報をどのように取得するかを説明しています。これは、Cの複雑なプリプロセッサやシステム固有の拡張に対応するための非常に巧妙なアプローチです。
  2. Goの翻訳: cgoがGoとCの間のブリッジを構築するために、どのような中間ファイルを生成するかを詳述しています。x.cgo1.goでGoコード内のC.xxx参照を内部名に変換し、_cgo_gotypes.goでそれらの内部名のGo側の定義を提供します。また、_cgo_defun.cx.cgo2.cでGoからCへの呼び出しを仲介するラッパー関数を生成し、runtime·cgocallを通じてGoランタイムとCランタイム間のコンテキストスイッチを行う仕組みを解説しています。
  3. リンキング: Goのリンカー(6l)がCの複雑なオブジェクトファイルを直接処理できないという制約を克服するために、cgoがどのように動的リンクと_cgo_import.cファイルを利用するかを説明しています。一時的な動的実行ファイルを生成して必要な共有ライブラリ情報を抽出し、それを#pragma dynimportディレクティブとしてGoのリンカーに渡すことで、最終的なバイナリが正しくリンクされるようにするプロセスが詳細に記述されています。
  4. ランタイム: cgoを使用する際に、GoランタイムがCランタイムとどのように協調するかを説明しています。特に、スレッド管理やスレッドローカルストレージの調整、およびruntime/cgoパッケージが提供する初期化フックやスレッド作成フックを通じて、両方のランタイムが同じプロセス空間内で安全に共存するためのメカニズムが解説されています。

このコメントは、cgoの設計が、Goのツールチェインのシンプルさを維持しつつ、Cとの高度な相互運用性を実現するためにいかに工夫されているかを明確に示しています。

関連リンク

参考にした情報源リンク

このコミットは、Go言語のcgoツールに関する実装詳細を記述したドキュメントファイルsrc/cmd/cgo/doc.goに、包括的なコメントを追加するものです。この追加されたコメントは、cgoがGoプログラムとCコード間の相互運用性をどのように実現しているかについて、その内部動作を詳細に解説しています。

コミット

commit 062a239046974229a03d13d7e1a7bdedbb247292
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 27 20:55:01 2013 -0800

    cmd/cgo: add implementation comment
    
    R=golang-dev, r, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/7407050

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

https://github.com/golang/go/commit/062a239046974229a03d13d7e1a7bdedbb247292

元コミット内容

cmd/cgo: add implementation comment

変更の背景

cgoはGoプログラムからCコードを呼び出すための重要なツールですが、その内部動作は非常に複雑です。このコミットの背景には、cgoの設計思想、GoとCの間のデータ変換、リンキングプロセス、およびランタイムの相互作用といった、技術的な詳細を開発者や興味を持つユーザーがより深く理解できるようにするという目的があります。特に、cgoが完全なCパーサーを持たずにCの情報を取得する巧妙な方法や、Goのリンカー(6l)の制約をどのように克服しているかといった点は、ドキュメントとして明文化する価値が高いと判断されたと考えられます。これにより、cgoのデバッグや改善、あるいはcgoを利用する際のより深い理解に役立つことが期待されます。

前提知識の解説

cgoとは

cgoは、Go言語のプログラムからC言語のコードを呼び出したり、逆にC言語のコードからGo言語の関数を呼び出したりするためのGoツールです。import "C"という特殊なインポート文を使用することで、Goコード内でCの型、変数、関数などを参照できるようになります。これにより、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。

GoとCの相互運用性の課題

GoとCは異なるメモリ管理モデル(Goはガベージコレクション、Cは手動管理)と型システムを持っています。また、関数呼び出し規約やスタックフレームの構造も異なります。これらの違いを吸収し、安全かつ効率的に相互運用を実現することがcgoの主要な課題です。特に、GoのランタイムとCのランタイムが同じプロセス空間で協調して動作する必要があるため、スレッド管理やシグナル処理などにおいて複雑な調整が必要となります。

GCC (GNU Compiler Collection)

GCCは、C、C++、Objective-C、Fortran、Ada、Goなどの多くのプログラミング言語をサポートするコンパイラ群です。cgoは、CコードのコンパイルやCの型情報の取得にGCCを積極的に利用します。特に、GCCの-E -dMオプション(プリプロセッサの定義済みマクロをダンプ)や、生成されるオブジェクトファイルのDWARFデバッグ情報からCのシンボル情報を解析するのに使われます。

DWARFデバッグ情報

DWARF (Debugging With Arbitrary Record Formats) は、ソースコードとコンパイルされたバイナリコード間の関係を記述するための標準的なデバッグファイル形式です。コンパイラは、変数名、型情報、関数名、ソースファイルの行番号などのデバッグ情報をDWARF形式でオブジェクトファイルや実行ファイルに埋め込みます。cgoは、このDWARF情報を解析して、Cの型や変数の詳細な情報を取得します。

ELF (Executable and Linkable Format) / Mach-O

ELFは、Unix系システムで広く使われている実行ファイル、オブジェクトファイル、共有ライブラリの標準ファイル形式です。Mach-Oは、macOSやiOSで使われる同様のファイル形式です。これらの形式は、プログラムのセクション(コード、データなど)、シンボルテーブル、リロケーション情報などを定義します。cgoは、生成されたCのオブジェクトファイルをこれらの形式で扱い、Goのリンカーが処理できるように調整します。

動的リンクと静的リンク

  • 静的リンク: プログラムが必要とするすべてのライブラリコードが、コンパイル時に最終的な実行ファイルに直接組み込まれる方式です。実行ファイルは自己完結型となり、他のライブラリファイルに依存しません。
  • 動的リンク: プログラムが必要とするライブラリコードが、実行時に共有ライブラリ(WindowsではDLL、Unix系では.soファイル)としてロードされる方式です。実行ファイルのサイズは小さくなり、複数のプログラムで同じライブラリを共有できます。cgoは、Goのリンカーの制約を回避するために、Cライブラリとのリンクに動的リンクを多用します。

Goのツールチェイン (6g, 6c, 6l)

Goの初期のコンパイラとリンカーは、ターゲットアーキテクチャに応じて命名されていました(例: 6gはamd64用Goコンパイラ、6cはamd64用Cコンパイラ、6lはamd64用リンカー)。このコミットのコメントでは、これらの古い命名が使われていますが、概念は現在のGoツールチェインにも引き継がれています。

  • Goコンパイラ (例: 6g): Goソースコードをオブジェクトファイルにコンパイルします。
  • GoのCコンパイラ (例: 6c): Goランタイムの一部や、cgoが生成する一部のCコードをコンパイルします。これはGCCとは異なり、Goツールチェインの一部です。
  • Goリンカー (例: 6l): Goのオブジェクトファイルと、cgoが生成するCのオブジェクトファイルをリンクして、最終的な実行ファイルを生成します。このリンカーは、一般的なCのリンカー(ldなど)ほど複雑なELF/Mach-Oの機能を完全にサポートしているわけではありません。

技術的詳細

このコミットで追加されたコメントは、cgoの内部動作を以下の主要なフェーズに分けて詳細に解説しています。

1. Cの理解 (Understanding C)

cgoは、Goソースファイル内のimport "C"文をスキャンし、C.xxx形式で参照されているすべての識別子(xxx)を収集します。これらの識別子がCの型、関数、定数、グローバル変数のいずれであるかを判断する必要があります。

cgoは、完全なCパーサーを持つ代わりに、システムCコンパイラ(GCC)を巧妙に利用します。

  • 定数の検出: まず、gcc -E -dMをプリアンブル(import "C"のドキュメントコメント内のCコード)に対して実行し、#defineされた定数などの情報を取得します。
  • 識別子の種類の推論: cgoは、収集した識別子(例: C.foo, C.bar)を含むCプログラムを生成し、GCCでコンパイルします。このプログラムは意図的にコンパイルエラーを発生させるように作られています。
    <preamble>
    void __cgo__f__(void) {
    #line 1 "cgo-test"
        foo;
        enum { _cgo_enum_0 = foo };
        bar;
        enum { _cgo_enum_1 = bar };
    }
    
    GCCの出力するエラーメッセージ(例: "unexpected type name", "statement with no effect", "not an integer constant")と、エラーが発生した行番号を解析することで、cgofoobarが型、関数、変数、定数のいずれであるかを推論します。
  • 詳細情報の取得: 識別子の種類が判明した後、cgoはさらに詳細な情報を得るために別のCプログラムを生成し、GCCでコンパイルしてオブジェクトファイルを生成します。
    <preamble>
    typeof(t1) *__cgo__1; // t1は型
    typeof(v2) *__cgo__2; // v2は変数または関数
    // ...
    long long __cgo_debug_data[] = {
        0, // t1
        0, // v2
        0, // v3
        c4, // c4は定数
        c5,
        c6,
        1
    };
    
    cgoは、このオブジェクトファイルに含まれるDWARFデバッグ情報を解析することで、各識別子の正確な型(例: int, char*)や、関数とグローバル変数の区別、定数の値などを取得します。OS X上のLLVMベースのGCCのように、DWARF情報が不完全な場合は、__cgo_debug_dataセグメントから定数値を読み取るといった工夫も行われます。

この段階で、cgoはGoコードから参照されるCの識別子について、翻訳に必要なすべての情報を把握します。

2. Goの翻訳 (Translating Go)

cgoは、入力されたGoソースファイル(例: x.go, y.go)とCの情報を基に、複数のGoおよびCのソースファイルを生成します。

  • x.cgo1.go, y.cgo1.go: 元のGoファイルからimport "C"を削除し、C.xxx形式の参照を_Cfunc_xxx_Ctype_xxxといった内部的な名前に置き換えたGoソースファイルです。これらはGoコンパイラ(6g)によってコンパイルされます。
  • _cgo_gotypes.go: _Cfunc_xxx_Ctype_xxxといった内部的な識別子の定義を含むGoソースファイルです。これには、Cの型に対応するGoの型定義(例: type _Ctype_char int8)や、C関数に対応するGoの外部関数宣言(例: func _Cfunc_puts(*_Ctype_char) _Ctype_int)が含まれます。これもGoコンパイラ(6g)によってコンパイルされます。
  • _cgo_defun.c: GoのCコンパイラ(6c)によってコンパイルされるCソースファイルです。ここには、GoからC関数を呼び出すためのラッパー関数が定義されています。これらのラッパーは、runtime·cgocallを呼び出してGoランタイムからCランタイムへのコンテキストスイッチを行います。 例: _Cfunc_putsの定義
    void _cgo_be59f0f25121_Cfunc_puts(void*); // GCCでコンパイルされる実際のC関数
    void
    ·_Cfunc_puts(struct{uint8 x[1];}p) // Goから呼び出されるラッパー
    {
        runtime·cgocall(_cgo_be59f0f25121_Cfunc_puts, &p);
    }
    
    _cgo_be59f0f25121のような16進数は、cgoの入力のハッシュ値であり、衝突を避けるためのものです。
  • x.cgo2.c, y.cgo2.c: GCCによってコンパイルされるCソースファイルです。ここには、Goから呼び出される実際のC関数(例: _cgo_be59f0f25121_Cfunc_puts)の実装が含まれます。これらの関数は、Goから渡された引数フレームから引数を抽出し、実際のシステムC関数(例: puts)を呼び出し、結果をフレームに格納して返します。
  • _cgo_export.c: GCCによってコンパイルされるCソースファイルです。GoからCにエクスポートされる関数(CからGoを呼び出す場合)の定義が含まれます。
  • _cgo_main.c: GCCによってコンパイルされるCソースファイルです。リンキングプロセスで使用されるスタブ関数(main, crosscall2など)が含まれます。

3. リンキング (Linking)

cgoを使用するプログラムのリンキングは、Goのリンカー(6l)が現代のCライブラリの複雑なELF/Mach-O機能を完全に理解できないという制約があるため、特別なアプローチが取られます。

  • 動的リンクの利用: _cgo_export.c*.cgo2.cなどのGCCでコンパイルされたCのオブジェクトファイルは、最終的なバイナリにリンクされる必要があります。しかし、6lはシステムライブラリへの直接参照を一般的に処理できません。
  • 中間実行ファイルの生成: 代わりに、ビルドプロセスは、_cgo_main.c(スタブ関数を含む)、_cgo_export.c*.cgo2.cをGCCでコンパイルし、動的リンクを使用して一時的な実行ファイルを生成します。
  • _cgo_import.cの生成: cgoはこの一時的な実行ファイルを検査し、必要な共有ライブラリのリストと解決されたシンボル名(例: puts#GLIBC_2.2.5 "libc.so.6")を記録します。これらの情報は、_cgo_import.cという新しいファイルに#pragma dynlinker#pragma dynimportディレクティブとして書き込まれます。 例: _cgo_import.cの内容
    #pragma dynlinker "/lib64/ld-linux-x86-64.so.2"
    #pragma dynimport puts puts#GLIBC_2.2.5 "libc.so.6"
    // ...
    
    この_cgo_import.cはGoのCコンパイラ(6c)によってコンパイルされ、_cgo_import.6というオブジェクトファイルになります。
  • 最終的なGoパッケージの構成: 最終的に、Goのリンカー(6l)に渡されるGoパッケージは、以下のオブジェクトファイルを含みます。
    • _go_.6: _cgo_gotypes.go*.cgo1.go6gでコンパイルしたもの。
    • _cgo_defun.6: _cgo_defun.c6cでコンパイルしたもの。
    • _all.o: _cgo_export.c*.cgo2.cをGCCでコンパイルしたもの。
    • _cgo_import.6: _cgo_import.c6cでコンパイルしたもの。

この複雑なリンキングプロセスにより、6lは比較的シンプルに保たれ(完全なELF/Mach-Oリンカーを実装する必要がない)、また、パッケージがコンパイルされた後はGCCが不要になるという利点があります。例えば、netパッケージはlibcの関数にアクセスするためにcgoを使用しますが、netパッケージをインポートするプログラムをリンクする際にはGCCは必要ありません。

4. ランタイム (Runtime)

cgoを使用する場合、Goランタイムはプロセス内のすべてを完全に制御しているわけではないという前提で動作する必要があります。特に、スレッドやスレッドローカルストレージの使用においてCランタイムと協調する必要があります。

Goのランタイムパッケージは、cgo関連のいくつかの未初期化変数(runtime·iscgo, libcgo_thread_start, initcgo)を宣言しています。cgoを使用するパッケージは"runtime/cgo"をインポートし、これらの変数を初期化します。

  • runtime·iscgo: cgoが使用されていることを示すフラグ。
  • initcgo: プログラム起動の早い段階で呼び出されるGCCコンパイル済みの関数。
  • libcgo_thread_start: Goランタイムが通常直接システムコールでスレッドを作成する代わりに、新しいスレッドを作成するために使用できるGCCコンパイル済みの関数。

これにより、GoとCの両方のランタイムが同じプロセス空間内で適切に協調し、スレッドの作成や管理、リソースの共有などを安全に行うことが可能になります。

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

このコミットのコアとなる変更は、src/cmd/cgo/doc.goファイルに以下の形式で追加された263行のコメントブロック全体です。

/*
Implementation details.

Cgo provides a way for Go programs to call C code linked into the same
address space. This comment explains the operation of cgo.

... (中略:上記「技術的詳細」で解説した内容が続く) ...

*/

このコメントブロックは、Goのドキュメンテーションツールによって解析され、cgoの公式ドキュメントの一部として公開されることを意図しています。

コアとなるコードの解説

追加されたコメントは、cgoの内部実装を「Cの理解 (Understanding C)」、「Goの翻訳 (Translating Go)」、「リンキング (Linking)」、「ランタイム (Runtime)」の4つの主要なセクションに分けて詳細に解説しています。

  1. Cの理解: cgoがCのヘッダーファイルを直接パースするのではなく、GCCを「オラクル」として利用し、コンパイルエラーメッセージやDWARFデバッグ情報を解析することで、Cの型、関数、変数、定数の情報をどのように取得するかを説明しています。これは、Cの複雑なプリプロセッサやシステム固有の拡張に対応するための非常に巧妙なアプローチです。
  2. Goの翻訳: cgoがGoとCの間のブリッジを構築するために、どのような中間ファイルを生成するかを詳述しています。x.cgo1.goでGoコード内のC.xxx参照を内部名に変換し、_cgo_gotypes.goでそれらの内部名のGo側の定義を提供します。また、_cgo_defun.cx.cgo2.cでGoからCへの呼び出しを仲介するラッパー関数を生成し、runtime·cgocallを通じてGoランタイムとCランタイム間のコンテキストスイッチを行う仕組みを解説しています。
  3. リンキング: Goのリンカー(6l)がCの複雑なオブジェクトファイルを直接処理できないという制約を克服するために、cgoがどのように動的リンクと_cgo_import.cファイルを利用するかを説明しています。一時的な動的実行ファイルを生成して必要な共有ライブラリ情報を抽出し、それを#pragma dynimportディレクティブとしてGoのリンカーに渡すことで、最終的なバイナリが正しくリンクされるようにするプロセスが詳細に記述されています。
  4. ランタイム: cgoを使用する際に、GoランタイムがCランタイムとどのように協調するかを説明しています。特に、スレッド管理やスレッドローカルストレージの調整、およびruntime/cgoパッケージが提供する初期化フックやスレッド作成フックを通じて、両方のランタイムが同じプロセス空間内で安全に共存するためのメカニズムが解説されています。

このコメントは、cgoの設計が、Goのツールチェインのシンプルさを維持しつつ、Cとの高度な相互運用性を実現するためにいかに工夫されているかを明確に示しています。

関連リンク

参考にした情報源リンク