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

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

このコミットは、Go言語のcgoツールにおける複数のバグ修正と改善を目的としています。cgoはGoプログラムからC言語のコードを呼び出すためのメカニズムを提供し、その逆も可能です。このコミットは、C言語の型をGoの構造体に埋め込む際の制限、ゼロ長配列の検出、typedefの利用、そしてC言語の型から構築されたGoの型を元のC言語の型名で表示する機能に焦点を当てています。特に、_cgo_export.hファイルの生成方法が変更され、Cのプリアンブルが繰り返されるようになりました。

コミット

commit 1a0c8fe9bb4498024c82dcc9d1beeb3e60cfe5d8
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 19 13:32:55 2012 -0500

    cmd/cgo: bug fixes
    
    * disallow embedding of C type (Fixes issue 2552)
    * detect 0-length array (Fixes issue 2806)
    * use typedefs when possible, to avoid attribute((unavailable)) (Fixes issue 2888)
    * print Go types constructed from C types using original C types (Fixes issue 2612)
    
    This fix changes _cgo_export.h to repeat the preamble from import "C".
    Otherwise the fix to issue 2612 is impossible, since it cannot refer to
    types that have not been defined.  If people are using //export and
    putting non-header information in the preamble, they will need to
    refactor their code.
    
    R=golang-dev, r, r
    CC=golang-dev
    https://golang.org/cl/5672080

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

https://github.com/golang/go/commit/1a0c8fe9bb4498024c82dcc9d1beeb3e60cfe5d8

元コミット内容

cmd/cgo: バグ修正

  • C型を埋め込むことを禁止 (Issue 2552を修正)
  • 0長配列を検出 (Issue 2806を修正)
  • attribute((unavailable))を避けるため、可能な限りtypedefを使用 (Issue 2888を修正)
  • C型から構築されたGoの型を元のC型を使用して表示 (Issue 2612を修正)

この修正は、_cgo_export.himport "C"からのプリアンブルを繰り返すように変更します。そうしないと、Issue 2612の修正は不可能になります。なぜなら、定義されていない型を参照できないからです。もし//exportを使用しており、プリアンブルに非ヘッダー情報を入れている場合、コードのリファクタリングが必要になります。

変更の背景

このコミットは、Go 1のリリースに向けてcgoツールの堅牢性と使いやすさを向上させるために行われました。具体的には、以下の4つの主要な問題に対処しています。

  1. Issue 2552: C型のGo構造体への埋め込みの禁止 Goの構造体は、他のGoの型を匿名フィールドとして埋め込むことで、その型のメソッドを「昇格」させることができます。しかし、Cの型をGoの構造体に埋め込むことは、Goの型システムとCの型システムの間のセマンティックな不一致を引き起こす可能性があり、予期せぬ動作やコンパイルエラーにつながるため、これを明示的に禁止する必要がありました。

  2. Issue 2806: 0長配列の検出 C言語では、構造体の末尾に0長配列(Flexible Array Member, FAM)を宣言することができ、これは可変長データを扱う一般的なパターンです。cgoがこのような構造体を正しく解釈し、Goの型にマッピングするためには、0長配列を正確に検出して処理する必要がありました。特に、DWARFデバッグ情報において0長配列が1長配列として誤って表現されることがあるため、これを修正する必要がありました。

  3. Issue 2888: attribute((unavailable))の回避とtypedefの利用 C言語のヘッダーファイルには、特定のプラットフォームやコンパイラバージョンで利用できない関数や型に__attribute__((unavailable))のような属性が付与されていることがあります。cgoがこれらの型をGoに変換しようとすると問題が発生する可能性がありました。この問題を回避し、より堅牢な型変換を行うために、可能な限りCのtypedefを利用して型定義を解決するアプローチが採用されました。これにより、cgoがGoの型を生成する際に、利用できない属性を持つCの型を直接参照するのを避けることができます。

  4. Issue 2612: C型から構築されたGoの型の表示改善 cgoはCの型をGoの型に変換しますが、デバッグ情報やエラーメッセージにおいて、変換後のGoの型が元のCの型名を保持していることが望ましい場合があります。これにより、開発者はGoのコードを見ても、それがどのCの型に由来するのかを容易に理解できるようになります。この修正は、特に//exportディレクティブを使用してGoの関数をCから呼び出せるようにする場合に重要となります。_cgo_export.hファイルが生成される際に、Cのプリアンブルが繰り返されることで、Cの型定義が利用可能になり、Goの型が元のCの型名を参照できるようになります。

