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

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

このコミットは、Go言語の実験的な型チェッカー (exp/types) における GCImporter API の汎用化を目的としたものです。具体的には、オブジェクトファイルからのエクスポートデータ読み込み方法を柔軟にし、GCImporter の責務を分割することで、異なるストレージ環境での利用を容易にしています。

コミット

commit 53907221007ebf42cca9ef945550f59ef4478c8c
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Mar 26 11:26:05 2012 -0700

    exp/types: generalized GCImporter API.
    
    - Renamed ExportData -> FindGcExportData
      and base it on an a bufio.Reader rather
      than a filename so it can be used in
      environments where object files are
      stored elsewhere.
    
    - Factor former GcImporter into GcImportData
      and GcImport. Implementations with different
      storage locations for object files can build
      a customized GcImport using GcImportData.
    
    This is pkg/exp only - no impact on Go 1.
    
    R=golang-dev, lvd, rsc
    CC=golang-dev
    https://golang.org/cl/5574069

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

https://github.com/golang/go/commit/53907221007ebf42cca9ef945550f59ef4478c8c

元コミット内容

Go言語の実験的な型チェッカー (exp/types) において、GCImporter API を汎用化する変更です。

主な変更点は以下の通りです。

  1. ExportData 関数を FindGcExportData にリネームし、ファイル名ではなく bufio.Reader を引数として受け取るように変更しました。これにより、オブジェクトファイルがファイルシステム上の特定の場所に存在しない環境(例: メモリ上やネットワーク経由)でもエクスポートデータを読み込めるようになります。
  2. 従来の GcImporterGcImportDataGcImport に分割しました。
    • GcImportData: bufio.Reader から直接エクスポートデータをインポートする低レベルな機能を提供します。
    • GcImport: GcImportData を利用し、ファイルパスからオブジェクトファイルを読み込み、エクスポートデータをインポートする高レベルな機能を提供します。 この分割により、オブジェクトファイルの保存場所が異なる実装でも、GcImportData を利用してカスタマイズされた GcImport を構築できるようになります。

この変更は pkg/exp (実験的なパッケージ) のみに影響し、Go 1 のリリースには影響しません。

変更の背景

このコミットの背景には、Go言語の型チェッカーがパッケージの型情報をインポートする際の柔軟性を高めるという目的があります。従来の ExportData 関数はファイル名を直接受け取っていたため、型情報がファイルシステム上に存在するオブジェクトファイルからのみ読み込むことができました。しかし、Goのツールチェインや開発環境が進化するにつれて、オブジェクトファイルが必ずしもディスク上のファイルとして存在するとは限らないシナリオが増えてきました。例えば、ビルドシステムがメモリ上で中間ファイルを生成したり、ネットワーク経由で型情報を取得したりするようなケースです。

このような多様な環境に対応するため、型情報の読み込みメカニズムをより抽象化し、ファイルシステムへの依存を減らす必要がありました。bufio.Reader を介してデータストリームとして型情報を扱えるようにすることで、ファイル、ネットワークストリーム、メモリバッファなど、様々なソースから型情報を透過的に読み込めるようになります。

また、GCImporter の責務を分割することで、型情報の「取得」と「解析」のフェーズを分離し、コードのモジュール性と再利用性を向上させています。これにより、将来的に異なる形式の型情報(例: JSON形式の型情報)をサポートする場合でも、既存の解析ロジックを再利用しやすくなります。

前提知識の解説

