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

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

このコミットは、Go言語のmisc/cgo/stdioパッケージにおける標準入出力(stdoutおよびstderr)の扱いを改善し、特にWindows環境での互換性を確保するとともに、関連するテストの堅牢性を向上させるものです。具体的には、C言語の標準ライブラリ(libc)の実装に依存せずにstdoutstderrを取得する汎用的な方法を導入し、テストスクリプトが異なるOSの改行コードの違いを吸収できるように修正しています。

コミット

commit 674bbafce6a52ef843eb130200d2946c92d9934d
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Sep 20 00:27:23 2012 +0800

    misc/cgo/stdio: make it work on Windows and also test it
    use a function to get stdout and stderr, instead of depending
    on a specific libc implementation.
    also make test/run.go replace \r\n by \n before comparing
    output.
    
            Fixes #2121.
            Part of issue 1741.
    
    R=alex.brainman, rsc, r, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5847068

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

https://github.com/golang/go/commit/674bbafce6a52ef843eb130200d2946c92d9934d

元コミット内容

    misc/cgo/stdio: make it work on Windows and also test it
    use a function to get stdout and stderr, instead of depending
    on a specific libc implementation.
    also make test/run.go replace \r\n by \n before comparing
    output.
    
            Fixes #2121.
            Part of issue 1741.

変更の背景