これらの問題は、cgoの安定性とGoとCの相互運用性の正確性を確保するために、Go 1のリリース前に解決されるべき重要な課題でした。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が役立ちます。

  1. cgo:

    • Go言語の標準ライブラリの一部であり、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムを提供します。
    • import "C"という特別なインポート宣言を使用し、その直前のコメントブロックにC言語のコード(プリアンブル)を記述できます。このプリアンブルは、Cのヘッダーファイルのインクルード、型定義、変数宣言などを含めることができます。
    • //exportディレクティブを使用すると、Goの関数をCから呼び出せるようにエクスポートできます。
    • cgoは、GoとCの間のデータ型変換、関数呼び出しのスタブ生成、メモリ管理などを自動的に処理します。
  2. DWARF (Debugging With Attributed Record Formats):

    • プログラムのデバッグ情報(変数名、型情報、ソースコードの行番号など)を格納するための標準的なフォーマットです。
    • コンパイラによって生成され、実行可能ファイルに埋め込まれるか、別のファイルに格納されます。
    • デバッガはDWARF情報を使用して、実行中のプログラムの状態を解釈し、ソースコードレベルでのデバッグを可能にします。
    • このコミットでは、src/pkg/debug/dwarfパッケージが変更されており、Cの型情報(特に構造体内の配列のサイズなど)を正確に解釈するためにDWARF情報が利用されています。
  3. Goの型システムとCの型システムの違い:

    • 構造体の埋め込み: Goでは、構造体に匿名フィールドとして別の構造体を埋め込むことで、その埋め込まれた構造体のフィールドやメソッドを外部の構造体から直接アクセスできるようにする「昇格」の概念があります。Cにはこのような直接的な埋め込みの概念はありません。
    • 配列: Cでは、配列は固定長ですが、構造体内で0長配列(Flexible Array Member)を使用して可変長データを表現する慣習があります。Goの配列は通常固定長ですが、スライスによって可変長シーケンスを扱います。
    • typedef: Cではtypedefキーワードを使用して既存の型に新しい名前を付けることができます。これはコードの可読性を高めたり、プラットフォーム依存の型を抽象化したりするのに役立ちます。cgoはCのtypedefをGoの型に適切にマッピングする必要があります。
    • __attribute__((unavailable)): GCCなどのコンパイラ拡張で、特定の関数や型が利用できないことを示す属性です。cgoがこのような属性を持つCの型をGoに変換しようとすると、問題が発生する可能性があります。
  4. _cgo_export.h:

    • cgoがGoの関数をCにエクスポートする際に生成するヘッダーファイルです。
    • このファイルには、エクスポートされたGoの関数に対応するCの関数プロトタイプが含まれています。
    • CのコードがGoの関数を呼び出すためには、このヘッダーファイルをインクルードする必要があります。
    • このコミットでは、このファイルにimport "C"のプリアンブルが繰り返して含まれるように変更されており、これによりエクスポートされたGoの関数がCの型定義を参照できるようになります。

これらの概念を理解することで、コミットが解決しようとしている問題と、その解決策がどのように機能するのかをより深く把握できます。

技術的詳細

