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

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

このコミットは、Go言語のcmd/cgoツールにおけるバグ修正です。具体的には、C言語の匿名構造体へのtypedefが複数のGoファイルで参照された際に、cgoがそれぞれ異なるGoの型を生成してしまう問題を解決し、一貫したGoの型が使用されるように改善しています。

コミット

commit 4e65f18cae1b4ee6074e3e544c322af030d04288
Author: Ian Lance Taylor <iant@golang.org>
Date:   Mon Jun 2 12:55:43 2014 -0700

    cmd/cgo: use same Go type for typedef to anonymous struct
    
    If we see a typedef to an anonymous struct more than once,
    presumably in two different Go files that import "C", use the
    same Go type name.
    
    Fixes #8133.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/102080043

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

https://github.com/golang/go/commit/4e65f18cae1b4ee6074e3e544c322af030d04288

元コミット内容

cmd/cgo: use same Go type for typedef to anonymous struct

If we see a typedef to an anonymous struct more than once,
presumably in two different Go files that import "C", use the
same Go type name.

Fixes #8133.

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/102080043

変更の背景

この変更は、Go言語のcgoツールがC言語の型定義をGoの型に変換する際の問題を修正するために行われました。具体的には、C言語で匿名構造体(名前を持たない構造体)がtypedefによって新しい型名を与えられた場合、そのtypedefが複数のGoファイル(それぞれがimport "C"を通じてCのヘッダーファイルをインポートしている)で参照されると、cgoがそれぞれ異なるGoの型を生成してしまうという問題がありました。

この問題が発生すると、論理的には同じCの型であるにもかかわらず、Goのコンパイラはそれらを異なる型として認識するため、型不一致のエラーが発生したり、予期せぬ実行時動作を引き起こしたりする可能性がありました。例えば、C.MyStructという型がfileA.gofileB.goで異なる内部表現を持つGoの型に変換されてしまうと、両ファイル間でその型を共有する際に問題が生じます。

このコミットは、Fixes #8133と関連付けられています。issue 8133は、cmd/cgoがCの型定義に対して一貫性のないGoの型を生成することに関する広範な問題の一部として報告されており、このコミットはその中でも特に匿名構造体のtypedefに起因する具体的なケースを解決することを目的としています。

前提知識の解説

このコミットの理解には、以下の概念が役立ちます。

  • cgo: GoプログラムからC言語のコードを呼び出すためのGoのツールです。import "C"という特殊なインポート宣言を使用することで、Goコード内でCの関数を呼び出したり、Cの型を使用したりできるようになります。cgoは、GoとCの間の型変換、メモリ管理、関数呼び出しの橋渡しを行います。ビルド時にCのコンパイラ(通常はGCC)と連携して、GoとCの間のバインディングコードを生成します。

  • typedef (C言語): C言語のキーワードで、既存のデータ型に新しい名前を付けるために使用されます。これにより、コードの可読性が向上し、複雑な型宣言を簡略化できます。 例: typedef struct { int x; int y; } Point; この例では、struct { int x; int y; } という匿名構造体に Point という新しい型名を与えています。

  • 匿名構造体 (C言語): 名前を持たない構造体です。通常、typedefと組み合わせて使用されるか、あるいは一度だけ使用される場合にインラインで定義されます。 例: struct { int a; char b; } my_var; この構造体自体には名前がありませんが、my_varという変数の型として定義されています。typedefと組み合わせることで、この匿名構造体を再利用可能な型として扱うことができます。

  • GoにおけるC型: cgoは、Cの型をGoの型にマッピングします。例えば、CのintはGoではC.intとして、Cのstruct MyStructはGoではC.struct_MyStructとして扱われます。このマッピングは、cgoがCのヘッダーファイルを解析し、GoのコードからCの型を参照できるようにGoの型定義を生成する過程で行われます。

  • DWARF (Debugging With Attributed Record Formats): プログラムのデバッグ情報のための標準的なフォーマットです。コンパイラはソースコードから実行可能ファイルを生成する際に、変数名、型情報、関数名、ソースコードの行番号などのデバッグ情報をDWARF形式で出力できます。cmd/cgoは、Cのコンパイラが生成するDWARF情報からCの型情報を読み取り、それをGoの型に変換するために利用します。

  • dwarf.Type: Goの標準ライブラリdebug/dwarfパッケージに含まれる型で、DWARF形式で表現された型情報をGoのプログラム内で扱うための構造体です。cmd/cgoはこの型を使用してCの型定義を解析します。

  • token.Pos: Goの標準ライブラリgo/tokenパッケージに含まれる型で、Goのソースコード内の位置(ファイル名、行番号、列番号など)を表します。コンパイラやツールがエラーメッセージやデバッグ情報で正確な位置を示すために使用されます。

技術的詳細

cmd/cgoは、Goのソースファイルに埋め込まれたCコード(import "C"ブロック内)と、Goコードが参照するCのヘッダーファイルを解析し、GoとCの間の相互運用に必要なコードを生成します。このプロセスには、Cの型を対応するGoの型に変換する作業が含まれます。

このコミット以前のcmd/cgoの動作では、Cのtypedefが匿名構造体を参照している場合、cgotypedefの名前自体は認識していましたが、その背後にある匿名構造体の「同一性」を適切に追跡できていませんでした。その結果、同じtypedef名を持つ匿名構造体が、異なるGoのパッケージやファイルでimport "C"されるたびに、cgoはそれぞれを新しい、独立したGoの型として扱ってしまっていました。

例えば、issue8331.hというヘッダーファイルに以下のような定義があったとします。

// issue8331.h
typedef struct {
    int i;
} issue8331;