このコミットを理解するためには、以下のGo言語および関連技術の概念を理解しておく必要があります。

  • Go言語のパッケージシステム: Go言語はパッケージによってコードをモジュール化します。あるパッケージが別のパッケージの型や関数を利用する場合、そのパッケージの「エクスポートされた」情報(公開された型、関数、変数など)をインポートする必要があります。
  • Goコンパイラ (gc): Go言語の公式コンパイラは gc と呼ばれます。gc はGoのソースコードをコンパイルし、実行可能なバイナリやオブジェクトファイルを生成します。このオブジェクトファイルには、コンパイルされたコードだけでなく、他のパッケージがインポートするために必要な型情報も含まれています。
  • オブジェクトファイル: コンパイラによって生成される中間ファイルで、機械語コードやメタデータ(型情報など)が含まれます。Goのオブジェクトファイルは通常 .a (アーカイブ) やプラットフォーム固有の拡張子 (.5, .6, .8 など) を持ちます。
  • エクスポートデータ: Goのオブジェクトファイル内に含まれる、そのパッケージが外部に公開している型や関数の定義情報です。他のパッケージがこのパッケージをインポートする際に、このエクスポートデータを読み込んで型チェックやコード生成を行います。
  • go/ast パッケージ: Goのソースコードの抽象構文木 (AST: Abstract Syntax Tree) を表現するためのパッケージです。コンパイラやツールがGoのコードを解析する際に利用します。ast.Importer は、パッケージのインポート処理を抽象化するためのインターフェースです。
  • go/types パッケージ: Goの型システムを扱うためのパッケージです。型チェック、型推論、型の比較など、Goの型に関する様々な操作を提供します。このコミットの変更は、このパッケージの実験的な部分 (exp/types) に関連しています。
  • bufio.Reader: Goの io パッケージの一部で、バッファリングされたI/O操作を提供します。これにより、ディスクI/Oの回数を減らし、読み込み効率を向上させることができます。ファイルだけでなく、任意の io.Reader インターフェースを実装するデータソース(例: ネットワーク接続、メモリバッファ)に対しても利用できます。
  • go/build パッケージ: Goのビルドシステムに関する情報を提供するパッケージです。build.Import 関数は、指定されたインポートパスに対応するパッケージの情報を検索するために使用されます。これには、パッケージのソースディレクトリやオブジェクトファイルのパスなどが含まれます。
  • __.SYMDEF__.PKGDEF: Goのオブジェクトファイル(特にアーカイブ形式の .a ファイル)内部に存在するセクション名です。__.SYMDEF はシンボル定義に関する情報、__.PKGDEF はパッケージのエクスポートデータに関する情報を含んでいます。FindGcExportData 関数は、これらのセクションを解析して __.PKGDEF セクションの開始位置を見つけ出します。

技術的詳細

このコミットの技術的詳細は、Goの型チェッカーがどのようにパッケージの型情報を取得し、解析するかという点に集約されます。

ExportData から FindGcExportData への変更

  • ExportData(filename string): この関数は、指定された filename を開いて os.Open を使用し、そのファイルからエクスポートデータを読み込む責任を負っていました。ファイルを開く、エラーハンドリング、そして bufio.Reader の作成までを一手に引き受けていました。
  • FindGcExportData(r *bufio.Reader): この関数は、既に開かれて bufio.Reader にラップされたデータストリームを受け取ります。その役割は、bufio.Reader の現在の位置からGoのオブジェクトファイルフォーマットを解析し、エクスポートデータが始まる __.PKGDEF セクションの先頭までリーダーの位置を進めることです。これにより、ファイルを開くというI/O層の責務が分離され、FindGcExportData は純粋にデータフォーマットの解析に集中できるようになりました。

この変更により、FindGcExportData はファイルシステムに依存せず、任意の io.Reader から提供されるデータストリームを処理できるようになります。これは、メモリ内のバッファやネットワーク経由で取得したデータなど、ファイル以外のソースから型情報をインポートする際に非常に有用です。

GcImporter の分割 (GcImportDataGcImport)

  • GcImporter(imports map[string]*ast.Object, path string): この関数は、インポートパス (path) を受け取り、対応するオブジェクトファイルを見つけ、ExportData を呼び出してエクスポートデータを読み込み、そのデータを解析して ast.Object (パッケージオブジェクト) を構築するまでの一連の処理を行っていました。
  • GcImportData(imports map[string]*ast.Object, filename, id string, data *bufio.Reader): この関数は、既にエクスポートデータの開始位置に設定された bufio.Reader (data) を受け取ります。その役割は、このリーダーから型情報を解析し、ast.Object を構築することです。ファイルを見つける、開くといったI/O層の責務は持ちません。これは、型情報の「解析」ロジックをカプセル化したものです。
  • GcImport(imports map[string]*ast.Object, path string): この関数は、従来の GcImporter の役割を引き継ぎますが、内部で GcImportData を利用します。具体的には、インポートパス (path) から FindPkg を使ってオブジェクトファイルのパスを見つけ、os.Open でファイルを開き、bufio.NewReader でラップし、FindGcExportData でエクスポートデータの開始位置にリーダーを移動させ、最後に GcImportData を呼び出して実際の解析とパッケージオブジェクトの構築を行います。これは、型情報の「取得」と「解析」を連携させる高レベルなインターフェースです。

この分割により、GcImportData は低レベルなデータ解析の再利用可能なコンポーネントとなり、GcImport はファイルシステムからのインポートという具体的なシナリオを扱う高レベルなコンポーネントとなります。これにより、異なるデータソースからのインポートが必要な場合に、GcImportData を直接利用してカスタムのインポーターを容易に構築できるようになります。