このコミットは、cgoの内部動作とGoおよびCの型システム間の相互作用に関するいくつかの重要な技術的詳細に触れています。

  1. C型埋め込みの禁止 (src/cmd/cgo/ast.go):

    • GoのAST (Abstract Syntax Tree) を走査するFile.walk関数において、構造体フィールドが匿名(len(n.Names) == 0)であり、かつコンテキストが"field"(構造体フィールドの定義中)である場合に、その型がCの型であるかどうかをチェックする新しいコンテキスト"embed-type"が導入されました。
    • もしCの型が匿名フィールドとして埋め込まれようとした場合、error_関数が呼び出され、「cannot embed C type」というエラーが報告されるようになりました。これは、Goの構造体埋め込みのセマンティクスがCの型には適用できないため、コンパイル時に明確なエラーを出すことで開発者の混乱を防ぐための変更です。
  2. 0長配列の検出と修正 (src/pkg/debug/dwarf/type.go, src/pkg/debug/dwarf/testdata/typedef.c, src/pkg/debug/dwarf/type_test.go):

    • DWARFデバッグ情報から型を読み取るData.Type関数において、構造体フィールドの処理が強化されました。
    • 特に、AttrBitOffsetが利用可能でない場合でも、ByteOffsetからビットオフセットを計算するロジックが追加されました。
    • 重要な変更は、zeroArrayヘルパー関数の導入です。これは、DWARFが0長配列を1長配列として報告するバグ(または慣習)に対処するためのものです。構造体フィールドを走査する際に、現在のフィールドのビットオフセットが前のフィールドのビットオフセットと同じである場合(かつユニオンでない場合)、前のフィールドが実際には0長配列であったと判断し、そのArrayTypeCountを0に設定します。
    • これにより、cgoがCの構造体内の0長配列を正しくGoの型にマッピングできるようになり、メモリレイアウトの不一致や誤ったサイズ計算を防ぎます。
    • typedef.cには、int z[0];のような0長配列を含む構造体のテストケースが追加され、type_test.goにはこれらの変更を検証するためのテストが追加されました。
  3. typedefの利用とattribute((unavailable))の回避 (src/cmd/cgo/gcc.go, src/cmd/cgo/main.go, src/cmd/cgo/out.go):

    • src/cmd/cgo/gcc.gotypedefマップの型がmap[string]ast.Exprからmap[string]*Typeに変更されました。これは、typedefが単なるGoのAST表現だけでなく、Cの表現やその他のメタデータを含むType構造体全体を保持できるようにするためです。
    • typeConv.Type関数内で、Cのtypedef型を処理する際に、元のCの型名(dt.Name)をType.Typedefフィールドに保存するロジックが追加されました。これにより、cgoはGoの型を生成する際に、元のCのtypedef名を保持し、attribute((unavailable))のような属性を持つ型を直接参照するのを避けることができます。
    • src/cmd/cgo/out.goPackage.structType関数では、構造体フィールドのC表現を生成する際に、Type.Typedefが設定されていればそれを使用し、そうでなければType.C.String()を使用するように変更されました。これにより、生成されるCのコードがより正確で、typedefを尊重するようになります。
  4. Go型表示の改善と_cgo_export.hのプリアンブル繰り返し (src/cmd/cgo/main.go, src/cmd/cgo/out.go, doc/go1.html, doc/go1.tmpl):

    • src/cmd/cgo/main.goPackage構造体にPreamble stringフィールドが追加されました。これは、各Go入力ファイルから収集されたCのプリアンブルを結合して保持するためのものです。
    • Package.Record関数内で、File.PreamblePackage.Preambleに追加されるようになりました。これにより、複数のGoファイルがそれぞれimport "C"のプリアンブルを持つ場合でも、それらがすべて結合されます。
    • src/cmd/cgo/out.goPackage.writeExports関数では、_cgo_export.hを生成する際に、収集されたp.Preambleがヘッダーの冒頭に書き込まれるようになりました。
    • この変更により、_cgo_export.hがGoの関数がエクスポートされる際に必要となるCの型定義をすべて含むことができるようになり、Issue 2612で指摘された、Goの型が元のCの型名を参照できない問題が解決されます。
    • doc/go1.htmldoc/go1.tmplには、cgoコマンドに関する新しいセクションが追加され、_cgo_export.hの変更と、プリアンブルに非ヘッダー情報を置くべきではないという注意点が記載されました。

