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

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

このコミットは、Go言語のcmd/cgoツールにおけるcdefsのバグ修正に関するものです。具体的には、cgoがC言語の型定義をGo言語の型に変換し、さらにそれをC言語の型に再変換する際に、ポインタと配列の宣言順序に関する誤った解釈を修正しています。特に、Goの配列型宣言でアスタリスク(ポインタを示す)が[]の後に続く形式(例: name []*type;)が正しくCの配列型に変換されない問題に対処しています。

コミット

commit f7dfeea90fcbb623a7cd11bccde6c4a4de7f2386
Author: Kevin Klues <klueska@gmail.com>
Date:   Wed Jul 24 17:27:42 2013 -0700

    cmd/cgo: Fix issue with cgo cdefs
    
    The problem is that the cdecl() function in cmd/cgo/godefs.go isn't
    properly translating the Go array type to a C array type when an
    asterisk follows the [] in the array type declaration (it is perfectly
    legal to put the asterisk on either side of the [] in go syntax,
    depending on how you set up your pointers).
    
    That said, the cdefs tool is only designed to translate from Go types
    generated using the cgo *godefs* tool -- where the godefs tool is
    designed to translate gcc-style C types into Go types. In essence, the
    cdefs tool translates from gcc-style C types to Go types (via the godefs
    tool), then back to kenc-style C types. Because of this, cdefs does not
    need to know how to translate arbitraty Go types into C, just the ones
    produced by godefs.
    
    The problem is that during this translation process, the logic is
    slightly wrong when going from (e.g.):
    
    char *array[10];
    to:
    array [10]*int8;
    back to:
    int8 *array[10];
    
    In the current implementation of cdecl(), the translation from the Go
    type declaration back to the kenc-style declaration looks for Go
    types of the form:
    
    name *[]type;
    rather than the actual generated Go type declaration of:
    name []*type;
    
    Both are valid Go syntax, with slightly different semantics, but the
    latter is the only one that can ever be generated by the godefs tools.
    (The semantics of the former are not directly expressible in a
    single C statement -- you would have to have to first typedef the array
    type, then declare a pointer to that typedef'd type in a separate
    statement).
    
    This commit changes the logic of cdecl() to look properly for, and
    translate, Go type declarations of the form:
    name []*type;
    
    Additionally, the original implementation only allowed for a single
    asterisk and a single sized aray (i.e. only a single level of pointer
    indirection, and only one set of []) on the type, whereas the patched
    version allows for an arbitrary number of both.
    
    Tests are included in misc/cgo/testcdefs and the all.bash script has been
    updated to account for these.
    
    R=golang-dev, bradfitz, dave, iant
    CC=golang-dev
    https://golang.org/cl/11377043

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

https://github.com/golang/go/commit/f7dfeea90fcbb623a7cd11bccde6c4a4de7f2386

元コミット内容

cmd/cgo: cgo cdefs の問題を修正

cmd/cgo/godefs.go内のcdecl()関数が、Goの配列型宣言でアスタリスクが[]の後に続く場合に、Goの配列型をCの配列型に適切に変換していなかった。Goの構文ではアスタリスクを[]のどちら側に置いても合法だが、cdefsツールはcgo *godefs*ツールによって生成されたGo型のみを変換するように設計されている。godefsツールはgccスタイルのC型をGo型に変換する。本質的に、cdefsツールはgccスタイルのC型からGo型へ(godefsツールを介して)、そしてkencスタイルのC型へと変換する。このため、cdefsは任意のGo型をCに変換する必要はなく、godefsによって生成されたものだけを変換すればよい。

問題は、この変換プロセス中に、例えばchar *array[10];からarray [10]*int8;へ、そしてint8 *array[10];へと戻す際のロジックがわずかに間違っていたことである。

cdecl()の現在の実装では、Goの型宣言からkencスタイルの宣言への変換において、name *[]type;という形式のGo型を探していたが、実際に生成されるGo型宣言はname []*type;という形式であった。

どちらも有効なGo構文であり、わずかに異なるセマンティクスを持つが、後者(name []*type;)のみがgodefsツールによって生成され得る。前者(name *[]type;)のセマンティクスは、単一のCステートメントでは直接表現できない(まず配列型をtypedefし、そのtypedefされた型へのポインタを別のステートメントで宣言する必要がある)。

