[インデックス 18976] ファイルの概要
このコミットは、Go言語のCgoツールにおける型安全性の問題を修正するものです。具体的には、C言語の0サイズ型(例: 不完全型)がGo言語側で[0]byte
として内部的に表現される際に、typedef
によって異なる名前が付けられていても型が区別されず、互換性のない型間で代入が可能になってしまう問題を解決します。これにより、Cgoが生成するGoコードの型安全性が向上します。
コミット
commit 0f82cfd3f0ef84b553cd0f1e8cd578b3c29ea5d9
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Thu Mar 27 20:23:16 2014 +0000
cmd/cgo: enforce typing of 0-sized types
cgo represents all 0-sized and unsized types internally as [0]byte. This means that pointers to incomplete types would be interchangable, even if given a name by typedef.
Fixes #7409.
LGTM=iant
R=golang-codereviews, bradfitz, iant
CC=golang-codereviews
https://golang.org/cl/76450043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0f82cfd3f0ef84b553cd0f1e8cd578b3c29ea5d9
元コミット内容
cmd/cgo: enforce typing of 0-sized types
cgo represents all 0-sized and unsized types internally as [0]byte. This means that pointers to incomplete types would be interchangable, even if given a name by typedef.
Fixes #7409.
LGTM=iant
R=golang-codereviews, bradfitz, iant
CC=golang-codereviews
https://golang.org/cl/76450043
変更の背景
このコミットは、Go言語のCgoツールがC言語の0サイズ型(または不完全型)を扱う際の問題に対処するために行われました。C言語では、struct foo;
のように宣言された構造体は、その定義が後で提供されるまで「不完全型」として扱われ、サイズが不明です。このような型や、意図的にサイズを0として定義された型(Goのstruct{}
や[0]byte
に相当)は、Cgoの内部処理において一律にGoの[0]byte
として表現されていました。
この一律の表現には問題がありました。例えば、C言語側でtypedef struct foo foo_t;
とtypedef struct bar bar_t;
のように、異なる不完全型にそれぞれtypedef
で別々の名前を付けても、Cgoの内部では両者が同じ[0]byte
として扱われていました。その結果、Go言語側で*C.foo_t
型のポインタと*C.bar_t
型のポインタが、本来は互換性がないにもかかわらず、型システムによって互換性があると誤認され、互いに代入できてしまうという型安全性の問題が発生していました。
この問題は、Goの厳格な型システムとCのより柔軟な型システムとの間のミスマッチに起因します。コミットメッセージに記載されているFixes #7409
は、この特定の型安全性の問題を追跡していたGoのイシュートラッカーの項目を指しています。このコミットは、このような誤った型互換性を排除し、CgoがC言語の型定義をより正確にGo言語の型にマッピングするようにすることで、Goプログラムの堅牢性を高めることを目的としています。
前提知識の解説
Cgo
Cgoは、GoプログラムからC言語のコードを呼び出すためのGoのツールです。Goのソースファイル内に特別なimport "C"
ブロックを記述することで、C言語の関数や型をGoコードから直接利用できるようになります。Cgoは、GoとCの間のデータ変換や関数呼び出しの橋渡しとなるコードを自動生成します。これにより、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。
C言語における0サイズ型(不完全型)
C言語には、そのサイズがコンパイル時にまだ決定されていない型が存在します。これらは「不完全型 (incomplete types)」と呼ばれます。最も一般的な例は、struct foo;
のように前方宣言された構造体です。この宣言だけではfoo
構造体の具体的なメンバやサイズは不明ですが、struct foo *p;
のようにその型へのポインタを宣言することは可能です。このような不完全型は、主に「不透明なポインタ (opaque pointer)」として使用され、ライブラリの内部実装を隠蔽しつつ、その型へのポインタを関数間で受け渡すインターフェースを提供するために利用されます。これらの型は、具体的な定義が提供されるまでサイズが0であるかのように扱われることがあります。
Go言語における0サイズ型 ([0]byte
など)
Go言語にも、メモリを消費しない「0サイズ型 (zero-sized types)」が存在します。代表的なものは空の構造体struct{}
や、長さ0の配列[0]byte
です。これらの型は、メモリレイアウト上はサイズが0と見なされ、異なる変数が同じメモリ位置を指す可能性や、ポインタの比較が予測不能になるなどの特殊な振る舞いをすることがあります。Cgoは、C言語の不完全型や0サイズ型をGo言語側で表現する際に、内部的に[0]byte
を使用することがありました。これは、Goの型システムでCの「サイズ不明」という概念を表現する一つの方法でした。
型安全性
型安全性とは、プログラムが型の不一致によって不正な操作(例えば、整数をポインタとして解釈しようとするなど)を行うことを防ぐ性質です。型安全な言語では、コンパイル時または実行時に型の整合性が厳密にチェックされ、型エラーが原因でプログラムがクラッシュしたり、未定義の動作を引き起こしたりするリスクが低減されます。Go言語は強い静的型付け言語であり、高い型安全性を特徴としています。Cgoのように異なる型システムを持つ言語間を橋渡しするツールでは、この型安全性を維持することが特に重要になります。
技術的詳細
このコミットの技術的詳細は、CgoがC言語の型情報をGo言語の型に変換するプロセス、特にsrc/cmd/cgo/gcc.go
内のType
メソッドに焦点を当てています。
以前のCgoの実装では、C言語の型がGo言語に変換される際、その型が0サイズである(またはサイズが不明である)と判断されると、一律にGoの[0]byte
型として扱われていました。これは、dwarf.Type
(DWARFデバッグ情報から取得される型情報)のSize()
メソッドが負の値を返す場合に発生していました。負の値は、その型が不完全型であり、サイズがまだ決定されていないことを示します。
問題は、C言語のtypedef
によって異なる名前が付けられた不完全型(例: typedef struct foo foo_t;
と typedef struct bar bar_t;
)が、Go言語側ではどちらも[0]byte
として表現されてしまう点にありました。Goの型システムは、[0]byte
型のポインタ同士は互換性があると判断するため、*C.foo_t
と*C.bar_t
の間で誤った代入が可能になっていました。これは、C言語のセマンティクス(foo_t
とbar_t
は異なる型である)に反するものであり、型安全性の侵害でした。
このコミットによる修正は、gcc.go
のType
メソッド内で、0サイズ型を[0]byte
として扱うかどうかの判断ロジックをより洗練させることで行われました。具体的には、dwarf.Type
がdwarf.TypedefType
であるかどうかをチェックする条件が追加されました。
- 修正前:
t.Size < 0
(サイズが不明)であれば、無条件にt.Size = 0
とし、t.Go = c.Opaque(0)
(Goの[0]byte
に相当する不透明な型)を設定していました。 - 修正後:
t.Size < 0
の場合でも、その型がdwarf.TypedefType
(つまり、既存の型に新しい名前を付けたもの)である場合は、t.Go = c.Opaque(0)
の設定を行わないように変更されました。
この変更の意図は、typedef
された不完全型に対しては、CgoがGoの[0]byte
という汎用的な型を割り当てるのではなく、そのtypedef
された名前をGoの型名として保持しようとすることにあります。これにより、typedef
によって区別されたCの型が、Go側でも異なる型として認識され、型チェックが正しく機能するようになります。結果として、*C.foo_t
と*C.bar_t
のような異なるtypedef
された不完全型へのポインタ間で、誤った代入が行われることが防止され、Cgoが生成するGoコードの型安全性が強化されます。
dwarf
パッケージは、Goのコンパイラツールチェーンの一部であり、コンパイルされたバイナリに含まれるデバッグ情報(DWARF形式)を解析するために使用されます。このデバッグ情報には、プログラム内の変数、関数、そして型の詳細な情報が含まれています。Cgoは、Cコンパイラが生成したDWARF情報からC言語の型定義を読み取り、それをGo言語の型にマッピングするためにdwarf
パッケージを利用しています。今回の修正では、dwarf.TypedefType
という特定の型情報(typedef
によって定義された型を示す)を識別することで、より正確な型変換を実現しています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
misc/cgo/errors/err3.go
:- このファイルは新しく追加されたテストケースです。
- C言語の不完全型
struct foo
とstruct bar
をtypedef
でfoo_t
とbar_t
として定義し、それぞれへのポインタfoop
を宣言しています。 - Goコード内で
(*C.bar_t)(nil)
をC.foop
(*C.foo_t
型)に代入しようとしています。 - 修正前は、Cgoが両者を
[0]byte
として扱うため、この代入がエラーにならずにコンパイルが通ってしまっていました。 - 修正後は、型不一致としてコンパイルエラーが発生するようになります。
-
misc/cgo/errors/test.bash
:misc/cgo/errors/err3.go
が新しいテストケースとして追加されたため、このシェルスクリプトにcheck err3.go
の行が追加され、新しいテストが実行されるように更新されています。
-
src/cmd/cgo/gcc.go
:- Cgoの型変換ロジックの核心部分です。
func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type
メソッド内で変更が行われました。- 具体的には、
t.Size < 0
(型が不完全でサイズが不明な場合)の処理ブロック内に、dwarf.TypedefType
であるかどうかのチェックが追加されました。
--- a/src/cmd/cgo/gcc.go +++ b/src/cmd/cgo/gcc.go @@ -1327,9 +1327,12 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type { // be correct, so calling dtype.Size again will produce the correct value. t.Size = dtype.Size() if t.Size < 0 { - // Unsized types are [0]byte + // Unsized types are [0]byte, unless they're typedefs of other types. + // if so, use the name of the typedef for the go name. t.Size = 0 - t.Go = c.Opaque(0) + if _, ok := dtype.(*dwarf.TypedefType); !ok { + t.Go = c.Opaque(0) + } if t.C.Empty() { t.C.Set("void") }
コアとなるコードの解説
misc/cgo/errors/err3.go
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
/*
typedef struct foo foo_t;
typedef struct bar bar_t;
foo_t *foop;
*/
import "C"
func main() {
x := (*C.bar_t)(nil)
C.foop = x // ERROR HERE
}
このテストケースは、問題の核心を非常に明確に示しています。
C言語のコメントブロック内で、struct foo
とstruct bar
という2つの不完全型が宣言され、それぞれfoo_t
とbar_t
というtypedef
名が与えられています。また、foo_t
型へのグローバルポインタfoop
が宣言されています。
Goコードでは、(*C.bar_t)(nil)
という*C.bar_t
型のnilポインタを宣言し、それをC.foop
(これは*C.foo_t
型)に代入しようとしています。
C言語の観点からは、foo_t
とbar_t
は異なる型であり、これらのポインタ間で直接代入を行うことは型エラーとなるべきです。しかし、修正前のCgoは両者を[0]byte
として扱っていたため、Goコンパイラは型エラーを検出できませんでした。
このコミットの修正により、Cgoはfoo_t
とbar_t
を異なる型としてGoに公開するようになり、結果としてC.foop = x
の行でGoコンパイラが型不一致エラーを正しく報告するようになります。コメントの// ERROR HERE
は、まさにその場所でエラーが期待されることを示しています。
src/cmd/cgo/gcc.go
の変更点
// ... (前略) ...
func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
// ... (中略) ...
t.Size = dtype.Size()
if t.Size < 0 {
// Unsized types are [0]byte, unless they're typedefs of other types.
// if so, use the name of the typedef for the go name.
t.Size = 0
if _, ok := dtype.(*dwarf.TypedefType); !ok {
t.Go = c.Opaque(0)
}
if t.C.Empty() {
t.C.Set("void")
}
}
// ... (後略) ...
}
このコードスニペットは、CgoがC言語の型をGo言語の型に変換する際の中心的なロジックの一部です。
dtype
は、C言語の型を表すdwarf.Type
インターフェースのインスタンスです。dtype.Size()
は、その型のサイズを返します。サイズが不明な場合(不完全型など)は負の値を返します。
if t.Size < 0
: この条件は、C言語の型が不完全型であるか、またはサイズが不明であることを示します。t.Size = 0
: 不完全型の場合、Go側ではサイズを0として扱います。if _, ok := dtype.(*dwarf.TypedefType); !ok { ... }
: これがこのコミットの核心的な変更点です。dtype.(*dwarf.TypedefType)
は、dtype
がdwarf.TypedefType
インターフェースを実装しているかどうか(つまり、そのC言語の型がtypedef
によって定義されたものであるかどうか)をチェックする型アサーションです。!ok
は、dtype
がtypedef
された型ではない場合に真となります。- 修正前: この
if
文がなく、t.Size < 0
であれば無条件にt.Go = c.Opaque(0)
が実行されていました。c.Opaque(0)
は、Goの[0]byte
に相当する不透明な型を生成します。これにより、typedef
された不完全型もそうでない不完全型も、すべて[0]byte
として扱われていました。 - 修正後:
typedef
された型ではない場合にのみt.Go = c.Opaque(0)
が実行されるようになりました。つまり、typedef
された不完全型(例:foo_t
,bar_t
)は、Go側で[0]byte
という汎用的な型にマッピングされるのではなく、そのtypedef
名に基づいたより具体的な型として扱われるようになります。これにより、foo_t
とbar_t
がGo側でも異なる型として認識され、型チェックが正しく機能するようになります。
この変更により、CgoはC言語のtypedef
のセマンティクスをより正確にGo言語の型システムに反映させることができ、結果としてGoとCの間のインターフェースにおける型安全性が大幅に向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/0f82cfd3f0ef84b553cd0f1e8cd578b3c29ea5d9
- Go Change List (CL):
https://golang.org/cl/76450043
(このコミットに関連するGoのコードレビューシステム上の変更リスト) - Go Issue #7409: コミットメッセージに記載されていますが、直接のリンクは現在のGoイシュートラッカーでは見つかりませんでした。これは古いイシュー番号であるか、内部的なトラッキングシステムのものである可能性があります。しかし、コミットメッセージが示す通り、このコミットはこの問題の解決を目的としています。
参考にした情報源リンク
- Go言語のゼロサイズ型に関する議論:
- Cgoとゼロサイズ型に関連するGoのイシュー(参考情報として):
- https://github.com/golang/go/issues/20275 (cmd/cgo: never generate Go fields for zero-sized C fields)
- https://github.com/golang/go/issues/11925 (cmd/cgo: size of struct with 0 length trailing field has changed)
- Goの
dwarf
パッケージに関する一般的な情報(Goのツールチェーンがデバッグ情報をどのように扱うか):- Goのソースコードリポジトリ内の
src/debug/dwarf
パッケージのドキュメントなどが参考になります。
- Goのソースコードリポジトリ内の