[インデックス 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
ツールチェーン全体の堅牢性と正確性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
cgo:
- Go言語の標準ツールチェーンの一部であり、GoプログラムからC言語のコードを呼び出す(またはC言語のコードからGoのコードを呼び出す)ためのメカニズムを提供します。
- Goのソースファイル内に特別な
import "C"
という擬似パッケージを記述し、その後にCのコードをコメント形式で埋め込むことで、Cの関数や型をGoから利用できるようになります。 cgo
は、GoとCの間の型変換や関数呼び出しの橋渡しとなるコードを生成します。
-
godefs:
cgo
ツールの一部として機能する内部ツールです。- C言語の構造体や型定義を読み込み、それに対応するGo言語の型定義(通常は
unsafe.Pointer
や適切なサイズの配列などを用いて、Cのメモリレイアウトを模倣したもの)を生成する役割を担います。 - これにより、Goプログラム内でCのデータ構造を安全に扱うためのGoの型が提供されます。
-
cdefs:
- これも
cgo
ツールの一部として機能する内部ツールです。 godefs
によって生成されたGoの型定義を、再びC言語の型定義(特にK&RスタイルのC宣言)に変換する役割を担います。- この逆変換は、Cのヘッダファイルを生成したり、Goの型をCのコンテキストで利用可能にするために必要となります。
- これも
-
Go言語の配列とポインタの宣言:
- Go言語では、配列とポインタの宣言において、アスタリスク(
*
)とブラケット([]
)の位置に関して柔軟性があります。 []*T
(例:[]*int8
): これは「T
型へのポインタの配列」を意味します。配列の各要素がポインタです。godefs
はこの形式でGoの型を生成します。*[]T
(例:*[]int8
): これは「T
型の配列へのポインタ」を意味します。配列全体へのポインタです。- これらのセマンティクスの違いは、C言語の型システムに変換する際に重要になります。C言語では、
int *array[10];
は「int
型へのポインタの配列」を意味し、int (*array)[10];
は「int
型の配列へのポインタ」を意味します。
- Go言語では、配列とポインタの宣言において、アスタリスク(
-
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()
関数内のポジションとポインタの処理順序を入れ替えることで、この問題を解決しています。
変更前:
*
の処理(もしあれば、name
に*
を追加し、typ
から*
を削除)[]
の処理(もしあれば、name
に[]
を追加し、typ
から[]
を削除)
変更後:
[]
の処理(もしあれば、name
に[]
を追加し、typ
から[]
を削除)*
の処理(もしあれば、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の型が[]*int8
(int8
へのポインタの配列)である場合、typ
は[]*int8
として渡されます。
- 最初の
if strings.HasPrefix(typ, "*")
はfalse
になります(typ
は[
で始まるため)。 - 次に
if strings.HasPrefix(typ, "[")
がtrue
になり、name
はname[]
となり、typ
は*int8
になります。 - しかし、この後の処理で
*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
として渡されます。
- 最初の
for strings.HasPrefix(typ, "[")
がtrue
になり、name
はname[]
となり、typ
は*int8
になります。 - 次に
for strings.HasPrefix(typ, "*")
がtrue
になり、name
は*name[]
となり、typ
はint8
になります。 - 最終的に、
int8 *name[];
のような正しいCの宣言が生成されるようになります。
for
ループの導入は、多次元配列(例: [][]
)や多重ポインタ(例: **
)のような、より複雑な型宣言にも対応できるようにするためのものです。これにより、cdecl
関数はGoの型宣言をより柔軟かつ正確にCの型宣言に変換できるようになりました。
テストケースの追加は、この修正が正しく機能することを確認し、将来の回帰を防ぐための重要なステップです。misc/cgo/testcdefs/cdefstest.go
内のコメントは、バグのある変換と正しい変換の例を明確に示しており、問題の性質を理解するのに役立ちます。
関連リンク
- Go言語の
cgo
ドキュメント: https://pkg.go.dev/cmd/cgo - Go言語の
unsafe.Pointer
ドキュメント: https://pkg.go.dev/unsafe#Pointer
参考にした情報源リンク
- コミットメッセージ自体
- Go言語の公式ドキュメント(
cgo
、unsafe
パッケージに関する情報) - C言語のポインタと配列の宣言に関する一般的な知識