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

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

このコミットは、Go言語のcgoツールにおけるC言語の識別子(変数、型、定数など)の種類の判別方法を根本的に変更するものです。以前はコンパイラの出力するエラーメッセージのテキスト内容を解析していましたが、このコミットでは、より堅牢な方法として、特定のCコードスニペットをコンパイルし、そのコンパイルエラーの有無と発生行番号のみに基づいて識別子の種類を推測する方式に切り替えています。これにより、コンパイラのバージョン変更によるcgoの動作不安定性を解消し、特にXcode 5環境での問題を修正することを目的としています。

コミット

commit 06ad3b2de1c6608d3ec3139f7daddb67dc03e1cc
Author: Russ Cox <rsc@golang.org>
Date:   Fri Oct 18 15:56:25 2013 -0400

    cmd/cgo: stop using compiler error message text to analyze C names
    
    The old approach to determining whether "name" was a type, constant,
    or expression was to compile the C program
    
            name;
    
    and scan the errors and warnings generated by the compiler.
    This requires looking for specific substrings in the errors and warnings,
    which ties the implementation to specific compiler versions.
    As compilers change their errors or drop warnings, cgo breaks.
    This happens slowly but it does happen.
    Clang in particular (now required on OS X) has a significant churn rate.
    
    The new approach compiles a slightly more complex program
    that is either valid C or not valid C depending on what kind of
    thing "name" is. It uses only the presence or absence of an error
    message on a particular line, not the error text itself. The program is:
    
            // error if and only if name is undeclared
            void f1(void) { typeof(name) *x; }
    
            // error if and only if name is not a type
            void f2(void) { name *x; }
    
            // error if and only if name is not an integer constant
            void f3(void) { enum { x = (name)*1 }; }
    
    I had not been planning to do this until Go 1.3, because it is a
    non-trivial change, but it fixes a real Xcode 5 problem in Go 1.2,
    and the new code is easier to understand than the old code.
    It should be significantly more robust.
    
    Fixes #6596.
    Fixes #6612.
    
    R=golang-dev, r, james, iant
    CC=golang-dev
    https://golang.org/cl/15070043

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

https://github.com/golang/go/commit/06ad3b2de1c6608d3ec3139f7daddb67dc03e1cc

元コミット内容

このコミットの元々の内容は、cmd/cgoがC言語の識別子の種類(型、定数、式など)を判別するために、コンパイラ(GCCなど)が生成するエラーメッセージのテキスト内容に依存していた問題を解決することです。具体的には、name;というCコードをコンパイルし、その際に発生する警告やエラーメッセージの特定の文字列(例:「useless type name in empty declaration」、「statement with no effect」、「undeclared」など)を解析して識別子の種類を推測していました。

しかし、この方法はコンパイラのバージョンアップによってエラーメッセージのテキストが変更されたり、警告が廃止されたりすると、cgoの動作が不安定になるという脆弱性がありました。特にOS Xで必須となったClangコンパイラは、エラーメッセージの変更頻度が高く、この問題が顕著になっていました。

変更の背景

