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

[インデックス 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_tbar_tは異なる型である)に反するものであり、型安全性の侵害でした。

このコミットによる修正は、gcc.goTypeメソッド内で、0サイズ型を[0]byteとして扱うかどうかの判断ロジックをより洗練させることで行われました。具体的には、dwarf.Typedwarf.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によって定義された型を示す)を識別することで、より正確な型変換を実現しています。

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

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

  1. misc/cgo/errors/err3.go:

    • このファイルは新しく追加されたテストケースです。
    • C言語の不完全型struct foostruct bartypedeffoo_tbar_tとして定義し、それぞれへのポインタfoopを宣言しています。
    • Goコード内で(*C.bar_t)(nil)C.foop*C.foo_t型)に代入しようとしています。
    • 修正前は、Cgoが両者を[0]byteとして扱うため、この代入がエラーにならずにコンパイルが通ってしまっていました。
    • 修正後は、型不一致としてコンパイルエラーが発生するようになります。
  2. misc/cgo/errors/test.bash:

    • misc/cgo/errors/err3.goが新しいテストケースとして追加されたため、このシェルスクリプトにcheck err3.goの行が追加され、新しいテストが実行されるように更新されています。
  3. 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 foostruct barという2つの不完全型が宣言され、それぞれfoo_tbar_tというtypedef名が与えられています。また、foo_t型へのグローバルポインタfoopが宣言されています。 Goコードでは、(*C.bar_t)(nil)という*C.bar_t型のnilポインタを宣言し、それをC.foop(これは*C.foo_t型)に代入しようとしています。 C言語の観点からは、foo_tbar_tは異なる型であり、これらのポインタ間で直接代入を行うことは型エラーとなるべきです。しかし、修正前のCgoは両者を[0]byteとして扱っていたため、Goコンパイラは型エラーを検出できませんでした。 このコミットの修正により、Cgoはfoo_tbar_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)は、dtypedwarf.TypedefTypeインターフェースを実装しているかどうか(つまり、そのC言語の型がtypedefによって定義されたものであるかどうか)をチェックする型アサーションです。
    • !okは、dtypetypedefされた型ではない場合に真となります。
    • 修正前: この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_tbar_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イシュートラッカーでは見つかりませんでした。これは古いイシュー番号であるか、内部的なトラッキングシステムのものである可能性があります。しかし、コミットメッセージが示す通り、このコミットはこの問題の解決を目的としています。

参考にした情報源リンク