findPkg の変更

  • findPkg 関数は FindPkg にリネームされ、srcDir 引数が追加されました。これにより、ローカルインポートパス (./x) の解決が、現在の作業ディレクトリ (os.Getwd()) ではなく、指定された srcDir を基準に行われるようになります。これは、ビルドシステムが特定のソースディレクトリ内でコンパイルを行う際に、より正確なパス解決を可能にします。

これらの変更は、Goの型チェッカーの内部構造をよりモジュール化し、将来的な拡張性や異なる環境への適応性を高めるための重要なステップです。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/pkg/exp/types/exportdata.go:
    • ExportData 関数が FindGcExportData にリネームされ、filename string の代わりに *bufio.Reader を引数として受け取るように変更されました。
    • ファイルを開く (os.Open) および閉じる (file.Close()) ロジックが削除され、FindGcExportData は純粋に bufio.Reader からエクスポートデータの開始位置を見つける責務のみを持つようになりました。
    • dataReader 構造体と関連するロジックが削除されました。
  • src/pkg/exp/types/gcimporter.go:
    • 従来の GcImporter 関数が削除されました。
    • 新たに GcImportData 関数が追加されました。これは bufio.Reader から直接エクスポートデータを解析し、パッケージオブジェクトを構築する低レベルな関数です。
    • 新たに GcImport 関数が追加されました。これは FindPkgos.OpenFindGcExportData を利用してオブジェクトファイルを読み込み、GcImportData を呼び出してパッケージをインポートする高レベルな関数です。
    • findPkg 関数が FindPkg にリネームされ、srcDir 引数が追加されました。また、ローカルインポートの解決ロジックが build.IsLocalImportfilepath.Join(srcDir, path) を使用するように変更されました。
  • src/pkg/exp/gotype/gotype.go および src/pkg/exp/types/check_test.go:
    • types.GcImporter の呼び出しが types.GcImport に変更されました。これは、APIの変更に伴う利用箇所の更新です。
  • src/pkg/exp/types/gcimporter_test.go:
    • テストコード内で GcImporter の呼び出しが GcImport に変更されました。
    • コンパイラ名の決定ロジックが簡素化されました。

コアとなるコードの解説

src/pkg/exp/types/exportdata.go の変更

--- a/src/pkg/exp/types/exportdata.go
+++ b/src/pkg/exp/types/exportdata.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// This file implements ExportData.
+// This file implements FindGcExportData.
 
 package types
 
@@ -35,33 +35,14 @@ func readGopackHeader(buf *bufio.Reader) (name string, size int, err error) {
 	return
 }
 
-type dataReader struct {
-	*bufio.Reader
-	io.Closer
-}
-
-// ExportData returns a readCloser positioned at the beginning of the
-// export data section of the given object/archive file, or an error.
-// It is the caller's responsibility to close the readCloser.
+// FindGcExportData positions the reader r at the beginning of the
+// export data section of an underlying GC-created object/archive
+// file by reading from it. The reader must be positioned at the
+// start of the file before calling this function.
 //