この変更の主な背景には、以下の問題がありました。

  1. コンパイラ依存性の高さ: cgoがC言語の識別子の種類を判別するために、コンパイラ(特にGCCやClang)が生成するエラーメッセージの具体的なテキスト内容に依存していたため、コンパイラのバージョンが更新されるたびにcgoが正しく動作しなくなる可能性がありました。これは、コンパイラのエラーメッセージが将来的に変更される可能性があるため、長期的な安定性を損なう要因となっていました。
  2. Clangの挙動変化: 特にOS XでGo 1.2からClangが必須になったことで、Clangのエラーメッセージの変更頻度が高いことが問題視されました。これにより、Xcode 5環境でcgoが正しく動作しないという具体的な問題(Go issue #6596および#6612)が発生していました。
  3. 堅牢性の向上: 既存の解析方法は、エラーメッセージの「文字列」に依存しているため、コンパイラが少しでもメッセージを変更すると、cgoがその識別子を正しく認識できなくなるリスクがありました。より堅牢な方法が求められていました。
  4. コードの理解しやすさ: 新しいアプローチは、旧来の方法よりもコードが理解しやすいという利点も挙げられています。

これらの問題を解決し、cgoの安定性と将来性を確保するために、コンパイラのエラーメッセージのテキスト内容に依存しない、より抽象的なエラーの有無と行番号に基づく判別方法への移行が決定されました。当初はGo 1.3での導入が検討されていましたが、Xcode 5の問題が喫緊の課題であったため、Go 1.2での修正として前倒しで導入されました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. cgoの役割:

    • cgoはGoプログラムからC言語のコードを呼び出すためのツールです。GoとCの間の相互運用性を提供します。
    • Goコード内でimport "C"と記述することで、C言語の関数、変数、型などをGoから利用できるようになります。
    • cgoは、GoとCの間のデータ変換、関数呼び出しのラッパー生成、Cコンパイラの呼び出しなどを担当します。
    • GoコードからC.nameのようにC言語の識別子を参照する際、cgonameがC言語においてどのような種類(変数、関数、型、定数など)であるかを正確に知る必要があります。この判別が今回のコミットの主題です。
  2. C言語の識別子の種類:

    • C言語には、変数、関数、型(typedefされたもの、structunionenum)、マクロ定義された定数など、様々な種類の識別子があります。
    • cgoは、これらの種類に応じてGo側での扱い方を変える必要があります。例えば、Cの型はGoの型にマッピングされ、Cの関数はGoの関数として呼び出せるように、Cの変数はGoの変数としてアクセスできるようにします。
  3. Cコンパイラのエラーと警告:

    • Cコンパイラ(GCC, Clangなど)は、Cコードをコンパイルする際に、構文エラー、型エラー、未定義の識別子などの問題を発見すると、エラーメッセージや警告メッセージを出力します。
    • これらのメッセージには、問題の種類、発生したファイル名、行番号、列番号、そして問題の詳細を説明するテキストが含まれます。
    • 旧来のcgoは、このメッセージテキストの特定のパターンを正規表現などで解析し、識別子の種類を推測していました。
  4. C言語のtypeof演算子:

    • typeofはGCC拡張(GNU C)として導入された演算子で、式の型を取得するために使用されます。例えば、typeof(x)は変数xの型を返します。
    • このコミットの新しいアプローチでは、typeof(name)という構文を利用して、nameが宣言されているかどうかをチェックします。もしnameが未宣言であれば、typeof(name)はコンパイルエラーになります。このエラーの有無を利用して、識別子の宣言状態を判別します。
  5. C言語のenum:

    • enumは列挙型を定義するために使用されます。列挙子には整数定数のみが指定できます。
    • 新しいアプローチでは、enum { x = (name)*1 };という構文を利用して、nameが整数定数であるかどうかをチェックします。もしnameが整数定数でなければ、(name)*1はコンパイルエラーになるか、enumの初期化子として不正であるというエラーが発生します。このエラーの有無を利用して、識別子が整数定数であるかを判別します。

技術的詳細

このコミットの技術的な核心は、cgoがC言語の識別子の種類を判別するロジックを、コンパイラのエラーメッセージの「テキスト内容」から「エラーの有無と発生行番号」に切り替えた点にあります。

旧アプローチの問題点: 旧アプローチでは、cgoはCの識別子nameの種類を判別するために、以下のようなCコードを生成し、コンパイラに渡していました。

name;
enum { _cgo_enum_0 = name };

そして、コンパイラがこのコードをコンパイルした際に出力するエラーや警告のテキスト内容を解析していました。

  • name;に対して「useless type name in empty declaration」のような警告が出れば、nameは型であると判断。
  • name;に対して「statement with no effect」のような警告が出れば、nameは式(変数や関数)であると判断。
  • name;に対して「undeclared」のようなエラーが出れば、nameは未宣言であると判断。
  • enum { _cgo_enum_0 = name };に対して「is not an integer constant」のようなエラーが出れば、nameは整数定数ではないと判断。

この方法は、コンパイラの出力するメッセージが少しでも変わると、cgoの解析ロジックが破綻するという根本的な脆弱性を抱えていました。

新アプローチの詳細: 新しいアプローチでは、cgoは各識別子nameに対して、以下の3種類のCコードスニペットを生成し、これらをまとめてコンパイラに渡します。重要なのは、これらのスニペットはnameの種類に関わらずCの構文として有効であるか、あるいは特定の種類の場合にのみエラーを発生させるように設計されている点です。そして、エラーが発生した行番号と、そのエラーがどのスニペットに対応するかによって、nameの種類を推測します。

  1. 未宣言のチェック:

    #line XXX "not-declared"
    void __cgo_f_XXX_1(void) { typeof(name) *__cgo_undefined__; }
    
    • typeof(name): nameが未宣言の場合、この行でコンパイルエラーが発生します。
    • *__cgo_undefined__: typeof(name)が型を返した場合、その型へのポインタを宣言します。これはnameが型であっても式であっても構文的に有効です。
    • #line XXX "not-declared": エラーが発生した場合、そのエラーがこのスニペットに対応することをcgoに伝えます。
  2. 型のチェック:

    #line XXX "not-type"
    void __cgo_f_XXX_2(void) { name *__cgo_undefined__; }
    
    • name *__cgo_undefined__: nameが型(int, struct MyStructなど)であれば、これはname型のポインタ__cgo_undefined__の宣言として有効です。
    • nameが型でなく、変数や関数などの式であれば、この行でコンパイルエラーが発生します(例: int *x;は有効だが、my_variable *x;my_variableが型でなければエラー)。
    • #line XXX "not-type": エラーが発生した場合、nameが型ではないことを示します。
  3. 整数定数のチェック:

    #line XXX "not-const"
    void __cgo_f_XXX_3(void) { enum { __cgo__undefined__ = (name)*1 }; }
    
    • enum { __cgo__undefined__ = (name)*1 };: enumの列挙子には整数定数のみが許可されます。
    • (name)*1: nameが整数定数であれば、この式は有効な整数定数になります。
    • nameが整数定数でなければ、この行でコンパイルエラーが発生します(例: float_var * 1enumの初期化子として不正)。
    • #line XXX "not-const": エラーが発生した場合、nameが整数定数ではないことを示します。

cgoは、これらのスニペットをコンパイルした結果の標準エラー出力(stderr)を解析します。ただし、解析するのはエラーメッセージのテキスト内容ではなく、エラーが発生したファイル名(not-declared, not-type, not-const)と行番号(XXX)のみです。

  • not-declared:XXXでエラーがあれば、nameは未宣言。
  • not-type:XXXでエラーがあれば、nameは型ではない。
  • not-const:XXXでエラーがあれば、nameは整数定数ではない。

これらのエラーの組み合わせによって、cgoは識別子の種類を正確に推測します。例えば、not-typeでエラーがなく、not-constでエラーがあれば、それは型であり、かつ整数定数ではない、といった具合です。

このアプローチの利点は、コンパイラのエラーメッセージの「テキスト」に依存しないため、コンパイラのバージョンアップによるメッセージの変更に影響されにくい点です。エラーの「有無」と「発生行」という、より安定した情報源に依存することで、cgoの堅牢性が大幅に向上します。

また、gccCmd関数において、コンパイラに渡すフラグが-Wall -Werror(すべての警告をエラーとして扱う)から-Wno-all -Wno-error(すべての警告を無効にし、警告をエラーとして扱わない)に変更されています。これは、新しいアプローチがエラーメッセージのテキストではなく、エラーの有無のみに依存するため、警告を抑制しても問題ないということを示しています。これにより、不要な警告がcgoの解析を妨げる可能性も排除されます。

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

このコミットにおける主要なコード変更は、src/cmd/cgo/gcc.goファイルのguessKinds関数に集中しています。

  1. src/cmd/cgo/gcc.go:

    • guessKinds関数内のCコード生成ロジックが完全に書き換えられました。
    • 旧来のname;enum { _cgo_enum_X = name };の生成部分が削除され、新しい3種類のスニペット(typeof(name), name *x, enum { x = (name)*1 })を生成するロジックに置き換えられました。
    • コンパイラのエラー出力を解析する部分も変更され、エラーメッセージのテキスト内容を解析する代わりに、エラーが発生したファイル名(not-declared, not-type, not-const)と行番号に基づいて識別子の種類を判別するようになりました。
    • gccCmd関数において、コンパイラに渡すフラグが-Wall -Werrorから-Wno-all -Wno-errorに変更されました。
    • GCC 4.8.0のバグを回避するための-Wsystem-headersフラグの追加が削除されました。これは、新しいアプローチがエラーメッセージのテキストに依存しないため、もはや不要になったためです。
  2. src/cmd/cgo/doc.go:

    • cgoがC言語の識別子の種類を推測する方法に関するドキュメントが更新されました。新しいアプローチで使用されるCコードスニペットと、エラーの解釈方法が詳細に記述されています。
  3. misc/cgo/test/cgo_test.go:

    • TestNamingという新しいテストケースが追加されました。
  4. misc/cgo/test/issue6612.go:

    • TestNamingテストケースの実装を含む新しいテストファイルが追加されました。このテストは、新しい識別子判別スキームが正しく機能するかどうかを検証するために、様々なC言語の識別子(関数、変数、定数、型、マクロなど)をGoから参照し、その値や型が期待通りであるかを確認します。特に、Clangがマクロ定義された識別子に対して警告を抑制する挙動も考慮に入れています。

コアとなるコードの解説

src/cmd/cgo/gcc.goguessKinds関数がこのコミットの心臓部です。

変更前は、guessKinds関数は以下のようなロジックで識別子の種類を推測していました(簡略化)。

// 旧アプローチ (簡略化)
func (p *Package) guessKinds(f *File) []*Name {
    // ...
    var b bytes.Buffer
    b.WriteString(f.Preamble)
    b.WriteString(builtinProlog)
    b.WriteString("void __cgo__f__(void) {\n")
    // 各識別子 n.C に対して
    // fmt.Fprintf(&b, "%s; /* #%d */\nenum { _cgo_enum_%d = %s }; /* #%d */\n", n.C, i, i, n.C, i)
    // ...
    b.WriteString("}\n")

    stderr := p.gccErrors(b.Bytes())
    // stderr の内容を解析し、特定の警告/エラーメッセージのテキストを検索して識別子の種類を判別
    // 例: "useless type name in empty declaration", "statement with no effect", "undeclared", "is not an integer constant"
    // ...
}

変更後は、guessKinds関数は以下のようなロジックに変わりました。

// 新アプローチ (簡略化)
func (p *Package) guessKinds(f *File) []*Name {
    // ...
    var b bytes.Buffer
    b.WriteString(f.Preamble)
    b.WriteString(builtinProlog)

    // 各識別子 n.C に対して、3種類のテストスニペットを生成
    for i, n := range names { // names は f.Name からフィルタリングされた識別子のリスト
        fmt.Fprintf(&b, "#line %d \"not-declared\"\\n"+
            "void __cgo_f_%d_1(void) { typeof(%s) *__cgo_undefined__; }\\n"+
            "#line %d \"not-type\"\\n"+
            "void __cgo_f_%d_2(void) { %s *__cgo_undefined__; }\\n"+
            "#line %d \"not-const\"\\n"+
            "void __cgo_f_%d_3(void) { enum { __cgo__undefined__ = (%s)*1 }; }\\n",
            i+1, i+1, n.C,
            i+1, i+1, n.C,
            i+1, i+1, n.C)
    }
    // コンパイルが完了したことを示すマーカー
    fmt.Fprintf(&b, "#line 1 \"completed\"\\n"+"int __cgo__1 = __cgo__2;\\n")

    stderr := p.gccErrors(b.Bytes())
    // stderr の内容を解析し、エラーが発生したファイル名と行番号のみをチェック
    // 例: "not-declared:XXX", "not-type:XXX", "not-const:XXX"
    // completed:1 のエラーがなければ、コンパイルが最後まで行われなかったと判断し fatalf
    // ...
    for _, line := range strings.Split(stderr, "\\n") {
        if !strings.Contains(line, ": error:") {
            continue // エラー行のみを処理
        }
        // ファイル名と行番号を抽出
        // 例: "not-declared:1: error: 'name' undeclared" から "not-declared" と "1" を取得
        filename := line[:c1] // c1 は最初のコロンの位置
        i, _ := strconv.Atoi(line[c1+1 : c2]) // c2 は2番目のコロンの位置
        i-- // 0-based index に変換

        switch filename {
        case "completed":
            completed = true
        case "not-declared":
            error_(token.NoPos, "%s", strings.TrimSpace(line[c2+1:])) // 未宣言エラーはそのまま報告
        case "not-type":
            sniff[i] |= notType // name は型ではない
        case "not-const":
            sniff[i] |= notConst // name は整数定数ではない
        }
    }

    // sniff の結果に基づいて識別子の種類を最終決定
    for i, n := range names {
        switch sniff[i] {
        case 0: // エラーなし
            fatalf("could not determine kind of name for C.%s", fixGo(n.Go))
        case notType: // not-type でのみエラー
            n.Kind = "const" // 型ではないが、整数定数である可能性
        case notConst: // not-const でのみエラー
            n.Kind = "type" // 整数定数ではないが、型である可能性
        case notConst | notType: // not-type と not-const の両方でエラー
            n.Kind = "not-type" // 型でも整数定数でもない (変数や関数)
        }
    }
    // ...
    return needType
}

この変更により、cgoはコンパイラの詳細なエラーメッセージに依存することなく、より抽象的なエラーの有無と発生行番号という情報のみでC言語の識別子の種類を判別できるようになりました。これにより、コンパイラのバージョンアップに対する耐性が向上し、cgoの堅牢性が大幅に強化されました。

関連リンク

参考にした情報源リンク

  • Go issue #6596: cmd/cgo: Xcode 5 breaks cgo - このコミットが修正した具体的な問題の一つ。Xcode 5のClangコンパイラの挙動変更により、cgoがC言語の識別子を正しく認識できなくなった問題が報告されています。
  • Go issue #6612: cmd/cgo: cgo fails to find C.myvar when myvar is a #define - このコミットが修正したもう一つの問題。#defineされたC言語の識別子をcgoが正しく扱えないケースが報告されています。
  • Go CL 15070043: このコミットの変更を提案したGoのコードレビューページ。詳細な議論や変更の背景が記述されています。
  • GCC typeof operator: https://gcc.gnu.org/onlinedocs/gcc/Typeof.html - C言語のtypeof演算子に関するGCCのドキュメント。新しいアプローチでtypeofがどのように利用されているかを理解する上で参考になります。
  • C enum declaration: https://en.cppreference.com/w/c/language/enum - C言語のenumに関する情報。enumが整数定数のみを列挙子として受け入れるという特性が、新しいアプローチでどのように利用されているかを理解する上で参考になります。
  • cgo documentation: https://pkg.go.dev/cmd/cgo - Goのcgoツールの公式ドキュメント。cgoの基本的な使い方や内部動作について理解を深めることができます。
  • Go 1.2 Release Notes: https://go.dev/doc/go1.2 - Go 1.2のリリースノート。このコミットがGo 1.2の修正として導入された背景を理解する上で参考になります。
  • Clang documentation: https://clang.llvm.org/ - Clangコンパイラの公式ドキュメント。Clangのエラーメッセージの挙動や変更頻度について理解を深めることができます。# [インデックス 17822] ファイルの概要

このコミットは、Go言語のcgoツールにおけるC言語の識別子(変数、型、定数など)の種類の判別方法を根本的に変更するものです。以前はコンパイラの出力するエラーメッセージのテキスト内容を解析していましたが、このコミットでは、より堅牢な方法として、特定のCコードスニペットをコンパイルし、そのコンパイルエラーの有無と発生行番号のみに基づいて識別子の種類を推測する方式に切り替えています。これにより、コンパイラのバージョン変更によるcgoの動作不安定性を解消し、特にXcode 5環境での問題を修正することを目的としています。

コミット

commit 06ad3b2de1c6608d3ec3139f7daddb67dc03e1cc
Author: Russ Cox <rsc@golang.org>
Date:   Fri Oct 18 15:56:25 2013 -0400

    cmd/cgo: stop using compiler error message text to analyze C names
    
    The old approach to determining whether "name" was a type, constant,
    or expression was to compile the C program
    
            name;
    
    and scan the errors and warnings generated by the compiler.
    This requires looking for specific substrings in the errors and warnings,
    which ties the implementation to specific compiler versions.
    As compilers change their errors or drop warnings, cgo breaks.
    This happens slowly but it does happen.
    Clang in particular (now required on OS X) has a significant churn rate.
    
    The new approach compiles a slightly more complex program
    that is either valid C or not valid C depending on what kind of
    thing "name" is. It uses only the presence or absence of an error
    message on a particular line, not the error text itself. The program is:
    
            // error if and only if name is undeclared
            void f1(void) { typeof(name) *x; }
    
            // error if and only if name is not a type
            void f2(void) { name *x; }
    
            // error if and only if name is not an integer constant
            void f3(void) { enum { x = (name)*1 }; }
    
    I had not been planning to do this until Go 1.3, because it is a
    non-trivial change, but it fixes a real Xcode 5 problem in Go 1.2,
    and the new code is easier to understand than the old code.
    It should be significantly more robust.
    
    Fixes #6596.
    Fixes #6612.
    
    R=golang-dev, r, james, iant
    CC=golang-dev
    https://golang.org/cl/15070043

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

https://github.com/golang/go/commit/06ad3b2de1c6608d3ec3139f7daddb67dc03e1cc

元コミット内容

このコミットの元々の内容は、cmd/cgoがC言語の識別子の種類(型、定数、式など)を判別するために、コンパイラが生成するエラーメッセージのテキスト内容に依存していた問題を解決することです。具体的には、name;というCコードをコンパイルし、その際に発生する警告やエラーメッセージの特定の文字列(例:「useless type name in empty declaration」、「statement with no effect」、「undeclared」など)を解析して識別子の種類を推測していました。

しかし、この方法はコンパイラのバージョンアップによってエラーメッセージのテキストが変更されたり、警告が廃止されたりすると、cgoの動作が不安定になるという脆弱性がありました。特にOS Xで必須となったClangコンパイラは、エラーメッセージの変更頻度が高く、この問題が顕著になっていました。

変更の背景

この変更の主な背景には、以下の問題がありました。

  1. コンパイラ依存性の高さ: cgoがC言語の識別子の種類を判別するために、コンパイラ(特にGCCやClang)が生成するエラーメッセージの具体的なテキスト内容に依存していたため、コンパイラのバージョンが更新されるたびにcgoが正しく動作しなくなる可能性がありました。これは、コンパイラのエラーメッセージが将来的に変更される可能性があるため、長期的な安定性を損なう要因となっていました。
  2. Clangの挙動変化: 特にOS XでGo 1.2からClangが必須になったことで、Clangのエラーメッセージの変更頻度が高いことが問題視されました。これにより、Xcode 5環境でcgoが正しく動作しないという具体的な問題(Go issue #6596および#6612)が発生していました。
  3. 堅牢性の向上: 既存の解析方法は、エラーメッセージの「文字列」に依存しているため、コンパイラが少しでもメッセージを変更すると、cgoがその識別子を正しく認識できなくなるリスクがありました。より堅牢な方法が求められていました。
  4. コードの理解しやすさ: 新しいアプローチは、旧来の方法よりもコードが理解しやすいという利点も挙げられています。

これらの問題を解決し、cgoの安定性と将来性を確保するために、コンパイラのエラーメッセージのテキスト内容に依存しない、より抽象的なエラーの有無と行番号に基づく判別方法への移行が決定されました。当初はGo 1.3での導入が検討されていましたが、Xcode 5の問題が喫緊の課題であったため、Go 1.2での修正として前倒しで導入されました。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. cgoの役割:

    • cgoはGoプログラムからC言語のコードを呼び出すためのツールです。GoとCの間の相互運用性を提供します。
    • Goコード内でimport "C"と記述することで、C言語の関数、変数、型などをGoから利用できるようになります。
    • cgoは、GoとCの間のデータ変換、関数呼び出しのラッパー生成、Cコンパイラの呼び出しなどを担当します。
    • GoコードからC.nameのようにC言語の識別子を参照する際、cgonameがC言語においてどのような種類(変数、関数、型、定数など)であるかを正確に知る必要があります。この判別が今回のコミットの主題です。
  2. C言語の識別子の種類:

    • C言語には、変数、関数、型(typedefされたもの、structunionenum)、マクロ定義された定数など、様々な種類の識別子があります。
    • cgoは、これらの種類に応じてGo側での扱い方を変える必要があります。例えば、Cの型はGoの型にマッピングされ、Cの関数はGoの関数として呼び出せるように、Cの変数はGoの変数としてアクセスできるようにします。
  3. Cコンパイラのエラーと警告:

    • Cコンパイラ(GCC, Clangなど)は、Cコードをコンパイルする際に、構文エラー、型エラー、未定義の識別子などの問題を発見すると、エラーメッセージや警告メッセージを出力します。
    • これらのメッセージには、問題の種類、発生したファイル名、行番号、列番号、そして問題の詳細を説明するテキストが含まれます。
    • 旧来のcgoは、このメッセージテキストの特定のパターンを正規表現などで解析し、識別子の種類を推測していました。
  4. C言語のtypeof演算子:

    • typeofはGCC拡張(GNU C)として導入された演算子で、式の型を取得するために使用されます。例えば、typeof(x)は変数xの型を返します。
    • このコミットの新しいアプローチでは、typeof(name)という構文を利用して、nameが宣言されているかどうかをチェックします。もしnameが未宣言であれば、typeof(name)はコンパイルエラーになります。このエラーの有無を利用して、識別子の宣言状態を判別します。
  5. C言語のenum:

    • enumは列挙型を定義するために使用されます。列挙子には整数定数のみが指定できます。
    • 新しいアプローチでは、enum { x = (name)*1 };という構文を利用して、nameが整数定数であるかどうかをチェックします。もしnameが整数定数でなければ、(name)*1はコンパイルエラーになるか、enumの初期化子として不正であるというエラーが発生します。このエラーの有無を利用して、識別子が整数定数であるかを判別します。

技術的詳細

このコミットの技術的な核心は、cgoがC言語の識別子の種類を判別するロジックを、コンパイラのエラーメッセージの「テキスト内容」から「エラーの有無と発生行番号」に切り替えた点にあります。

旧アプローチの問題点: 旧アプローチでは、cgoはCの識別子nameの種類を判別するために、以下のようなCコードを生成し、コンパイラに渡していました。

name;
enum { _cgo_enum_0 = name };

そして、コンパイラがこのコードをコンパイルした際に出力するエラーや警告のテキスト内容を解析していました。

  • name;に対して「useless type name in empty declaration」のような警告が出れば、nameは型であると判断。
  • name;に対して「statement with no effect」のような警告が出れば、nameは式(変数や関数)であると判断。
  • name;に対して「undeclared」のようなエラーが出れば、nameは未宣言であると判断。
  • enum { _cgo_enum_0 = name };に対して「is not an integer constant」のようなエラーが出れば、nameは整数定数ではないと判断。

この方法は、コンパイラの出力するメッセージが少しでも変わると、cgoの解析ロジックが破綻するという根本的な脆弱性を抱えていました。

新アプローチの詳細: 新しいアプローチでは、cgoは各識別子nameに対して、以下の3種類のCコードスニペットを生成し、これらをまとめてコンパイラに渡します。重要なのは、これらのスニペットはnameの種類に関わらずCの構文として有効であるか、あるいは特定の種類の場合にのみエラーを発生させるように設計されている点です。そして、エラーが発生した行番号と、そのエラーがどのスニペットに対応するかによって、nameの種類を推測します。

  1. 未宣言のチェック:

    #line XXX "not-declared"
    void __cgo_f_XXX_1(void) { typeof(name) *__cgo_undefined__; }
    
    • typeof(name): nameが未宣言の場合、この行でコンパイルエラーが発生します。
    • *__cgo_undefined__: typeof(name)が型を返した場合、その型へのポインタを宣言します。これはnameが型であっても式であっても構文的に有効です。
    • #line XXX "not-declared": エラーが発生した場合、そのエラーがこのスニペットに対応することをcgoに伝えます。
  2. 型のチェック:

    #line XXX "not-type"
    void __cgo_f_XXX_2(void) { name *__cgo_undefined__; }
    
    • name *__cgo_undefined__: nameが型(int, struct MyStructなど)であれば、これはname型のポインタ__cgo_undefined__の宣言として有効です。
    • nameが型でなく、変数や関数などの式であれば、この行でコンパイルエラーが発生します(例: int *x;は有効だが、my_variable *x;my_variableが型でなければエラー)。
    • #line XXX "not-type": エラーが発生した場合、nameが型ではないことを示します。
  3. 整数定数のチェック:

    #line XXX "not-const"
    void __cgo_f_XXX_3(void) { enum { __cgo__undefined__ = (name)*1 }; }
    
    • enum { __cgo__undefined__ = (name)*1 };: enumの列挙子には整数定数のみが許可されます。
    • (name)*1: nameが整数定数であれば、この式は有効な整数定数になります。
    • nameが整数定数でなければ、この行でコンパイルエラーが発生します(例: float_var * 1enumの初期化子として不正)。
    • #line XXX "not-const": エラーが発生した場合、nameが整数定数ではないことを示します。

cgoは、これらのスニペットをコンパイルした結果の標準エラー出力(stderr)を解析します。ただし、解析するのはエラーメッセージのテキスト内容ではなく、エラーが発生したファイル名(not-declared, not-type, not-const)と行番号(XXX)のみです。

  • not-declared:XXXでエラーがあれば、nameは未宣言。
  • not-type:XXXでエラーがあれば、nameは型ではない。
  • not-const:XXXでエラーがあれば、nameは整数定数ではない。

これらのエラーの組み合わせによって、cgoは識別子の種類を正確に推測します。例えば、not-typeでエラーがなく、not-constでエラーがあれば、それは型であり、かつ整数定数ではない、といった具合です。

このアプローチの利点は、コンパイラのエラーメッセージの「テキスト」に依存しないため、コンパイラのバージョンアップによるメッセージの変更に影響されにくい点です。エラーの「有無」と「発生行」という、より安定した情報源に依存することで、cgoの堅牢性が大幅に向上します。

また、gccCmd関数において、コンパイラに渡すフラグが-Wall -Werror(すべての警告をエラーとして扱う)から-Wno-all -Wno-error(すべての警告を無効にし、警告をエラーとして扱わない)に変更されています。これは、新しいアプローチがエラーメッセージのテキストではなく、エラーの有無のみに依存するため、警告を抑制しても問題ないということを示しています。これにより、不要な警告がcgoの解析を妨げる可能性も排除されます。

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

このコミットにおける主要なコード変更は、src/cmd/cgo/gcc.goファイルのguessKinds関数に集中しています。

  1. src/cmd/cgo/gcc.go:

    • guessKinds関数内のCコード生成ロジックが完全に書き換えられました。
    • 旧来のname;enum { _cgo_enum_X = name };の生成部分が削除され、新しい3種類のスニペット(typeof(name), name *x, enum { x = (name)*1 })を生成するロジックに置き換えられました。
    • コンパイラのエラー出力を解析する部分も変更され、エラーメッセージのテキスト内容を解析する代わりに、エラーが発生したファイル名(not-declared, not-type, not-const)と行番号に基づいて識別子の種類を判別するようになりました。
    • gccCmd関数において、コンパイラに渡すフラグが-Wall -Werrorから-Wno-all -Wno-errorに変更されました。
    • GCC 4.8.0のバグを回避するための-Wsystem-headersフラグの追加が削除されました。これは、新しいアプローチがエラーメッセージのテキストに依存しないため、もはや不要になったためです。
  2. src/cmd/cgo/doc.go:

    • cgoがC言語の識別子の種類を推測する方法に関するドキュメントが更新されました。新しいアプローチで使用されるCコードスニペットと、エラーの解釈方法が詳細に記述されています。
  3. misc/cgo/test/cgo_test.go:

    • TestNamingという新しいテストケースが追加されました。
  4. misc/cgo/test/issue6612.go:

    • TestNamingテストケースの実装を含む新しいテストファイルが追加されました。このテストは、新しい識別子判別スキームが正しく機能するかどうかを検証するために、様々なC言語の識別子(関数、変数、定数、型、マクロなど)をGoから参照し、その値や型が期待通りであるかを確認します。特に、Clangがマクロ定義された識別子に対して警告を抑制する挙動も考慮に入れています。

コアとなるコードの解説

src/cmd/cgo/gcc.goguessKinds関数がこのコミットの心臓部です。

変更前は、guessKinds関数は以下のようなロジックで識別子の種類を推測していました(簡略化)。

// 旧アプローチ (簡略化)
func (p *Package) guessKinds(f *File) []*Name {
    // ...
    var b bytes.Buffer
    b.WriteString(f.Preamble)
    b.WriteString(builtinProlog)
    b.WriteString("void __cgo__f__(void) {\n")
    // 各識別子 n.C に対して
    // fmt.Fprintf(&b, "%s; /* #%d */\nenum { _cgo_enum_%d = %s }; /* #%d */\n", n.C, i, i, n.C, i)
    // ...
    b.WriteString("}\n")

    stderr := p.gccErrors(b.Bytes())
    // stderr の内容を解析し、特定の警告/エラーメッセージのテキストを検索して識別子の種類を判別
    // 例: "useless type name in empty declaration", "statement with no effect", "undeclared", "is not an integer constant"
    // ...
}

変更後は、guessKinds関数は以下のようなロジックに変わりました。

// 新アプローチ (簡略化)
func (p *Package) guessKinds(f *File) []*Name {
    // ...
    var b bytes.Buffer
    b.WriteString(f.Preamble)
    b.WriteString(builtinProlog)

    // 各識別子 n.C に対して、3種類のテストスニペットを生成
    for i, n := range names { // names は f.Name からフィルタリングされた識別子のリスト
        fmt.Fprintf(&b, "#line %d \"not-declared\"\\n"+
            "void __cgo_f_%d_1(void) { typeof(%s) *__cgo_undefined__; }\\n"+
            "#line %d \"not-type\"\\n"+
            "void __cgo_f_%d_2(void) { %s *__cgo_undefined__; }\\n"+
            "#line %d \"not-const\"\\n"+
            "void __cgo_f_%d_3(void) { enum { __cgo__undefined__ = (%s)*1 }; }\\n",
            i+1, i+1, n.C,
            i+1, i+1, n.C,
            i+1, i+1, n.C)
    }
    // コンパイルが完了したことを示すマーカー
    fmt.Fprintf(&b, "#line 1 \"completed\"\\n"+"int __cgo__1 = __cgo__2;\\n")

    stderr := p.gccErrors(b.Bytes())
    // stderr の内容を解析し、エラーが発生したファイル名と行番号のみをチェック
    // 例: "not-declared:XXX", "not-type:XXX", "not-const:XXX"
    // completed:1 のエラーがなければ、コンパイルが最後まで行われなかったと判断し fatalf
    // ...
    for _, line := range strings.Split(stderr, "\\n") {
        if !strings.Contains(line, ": error:") {
            continue // エラー行のみを処理
        }
        // ファイル名と行番号を抽出
        // 例: "not-declared:1: error: 'name' undeclared" から "not-declared" と "1" を取得
        filename := line[:c1] // c1 は最初のコロンの位置
        i, _ := strconv.Atoi(line[c1+1 : c2]) // c2 は2番目のコロンの位置
        i-- // 0-based index に変換

        switch filename {
        case "completed":
            completed = true
        case "not-declared":
            error_(token.NoPos, "%s", strings.TrimSpace(line[c2+1:])) // 未宣言エラーはそのまま報告
        case "not-type":
            sniff[i] |= notType // name は型ではない
        case "not-const":
            sniff[i] |= notConst // name は整数定数ではない
        }
    }

    // sniff の結果に基づいて識別子の種類を最終決定
    for i, n := range names {
        switch sniff[i] {
        case 0: // エラーなし
            fatalf("could not determine kind of name for C.%s", fixGo(n.Go))
        case notType: // not-type でのみエラー
            n.Kind = "const" // 型ではないが、整数定数である可能性
        case notConst: // not-const でのみエラー
            n.Kind = "type" // 整数定数ではないが、型である可能性
        case notConst | notType: // not-type と not-const の両方でエラー
            n.Kind = "not-type" // 型でも整数定数でもない (変数や関数)
        }
    }
    // ...
    return needType
}

この変更により、cgoはコンパイラの詳細なエラーメッセージに依存することなく、より抽象的なエラーの有無と発生行番号という情報のみでC言語の識別子の種類を判別できるようになりました。これにより、コンパイラのバージョンアップに対する耐性が向上し、cgoの堅牢性が大幅に強化されました。

関連リンク

参考にした情報源リンク

  • Go issue #6596: cmd/cgo: Xcode 5 breaks cgo - このコミットが修正した具体的な問題の一つ。Xcode 5のClangコンパイラの挙動変更により、cgoがC言語の識別子を正しく認識できなくなった問題が報告されています。
  • Go issue #6612: cmd/cgo: cgo fails to find C.myvar when myvar is a #define - このコミットが修正したもう一つの問題。#defineされたC言語の識別子をcgoが正しく扱えないケースが報告されています。
  • Go CL 15070043: このコミットの変更を提案したGoのコードレビューページ。詳細な議論や変更の背景が記述されています。
  • GCC typeof operator: https://gcc.gnu.org/onlinedocs/gcc/Typeof.html - C言語のtypeof演算子に関するGCCのドキュメント。新しいアプローチでtypeofがどのように利用されているかを理解する上で参考になります。
  • C enum declaration: https://en.cppreference.com/w/c/language/enum - C言語のenumに関する情報。enumが整数定数のみを列挙子として受け入れるという特性が、新しいアプローチでどのように利用されているかを理解する上で参考になります。
  • cgo documentation: https://pkg.go.dev/cmd/cgo - Goのcgoツールの公式ドキュメント。cgoの基本的な使い方や内部動作について理解を深めることができます。
  • Go 1.2 Release Notes: https://go.dev/doc/go1.2 - Go 1.2のリリースノート。このコミットがGo 1.2の修正として導入された背景を理解する上で参考になります。
  • Clang documentation: https://clang.llvm.org/ - Clangコンパイラの公式ドキュメント。Clangのエラーメッセージの挙動や変更頻度について理解を深めることができます。