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

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

このコミットは、Go言語のgo/astパッケージ内のprint.goファイルに対する変更です。具体的には、抽象構文木(AST)の出力処理において、ゼロバイトの書き込みを避けるように修正されています。

コミット

commit 659d1df1bcd44ec6bb92f9592a8c92670d862d3c
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Mon Sep 24 08:30:28 2012 +1000

    pkg/go/ast: Avoid doing zero-length writes to the fd.
    
    After each line, ast.Print would do a zero-length write,
    which would hit the boundary condition on Plan 9 when
    reading over pipes (since message boundaries are
    preserved). This change makes sure we only do positive-
    length writes.
    
    R=rsc, rminnich, dave, r
    CC=golang-dev
    https://golang.org/cl/6558046

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

https://github.com/golang/go/commit/659d1df1bcd44ec6bb92f9592a8c92670d862d3c

元コミット内容

pkg/go/ast: Avoid doing zero-length writes to the fd.

このコミットは、go/astパッケージがファイルディスクリプタ(fd)へのゼロバイト書き込みを行わないように修正します。

各行の出力後、ast.Printはゼロバイト書き込みを行っていました。これは、Plan 9オペレーティングシステムにおいてパイプ経由で読み取る際に、メッセージ境界が保持されるため、境界条件に影響を与えていました。この変更により、正の長さの書き込みのみが行われるようになります。

変更の背景

この変更の背景には、Go言語のgo/astパッケージが抽象構文木(AST)を整形して出力する際に、特定の条件下でゼロバイトの書き込みを行っていた問題があります。特に、各行の出力の終わりに、実質的なデータがないにもかかわらず、Writeメソッドが呼び出されることがありました。

このゼロバイト書き込みは、一般的なUnix系システムでは通常問題になりません。しかし、Plan 9オペレーティングシステムのような、ファイルシステムやプロセス間通信の設計思想が異なる環境では、予期せぬ挙動を引き起こす可能性がありました。Plan 9では、パイプを介したデータ転送において「メッセージ境界」が厳密に保持される特性があります。これは、書き込まれたデータの塊が、読み取り側でも同じ塊として認識されることを意味します。

ゼロバイトの書き込みは、実質的なデータ転送がないにもかかわらず、一つの「メッセージ」として扱われる可能性があり、これが読み取り側のアプリケーションのロジックに影響を与え、特にパイプの終端やデータの区切りを誤認識させる原因となっていました。このコミットは、このようなPlan 9環境での互換性と堅牢性を向上させるために行われました。

前提知識の解説

Go言語のgo/astパッケージ

go/astパッケージは、Go言語のソースコードを抽象構文木(Abstract Syntax Tree, AST)として表現するためのデータ構造と、それを操作するための機能を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやリンター、コード分析ツールなどで広く利用されます。ast.Print関数は、このASTを人間が読める形式で出力するために使用されます。

ファイルディスクリプタ (File Descriptor, fd)

ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するために使用される整数値です。プログラムはファイルディスクリプタを通じて、これらのリソースに対して読み書きなどの操作を行います。

パイプ (Pipe)

パイプは、プロセス間通信(IPC)の一種で、一方のプロセスの出力がもう一方のプロセスの入力となるようにデータを流すためのメカニズムです。Unix系システムでは、シェルコマンドの|(パイプ)演算子でおなじみです。

Plan 9オペレーティングシステム

Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルとして扱うという徹底した「everything is a file」の原則に基づいています。

Plan 9のパイプは、Unixのストリーム指向のパイプとは異なり、よりメッセージ指向の特性を持っています。これは、書き込まれたデータの塊(メッセージ)が、読み取り側でもその塊として認識されることを意味します。この「メッセージ境界の保持」という特性が、ゼロバイト書き込みが問題となる原因でした。Unixのパイプでは、複数の小さな書き込みが読み取り側で一つの大きな読み取りとして結合されることがありますが、Plan 9では書き込みの単位が読み取りの単位に影響を与えることがあります。

ゼロバイト書き込み (Zero-length write)

ゼロバイト書き込みとは、writeシステムコールやそれに相当するI/O操作において、書き込むデータの長さが0バイトである呼び出しを指します。つまり、何もデータを転送しない書き込み操作です。

