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

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

このコミットは、Go言語のCgoツールにおいて、gccgoコンパイラを使用する際のerrno(エラー番号)の取り扱いに関するバグを修正するものです。具体的には、C関数呼び出し後にerrnoが正しく取得されない、または以前のerrno値が残ってしまう問題を解決し、syscall.GetErrno()の挙動を改善します。

コミット

commit 3a3c5aad4e6ccef38a1e6d56652523c3258da6a8
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Mar 21 10:38:58 2012 -0700

    cmd/cgo: fix handling of errno for gccgo
    
    Fixes #3332.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5868047

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

https://github.com/golang/go/commit/3a3c5aad4e6ccef38a1e6d56652523c3258da6a8

元コミット内容

cmd/cgo: gccgoにおけるerrnoの取り扱いを修正。 Issue #3332を修正。

変更の背景

この変更は、Go言語のCgoツールがC言語の関数を呼び出す際に、C言語の標準ライブラリが設定するerrno(エラー番号)の取り扱いに関する問題、特にgccgoコンパイラを使用した場合に発生するバグを修正するために行われました。

C言語の関数は、エラーが発生した場合にグローバル変数errnoにエラーコードを設定することが一般的です。Goのsyscallパッケージは、このerrnoの値をGoのエラーとして取得するためのsyscall.GetErrno()関数を提供しています。しかし、gccgoで生成されたCgoコードでは、C関数が成功した場合でもerrnoがクリアされず、以前のエラーコードが残ってしまう可能性がありました。これにより、C関数が実際には成功しているにもかかわらず、Go側で誤ってエラーが報告されるという問題が発生していました(Issue #3332)。

このコミットは、C関数呼び出しの前にerrnoを明示的にゼロにリセットし、C関数の戻り値とerrnoの値を適切にチェックすることで、この問題を解決し、gccgo環境下でのCgoの信頼性を向上させることを目的としています。

前提知識の解説

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出すためのGoツールです。これにより、既存のCライブラリをGoアプリケーションで利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoは、Goのソースコード内にCのコードを埋め込む特殊なコメント構文(import "C")を使用し、ビルド時にGoとCの間のブリッジコードを生成します。

errno

errnoは、C言語の標準ライブラリ関数がエラーを報告するために使用するグローバル変数です。多くのシステムコールやライブラリ関数は、失敗した場合に特定の値を返し(例: -1NULL)、同時にerrnoに具体的なエラーコード(例: EACCESENOENTなど)を設定します。errnoの値は、perror()関数やstrerror()関数を使って人間が読めるエラーメッセージに変換できます。

syscall.GetErrno()syscall.SetErrno()

Go言語のsyscallパッケージは、オペレーティングシステムのプリミティブな機能への低レベルなインターフェースを提供します。

  • syscall.GetErrno(): 現在のスレッドのerrnoの値をGoのsyscall.Errno型として取得します。Cgoを通じてC関数を呼び出した後、C関数が設定したerrnoの値を取得するために使用されます。
  • syscall.SetErrno(int): 現在のスレッドのerrnoの値を設定します。このコミットでは、C関数呼び出しの前にerrno0にリセットするために使用されています。これは、C関数が成功した場合にerrnoがクリアされないというC言語の慣習に対応するための重要なステップです。

gccgo

gccgoは、GCC(GNU Compiler Collection)のフロントエンドとして実装されたGoコンパイラです。標準のGoコンパイラ(gc)とは異なる実装であり、Cgoのコード生成やランタイムの挙動に微妙な違いがある場合があります。このコミットで修正された問題は、特にgccgo環境で顕在化していました。

技術的詳細

このコミットの技術的な詳細は、Cgoが生成するGoコードにおけるerrnoの取り扱いロジックの改善にあります。

従来のCgoのコード生成では、C関数を呼び出した後、無条件にsyscall.GetErrno()を呼び出してその値を返していました。しかし、C言語の慣習として、関数が成功した場合にerrno0にリセットすることは保証されていません。そのため、以前のC関数呼び出しで設定されたerrnoの値がそのまま残ってしまい、Go側で誤ってエラーとして解釈される可能性がありました。

この修正では、以下の2つの主要な変更が導入されています。

  1. errnoの事前リセット: C関数を呼び出す直前に、syscall.SetErrno(0)を呼び出してerrnoを明示的に0に設定します。これにより、C関数が成功した場合にerrno0のままであることが保証され、以前の呼び出しによる「古い」errno値が誤って取得されることを防ぎます。
  2. errnoの条件付き取得: C関数の呼び出し結果を受け取った後、syscall.GetErrno()errnoの値を取得します。しかし、このerrnoの値が0でない場合にのみ、それをエラーとして返します。errno0の場合は、エラーがないことを示すnilを返します。

このロジックにより、C関数が成功した場合にはnilエラーが返され、C関数がエラーを報告するためにerrnoを設定した場合にのみ、そのerrno値がGoのエラーとして適切に伝播されるようになります。これは、C言語のerrnoのセマンティクスとGoのエラーハンドリングモデルをより正確に整合させるための重要な改善です。

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

--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -284,8 +284,13 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
 			}
 			conf.Fprint(fgo2, fset, d)
 			fmt.Fprintf(fgo2, "{\\n")