このコミットは、cdecl()のロジックを変更し、name []*type;という形式のGo型宣言を適切に探し、変換するようにする。

さらに、元の実装では単一のアスタリスクと単一のサイズの配列(つまり、ポインタの間接参照レベルが1つ、[]のセットが1つ)しか許可していなかったが、パッチ適用後のバージョンでは、両方を任意の数だけ許可する。

テストはmisc/cgo/testcdefsに含まれており、all.bashスクリプトもこれらを考慮して更新された。

変更の背景

Go言語は、C言語のコードをGoプログラムから呼び出すためのcgoツールを提供しています。このcgoツールは、C言語のヘッダファイルを読み込み、それに対応するGo言語の型定義や関数シグネチャを生成する機能を持っています。このプロセスにおいて、godefsというサブツールがCの型をGoの型に変換し、さらにcdefsというツールがGoの型をCの型に再変換する役割を担っています。

このコミット以前には、cdefsツールがGoの配列型をCの配列型に変換する際に、特定のパターンで誤った変換を行うバグが存在しました。具体的には、Go言語の型宣言において、ポインタを示すアスタリスクが配列のブラケット[]の「後」に続く形式(例: []*int8)が、C言語の同等の表現に正しくマッピングされていませんでした。

この問題は、cgoが生成するGoの型定義と、cdefsが期待するGoの型定義の間に不一致があったために発生しました。godefsは常にname []*type;の形式でGoの型を生成するにもかかわらず、cdecl()関数はname *[]type;の形式を期待して処理を行っていたため、誤ったCの型が生成されていました。この不正確な変換は、CとGoの間でデータ構造をやり取りする際に、予期せぬ動作やコンパイルエラーを引き起こす可能性がありました。

このコミットは、この型変換の不整合を解消し、cgoツールチェーン全体の堅牢性と正確性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  1. cgo:

    • Go言語の標準ツールチェーンの一部であり、GoプログラムからC言語のコードを呼び出す(またはC言語のコードからGoのコードを呼び出す)ためのメカニズムを提供します。
    • Goのソースファイル内に特別なimport "C"という擬似パッケージを記述し、その後にCのコードをコメント形式で埋め込むことで、Cの関数や型をGoから利用できるようになります。
    • cgoは、GoとCの間の型変換や関数呼び出しの橋渡しとなるコードを生成します。
  2. godefs:

    • cgoツールの一部として機能する内部ツールです。
    • C言語の構造体や型定義を読み込み、それに対応するGo言語の型定義(通常はunsafe.Pointerや適切なサイズの配列などを用いて、Cのメモリレイアウトを模倣したもの)を生成する役割を担います。
    • これにより、Goプログラム内でCのデータ構造を安全に扱うためのGoの型が提供されます。
  3. cdefs:

    • これもcgoツールの一部として機能する内部ツールです。
    • godefsによって生成されたGoの型定義を、再びC言語の型定義(特にK&RスタイルのC宣言)に変換する役割を担います。
    • この逆変換は、Cのヘッダファイルを生成したり、Goの型をCのコンテキストで利用可能にするために必要となります。
  4. Go言語の配列とポインタの宣言:

    • Go言語では、配列とポインタの宣言において、アスタリスク(*)とブラケット([])の位置に関して柔軟性があります。
    • []*T (例: []*int8): これは「T型へのポインタの配列」を意味します。配列の各要素がポインタです。godefsはこの形式でGoの型を生成します。
    • *[]T (例: *[]int8): これは「T型の配列へのポインタ」を意味します。配列全体へのポインタです。
    • これらのセマンティクスの違いは、C言語の型システムに変換する際に重要になります。C言語では、int *array[10];は「int型へのポインタの配列」を意味し、int (*array)[10];は「int型の配列へのポインタ」を意味します。
  5. K&RスタイルC宣言:

    • C言語の初期の標準であるK&R (Kernighan and Ritchie) スタイルでは、型宣言の構文が現代のANSI Cとは異なる場合があります。特に、ポインタと配列の組み合わせにおいて、その解釈が厳密に求められます。