一般的なUnix系システムでは、write(fd, buf, 0)のようなゼロバイト書き込みは、通常は成功し、何もデータが転送されないことを示します。これは、ファイルポインタの移動やエラーの発生にはつながりません。しかし、Plan 9のようなメッセージ指向のI/Oシステムでは、ゼロバイト書き込みであっても、それが一つの「空のメッセージ」として扱われ、パイプの読み取り側でイベントとして検出される可能性があります。これにより、読み取り側が予期せぬ空のメッセージを受け取り、処理ロジックに影響を与えることが問題でした。

技術的詳細

このコミットの技術的詳細は、go/astパッケージのprinter構造体のWriteメソッドにおけるI/O処理の改善にあります。

元のコードでは、printerWriteメソッドは、内部バッファにデータを書き込んだ後、残りのデータをp.output.Write(data[n:])として出力先に書き込んでいました。ここでnは既に処理されたバイト数を示します。問題は、data[n:]が空のスライス(長さ0)になる場合でも、p.output.Writeが呼び出されていた点です。

Plan 9のパイプでは、ゼロバイトの書き込みであっても、それが独立したメッセージとして扱われる可能性があります。例えば、readシステムコールが呼び出された際に、ゼロバイトのメッセージが返されることで、読み取り側がデータの終端や特定のイベントを誤って解釈する可能性がありました。これは、特にストリームではなくメッセージとしてデータを扱うシステムにおいて、プロトコルの整合性やアプリケーションのロジックに影響を与えます。

このコミットでは、if len(data) > nという条件を追加することで、p.output.Writeが呼び出される前に、実際に書き込むべきデータが残っているかどうかを確認しています。len(data) > nfalseの場合、つまりdata[n:]が空のスライスである場合は、p.output.Writeは呼び出されません。これにより、不必要なゼロバイト書き込みが抑制され、Plan 9環境におけるパイプ経由のI/Oの堅牢性が向上します。

この変更は、Goの標準ライブラリが様々なオペレーティングシステムや環境で正しく動作するための、クロスプラットフォーム互換性への配慮の一例と言えます。

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

変更はsrc/pkg/go/ast/print.goファイル内の(*printer).Writeメソッドにあります。

--- a/src/pkg/go/ast/print.go
+++ b/src/pkg/go/ast/print.go
@@ -108,8 +108,10 @@ func (p *printer) Write(data []byte) (n int, err error) {
 		}
 		p.last = b
 	}
-	m, err = p.output.Write(data[n:])
-	n += m
+	if len(data) > n {
+		m, err = p.output.Write(data[n:])
+		n += m
+	}
 	return
 }

コアとなるコードの解説

変更されたWriteメソッドは、io.Writerインターフェースを満たすprinter構造体のメソッドです。このメソッドは、ASTの整形された出力を実際に書き込む役割を担っています。

元のコードでは、以下の行がありました。

m, err = p.output.Write(data[n:])
n += m

ここで、dataは書き込むべきバイトスライス全体、nは既に内部バッファに書き込まれたバイト数です。したがって、data[n:]はまだ書き込まれていない残りのデータを示します。問題は、data[n:]が空のスライス(つまり、len(data[n:])が0)である場合でも、p.output.Writeが呼び出されていたことです。

変更後のコードでは、この部分が以下のように修正されました。

if len(data) > n {
    m, err = p.output.Write(data[n:])
    n += m
}

このif len(data) > nという条件が追加されたことで、p.output.Writeが呼び出されるのは、dataスライスの中にまだnバイトを超えて書き込むべきデータが残っている場合のみとなりました。言い換えれば、data[n:]が空でない場合にのみ、実際の書き込み操作が行われます。

このシンプルな条件分岐の追加により、ゼロバイトの書き込みが効果的に回避され、Plan 9のようなメッセージ指向のI/Oシステムにおける潜在的な問題を解決しています。これにより、go/astパッケージの出力がより堅牢になり、異なるオペレーティングシステム環境での互換性が向上しました。

関連リンク

参考にした情報源リンク

  • コミットメッセージとGitHubのコミットページ
  • Go言語の公式ドキュメント
  • Plan 9に関する一般的な情報源(例: Wikipedia, 関連する技術記事)
  • Unix系システムにおけるファイルディスクリプタとパイプに関する一般的な知識
  • ゼロバイト書き込みに関するI/Oシステムの挙動についての情報