+			fmt.Fprintf(fgo2, "\\tsyscall.SetErrno(0)\\n")
 			fmt.Fprintf(fgo2, "\\tr := %s(%s)\\n", cname, strings.Join(paramnames, ", "))
-\t\t\tfmt.Fprintf(fgo2, "\\treturn r, syscall.GetErrno()\\n")
+\t\t\tfmt.Fprintf(fgo2, "\\te := syscall.GetErrno()\\n")
+\t\t\tfmt.Fprintf(fgo2, "\\tif e != 0 {\\n")
+\t\t\tfmt.Fprintf(fgo2, "\\t\\treturn r, e\\n")
+\t\t\tfmt.Fprintf(fgo2, "\\t}\\n")
+\t\t\tfmt.Fprintf(fgo2, "\\treturn r, nil\\n")
 			fmt.Fprintf(fgo2, "}\\n")
 			// declare the C function.
 			fmt.Fprintf(fgo2, "//extern %s\\n", n.C)

コアとなるコードの解説

この変更は、src/cmd/cgo/out.goファイル内のwriteDefsFunc関数にあります。この関数は、CgoがGoのラッパー関数を生成する際に使用されます。

変更前は、C関数呼び出し後のerrnoの取り扱いが以下のようになっていました。

// 変更前 (擬似コード)
r := C.c_function(params)
return r, syscall.GetErrno() // 無条件にerrnoを返す

このコードでは、C関数が成功した場合でもsyscall.GetErrno()が非ゼロの値を返す可能性があり、Go側で誤ったエラーが報告される原因となっていました。

変更後は、以下のようになっています。

// 変更後 (擬似コード)
syscall.SetErrno(0) // C関数呼び出し前にerrnoを0にリセット
r := C.c_function(params)
e := syscall.GetErrno() // errnoの値を取得
if e != 0 { // errnoが0でない場合のみエラーとして返す
    return r, e
}
return r, nil // errnoが0の場合はnilエラーを返す

各行の変更点を詳しく見ていきます。

  1. fmt.Fprintf(fgo2, "\\tsyscall.SetErrno(0)\\n")

    • C関数を呼び出す直前に、syscall.SetErrno(0)を挿入します。これにより、現在のスレッドのerrnoが明示的に0にリセットされます。これは、C関数が成功した場合にerrnoをクリアしないというC言語の慣習に対応するための重要なステップです。
  2. fmt.Fprintf(fgo2, "\\tr := %s(%s)\\n", cname, strings.Join(paramnames, ", "))

    • これは変更されていません。C関数を呼び出し、その戻り値をrに格納します。
  3. fmt.Fprintf(fgo2, "\\te := syscall.GetErrno()\\n")

    • 変更前はreturn r, syscall.GetErrno()と直接返していましたが、変更後はsyscall.GetErrno()の結果を一時変数eに格納します。
  4. fmt.Fprintf(fgo2, "\\tif e != 0 {\\n")

    • 取得したerrnoの値e0でない場合にのみ、エラーとして処理するための条件分岐を追加します。
  5. fmt.Fprintf(fgo2, "\\t\\treturn r, e\\n")

    • e0でない場合(つまり、C関数がエラーを報告したと判断できる場合)は、C関数の戻り値rerrnoの値eをGoの関数から返します。
  6. fmt.Fprintf(fgo2, "\\t}\\n")

    • ifブロックの閉じ括弧です。
  7. fmt.Fprintf(fgo2, "\\treturn r, nil\\n")

    • e0の場合(つまり、C関数が成功したと判断できる場合)は、C関数の戻り値rnil(エラーなし)をGoの関数から返します。

この一連の変更により、Cgoが生成するGoのラッパー関数は、C言語のerrnoのセマンティクスをより正確に反映し、gccgo環境下でのerrnoの誤った報告を防ぐことができます。

関連リンク

参考にした情報源リンク