[インデックス 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.h
がimport "C"
からのプリアンブルを繰り返すように変更します。そうしないと、Issue 2612の修正は不可能になります。なぜなら、定義されていない型を参照できないからです。もし//export
を使用しており、プリアンブルに非ヘッダー情報を入れている場合、コードのリファクタリングが必要になります。
変更の背景
このコミットは、Go 1のリリースに向けてcgo
ツールの堅牢性と使いやすさを向上させるために行われました。具体的には、以下の4つの主要な問題に対処しています。
-
Issue 2552: C型のGo構造体への埋め込みの禁止 Goの構造体は、他のGoの型を匿名フィールドとして埋め込むことで、その型のメソッドを「昇格」させることができます。しかし、Cの型をGoの構造体に埋め込むことは、Goの型システムとCの型システムの間のセマンティックな不一致を引き起こす可能性があり、予期せぬ動作やコンパイルエラーにつながるため、これを明示的に禁止する必要がありました。
-
Issue 2806: 0長配列の検出 C言語では、構造体の末尾に0長配列(Flexible Array Member, FAM)を宣言することができ、これは可変長データを扱う一般的なパターンです。
cgo
がこのような構造体を正しく解釈し、Goの型にマッピングするためには、0長配列を正確に検出して処理する必要がありました。特に、DWARFデバッグ情報において0長配列が1長配列として誤って表現されることがあるため、これを修正する必要がありました。 -
Issue 2888:
attribute((unavailable))
の回避とtypedef
の利用 C言語のヘッダーファイルには、特定のプラットフォームやコンパイラバージョンで利用できない関数や型に__attribute__((unavailable))
のような属性が付与されていることがあります。cgo
がこれらの型をGoに変換しようとすると問題が発生する可能性がありました。この問題を回避し、より堅牢な型変換を行うために、可能な限りCのtypedef
を利用して型定義を解決するアプローチが採用されました。これにより、cgo
がGoの型を生成する際に、利用できない属性を持つCの型を直接参照するのを避けることができます。 -
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のリリース前に解決されるべき重要な課題でした。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
-
cgo:
- Go言語の標準ライブラリの一部であり、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムを提供します。
import "C"
という特別なインポート宣言を使用し、その直前のコメントブロックにC言語のコード(プリアンブル)を記述できます。このプリアンブルは、Cのヘッダーファイルのインクルード、型定義、変数宣言などを含めることができます。//export
ディレクティブを使用すると、Goの関数をCから呼び出せるようにエクスポートできます。cgo
は、GoとCの間のデータ型変換、関数呼び出しのスタブ生成、メモリ管理などを自動的に処理します。
-
DWARF (Debugging With Attributed Record Formats):
- プログラムのデバッグ情報(変数名、型情報、ソースコードの行番号など)を格納するための標準的なフォーマットです。
- コンパイラによって生成され、実行可能ファイルに埋め込まれるか、別のファイルに格納されます。
- デバッガはDWARF情報を使用して、実行中のプログラムの状態を解釈し、ソースコードレベルでのデバッグを可能にします。
- このコミットでは、
src/pkg/debug/dwarf
パッケージが変更されており、Cの型情報(特に構造体内の配列のサイズなど)を正確に解釈するためにDWARF情報が利用されています。
-
Goの型システムとCの型システムの違い:
- 構造体の埋め込み: Goでは、構造体に匿名フィールドとして別の構造体を埋め込むことで、その埋め込まれた構造体のフィールドやメソッドを外部の構造体から直接アクセスできるようにする「昇格」の概念があります。Cにはこのような直接的な埋め込みの概念はありません。
- 配列: Cでは、配列は固定長ですが、構造体内で0長配列(Flexible Array Member)を使用して可変長データを表現する慣習があります。Goの配列は通常固定長ですが、スライスによって可変長シーケンスを扱います。
typedef
: Cではtypedef
キーワードを使用して既存の型に新しい名前を付けることができます。これはコードの可読性を高めたり、プラットフォーム依存の型を抽象化したりするのに役立ちます。cgo
はCのtypedef
をGoの型に適切にマッピングする必要があります。__attribute__((unavailable))
: GCCなどのコンパイラ拡張で、特定の関数や型が利用できないことを示す属性です。cgo
がこのような属性を持つCの型をGoに変換しようとすると、問題が発生する可能性があります。
-
_cgo_export.h
:cgo
がGoの関数をCにエクスポートする際に生成するヘッダーファイルです。- このファイルには、エクスポートされたGoの関数に対応するCの関数プロトタイプが含まれています。
- CのコードがGoの関数を呼び出すためには、このヘッダーファイルをインクルードする必要があります。
- このコミットでは、このファイルに
import "C"
のプリアンブルが繰り返して含まれるように変更されており、これによりエクスポートされたGoの関数がCの型定義を参照できるようになります。
これらの概念を理解することで、コミットが解決しようとしている問題と、その解決策がどのように機能するのかをより深く把握できます。
技術的詳細
このコミットは、cgo
の内部動作とGoおよびCの型システム間の相互作用に関するいくつかの重要な技術的詳細に触れています。
-
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の型には適用できないため、コンパイル時に明確なエラーを出すことで開発者の混乱を防ぐための変更です。
- GoのAST (Abstract Syntax Tree) を走査する
-
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長配列であったと判断し、そのArrayType
のCount
を0に設定します。 - これにより、
cgo
がCの構造体内の0長配列を正しくGoの型にマッピングできるようになり、メモリレイアウトの不一致や誤ったサイズ計算を防ぎます。 typedef.c
には、int z[0];
のような0長配列を含む構造体のテストケースが追加され、type_test.go
にはこれらの変更を検証するためのテストが追加されました。
- DWARFデバッグ情報から型を読み取る
-
typedef
の利用とattribute((unavailable))
の回避 (src/cmd/cgo/gcc.go
,src/cmd/cgo/main.go
,src/cmd/cgo/out.go
):src/cmd/cgo/gcc.go
のtypedef
マップの型が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.go
のPackage.structType
関数では、構造体フィールドのC表現を生成する際に、Type.Typedef
が設定されていればそれを使用し、そうでなければType.C.String()
を使用するように変更されました。これにより、生成されるCのコードがより正確で、typedef
を尊重するようになります。
-
Go型表示の改善と
_cgo_export.h
のプリアンブル繰り返し (src/cmd/cgo/main.go
,src/cmd/cgo/out.go
,doc/go1.html
,doc/go1.tmpl
):src/cmd/cgo/main.go
のPackage
構造体にPreamble string
フィールドが追加されました。これは、各Go入力ファイルから収集されたCのプリアンブルを結合して保持するためのものです。Package.Record
関数内で、File.Preamble
がPackage.Preamble
に追加されるようになりました。これにより、複数のGoファイルがそれぞれimport "C"
のプリアンブルを持つ場合でも、それらがすべて結合されます。src/cmd/cgo/out.go
のPackage.writeExports
関数では、_cgo_export.h
を生成する際に、収集されたp.Preamble
がヘッダーの冒頭に書き込まれるようになりました。- この変更により、
_cgo_export.h
がGoの関数がエクスポートされる際に必要となるCの型定義をすべて含むことができるようになり、Issue 2612で指摘された、Goの型が元のCの型名を参照できない問題が解決されます。 doc/go1.html
とdoc/go1.tmpl
には、cgo
コマンドに関する新しいセクションが追加され、_cgo_export.h
の変更と、プリアンブルに非ヘッダー情報を置くべきではないという注意点が記載されました。
これらの変更は、cgo
がCのコードをより正確に解析し、Goの型にマッピングし、GoとCの間の相互運用性を向上させるための基盤を強化しています。特に、型情報の正確な伝達と、デバッグ情報の適切な解釈が重視されています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/cmd/cgo/ast.go
:File.walk
関数において、Goの構造体フィールドを走査する際に、匿名フィールド(埋め込み)のコンテキストを"embed-type"
として特別に処理するロジックが追加されました。File.saveRef
関数内で、"embed-type"
コンテキストでCの型が参照された場合にエラーを発生させるようになりました。
-
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
を使用するように変更されました。
-
src/cmd/cgo/main.go
:Package
構造体にPreamble string
フィールドが追加され、Type
構造体にTypedef string
フィールドが追加されました。Package.Record
関数内で、各ファイルのプリアンブルをPackage.Preamble
に結合するロジックが追加されました。
-
src/cmd/cgo/out.go
:Package.writeExports
関数内で、_cgo_export.h
の冒頭にPackage.Preamble
を書き出すようになりました。Package.structType
関数内で、構造体フィールドのC表現を生成する際に、Type.Typedef
が設定されていればそれを使用するように変更されました。writeDefs
関数でtypedef
を処理する際に、def.Go
を使用するように変更されました。cgoType
関数で、Goの型がエクスポートでサポートされていない場合に、より詳細なエラーメッセージを出すように変更されました。
-
src/pkg/debug/dwarf/type.go
:Data.Type
関数内で、構造体フィールドのビットオフセット計算ロジックが改善され、特に0長配列を検出してそのArrayType
のCount
を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の型システム間の相互運用性を大幅に改善し、より正確で堅牢なコード生成を可能にします。
関連リンク
- Go Issue 2552: cmd/cgo: disallow embedding of C type
- Go Issue 2806: cmd/cgo: detect 0-length array
- Go Issue 2888: cmd/cgo: use typedefs when possible, to avoid attribute((unavailable))
- Go Issue 2612: cmd/cgo: print Go types constructed from C types using original C types
- Gerrit Change 5672080: cmd/cgo: bug fixes
参考にした情報源リンク
- Go Programming Language Specification - Struct types
- DWARF Debugging Information Format
- The Go Blog: C? Go? Cgo!
- Flexible array member - Wikipedia
- GCC - Common Type Attributes (特に
unavailable
属性について) - Go 1 Release Notes (このコミットがGo 1リリースの一部として含まれているため、関連する情報がある可能性があります)