このコミットは、godefsが生成するGoの型([]*type形式)を、cdefsがCの型に逆変換する際に、cdecl()関数が誤って*[]type形式を期待していたために発生した不整合を修正しています。

技術的詳細

このコミットの核心は、src/cmd/cgo/godefs.go内のcdecl()関数のロジック変更にあります。この関数は、Goの型宣言文字列を受け取り、それをK&RスタイルのC言語の宣言文字列に変換する役割を担っています。

変更前のcdecl()関数は、Goの型文字列を解析する際に、ポインタを示すアスタリスク(*)と配列を示すブラケット([])の順序を誤って解釈していました。具体的には、Goの型がname *[]type;(配列へのポインタ)の形式であると仮定して処理を進めていましたが、godefsツールがCの型からGoの型を生成する際には、常にname []*type;(ポインタの配列)の形式でGoの型を生成していました。

この不一致が、以下のような誤った変換を引き起こしていました。

  • Cの元の型: char *array[10]; (10個のcharポインタの配列)
  • godefsによるGoへの変換: array [10]*int8; (10個のint8ポインタの配列)
  • cdecl()によるCへの再変換(バグあり): *int8 array[10]; (10個のint8配列へのポインタ)
  • 期待されるCへの再変換(修正後): int8 *array[10]; (10個のint8ポインタの配列)

このバグは、cdecl()関数がGoの型文字列を処理する際のループの順序と条件に起因していました。変更前は、ポインタ(*)の処理が配列([])の処理よりも先に行われる可能性があり、これが誤った解釈につながっていました。

コミットによる修正は、cdecl()関数内のポジションとポインタの処理順序を入れ替えることで、この問題を解決しています。

変更前:

  1. *の処理(もしあれば、name*を追加し、typから*を削除)
  2. []の処理(もしあれば、name[]を追加し、typから[]を削除)

変更後:

  1. []の処理(もしあれば、name[]を追加し、typから[]を削除)
  2. *の処理(もしあれば、name*を追加し、typから*を削除)

この順序の変更により、name []*type;のようなGoの型が正しく「ポインタの配列」として解釈され、Cのtype *name[];のような形式に変換されるようになります。

さらに、このコミットは、元の実装が単一のポインタ間接参照レベルと単一の配列サイズ(例: *[])しか扱えなかった制限も解消しています。修正後のcdecl()は、複数のポインタ(例: **[])や多次元配列(例: [][])を含む複雑な型宣言も正しく処理できるよう、forループを使用して*[]の出現を繰り返し処理するように改善されています。これにより、より複雑なCの型定義がGoを介して正しく変換されるようになります。

この修正は、cgoがCとGoの間で型情報を正確にやり取りするために不可欠であり、特にCのライブラリをGoから利用する際の堅牢性を高めます。

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

このコミットの主要な変更は、src/cmd/cgo/godefs.goファイルのcdecl関数に集中しています。