そして、issue8331a.goissue8331b.goという2つのGoファイルがそれぞれこのヘッダーファイルをimport "C"で取り込んだ場合、cgoC.issue8331というGoの型を生成します。しかし、このコミット以前は、issue8331a.goで生成されたC.issue8331issue8331b.goで生成されたC.issue8331が、Goの型システム上では異なる型として扱われてしまう可能性がありました。これは、匿名構造体自体には名前がないため、cgoがその構造体の「実体」を識別する際に、typedef名だけでは不十分だったためと考えられます。

このコミットの目的は、この問題を解決し、同じtypedef名が匿名構造体を参照している場合には、cgoが常に同じGoの型を生成するようにすることです。これにより、Goの型システムにおけるCの型の整合性が保たれ、異なるGoファイル間でCの型を安全に共有できるようになります。

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

変更は主に src/cmd/cgo/gcc.go ファイルの func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type メソッド内で行われています。

--- a/src/cmd/cgo/gcc.go
+++ b/src/cmd/cgo/gcc.go
@@ -1269,7 +1269,8 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		sub := c.Type(dt.Type, pos)
 		t.Size = sub.Size
 		t.Align = sub.Align
-		if _, ok := typedef[name.Name]; !ok {
+		oldType := typedef[name.Name]
+		if oldType == nil {
 			ttt := *t
 			ttt.Go = sub.Go
 			typedef[name.Name] = &ttt
@@ -1281,6 +1282,15 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		// In -godefs and -cdefs mode, do this for all typedefs.
 		if isStructUnionClass(sub.Go) || *godefs || *cdefs {
 			t.Go = sub.Go
+
+			// If we've seen this typedef before, and it
+			// was an anonymous struct/union/class before
+			// too, use the old definition.
+			// TODO: it would be safer to only do this if
+			// we verify that the types are the same.
+			if oldType != nil && isStructUnionClass(oldType.Go) {
+				t.Go = oldType.Go
+			}
 		}

 	case *dwarf.UcharType:

コアとなるコードの解説

src/cmd/cgo/gcc.go 内の typeConv.Type メソッドは、Cの型情報を表すdwarf.Typeオブジェクトを受け取り、それをGoの型システムで表現可能な*Typeオブジェクトに変換する役割を担っています。このメソッドは、cgoがCのヘッダーファイルを解析し、Goの型定義を生成する際の中心的なロジックを含んでいます。

変更点の詳細:

  1. 既存のtypedefの取得:

    oldType := typedef[name.Name]
    

    この行が追加されました。typedefcmd/cgoが内部的に管理しているマップで、Cのtypedef名とそれに対応するGoの型定義を関連付けています。ここで、現在処理しているtypedef名 (name.Name) が既にマップに存在するかどうかを確認し、存在すればその既存のGoの型定義をoldType変数に格納します。

  2. 新しいtypedefの登録ロジックの維持:

    if oldType == nil {
        ttt := *t
        ttt.Go = sub.Go
        typedef[name.Name] = &ttt
    }
    

    このブロックは、typedefがまだtypedefマップに登録されていない場合に、新しいGoの型を生成してマップに保存するという従来のロジックを維持しています。sub.Goは、typedefが参照している基底の型(この場合は匿名構造体)に対応するGoの型です。

  3. 匿名構造体のtypedefに対するGoの型再利用ロジックの追加:

    if isStructUnionClass(sub.Go) || *godefs || *cdefs {
        t.Go = sub.Go
    
        // If we've seen this typedef before, and it
        // was an anonymous struct/union/class before
        // too, use the old definition.
        // TODO: it would be safer to only do this if
        // we verify that the types are the same.
        if oldType != nil && isStructUnionClass(oldType.Go) {
            t.Go = oldType.Go
        }
    }
    

    この部分がこのコミットの核心です。

    • 外側のifif isStructUnionClass(sub.Go) || *godefs || *cdefs は、変換対象の型が構造体、共用体、またはクラスである場合(あるいは-godefs-cdefsモードの場合)に、Goの型をsub.Goに設定する一般的な処理です。
    • その内側に新しく追加されたifif oldType != nil && isStructUnionClass(oldType.Go) が、匿名構造体のtypedefに対する修正ロジックです。
      • oldType != nil: 同じtypedef名が以前にも処理され、typedefマップに登録されていることを意味します。
      • isStructUnionClass(oldType.Go): 以前に登録されたtypedefのGoの型も、構造体、共用体、またはクラスであったことを確認します。これは、匿名構造体へのtypedefである可能性が高いことを示唆します。
      • この両方の条件が真である場合、t.Go = oldType.Go が実行されます。これは、現在処理しているtypedefに対応するGoの型として、以前に生成されたGoの型 (oldType.Go) を再利用することを意味します。

この変更により、cgoは同じtypedef名が匿名構造体を参照している場合、異なるGoファイルから参照されても、常に同じGoの型を生成するようになります。これにより、Goの型システムにおけるCの型の整合性が保たれ、型不一致の問題が解消されます。

コメント // TODO: it would be safer to only do this if // we verify that the types are the same. は、この修正が完全な型検証を行っているわけではないことを示唆していますが、この特定のケース(匿名構造体へのtypedef)においては、同じtypedef名であれば同じ型であると仮定して再利用することが、実用上は正しい解決策であると判断されたことを示しています。

関連リンク

参考にした情報源リンク

  • Go issue 8133に関するWeb検索結果
  • Go言語のcgoに関する一般的なドキュメントと知識
  • C言語のtypedefと匿名構造体に関する一般的な知識
  • DWARFデバッグ情報フォーマットに関する一般的な知識