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

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

このコミットは、Go言語のcmd/cgoツールにおいて、生成されるCコード(*.cgo2.cファイル)がGCCコンパイラによって2つの警告を発する問題を修正するものです。具体的には、「未使用変数」の警告と、「厳密なエイリアシング規則に違反する型変換されたポインタの逆参照」に関する警告を解消します。

コミット

commit 2a983aa3117a1647be2759edad8643cfdd5c7398
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Jul 12 04:35:53 2013 +0800

    cmd/cgo: silence two gcc warnings for *.cgo2.c
    1. "int e;" is unused, generating "unused variable" error.
    2. a->e was typed void *[2], but was accessed with *(int *)(a->e), this
    generated "dereferencing type-punned pointer will break strict-aliasing rules" error.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11009043

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

https://github.com/golang/go/commit/2a983aa3117a1647be2759edad8643cfdd5c7398

元コミット内容

cmd/cgo: silence two gcc warnings for *.cgo2.c

  1. "int e;" が未使用であり、「未使用変数」エラーを生成していた。
  2. a->evoid *[2] 型であったにもかかわらず、*(int *)(a->e) としてアクセスされており、これが「厳密なエイリアシング規則に違反する型変換されたポインタの逆参照」エラーを生成していた。

変更の背景

Go言語のcgoツールは、GoコードからCコードを呼び出すための接着コード(glue code)を生成します。この接着コードは通常、*.cgo2.cという名前のCファイルとして出力され、その後GCCなどのCコンパイラによってコンパイルされます。

このコミットが行われた当時、cgoが生成するCコードには、GCCコンパイラが警告を発する2つの問題がありました。これらの警告はコンパイルを妨げるものではありませんでしたが、クリーンなビルドプロセスを維持し、潜在的なバグや未定義の動作を防ぐために修正が必要でした。

具体的には、以下の2つの警告が問題となっていました。

  1. 未使用変数警告: cgoが生成する特定の関数内で、int e;という変数が宣言されているにもかかわらず、実際には使用されていない箇所がありました。これはコンパイラにとって無駄なコードであり、警告の対象となります。
  2. 厳密なエイリアシング規則違反警告: cgoが生成する構造体において、void *[2]型のメンバeが定義されていました。しかし、このメンバにアクセスする際に、*(int *)(a->e)のように、void *からint *へ型変換(type-punned)した上で逆参照が行われていました。これはC言語の厳密なエイリアシング規則(Strict Aliasing Rule)に違反する可能性があり、最適化の際に予期せぬ動作を引き起こす可能性があるため、GCCが警告を発していました。

これらの警告を解消することで、cgoが生成するコードの品質と移植性を向上させ、将来的なコンパイラのバージョンアップによる問題発生のリスクを低減することが目的でした。

前提知識の解説

1. cgoとは

cgoは、GoプログラムからC言語のコードを呼び出すためのGoツールです。Goのソースファイル内にCコードを直接記述したり、既存のCライブラリをリンクしたりすることを可能にします。cgoは、GoとCの間のデータ型変換や関数呼び出しの規約を処理するための接着コード(glue code)を自動生成します。この接着コードは通常、_cgo_export.c_cgo_main.c、そして*.cgo2.cといったCファイルとして出力され、GoコンパイラとCコンパイラによってそれぞれコンパイルされます。

2. GCCの警告

GCC(GNU Compiler Collection)は、C、C++、Goなど様々なプログラミング言語をサポートするコンパイラ群です。コンパイル時に、コードの潜在的な問題や非推奨の構文、未定義の動作につながる可能性のあるパターンに対して警告を発します。警告はエラーとは異なり、プログラムのコンパイル自体は成功しますが、開発者に対してコードの改善を促すための重要な情報源となります。

3. 未使用変数警告 (Unused Variable Warning)

これは最も一般的な警告の一つです。変数が宣言されたにもかかわらず、その変数がプログラムの実行フローのどこでも読み書きされない場合に発生します。コンパイラはこのような変数を最適化によって除去できますが、開発者に対しては、意図しない宣言やデッドコードの存在を示唆します。

4. 厳密なエイリアシング規則 (Strict Aliasing Rule)

C言語の標準(C99, C11など)で定義されている最適化規則の一つです。この規則は、異なる型のポインタが同じメモリ位置を指す(エイリアスする)場合に、コンパイラがどのような最適化を行えるかを規定します。