これらの変更は、cgoがCのコードをより正確に解析し、Goの型にマッピングし、GoとCの間の相互運用性を向上させるための基盤を強化しています。特に、型情報の正確な伝達と、デバッグ情報の適切な解釈が重視されています。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

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

    • File.walk関数において、Goの構造体フィールドを走査する際に、匿名フィールド(埋め込み)のコンテキストを"embed-type"として特別に処理するロジックが追加されました。
    • File.saveRef関数内で、"embed-type"コンテキストでCの型が参照された場合にエラーを発生させるようになりました。
  2. src/cmd/cgo/gcc.go:

    • typedefマップの型がmap[string]ast.Exprからmap[string]*Typeに変更されました。
    • typeConv.Type関数内で、Cのtypedef型を処理する際に、Type構造体にCの型表現とGoの型表現の両方を保持し、Type.Typedefフィールドに元のCのtypedef名を保存するロジックが追加されました。
    • rewriteRef関数でtypedefを解決する際に、def.Goを使用するように変更されました。
  3. src/cmd/cgo/main.go:

    • Package構造体にPreamble stringフィールドが追加され、Type構造体にTypedef stringフィールドが追加されました。
    • Package.Record関数内で、各ファイルのプリアンブルをPackage.Preambleに結合するロジックが追加されました。
  4. src/cmd/cgo/out.go:

    • Package.writeExports関数内で、_cgo_export.hの冒頭にPackage.Preambleを書き出すようになりました。
    • Package.structType関数内で、構造体フィールドのC表現を生成する際に、Type.Typedefが設定されていればそれを使用するように変更されました。
    • writeDefs関数でtypedefを処理する際に、def.Goを使用するように変更されました。
    • cgoType関数で、Goの型がエクスポートでサポートされていない場合に、より詳細なエラーメッセージを出すように変更されました。
  5. src/pkg/debug/dwarf/type.go:

    • Data.Type関数内で、構造体フィールドのビットオフセット計算ロジックが改善され、特に0長配列を検出してそのArrayTypeCountを0に設定するzeroArrayヘルパー関数が追加されました。

これらのファイルは、cgoの型解析、型変換、およびコード生成の核心部分を構成しており、このコミットの主要な機能改善とバグ修正が実装されています。

コアとなるコードの解説

src/cmd/cgo/ast.go の変更