--- a/src/cmd/cgo/godefs.go
+++ b/src/cmd/cgo/godefs.go
@@ -261,17 +261,17 @@ func cdecl(name, typ string) string {
 	if strings.HasPrefix(typ, "*[0]") {
 		typ = "*void"
 	}
-	// X *byte -> *X byte
-	if strings.HasPrefix(typ, "*") {
-		name = "*" + name
-		typ = typ[1:]
-	}
 	// X [4]byte -> X[4] byte
-	if strings.HasPrefix(typ, "[") {
+	for strings.HasPrefix(typ, "[") {
 		i := strings.Index(typ, "]") + 1
 		name = name + typ[:i]
 		typ = typ[i:]
 	}
+	// X *byte -> *X byte
+	for strings.HasPrefix(typ, "*") {
+		name = "*" + name
+		typ = typ[1:]
+	}
 	// X T -> T X
 	// Handle the special case: 'unsafe.Pointer' is 'void *'
 	if typ == "unsafe.Pointer" {

また、この修正を検証するための新しいテストケースがmisc/cgo/testcdefs/ディレクトリに追加されています。

  • misc/cgo/testcdefs/cdefstest.c
  • misc/cgo/testcdefs/cdefstest.go
  • misc/cgo/testcdefs/main.go
  • misc/cgo/testcdefs/test.bash

そして、これらのテストを実行するために、src/run.bashスクリプトが更新されています。

--- a/src/run.bash
+++ b/src/run.bash
@@ -124,6 +124,11 @@ freebsd-386 | freebsd-amd64 | linux-386 | linux-amd64 | netbsd-386 | netbsd-amd6
 esac
 ) || exit $?\n \n+[ "$CGO_ENABLED" != 1 ] ||\n+(xcd ../misc/cgo/testcdefs\n+./test.bash || exit 1\n+) || exit $?\n+\n [ "$CGO_ENABLED" != 1 ] ||\n [ "$GOHOSTOS" == windows ] ||\n (xcd ../misc/cgo/testso\n```

## コアとなるコードの解説

`src/cmd/cgo/godefs.go`内の`cdecl`関数は、Goの型宣言をCの型宣言に変換する中心的なロジックを含んでいます。この関数は、`name`(変数名)と`typ`(Goの型文字列)を受け取り、Cの宣言文字列を返します。

変更前のコードでは、`cdecl`関数はまずポインタ(`*`)の処理を行い、その後に配列(`[]`)の処理を行っていました。

```go
// 変更前: ポインタの処理が配列の処理より前
if strings.HasPrefix(typ, "*") { // typが"*"で始まる場合
    name = "*" + name // nameの前に"*"を追加
    typ = typ[1:]     // typから"*"を削除
}
// ...
if strings.HasPrefix(typ, "[") { // typが"["で始まる場合
    i := strings.Index(typ, "]") + 1
    name = name + typ[:i] // nameの後に"[]"を追加
    typ = typ[i:]         // typから"[]"を削除
}

この順序だと、例えばGoの型が[]*int8int8へのポインタの配列)である場合、typ[]*int8として渡されます。

  1. 最初のif strings.HasPrefix(typ, "*")falseになります(typ[で始まるため)。
  2. 次にif strings.HasPrefix(typ, "[")trueになり、namename[]となり、typ*int8になります。
  3. しかし、この後の処理で*int8が適切に処理されず、最終的にCの*int8 name[];のような形式ではなく、int8 *name[];のような形式が期待されるにもかかわらず、誤ったCの宣言が生成される可能性がありました。

修正後のコードでは、この処理順序が逆転し、さらにforループが導入されています。

// 変更後: 配列の処理がポインタの処理より前になり、両方にforループが導入
for strings.HasPrefix(typ, "[") { // typが"["で始まる限り繰り返す
    i := strings.Index(typ, "]") + 1
    name = name + typ[:i] // nameの後に"[]"を追加
    typ = typ[i:]         // typから"[]"を削除
}
// ...
for strings.HasPrefix(typ, "*") { // typが"*"で始まる限り繰り返す
    name = "*" + name // nameの前に"*"を追加
    typ = typ[1:]     // typから"*"を削除
}

この変更により、Goの型が[]*int8である場合、typ[]*int8として渡されます。

  1. 最初のfor strings.HasPrefix(typ, "[")trueになり、namename[]となり、typ*int8になります。
  2. 次にfor strings.HasPrefix(typ, "*")trueになり、name*name[]となり、typint8になります。
  3. 最終的に、int8 *name[];のような正しいCの宣言が生成されるようになります。

forループの導入は、多次元配列(例: [][])や多重ポインタ(例: **)のような、より複雑な型宣言にも対応できるようにするためのものです。これにより、cdecl関数はGoの型宣言をより柔軟かつ正確にCの型宣言に変換できるようになりました。

テストケースの追加は、この修正が正しく機能することを確認し、将来の回帰を防ぐための重要なステップです。misc/cgo/testcdefs/cdefstest.go内のコメントは、バグのある変換と正しい変換の例を明確に示しており、問題の性質を理解するのに役立ちます。

関連リンク

参考にした情報源リンク

  • コミットメッセージ自体
  • Go言語の公式ドキュメント(cgounsafeパッケージに関する情報)
  • C言語のポインタと配列の宣言に関する一般的な知識