簡単に言うと、厳密なエイリアシング規則は、コンパイラがポインタの型に基づいてメモリへのアクセスを最適化することを許可します。例えば、int *pfloat *qという2つのポインタがある場合、コンパイラは通常、*pへの書き込みが*qの値を変更しないと仮定できます。なぜなら、intfloatは異なる型であり、通常は同じメモリ位置を指すことはないからです。

しかし、void *char *のような汎用ポインタは例外で、これらは任意の型のオブジェクトを指すことができます。

5. 型変換されたポインタ (Type-punned Pointer)

これは、ある型のポインタを別の型のポインタにキャストし、その新しい型のポインタを通じて元のメモリ位置にアクセスする行為を指します。例えば、void *ptr;int型のアドレスを格納し、その後*(float *)ptrのようにfloat *にキャストしてアクセスするような場合です。

厳密なエイリアシング規則は、このような型変換されたポインタによるアクセスが、特定の条件下で未定義の動作を引き起こす可能性があると規定しています。特に、異なる非互換な型のポインタ間で型変換を行い、そのポインタを通じてメモリにアクセスする場合に問題となります。コンパイラは、型情報に基づいて最適化を行うため、型変換によって実際のデータ型と異なる型でアクセスされると、誤った最適化が行われる可能性があります。

この警告は、通常、メモリレイアウトを直接操作するような低レベルのコードや、異なるデータ型間でデータを共有する際に発生しやすいです。

技術的詳細

このコミットは、cgoが生成するCコードにおける2つのGCC警告を解消するために、src/cmd/cgo/out.goファイル内のコード生成ロジックを変更しています。

1. 未使用変数 int e; の警告解消

元のコードでは、writeOutputFunc関数内で、エラー処理のためにint e;という変数が宣言されていました。しかし、この変数は実際に使用されることがなく、GCCが「未使用変数」の警告を発していました。

この修正では、単純にこの未使用変数の宣言を削除しています。これにより、無駄なコードがなくなり、警告が解消されます。

2. 厳密なエイリアシング規則違反警告の解消

この問題はより複雑で、cgoがGoのerrorインターフェースをCの構造体で表現する際に発生していました。

元のコードでは、structType関数内で、Goの関数がエラーを返す可能性がある場合に、Cの構造体内にエラー情報を格納するためのフィールドとしてvoid *e[2]; /* error */を生成していました。これは、Goのerrorインターフェースが内部的に2つのポインタ(型情報と値情報)で構成されていることをC側で表現するためのものでした。

しかし、このvoid *e[2]にアクセスする際に、*(int *)(a->e)のように、void *からint *へ型変換して逆参照が行われていました。これは、void *が汎用ポインタであるため、int *へのキャスト自体は問題ありませんが、その後の逆参照が厳密なエイリアシング規則に違反する可能性がありました。コンパイラは、void *が指すメモリがint型であると仮定して最適化を行うため、もし実際にint型以外のデータが格納されていた場合、予期せぬ動作を引き起こす可能性がありました。

この修正では、void *e[2]の代わりに、int e[2*sizeof(void *)/sizeof(int)]; /* error */という宣言に変更しています。

  • sizeof(void *)はポインタのサイズ(通常は4バイトまたは8バイト)です。
  • sizeof(int)int型のサイズ(通常は4バイト)です。

したがって、2*sizeof(void *)/sizeof(int)は、void *が2つ分格納できるint型の配列の要素数を計算します。例えば、void *が8バイトでintが4バイトの場合、2 * 8 / 4 = 4となり、int e[4];と等価になります。

この変更の意図は以下の通りです。

  • 型の一貫性: int型の配列として宣言することで、int型のポインタとしてアクセスする際に型の一貫性が保たれます。これにより、GCCの厳密なエイリアシング規則に関する警告が解消されます。
  • メモリレイアウトの維持: void *[2]が占めるメモリサイズと同じサイズをint型の配列で確保することで、Go側とC側でのメモリレイアウトの互換性を維持しています。cgoはGoとCの間でデータをやり取りするため、メモリレイアウトの一貫性は非常に重要です。
  • ポータビリティの向上: sizeof演算子を使用することで、異なるアーキテクチャやコンパイラ設定においても、ポインタのサイズに応じて配列のサイズが適切に調整され、コードのポータビリティが向上します。

この修正により、cgoが生成するCコードはGCCの警告を発しなくなり、より堅牢で移植性の高いものとなりました。

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

src/cmd/cgo/out.go

