[インデックス 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 を汎用化する変更です。
主な変更点は以下の通りです。
ExportData
関数をFindGcExportData
にリネームし、ファイル名ではなくbufio.Reader
を引数として受け取るように変更しました。これにより、オブジェクトファイルがファイルシステム上の特定の場所に存在しない環境(例: メモリ上やネットワーク経由)でもエクスポートデータを読み込めるようになります。- 従来の
GcImporter
をGcImportData
とGcImport
に分割しました。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
の分割 (GcImportData
と GcImport
)
- 旧
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
関数が追加されました。これはFindPkg
、os.Open
、FindGcExportData
を利用してオブジェクトファイルを読み込み、GcImportData
を呼び出してパッケージをインポートする高レベルな関数です。 findPkg
関数がFindPkg
にリネームされ、srcDir
引数が追加されました。また、ローカルインポートの解決ロジックがbuild.IsLocalImport
とfilepath.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
この差分は、GcImporter
が GcImportData
と GcImport
に分割されたことを明確に示しています。
GcImportData
は、既にbufio.Reader
として提供されたデータからパッケージを解析する、より低レベルな関数です。これは、データソースの抽象化を可能にします。GcImport
は、ast.Importer
インターフェースを満たす高レベルな関数であり、ファイルパスからオブジェクトファイルを検索し、開いて、FindGcExportData
でエクスポートデータの開始位置を見つけ、最終的にGcImportData
を呼び出してパッケージをインポートします。findPkg
がFindPkg
にリネームされ、srcDir
引数が追加されたことで、ローカルインポートの解決がより柔軟になりました。
これらの変更により、Goの型チェッカーは、パッケージの型情報を取得する際のI/O層と解析層を分離し、より柔軟で拡張性の高い設計を実現しています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/astgo/types
パッケージのドキュメント: https://pkg.go.dev/go/typesgo/build
パッケージのドキュメント: https://pkg.go.dev/go/buildbufio
パッケージのドキュメント: https://pkg.go.dev/bufio
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc
やsrc/go/types
ディレクトリ): https://github.com/golang/go - Goのオブジェクトファイルフォーマットに関する非公式な情報源 (Goの内部実装に深く関わるため、公式ドキュメントは少ない):
- Goのツールチェインに関するブログ記事やカンファレンス発表
- Goのソースコード内のコメントや設計ドキュメント (もしあれば)
- Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5574069 (コミットメッセージに記載されているリンク)
- Goのコンパイラとツールに関する一般的な知識。