-func ExportData(filename string) (rc io.ReadCloser, err error) {
-	file, err := os.Open(filename)
-	if err != nil {
-		return
-	}
-
-	defer func() {
-		if err != nil {
-			file.Close()
-			// Add file name to error.
-			err = fmt.Errorf("reading export data: %s: %v", filename, err)
-		}
-	}()
-
-	buf := bufio.NewReader(file)
-
+func FindGcExportData(r *bufio.Reader) (err error) {
 	// Read first line to make sure this is an object file.
-	line, err := buf.ReadSlice('\n')
+	line, err := r.ReadSlice('\n')
 	if err != nil {
 		return
 	}
@@ -73,7 +54,7 @@ func ExportData(filename string) (rc io.ReadCloser, err error) {
 
 		// First entry should be __.SYMDEF.
 		// Read and discard.
-		if name, size, err = readGopackHeader(buf); err != nil {
+		if name, size, err = readGopackHeader(r); err != nil {
 			return
 		}
 		if name != "__.SYMDEF" {
@@ -87,15 +68,14 @@ func ExportData(filename string) (rc io.ReadCloser, err error) {
 			if n > block {
 				n = block
 			}
-			_, err = io.ReadFull(buf, tmp[:n])
-			if err != nil {
+			if _, err = io.ReadFull(r, tmp[:n]); err != nil {
 				return
 			}
 			size -= n
 		}
 
 		// Second entry should be __.PKGDEF.
-		if name, size, err = readGopackHeader(buf); err != nil {
+		if name, size, err = readGopackHeader(r); err != nil {
 			return
 		}
 		if name != "__.PKGDEF" {
@@ -105,19 +85,17 @@ func ExportData(filename string) (rc io.ReadCloser, err error) {
 
 		// Read first line of __.PKGDEF data, so that line
 		// is once again the first line of the input.
-		line, err = buf.ReadSlice('\n')
-		if err != nil {
+		if line, err = r.ReadSlice('\n'); err != nil {
 			return
 		}
 	}
 	// ... (rest of the function remains similar, operating on 'r' instead of 'buf')
 	// Skip over object header to export data.
 	// Begins after first line with $$.
 	for line[0] != '$' {
-		line, err = buf.ReadSlice('\n')
-		if err != nil {
+		if line, err = r.ReadSlice('\n'); err != nil {
 			return
 		}
 	}
 
-	rc = &dataReader{buf, file}
 	return
 }

この変更は、ExportData がファイルシステムからデータを読み込む責務を放棄し、既に bufio.Reader にラップされたデータストリームからエクスポートデータの開始位置を見つける純粋なパーサー (FindGcExportData) になったことを示しています。これにより、I/O層と解析層が明確に分離されました。

src/pkg/exp/types/gcimporter.go の変更

--- a/src/pkg/exp/types/gcimporter.go
+++ b/src/pkg/exp/types/gcimporter.go
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// This file implements an ast.Importer for gc generated object files.
+// This file implements an ast.Importer for gc-generated object files.
 // TODO(gri) Eventually move this into a separate package outside types.
 
 package types
 
 import (
+\t"bufio"
 	"errors"
 	"fmt"
 	"go/ast"
@@ -24,41 +25,40 @@ import (
 
 const trace = false // set to true for debugging
 
-var (
-\tpkgExts = [...]string{".a", ".5", ".6", ".8"}
-)
+var pkgExts = [...]string{".a", ".5", ".6", ".8"}
 
-// findPkg returns the filename and package id for an import path.
+// FindPkg returns the filename and unique package id for an import
+// path based on package information provided by build.Import (using
+// the build.Default build.Context).
 // If no file was found, an empty filename is returned.
-func findPkg(path string) (filename, id string) {
+//
+func FindPkg(path, srcDir string) (filename, id string) {
 	if len(path) == 0 {
 		return
 	}
 
 	id = path
 	var noext string
-\tswitch path[0] {
+\tswitch {
 	default:
 		// "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x"
-\t\tbp, _ := build.Import(path, "", build.FindOnly)
+\t\tbp, _ := build.Import(path, srcDir, build.FindOnly)
 		if bp.PkgObj == "" {
 			return
 		}
 		noext = bp.PkgObj
 		if strings.HasSuffix(noext, ".a") {
-\t\t\tnoext = noext[:len(noext)-2]
+\t\t\tnoext = noext[:len(noext)-len(".a")]
 		}
 
-\tcase '.':
+\tcase build.IsLocalImport(path):
 		// "./x" -> "/this/directory/x.ext", "/this/directory/x"
-\t\tcwd, err := os.Getwd()
-\t\tif err != nil {
-\t\t\treturn
-\t\t}\n-\t\tnoext = filepath.Join(cwd, path)
+\t\tnoext = filepath.Join(srcDir, path)
 		id = noext
 
-\tcase '/':
+\tcase filepath.IsAbs(path):
+\t\t// for completeness only - go/build.Import
+\t\t// does not support absolute imports
 		// "/x" -> "/x.ext", "/x"
 		noext = path
 	}
@@ -75,6 +75,89 @@ func findPkg(path string) (filename, id string) {
 	return
 }
 
+// GcImportData imports a package by reading the gc-generated export data,
+// adds the corresponding package object to the imports map indexed by id,
+// and returns the object.
+//
+// The imports map must contains all packages already imported, and no map
+// entry with id as the key must be present. The data reader position must
+// be the beginning of the export data section. The filename is only used
+// in error messages.
+//
+func GcImportData(imports map[string]*ast.Object, filename, id string, data *bufio.Reader) (pkg *ast.Object, err error) {
+	if trace {
+		fmt.Printf("importing %s (%s)\n", id, filename)
+	}
+
+	if imports[id] != nil {
+		panic(fmt.Sprintf("package %s already imported", id))
+	}
+
+	// support for gcParser error handling
+	defer func() {
+		if r := recover(); r != nil {
+			err = r.(importError) // will re-panic if r is not an importError
+		}
+	}()
+
+	var p gcParser
+	p.init(filename, id, data, imports)
+	pkg = p.parseExport()
+
+	return
+}
+
+// GcImport imports a gc-generated package given its import path, adds the
+// corresponding package object to the imports map, and returns the object.
+// Local import paths are interpreted relative to the current working directory.
+// The imports map must contains all packages already imported.
+// GcImport satisfies the ast.Importer signature.
+//
+func GcImport(imports map[string]*ast.Object, path string) (pkg *ast.Object, err error) {
+	if path == "unsafe" {
+		return Unsafe, nil
+	}
+
+	srcDir, err := os.Getwd()
+	if err != nil {
+		return
+	}
+	filename, id := FindPkg(path, srcDir)
+	if filename == "" {
+		err = errors.New("can't find import: " + id)
+		return
+	}
+
+	if pkg = imports[id]; pkg != nil {
+		return // package was imported before
+	}
+
+	// open file
+	f, err := os.Open(filename)
+	if err != nil {
+		return
+	}
+	defer func() {
+		f.Close()
+		if err != nil {
+			// Add file name to error.
+			err = fmt.Errorf("reading export data: %s: %v", filename, err)
+		}
+	}()
+
+	buf := bufio.NewReader(f)
+	if err = FindGcExportData(buf); err != nil {
+		return
+	}
+
+	pkg, err = GcImportData(imports, filename, id, buf)
+
+	return
+}
+
+// ----------------------------------------------------------------------------
+// gcParser
+
 // gcParser parses the exports inside a gc compiler-produced
 // object/archive file and populates its scope with the results.
 type gcParser struct {
@@ -109,47 +192,6 @@ func (p *gcParser) next() {
 	}
 }
 
-// GcImporter implements the ast.Importer signature.
-func GcImporter(imports map[string]*ast.Object, path string) (pkg *ast.Object, err error) {
-	if path == "unsafe" {
-		return Unsafe, nil
-	}
-
-	defer func() {
-		if r := recover(); r != nil {
-			err = r.(importError) // will re-panic if r is not an importError
-			if trace {
-				panic(err) // force a stack trace
-			}
-		}
-	}()
-
-	filename, id := findPkg(path)
-	if filename == "" {
-		err = errors.New("can't find import: " + id)
-		return
-	}
-
-	if pkg = imports[id]; pkg != nil {
-		return // package was imported before
-	}
-
-	buf, err := ExportData(filename)
-	if err != nil {
-		return
-	}
-	defer buf.Close()
-
-	if trace {
-		fmt.Printf("importing %s (%s)\n", id, filename)
-	}
-
-	var p gcParser
-	p.init(filename, id, buf, imports)
-	pkg = p.parseExport()
-	return
-}
-
 // Declare inserts a named object of the given kind in scope.
 func (p *gcParser) declare(scope *ast.Scope, kind ast.ObjKind, name string) *ast.Object {
 	// the object may have been imported before - if it exists

この差分は、GcImporterGcImportDataGcImport に分割されたことを明確に示しています。

  • GcImportData は、既に bufio.Reader として提供されたデータからパッケージを解析する、より低レベルな関数です。これは、データソースの抽象化を可能にします。
  • GcImport は、ast.Importer インターフェースを満たす高レベルな関数であり、ファイルパスからオブジェクトファイルを検索し、開いて、FindGcExportData でエクスポートデータの開始位置を見つけ、最終的に GcImportData を呼び出してパッケージをインポートします。
  • findPkgFindPkg にリネームされ、srcDir 引数が追加されたことで、ローカルインポートの解決がより柔軟になりました。

これらの変更により、Goの型チェッカーは、パッケージの型情報を取得する際のI/O層と解析層を分離し、より柔軟で拡張性の高い設計を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/gcsrc/go/types ディレクトリ): https://github.com/golang/go
  • Goのオブジェクトファイルフォーマットに関する非公式な情報源 (Goの内部実装に深く関わるため、公式ドキュメントは少ない):
    • Goのツールチェインに関するブログ記事やカンファレンス発表
    • Goのソースコード内のコメントや設計ドキュメント (もしあれば)
  • Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5574069 (コミットメッセージに記載されているリンク)
  • Goのコンパイラとツールに関する一般的な知識。