--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -47,7 +47,7 @@ func (p *Package) writeDefs() {
 	} else {
 		// If we're not importing runtime/cgo, we *are* runtime/cgo,
 		// which provides crosscall2.  We just need a prototype.
-		fmt.Fprintf(fm, "void crosscall2(void(*fn)(void*, int), void *a, int c);")
+		fmt.Fprintf(fm, "void crosscall2(void(*fn)(void*, int), void *a, int c);\\n")
 	}
 	fmt.Fprintf(fm, "void _cgo_allocate(void *a, int c) { }\\n")
 	fmt.Fprintf(fm, "void _cgo_panic(void *a, int c) { }\\n")
@@ -282,7 +282,7 @@ func (p *Package) structType(n *Name) (string, int64) {
 		off += pad
 	}
 	if n.AddError {
-		fmt.Fprint(&buf, "\t\tvoid *e[2]; /* error */\\n")
+		fmt.Fprint(&buf, "\t\tint e[2*sizeof(void *)/sizeof(int)]; /* error */\\n")
 		off += 2 * p.PtrSize
 	}
 	if off == 0 {
@@ -478,7 +478,6 @@ func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) {
 	fmt.Fprintf(fgcc, "_cgo%s%s(void *v)\\n", cPrefix, n.Mangle)\n")
 	fmt.Fprintf(fgcc, "{\\n")
 	if n.AddError {
-\t\tfmt.Fprintf(fgcc, "\t\tint e;\\n") // assuming 32 bit (see comment above structType)\n")
 	\tfmt.Fprintf(fgcc, "\t\terrno = 0;\\n")
 	}
 	// We're trying to write a gcc struct that matches 6c/8c/5c's layout.\n")

コアとなるコードの解説

1. crosscall2関数のプロトタイプ定義の修正 (L47)

-		fmt.Fprintf(fm, "void crosscall2(void(*fn)(void*, int), void *a, int c);")
+		fmt.Fprintf(fm, "void crosscall2(void(*fn)(void*, int), void *a, int c);\\n")

この変更は、crosscall2関数のCプロトタイプ定義の末尾に改行文字\nを追加したものです。これは、生成されるCコードのフォーマットを改善し、可読性を高めるための小さな修正であり、直接的にGCCの警告を解消するものではありませんが、コード生成の一貫性を保つためのものです。

2. エラー構造体フィールドの型変更 (L282)

-		fmt.Fprint(&buf, "\t\tvoid *e[2]; /* error */\\n")
+		fmt.Fprint(&buf, "\t\tint e[2*sizeof(void *)/sizeof(int)]; /* error */\\n")

これが厳密なエイリアシング規則違反の警告を解消するための主要な変更です。

  • 変更前: void *e[2];
    • void *型のポインタを2つ格納する配列として宣言されていました。Goのerrorインターフェースが内部的に2つのポインタ(型情報と値情報)で構成されるため、C側でそのメモリレイアウトを模倣していました。しかし、このvoid *int *としてアクセスする際に、GCCが厳密なエイリアシング規則違反の警告を発していました。
  • 変更後: int e[2*sizeof(void *)/sizeof(int)];
    • int型の配列として宣言されています。配列のサイズは、void *2つ分のメモリをint型で表現するために必要な要素数を計算しています。
    • 2 * sizeof(void *) は、GoのerrorインターフェースがC側で占めるべきバイト数です。
    • / sizeof(int) は、そのバイト数をint型のサイズで割ることで、必要なint要素の数を算出します。
    • この変更により、eフィールドはint型の配列として扱われるため、int型のポインタとしてアクセスしても型の一貫性が保たれ、厳密なエイリアシング規則違反の警告が解消されます。同時に、Go側とC側でのメモリレイアウトの互換性も維持されます。

3. 未使用変数int e;の削除 (L478)

-		fmt.Fprintf(fgcc, "\t\tint e;\\n") // assuming 32 bit (see comment above structType)\n")

この変更は、未使用変数int e;の宣言を削除するものです。

  • 変更前: int e;が宣言されていましたが、この変数はその後のコードで一切使用されていませんでした。
  • 変更後: この行が削除されました。これにより、GCCの「未使用変数」警告が解消され、生成されるCコードがよりクリーンになります。コメントにある「assuming 32 bit」は、この変数が32ビットシステムでのエラー処理に関連していた可能性を示唆していますが、最終的に不要と判断され削除されました。

これらの変更により、cgoが生成するCコードはGCCの警告を発しなくなり、より堅牢で標準に準拠したものとなりました。

関連リンク

参考にした情報源リンク