[インデックス 17554] ファイルの概要
このコミットは、Go言語のcmd/cgo
ツールにおいて、C言語のmalloc
関数の呼び出し方法を改善するためのものです。具体的には、C.malloc
を直接使用する代わりに、Goランタイムが提供する独自のラッパー関数を介してmalloc
を呼び出すように変更しています。これにより、malloc
の引数型の互換性問題(特にWindows環境でのsize_t
とulong
の違い)を解決し、malloc
がメモリ確保に失敗した場合にプログラムを安全に終了させる(パニックではなくruntime.throw
を呼び出す)メカニズムを導入しています。
コミット
commit 397ba2cb4a10ec5e383f1df7617b4b8bccf8dfab
Author: Russ Cox <rsc@golang.org>
Date: Wed Sep 11 11:30:08 2013 -0400
cmd/cgo: replace C.malloc with our own wrapper
This allows us to make two changes:
1. Force the argument type to be size_t, even on broken
systems that declare malloc to take a ulong.
2. Call runtime.throw if malloc fails.
(That is, the program crashes; it does not panic.)
Fixes #3403.
Fixes #5926.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13413047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/397ba2cb4a10ec5e383f1df7617b4b8bccf8dfab
元コミット内容
cmd/cgo: replace C.malloc with our own wrapper
この変更により、以下の2つの変更が可能になります。
malloc
の引数型をsize_t
に強制します。これは、malloc
がulong
を引数として宣言されているような問題のあるシステムでも適用されます。malloc
が失敗した場合にruntime.throw
を呼び出します。(つまり、プログラムはクラッシュし、パニックは発生しません。)
Fixes #3403. Fixes #5926.
変更の背景
このコミットは、Go言語のCgoツールにおける2つの主要な問題を解決するために導入されました。
-
malloc
の引数型に関するプラットフォーム間の非互換性(Issue #3403): C標準ではmalloc
の引数型はsize_t
と定義されています。しかし、一部のシステム(特に当時のWindows環境)では、malloc
がunsigned long
を引数として宣言されていることがありました。GoのCgoは、Cの関数をGoから呼び出す際に、Cの型定義に基づいてGoの型を生成します。この型定義の不一致が原因で、GoからC.malloc
を呼び出す際に型変換の問題や、場合によっては不正なメモリ割り当てが発生する可能性がありました。このコミットは、Go側でmalloc
を呼び出す際に、常にsize_t
として扱うように強制することで、このプラットフォーム間の差異を吸収しようとしています。 -
malloc
失敗時の挙動の改善(Issue #5926): C言語のmalloc
関数は、メモリ割り当てに失敗した場合にNULL
を返します。GoのプログラムがCgoを通じてmalloc
を呼び出し、それがNULL
を返した場合、Go側でそのNULL
を適切に処理しないと、後続のメモリアクセスでセグメンテーション違反などのクラッシュを引き起こす可能性がありました。このコミット以前は、C.malloc
が失敗してもGoランタイムがそれを検知して安全に終了させるメカニズムがありませんでした。この変更は、malloc
が失敗した際にGoランタイムのruntime.throw
関数を呼び出すことで、プログラムを即座に、かつ制御された形で終了させるようにします。これにより、未定義動作や予測不能なクラッシュを防ぎ、デバッグを容易にします。runtime.throw
はGoのパニックとは異なり、回復不能なエラーを示すもので、スタックトレースを出力してプログラムを終了させます。
これらの問題は、GoプログラムがCライブラリと連携する際の堅牢性と移植性を向上させる上で重要でした。
前提知識の解説
-
Cgo: Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Goのソースファイル内に
import "C"
という特殊なインポート文を記述し、その直後にCのコードをコメント形式で記述することで、Cの関数や型をGoから利用できるようになります。Cgoは、GoとCの間のデータ型変換や呼び出し規約の調整を自動的に行いますが、時にはプラットフォーム固有の差異やCの標準ライブラリ関数の挙動に関する深い理解が必要となります。 -
malloc
関数:malloc
はC標準ライブラリ(stdlib.h
)で提供されるメモリ割り当て関数です。指定されたサイズのメモリブロックをヒープから割り当て、そのブロックへのポインタを返します。メモリ割り当てに失敗した場合はNULL
を返します。 -
size_t
とunsigned long
:size_t
: C言語でオブジェクトのサイズや配列のインデックスを表すために使用される符号なし整数型です。sizeof
演算子の結果の型であり、システムによってその具体的なサイズ(ビット幅)は異なりますが、常に十分な大きさを持つことが保証されています。malloc
の引数型として標準で推奨されています。unsigned long
: 符号なし長整数型です。そのサイズはシステムによって異なりますが、通常は32ビットまたは64ビットです。一部の古いシステムや特定のコンパイラ環境では、malloc
の引数がunsigned long
として宣言されていることがありました。この型はsize_t
と互換性がない場合があり、特に異なるビット幅を持つシステム間で問題を引き起こす可能性があります。
-
runtime.throw
とpanic
: Go言語には、プログラムの異常終了を扱うための2つの主要なメカニズムがあります。panic
: Goの組み込み関数で、回復可能なエラーや予期せぬ状況を示すために使用されます。panic
が発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer
)が実行され、スタックがアンワインドされます。recover
関数を使ってpanic
を捕捉し、プログラムの実行を継続することも可能です。runtime.throw
: Goランタイム内部で使用される関数で、回復不能な致命的なエラーが発生した場合に呼び出されます。panic
とは異なり、runtime.throw
はdefer
関数を実行せず、recover
で捕捉することもできません。これは通常、ランタイムの内部的な整合性が損なわれた場合や、Cgo呼び出しで致命的なエラーが発生した場合など、プログラムが安全に続行できない状況で呼び出され、スタックトレースを出力してプログラムを即座に終了させます。このコミットでは、malloc
の失敗をこのような回復不能なエラーとして扱っています。
-
CgoのProlog: Cgoは、GoのコードとCのコードを結合する際に、Goのソースファイルに記述されたCコードや、Cgoが内部的に必要とする定義(ヘルパー関数、型定義など)をまとめたCのコードブロックを生成します。このコードブロックは「Prolog」と呼ばれ、最終的にコンパイルされるCファイルの一部となります。このコミットでは、
malloc
のラッパー関数をこのPrologに追加しています。
技術的詳細
このコミットの技術的詳細は、CgoがCのmalloc
をどのように扱うかを根本的に変更することにあります。
-
C.malloc
の内部的なリライト:src/cmd/cgo/ast.go
の変更により、Goのコード内でC.malloc
が参照された場合、Cgoはそれを内部的にC._CMalloc
という名前にリライトします。これは、Go開発者が引き続きC.malloc
という慣用的な名前を使用できるようにしつつ、Cgoの内部処理で特別なラッパー関数を呼び出すためのメカニズムです。エラーメッセージ表示の際には、_CMalloc
が再びmalloc
に変換されるようにfixGo
関数が導入されています。 -
_CMalloc
ラッパー関数の導入:src/cmd/cgo/out.go
では、Cgoが生成するCコードのProlog部分に_CMalloc
という新しいC関数が定義されています。この関数は、GoからC.malloc
が呼び出された際に実際に実行されるC側のラッパーです。void *_CMalloc(size_t n); // builtinPrologに追加
そして、その実装は以下のようになります。
void ·_Cfunc__CMalloc(uintptr n, int8 *p) { p = runtime·cmalloc(n); FLUSH(&p); }
この
_Cfunc__CMalloc
は、Goランタイムの内部関数であるruntime·cmalloc
を呼び出します。 -
runtime·cmalloc
の強化:src/pkg/runtime/cgocall.c
にあるruntime·cmalloc
関数は、GoランタイムがCのmalloc
を呼び出すための内部的なヘルパー関数です。このコミットでは、runtime·cmalloc
が_cgo_malloc
(これは最終的にCのmalloc
を呼び出す)を呼び出した後、返されたポインタがnil
(CのNULL
に相当)であるかどうかをチェックするロジックが追加されました。runtime·cmalloc(uintptr n) { struct { uintptr n; void *ret; } a; a.n = n; a.ret = nil; runtime·cgocall(_cgo_malloc, &a); if(a.ret == nil) // ここが追加されたチェック runtime·throw("runtime: C malloc failed"); // 失敗時にruntime·throwを呼び出す return a.ret; }
もし
_cgo_malloc
がNULL
を返した場合、runtime·throw("runtime: C malloc failed")
が呼び出され、プログラムは致命的なエラーとして終了します。これにより、malloc
の失敗がGoプログラム内で適切に処理され、未定義動作を防ぎます。 -
Cmalloc
ラッパーの追加(GCCGO向け):src/cmd/cgo/out.go
のGCCGO(GoコンパイラのGCCフロントエンド)向けのPrologには、Cmalloc
という別のラッパー関数が追加されています。これは、GCCGOのコンテキストでmalloc
の失敗を処理するためのものです。extern void runtime_throw(const char *); void *Cmalloc(size_t n) { void *p = malloc(n); if(p == NULL) runtime_throw("runtime: C malloc failed"); return p; }
この
Cmalloc
は、直接Cのmalloc
を呼び出し、NULL
が返された場合にruntime_throw
を呼び出します。これは、Goの標準コンパイラ(gc)とは異なるGCCGOのコンパイルパスにおけるmalloc
失敗時の安全性を確保するためのものです。
これらの変更により、GoのCgoはmalloc
の呼び出しをより堅牢にし、プラットフォーム間の差異を吸収し、メモリ割り当て失敗時の挙動を予測可能にしています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/cgo/ast.go
:File.saveRef
関数内で、C.malloc
への参照が_CMalloc
に内部的に書き換えられるロジックが追加されました。
--- a/src/cmd/cgo/ast.go +++ b/src/cmd/cgo/ast.go @@ -187,6 +187,13 @@ func (f *File) saveRef(x interface{}, context string) { error_(sel.Pos(), "cannot refer to errno directly; see documentation") return } + if goname == "_CMalloc" { + error_(sel.Pos(), "cannot refer to C._CMalloc; use C.malloc") + return + } + if goname == "malloc" { + goname = "_CMalloc" + } name := f.Name[goname] if name == nil { name = &Name{
-
src/cmd/cgo/out.go
:- Cgoが生成するCコードのPrologに
_CMalloc
が組み込み関数として追加されました。 - エラーメッセージで
_CMalloc
をmalloc
に戻すためのfixGo
関数が追加されました。 isBuiltin
マップに_Cfunc__CMalloc
が追加され、重複定義を防ぐようになりました。builtinProlog
に#include <sys/types.h>
とvoid *_CMalloc(size_t);
が追加されました。cProlog
に_Cfunc__CMalloc
の実装が追加されました。- GCCGO向けの
cPrologGccgo
にCmalloc
ラッパー関数が追加されました。
--- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -331,7 +331,7 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) { } // Builtins defined in the C prolog. - inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes" + inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes" || name == "_CMalloc" if *gccgo { // Gccgo style hooks. @@ -476,9 +476,27 @@ func (p *Package) writeOutput(f *File, srcfile string) { fgcc.Close()\n }\n +// fixGo convers the internal Name.Go field into the name we should show +// to users in error messages. There's only one for now: on input we rewrite +// C.malloc into C._CMalloc, so change it back here. +func fixGo(name string) string { +\tif name == "_CMalloc" { +\t\treturn "malloc" +\t} +\treturn name +}\n+\n+var isBuiltin = map[string]bool{ +\t"_Cfunc_CString": true,\n +\t"_Cfunc_GoString": true,\n +\t"_Cfunc_GoStringN": true,\n +\t"_Cfunc_GoBytes": true,\n +\t"_Cfunc__CMalloc": true,\n +}\n+\n func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) { name := n.Mangle - if name == "_Cfunc_CString" || name == "_Cfunc_GoString" || name == "_Cfunc_GoStringN" || name == "_Cfunc_GoBytes" || p.Written[name] { + if isBuiltin[name] || p.Written[name] { // The builtins are already defined in the C prolog, and we don't // want to duplicate function definitions we've already done.\n return @@ -1101,6 +1119,8 @@ __cgo_size_assert(double, 8)\n `\n \n const builtinProlog = `\n+#include <sys/types.h> /* for size_t below */\n+\n /* Define intgo when compiling with GCC. */\n #ifdef __PTRDIFF_TYPE__\n typedef __PTRDIFF_TYPE__ intgo;\n@@ -1116,6 +1136,7 @@ _GoString_ GoString(char *p);\n _GoString_ GoStringN(char *p, int l);\n _GoBytes_ GoBytes(void *p, int n);\n char *CString(_GoString_);\n+void *_CMalloc(size_t);\n `\n \n const cProlog = `\n@@ -1153,6 +1174,13 @@ void\n \tp[s.len] = 0;\n \tFLUSH(&p);\n }\n+\n+void\n+·_Cfunc__CMalloc(uintptr n, int8 *p)\n+{\n+\tp = runtime·cmalloc(n);\n+\tFLUSH(&p);\n+}\n `\n \n const cPrologGccgo = `\n@@ -1193,6 +1221,14 @@ Slice GoBytes(char *p, int32_t n) {\n \tstruct __go_string s = { (const unsigned char *)p, n };\n \treturn __go_string_to_byte_array(s);\n }\n+\n+extern void runtime_throw(const char *):\n+void *Cmalloc(size_t n) {\n+ void *p = malloc(n);\n+ if(p == NULL)\n+ runtime_throw("runtime: C malloc failed");\n+ return p;\n+}\n `\n \n func (p *Package) gccExportHeaderProlog() string {
- Cgoが生成するCコードのPrologに
-
src/pkg/runtime/cgocall.c
:runtime·cmalloc
関数内で、_cgo_malloc
の戻り値がnil
(NULL
)である場合にruntime·throw
を呼び出すロジックが追加されました。
--- a/src/pkg/runtime/cgocall.c +++ b/src/pkg/runtime/cgocall.c @@ -198,6 +198,8 @@ runtime·cmalloc(uintptr n)\n \ta.n = n;\n \ta.ret = nil;\n \truntime·cgocall(_cgo_malloc, &a);\n+\tif(a.ret == nil)\n+\t\truntime·throw("runtime: C malloc failed");\n \treturn a.ret;\n }\n \n ```
コアとなるコードの解説
このコミットの核心は、GoのCgoがC.malloc
を扱う方法を、透過的なラッパーを通じて変更することにあります。
-
src/cmd/cgo/ast.go
におけるC.malloc
のリライト: GoのソースコードでC.malloc
が記述されている場合、CgoのAST(抽象構文木)処理段階で、その参照が内部的に_CMalloc
という名前に変更されます。これは、Go開発者にはC.malloc
という馴染みのあるインターフェースを提供しつつ、Cgoの内部で特別な処理をフックするための巧妙な方法です。これにより、Cgoはmalloc
の呼び出しを制御し、カスタムの挙動を注入できるようになります。 -
src/cmd/cgo/out.go
における_CMalloc
の定義とfixGo
:out.go
はCgoが最終的に生成するCコードの構造を定義する部分です。builtinProlog
にvoid *_CMalloc(size_t);
が追加されることで、Cgoが生成するCファイル内で_CMalloc
がCの関数として宣言されます。これにより、Goから呼び出されるC._CMalloc
がCの関数としてリンクできるようになります。cProlog
に追加された·_Cfunc__CMalloc
の実装は、Goから_CMalloc
が呼び出された際に実行される実際のCコードです。この関数は、Goランタイムの内部関数であるruntime·cmalloc
を呼び出します。FLUSH(&p)
は、GoとCの間でポインタの受け渡しを行う際のメモリバリアのような役割を果たす可能性があります。fixGo
関数は、Cgoが生成するエラーメッセージにおいて、内部的な_CMalloc
という名前をユーザーが理解しやすいmalloc
に戻すために使用されます。これにより、ユーザーはC.malloc
に関するエラーメッセージを受け取ることになり、内部的な実装の詳細に惑わされることがありません。
-
src/pkg/runtime/cgocall.c
におけるruntime·cmalloc
の失敗処理:runtime·cmalloc
は、GoランタイムがCのmalloc
を呼び出すための低レベルなインターフェースです。この関数は、Cgoの内部関数_cgo_malloc
を呼び出して実際のメモリ割り当てを行います。このコミットの最も重要な変更点の一つは、_cgo_malloc
がNULL
を返した場合(つまり、メモリ割り当てに失敗した場合)に、runtime·throw("runtime: C malloc failed")
を呼び出すようにしたことです。runtime·throw
は、Goランタイムが回復不能なエラーを検出した際に使用するメカニズムです。これはGoのpanic
とは異なり、defer
の実行やrecover
による捕捉は行われず、プログラムは即座に終了します。これにより、malloc
の失敗という致命的な状況が、Goプログラム内で安全かつ予測可能な形で処理されるようになります。以前は、NULL
ポインタがGoに渡され、その後のアクセスでセグメンテーション違反などのクラッシュを引き起こす可能性がありました。この変更により、そのような未定義動作が回避されます。- また、
size_t
への引数型の強制も、このruntime·cmalloc
の呼び出しパスを通じて行われます。Go側から渡されるサイズ引数は、Cgoによって適切にsize_t
として扱われるようになります。
これらの変更は、GoとCの間のメモリ管理の境界をより明確にし、Cgoを使用するGoプログラムの堅牢性と信頼性を大幅に向上させています。
関連リンク
- Go Issue #3403: cmd/cgo: C.malloc takes unsigned long on Windows
- Go Issue #5926: cmd/cgo: C.malloc should throw on failure
- Go CL 13413047: cmd/cgo: replace C.malloc with our own wrapper
参考にした情報源リンク
- Go言語の公式ドキュメント (Cgoに関するセクション): https://pkg.go.dev/cmd/cgo
- C言語の
malloc
関数に関するドキュメント (例: cppreference.com): https://en.cppreference.com/w/c/memory/malloc - Goの
panic
とrecover
に関するドキュメント: https://go.dev/blog/defer-panic-and-recover - Goランタイムの内部動作に関する情報 (Goのソースコードや関連するブログ記事など)
size_t
に関する情報 (例: cppreference.com): https://en.cppreference.com/w/c/types/size_tunsigned long
に関する情報 (例: cppreference.com): https://en.cppreference.com/w/c/language/arithmetic_types