このコミットは、主に以下の2つの問題に対処するために行われました。

  1. Windows環境でのstdout/stderrの取得問題 (Fixes #2121): Go言語のmisc/cgo/stdioパッケージは、C言語の標準入出力ストリーム(stdout, stderr)をGoコードから利用するためのラッパーを提供しています。しかし、これらのストリームへのアクセス方法が、使用されるC標準ライブラリ(libc)の実装によって異なることが問題となっていました。特にWindows環境(MinGWなど)では、stdoutstderr_iobのような内部構造体のアドレスとして定義されていることがあり、GoのCgoがこれを直接認識できない、またはプラットフォーム間で互換性のない方法でアクセスしようとすると問題が発生していました。NetBSDのような他のOSでも同様の問題(__sF構造体への依存)が存在していました。このため、特定のlibc実装に依存しない、よりポータブルな方法でstdoutstderrを取得する必要がありました。

  2. クロスプラットフォームなテストにおける改行コードの問題: Windowsではテキストファイルの改行コードとしてCRLF(\r\n)が一般的に使用されますが、Unix系システム(Linux, macOSなど)ではLF(\n)が使用されます。Goのテストフレームワーク(特にtest/run.go)がプログラムの出力を比較する際、この改行コードの違いが原因でテストが失敗することがありました。例えば、テスト対象のプログラムがWindowsで実行されてCRLFを含む出力を生成した場合、期待される出力がLFのみで記述されていると、比較時に不一致と判断されてしまいます。この問題を解決し、テストの信頼性を向上させる必要がありました。

これらの問題は、Go言語が多様なプラットフォームで動作することを目標としている中で、特にCgoのようなOS固有の機能と連携する部分で顕在化する典型的なクロスプラットフォーム互換性の課題でした。

前提知識の解説

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。Goのソースファイル内にC言語のコードを直接記述したり、既存のCライブラリをリンクしたりすることができます。Cgoを使用すると、Goの型とCの型の間でデータの変換が行われ、GoのガベージコレクタとCのメモリ管理の間で連携が取られます。

Cgoの基本的な仕組みは以下の通りです。

  1. Goのソースファイル内にimport "C"という行を記述し、その直前のコメントブロックにC言語のコードを記述します。
  2. Cの関数や変数にアクセスする際は、C.プレフィックスを付けて参照します(例: C.printf, C.stdout)。
  3. CgoツールがGoとCの間のバインディングコードを生成し、GoコンパイラとCコンパイラ(通常はGCCやClang)が連携して最終的なバイナリを生成します。

stdoutstderr

stdout(標準出力)とstderr(標準エラー出力)は、C言語の標準ライブラリstdio.hで定義されているFILE型のポインタです。これらは、プログラムがテキストを出力するためのストリームを表します。

  • stdout: 通常のプログラム出力に使用されます。
  • stderr: エラーメッセージや診断情報に使用されます。

これらのポインタは、通常、プログラムの起動時にOSによって開かれ、それぞれコンソールやファイルに紐付けられます。しかし、これらのポインタがどのように実装されているかは、C標準ライブラリ(libc)の実装(例: glibc, musl, MSVCRT, NetBSDのlibcなど)によって異なります。

  • _iob: Microsoft Visual C++ Runtime (MSVCRT) やMinGWなどのWindows環境のCライブラリで、FILE構造体の配列を管理するために使われる内部的なシンボル名です。stdin, stdout, stderrは、この_iob配列の特定のエントリを指すポインタとして定義されることがあります。
  • __sF: NetBSDのCライブラリで、同様にFILE構造体の配列を管理するために使われる内部的なシンボル名です。

Cgoがこれらの内部的なシンボルを直接参照しようとすると、異なるOSやコンパイラ環境でシンボル名や構造体のレイアウトが異なるため、コンパイルエラーや実行時エラーが発生する可能性があります。

改行コード (CRLF vs LF)

テキストファイルにおける改行の表現方法は、オペレーティングシステムによって異なります。

  • LF (Line Feed, \n, ASCII 10): Unix系システム(Linux, macOS, BSDなど)で標準的に使用される改行コードです。
  • CRLF (Carriage Return + Line Feed, \r\n, ASCII 13 + ASCII 10): Windowsシステムで標準的に使用される改行コードです。CRはカーソルを行頭に戻し、LFはカーソルを次の行に進めるという、タイプライターの動作に由来します。

プログラムがテキスト出力を生成する際、OSの慣習に従って改行コードを付加することが一般的です。しかし、異なるOSで生成された出力を比較する場合、この改行コードの違いが問題となることがあります。特に自動テストでは、期待される出力と実際の出力がバイト単位で完全に一致することを要求する場合があるため、改行コードの正規化が必要になります。

技術的詳細

このコミットの技術的解決策は、上記の問題に直接対処しています。

  1. stdout/stderrの取得方法の抽象化: misc/cgo/stdio/stdio.goファイルにおいて、C言語のstdoutおよびstderrグローバル変数への直接参照を避け、代わりにCgo経由で呼び出せるC関数getStdout()getStderr()を導入しました。

    • 以前はvar Stdout = (*File)(C.stdout)のように直接C.stdoutを参照していました。
    • 新しいアプローチでは、GoコードからC.getStdout()C.getStderr()を呼び出し、これらのC関数が内部でstdoutstderrのポインタを返します。
    • この変更により、Cgoは特定のlibc実装の内部構造(_iob__sFなど)を直接「知る」必要がなくなり、Cコンパイラが提供する標準的なstdout/stderrシンボルを介してアクセスできるようになります。これにより、Windows(MinGW)やNetBSDなど、様々な環境での互換性が向上します。
    • misc/cgo/stdio/stdio_netbsd.goファイルは、NetBSD固有の__sFへの依存を解消するために削除されました。これは、新しい汎用的なアプローチがNetBSDでも機能するため、特定のOS向けのコードが不要になったことを意味します。
  2. テストにおける改行コードの正規化: test/run.goファイルにおいて、テスト対象プログラムの出力と期待される出力を比較する前に、実際の出力からCRLF(\r\n)をLF(\n)に置換する処理を追加しました。

    • 具体的には、strings.Replace(string(out), "\r\n", "\n", -1)というGoの標準ライブラリ関数を使用しています。
    • これにより、Windows環境で生成された出力がCRLFを含んでいても、比較時にはLFのみの形式に正規化されるため、期待される出力(通常はLFで記述されている)との比較が正確に行えるようになります。
    • この変更は、src/run.bat(Windows用のテスト実行スクリプト)にも反映され、misc/cgo/stdioのテストが有効化されました。以前はコメントアウトされていた行が有効化され、このモジュールのテストがCI/CDパイプラインで実行されるようになりました。

これらの変更は、Go言語のクロスプラットフォーム対応を強化し、特にCgoのような低レベルな連携が必要な部分での堅牢性を高める上で重要なステップです。

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

misc/cgo/stdio/stdio.go

--- a/misc/cgo/stdio/stdio.go
+++ b/misc/cgo/stdio/stdio.go
@@ -1,15 +1,22 @@
+// skip
+
 // Copyright 2009 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.
 
-// +build !netbsd
-
 package stdio
 
 /*
 #include <stdio.h>
+\n
+// on mingw, stderr and stdout are defined as &_iob[FILENO]
+// on netbsd, they are defined as &__sF[FILENO]
+// and cgo doesn't recognize them, so write a function to get them,
+// instead of depending on internals of libc implementation.
+FILE *getStdout(void) { return stdout; }
+FILE *getStderr(void) { return stderr; }
 */
 import "C"
 
-var Stdout = (*File)(C.stdout)
-var Stderr = (*File)(C.stderr)
+var Stdout = (*File)(C.getStdout())
+var Stderr = (*File)(C.getStderr())

misc/cgo/stdio/stdio_netbsd.go

このファイルは完全に削除されました。

--- a/misc/cgo/stdio/stdio_netbsd.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2009 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.\n
-package stdio
-
-/*
-#include <stdio.h>
-
-extern FILE __sF[3];
-*/
-import "C"
-import "unsafe"
-
-var Stdout = (*File)(unsafe.Pointer(&C.__sF[1]))
-var Stderr = (*File)(unsafe.Pointer(&C.__sF[2]))

src/run.bat

--- a/src/run.bat
+++ b/src/run.bat
@@ -70,11 +70,10 @@ if x%CGO_ENABLED% == x0 goto nocgo
 ::if errorlevel 1 goto fail
 ::echo.
 
-:: TODO ..\misc\cgo\stdio
-::echo # ..\misc\cgo\stdio
-::go run %GOROOT%\test\run.go - ..\misc\cgo\stdio
-::if errorlevel 1 goto fail
-::echo.
+echo # ..\misc\cgo\stdio
+go run %GOROOT%\test\run.go - ..\misc\cgo\stdio
+if errorlevel 1 goto fail
+echo.
 
 echo # ..\misc\cgo\test
 go test ..\misc\cgo\test

test/run.go

--- a/test/run.go
+++ b/test/run.go
@@ -344,7 +344,7 @@ func (t *test) run() {
 		if err != nil {
 			t.err = fmt.Errorf("%s\n%s", err, out)
 		}
-		if string(out) != t.expectedOutput() {
+		if strings.Replace(string(out), "\r\n", "\n", -1) != t.expectedOutput() {
 			t.err = fmt.Errorf("incorrect output\n%s", out)
 		}
 

コアとなるコードの解説

misc/cgo/stdio/stdio.goの変更

このファイルは、GoのstdioパッケージがCの標準入出力ストリームにアクセスするためのインターフェースを提供します。

  • C関数の追加:

    FILE *getStdout(void) { return stdout; }
    FILE *getStderr(void) { return stderr; }
    

    このCコードブロックは、CgoによってGoから呼び出し可能なC関数を定義しています。getStdout()はCのグローバル変数stdoutのポインタを返し、getStderr()stderrのポインタを返します。これにより、GoコードはCの内部実装に直接依存することなく、標準的なCのAPIを通じてこれらのポインタを取得できます。

  • Go変数の更新:

    var Stdout = (*File)(C.getStdout())
    var Stderr = (*File)(C.getStderr())
    

    GoのStdoutStderr変数は、以前はC.stdoutC.stderrを直接キャストしていましたが、この変更により、新しく定義されたC関数C.getStdout()C.getStderr()の戻り値を使用するようになりました。これにより、異なるCライブラリ実装間での互換性の問題が解消されます。

  • // skipコメントの追加: // skipコメントは、Goのビルドシステムに対して、このファイルが特定のビルド条件(例: +buildタグ)なしで常にコンパイルされるべきではないことを示唆している可能性があります。ただし、このコミットの文脈では、主にCgoの動作に関連する変更が重要です。

misc/cgo/stdio/stdio_netbsd.goの削除

このファイルは、NetBSD環境でのstdout/stderrの取得に特化したコードを含んでいました。具体的には、NetBSDのlibcが__sFという内部配列を使ってFILEポインタを管理していることに依存していました。

var Stdout = (*File)(unsafe.Pointer(&C.__sF[1]))
var Stderr = (*File)(unsafe.Pointer(&C.__sF[2]))

このようなunsafe.Pointerを使った直接的なメモリ操作は、libcの実装詳細に強く依存するため、非常に脆弱です。stdio.goで導入された汎用的なgetStdout()/getStderr()関数がNetBSDでも正しく機能するようになったため、このNetBSD固有のファイルは不要となり、削除されました。これにより、コードベースの複雑性が減少し、保守性が向上しました。

src/run.batの変更

run.batは、Windows環境でGoのテストスイートを実行するためのバッチスクリプトです。

  • misc/cgo/stdioテストの有効化:
    -:: TODO ..\misc\cgo\stdio
    -::echo # ..\misc\cgo\stdio
    -::go run %GOROOT%\test\run.go - ..\misc\cgo\stdio
    -::if errorlevel 1 goto fail
    -::echo.
    +echo # ..\misc\cgo\stdio
    +go run %GOROOT%\test\run.go - ..\misc\cgo\stdio
    +if errorlevel 1 goto fail
    +echo.
    
    以前はコメントアウトされていたmisc/cgo/stdioモジュールのテスト実行コマンドが有効化されました。これは、stdioパッケージのWindows互換性が向上したため、この環境でのテストが可能になったことを示しています。go run %GOROOT%\test\run.go - ..\misc\cgo\stdioコマンドは、Goのテストランナーを使用してmisc/cgo/stdioディレクトリ内のテストを実行します。

test/run.goの変更

test/run.goは、Goの標準テストフレームワークの一部であり、テストの実行と結果の比較を担当します。

  • 出力の改行コード正規化:
    -		if string(out) != t.expectedOutput() {
    +		if strings.Replace(string(out), "\r\n", "\n", -1) != t.expectedOutput() {
    
    この変更は、テスト対象プログラムの標準出力(out変数)を、期待される出力(t.expectedOutput())と比較する直前に行われます。strings.Replace(string(out), "\r\n", "\n", -1)は、outバイトスライスを文字列に変換し、その文字列内のすべてのCRLF(\r\n)シーケンスをLF(\n)に置換します。-1は、すべての出現箇所を置換することを意味します。 この正規化により、Windowsで生成されたCRLFを含む出力が、Unix系システムで期待されるLFのみの出力と正しく比較できるようになり、クロスプラットフォームなテストの信頼性が向上します。

これらの変更は、Go言語のCgo機能がより堅牢でクロスプラットフォーム互換性を持つようにするための重要な改善であり、テストインフラストラクチャもそれに合わせて適応されています。

関連リンク

参考にした情報源リンク