@@ -147,6 +147,9 @@ func (f *File) saveRef(x interface{}, context string) {
 		if context == "as2" {
 			context = "expr"
 		}
+		if context == "embed-type" {
+			error_(sel.Pos(), "cannot embed C type")
+		}
 		goname := sel.Sel.Name
 		if goname == "errno" {
 			error_(sel.Pos(), "cannot refer to errno directly; see documentation")
@@ -232,7 +235,11 @@ func (f *File) walk(x interface{}, context string, visit func(*File, interface{})
 
 	// These are ordered and grouped to match ../../pkg/go/ast/ast.go
 	case *ast.Field:
-		f.walk(&n.Type, "type", visit)
+		if len(n.Names) == 0 && context == "field" {
+			f.walk(&n.Type, "embed-type", visit)
+		} else {
+			f.walk(&n.Type, "type", visit)
+		}
 	case *ast.FieldList:
 		for _, field := range n.List {
 			f.walk(field, context, visit)

この変更は、Goの構造体へのC型の埋め込みを禁止します。ast.Fieldを走査する際に、フィールドが匿名(len(n.Names) == 0)で、かつコンテキストが"field"(構造体フィールド)の場合、その型を"embed-type"コンテキストで走査するように変更されました。saveRef関数は、この"embed-type"コンテキストでCの型が参照された場合にエラー"cannot embed C type"を発生させます。これにより、Goの構造体埋め込みのセマンティクスがCの型には適用できないという問題をコンパイル時に捕捉します。

src/cmd/cgo/gcc.go の変更

@@ -894,7 +894,7 @@ type typeConv struct {
 }
 
 var tagGen int
-var typedef = make(map[string]ast.Expr)
+var typedef = make(map[string]*Type)
 var goIdent = make(map[string]*ast.Ident)
 
 func (c *typeConv) Init(ptrSize int64) {
@@ -1164,17 +1164,22 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		goIdent[name.Name] = name
 		switch dt.Kind {
 		case "union", "class":
-			typedef[name.Name] = c.Opaque(t.Size)
 			if t.C.Empty() {
 				t.C.Set("typeof(unsigned char[%d])", t.Size)
 			}
+			typedef[name.Name] = t
 		case "struct":
 			g, csyntax, align := c.Struct(dt, pos)
 			if t.C.Empty() {
 				t.C.Set(csyntax)
 			}
 			t.Align = align
-			typedef[name.Name] = g
+			tt := *t
+			if tag != "" {
+				tt.C = &TypeRepr{"struct %s", []interface{}{tag}}
+			}
+			tt.Go = g
+			typedef[name.Name] = &tt
 		}
 
 	case *dwarf.TypedefType:
@@ -1203,7 +1208,9 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		t.Size = sub.Size
 		t.Align = sub.Align
 		if _, ok := typedef[name.Name]; !ok {
-			typedef[name.Name] = sub.Go
+			tt := *t
+			tt.Go = sub.Go
+			typedef[name.Name] = &tt
 		}
 		if *godefs || *cdefs {
 			t.Go = sub.Go
@@ -1250,7 +1257,8 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 			}
 			s = strings.Join(strings.Split(s, " "), "") // strip spaces
 			name := c.Ident("_Ctype_" + s)
-			typedef[name.Name] = t.Go
+			tt := *t
+			typedef[name.Name] = &tt
 			if !*godefs && !*cdefs {
 				t.Go = name
 			}
@@ -1288,9 +1296,18 @@ func (c *typeConv) FuncArg(dtype dwarf.Type, pos token.Pos) *Type {
 		if ptr, ok := base(dt.Type).(*dwarf.PtrType); ok {
 			// Unless the typedef happens to point to void* since
 			// Go has special rules around using unsafe.Pointer.
-			if _, void := base(ptr.Type).(*dwarf.VoidType); !void {
-				return c.Type(ptr, pos)
+			if _, void := base(ptr.Type).(*dwarf.VoidType); void {
+				break
 			}
+
+			t = c.Type(ptr, pos)
+			if t == nil {
+				return nil
+			}
+
+			// Remember the C spelling, in case the struct
+			// has __attribute__((unavailable)) on it.  See issue 2888.
+			t.Typedef = dt.Name
 		}
 	}
 	return t

typedefマップがast.Exprではなく*Typeを格納するように変更されました。これにより、Cのtypedefに関するより豊富な情報(Cの表現、Goの表現、元のtypedef名など)を保持できるようになります。typeConv.Type関数では、typedefを処理する際に、Type構造体のGoフィールドにGoのAST表現を、CフィールドにCの表現を、そしてTypedefフィールドに元のCのtypedef名を格納するように変更されました。特に、attribute((unavailable))を持つ型を回避するために、Typedefフィールドが利用されます。

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

@@ -39,6 +39,7 @@ type Package struct {\n 	Decl        []ast.Decl\n 	GoFiles     []string // list of Go files\n 	GccFiles    []string // list of gcc output files\n+\tPreamble    string   // collected preamble for _cgo_export.h\n }\n \n // A File collects information about a single Go input file.\n@@ -98,6 +99,7 @@ type Type struct {\n 	C          *TypeRepr\n 	Go         ast.Expr\n 	EnumValues map[string]int64\n+\tTypedef    string\n }\n \n // A FuncType collects information about a function type in both the C and Go worlds.\n@@ -312,6 +314,9 @@ func (p *Package) Record(f *File) {\n 		}\n 	}\n \n-\tp.ExpFunc = append(p.ExpFunc, f.ExpFunc...)\n+\tif f.ExpFunc != nil {\n+\t\tp.ExpFunc = append(p.ExpFunc, f.ExpFunc...)\n+\t\tp.Preamble += "\\n" + f.Preamble\n+\t}\n \tp.Decl = append(p.Decl, f.AST.Decls...)\n }\n```
`Package`構造体に`Preamble`フィールドが追加され、`Type`構造体に`Typedef`フィールドが追加されました。`Package.Record`関数は、各Goファイルからエクスポートされた関数と、そのファイルに含まれるCのプリアンブルを収集し、`Package`全体の`Preamble`フィールドに結合するようになりました。これにより、`_cgo_export.h`にすべての必要なCの型定義を含めることができるようになります。

### `src/cmd/cgo/out.go` の変更

```go
@@ -428,6 +432,7 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) {\n 	fgcch := creat(*objDir + "_cgo_export.h")\n \n 	fmt.Fprintf(fgcch, "/* Created by cgo - DO NOT EDIT. */\\n")\n+\tfmt.Fprintf(fgcch, "%s\\n", p.Preamble)\n 	fmt.Fprintf(fgcch, "%s\\n", gccExportHeaderProlog)\n \n 	fmt.Fprintf(fgcc, "/* Created by cgo - DO NOT EDIT. */\\n")

Package.writeExports関数は、_cgo_export.hファイルを生成する際に、収集されたp.Preambleをヘッダーの冒頭に書き出すようになりました。これは、Goの関数がCにエクスポートされる際に、CのコードがGoの関数を呼び出すために必要なCの型定義が_cgo_export.hに含まれるようにするための重要な変更です。

src/pkg/debug/dwarf/type.go の変更

@@ -426,6 +426,8 @@ func (d *Data) Type(off Offset) (Type, error) {
 		t.StructName, _ = e.Val(AttrName).(string)
 		t.Incomplete = e.Val(AttrDeclaration) != nil
 		t.Field = make([]*StructField, 0, 8)
+		var lastFieldType Type
+		var lastFieldBitOffset int64
 		for kid := next(); kid != nil; kid = next() {
 			if kid.Tag == TagMember {
 				f := new(StructField)
@@ -444,11 +446,32 @@ func (d *Data) Type(off Offset) (Type, error) {
 					goto Error
 				}
 			}
+\n			haveBitOffset := false
 			f.Name, _ = kid.Val(AttrName).(string)
 			f.ByteSize, _ = kid.Val(AttrByteSize).(int64)
-			f.BitOffset, _ = kid.Val(AttrBitOffset).(int64)
+			f.BitOffset, haveBitOffset = kid.Val(AttrBitOffset).(int64)
 			f.BitSize, _ = kid.Val(AttrBitSize).(int64)
 			t.Field = append(t.Field, f)
+\n			bito := f.BitOffset
+			if !haveBitOffset {
+				bito = f.ByteOffset * 8
+			}
+			if bito == lastFieldBitOffset && t.Kind != "union" {
+				// Last field was zero width.  Fix array length.
+				// (DWARF writes out 0-length arrays as if they were 1-length arrays.)
+				zeroArray(lastFieldType)
+			}
+			lastFieldType = f.Type
+			lastFieldBitOffset = bito
+			}
+		}
+		if t.Kind != "union" {
+			b, ok := e.Val(AttrByteSize).(int64)
+			if ok && b*8 == lastFieldBitOffset {
+				// Final field must be zero width.  Fix array length.
+				zeroArray(lastFieldType)
+			}
 		}
 
 Error:
@@ -579,3 +602,14 @@ Error:
 	delete(d.typeCache, off)
 	return nil, err
 }
+
+func zeroArray(t Type) {
+	for {
+		at, ok := t.(*ArrayType)
+		if !ok {
+			break
+		}
+		at.Count = 0
+		t = at.Type
+	}
+}

Data.Type関数は、DWARFデバッグ情報から構造体型を読み取る際に、0長配列を正しく検出して処理するように変更されました。zeroArrayヘルパー関数が導入され、DWARFが0長配列を1長配列として報告する問題を修正します。これにより、cgoがCの構造体内の0長配列をGoの型に正確にマッピングできるようになります。

これらの変更は、cgoの型システムとCの型システム間の相互運用性を大幅に改善し、より正確で堅牢なコード生成を可能にします。

関連リンク

参考にした情報源リンク