[インデックス 1058] ファイルの概要
このコミットは、Go言語のコンパイラ (6g
) とリンカ (6l
)、およびランタイムにおけるインターフェース関連の重要な変更を導入しています。主な目的は、nil
インターフェースと任意の型との間の変換を許可すること、そして複数のコンパイル済みオブジェクトファイル (.6
ファイル) に含まれる型シグネチャの重複を効率的に処理することです。これにより、Goのインターフェースシステムがより柔軟になり、バイナリサイズが最適化されます。
コミット
commit 1983121bbb5d8cb346c1baef18e3dc1a2cbfee10
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 5 11:27:50 2008 -0800
6g interface changes:
* allow conversion between nil interface and any type.
* mark signatures as DUPOK so that multiple .6 can
contain sigt.*[]byte and only one gets used.
R=ken
OCL=18538
CL=18542
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1983121bbb5d8cb346c1baef18e3dc1a2cbfee10
元コミット内容
6g interface changes:
* allow conversion between nil interface and any type.
* mark signatures as DUPOK so that multiple .6 can
contain sigt.*[]byte and only one gets used.
変更の背景
Go言語の初期段階において、インターフェースの扱い、特に nil
インターフェースの挙動や、コンパイルされた複数のパッケージ間で共有される型情報の管理には改善の余地がありました。
-
nil
インターフェースの柔軟性向上: 以前のGoでは、nil
インターフェースと具体的な型との間の変換が常にスムーズに行われるわけではありませんでした。これは、nil
インターフェースが(nil, nil)
という内部表現を持つため、特定の型への変換時にランタイムエラーを引き起こす可能性がありました。このコミットは、この変換をより直感的で安全なものにすることを目指しています。 -
型シグネチャの重複排除: Goのコンパイルプロセスでは、各型(特にインターフェースやメソッドを持つ型)のランタイム表現に必要な「型シグネチャ」が生成されます。これらのシグネチャは、型のアラインメント、サイズ、メソッドセットなどの情報を含みます。複数のソースファイルやパッケージが同じ型を参照する場合、それぞれのコンパイル済みオブジェクトファイル (
.6
ファイル) に同じ型シグネチャの定義が含まれる可能性がありました。これは最終的なバイナリの肥大化を招き、リンカが重複するシンボルをどのように解決するかという問題を引き起こします。このコミットは、リンカレベルでこれらの重複するシグネチャを効率的に処理し、最終バイナリに一意のシグネチャのみを含めるメカニズムを導入することで、この問題を解決しようとしています。
これらの変更は、Go言語のインターフェースのセマンティクスを洗練し、コンパイルおよびリンクプロセスの効率を高めるための基盤を築くものです。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラ/リンカに関する基本的な知識が必要です。
-
Go言語のインターフェース:
- Goのインターフェースは、メソッドのセットを定義する型です。
- インターフェース型の変数は、そのインターフェースが定義するすべてのメソッドを実装する任意の具象型の値を保持できます。
- 内部的には、Goのインターフェース値は2つのポインタで構成されます:
- 型ポインタ (type pointer): 格納されている具象値の型情報(メソッドセット、サイズ、アラインメントなど)を指します。
- データポインタ (data pointer): 格納されている具象値自体を指します。
nil
インターフェースは、両方のポインタがnil
である状態(nil, nil)
を指します。
-
Goコンパイラ (
6g
):- Goのソースコード (
.go
ファイル) を、特定のアーキテクチャ(この場合は6
、つまりamd64)向けのオブジェクトファイル (.6
ファイル) にコンパイルするツールです。 - コンパイル時に、型情報やメソッド情報がランタイムで利用できるように、特定のデータ構造(型シグネチャなど)を生成します。
- Goのソースコード (
-
Goリンカ (
6l
):- コンパイラによって生成された複数のオブジェクトファイル (
.6
ファイル) を結合し、必要なランタイムライブラリとリンクして、実行可能なバイナリを生成するツールです。 - リンカは、異なるオブジェクトファイル間で参照されるシンボル(関数、変数、データ構造など)を解決する役割を担います。
- コンパイラによって生成された複数のオブジェクトファイル (
-
型シグネチャ (Type Signatures):
- Goのランタイムが型に関する情報を取得するために使用する内部データ構造です。これには、型の名前、サイズ、アラインメント、そしてその型が実装するメソッドのリストなどが含まれます。
- 特にインターフェースのメソッドディスパッチや型アサーションにおいて、この情報が不可欠です。
sigt
は具象型(struct, intなど)のシグネチャを指し、sigi
はインターフェース型のシグネチャを指すことが多いです。
-
DUPOK
(Duplicate OK) シンボル:- リンカの概念の一つで、複数のオブジェクトファイルが同じ名前のシンボルを定義している場合に、リンカがエラーを発生させずに、そのうちの1つ(通常は最初に見つかったもの)を選択して使用することを許可するフラグです。
- これは、C/C++における「弱いシンボル (weak symbols)」に似ており、ライブラリのデフォルト実装を提供しつつ、ユーザーが独自のバージョンでオーバーライドできるようにする場合などに使用されます。Goの文脈では、重複する型シグネチャの定義を効率的に処理するために利用されます。
技術的詳細
このコミットは、Goのコンパイラ、リンカ、ランタイムの複数のコンポーネントにわたる協調的な変更を含んでいます。
-
nil
インターフェース変換の改善:src/cmd/gc/walk.c
のascompat
関数が変更され、isnilinter(t1)
およびisnilinter(t2)
のチェックが追加されました。これは、nil
インターフェースが他のインターフェース型や具象型に変換される際の互換性ルールを拡張するものです。- 具体的には、
nil
インターフェースは、その型がメソッドを持たない場合、任意の具象型やインターフェース型に変換可能であると見なされるようになります。これにより、var i interface{} = nil
のようなコードから、i.(MyType)
のような型アサーションがより安全に行えるようになります。
-
型シグネチャの動的生成と重複排除 (
DUPOK
フラグの導入):- コンパイラ (
6g
) 側の変更:src/cmd/gc/go.h
にType
およびSym
構造体にsiggen
(signature generated) フラグが追加されました。これは、特定の型シグネチャが既に生成済みであるかを追跡するために使用されます。src/cmd/gc/subr.c
のsigname
関数が大幅に簡素化されました。この関数は、与えられた型に対応するランタイムシグネチャのシンボル名を生成します。以前はblock
引数がありましたが、これが削除され、より汎用的なシグネチャ名生成ロジックになりました。また、signame
は、まだ生成されていない型シグネチャをsignatlist
に追加し、siggen
フラグをセットする役割も担います。src/cmd/6g/obj.c
のdumpsigt
(具象型シグネチャのダンプ) およびdumpsigi
(インターフェース型シグネチャのダンプ) 関数が変更され、生成されるAGLOBL
命令にDUPOK
フラグが設定されるようになりました。AGLOBL
は、グローバルシンボルを定義するためのアセンブラ命令です。dumpsignatures
関数が導入され、signatlist
を走査し、siggen
フラグに基づいて各型シグネチャが一度だけダンプされるように制御します。これにより、コンパイラは必要なすべての型シグネチャを生成しますが、重複は避けます。
- リンカ (
6l
) 側の変更:src/cmd/6l/l.h
のSym
構造体にdupok
(duplicate ok) フラグが追加されました。これは、リンカがシンボルを処理する際に、そのシンボルが重複を許容するかどうかを判断するために使用されます。src/cmd/6l/obj.c
では、AGLOBL
命令にDUPOK
フラグが設定されている場合、対応するシンボルのdupok
フラグがセットされます。- さらに重要なのは、
ADATA
(データセクション) 命令の処理において、もしシンボルがdupok
フラグを持っている場合、そのシンボルに対する後続のADATA
命令(つまり重複する定義)は無視されるようになりました。これにより、複数のオブジェクトファイルに同じ型シグネチャが含まれていても、リンカは最初の定義のみを採用し、最終バイナリには一意のシグネチャのみが残るようになります。
- ランタイム (
src/runtime/iface.c
) の変更:- 以前は、
int8
,string
,bool
などの基本的な組み込み型や空のインターフェース (interface{}
) の型シグネチャがsrc/runtime/iface.c
にハードコードされていました。このコミットでは、これらのハードコードされた定義が削除されました。 - これは、これらの型シグネチャがコンパイラによって動的に生成され、リンカによって適切に処理されるようになったため、ランタイムに静的に含める必要がなくなったことを意味します。これにより、ランタイムのコードベースが簡素化され、より汎用的なものになります。
sigi·inter
はsigi·empty
に置き換えられ、空のインターフェースのシグネチャがより明確に扱われるようになりました。
- 以前は、
- コンパイラ (
これらの変更により、Goのコンパイル・リンクシステムは、型シグネチャの管理においてより洗練されたアプローチを採用し、nil
インターフェースのセマンティクスを改善しています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/6g/obj.c
:dumpsigt
およびdumpsigi
関数のシグネチャ変更と内部ロジックの修正。AGLOBL
命令にDUPOK
フラグを設定するロジックの追加。dumpsignatures
関数がsignatlist
を走査し、siggen
フラグに基づいてdumpsigi
またはdumpsigt
を呼び出すように変更。
-
src/cmd/6l/l.h
:struct Sym
にuchar dupok;
フィールドを追加。
-
src/cmd/6l/obj.c
:AGLOBL
命令の処理でp->from.scale & DUPOK
に基づいてs->dupok = 1;
を設定するロジックを追加。ADATA
命令の処理でp->from.sym->dupok
が真の場合に重複する定義をスキップするロジックを追加。
-
src/cmd/gc/go.h
:struct Type
にuchar siggen;
フィールドを追加。struct Sym
にuchar siggen;
フィールドを追加。signame
関数のプロトタイプをSym* signame(Type*);
に変更(block
引数の削除)。
-
src/cmd/gc/subr.c
:globalsig
関数を削除。signame
関数の実装を大幅に簡素化し、block
引数を削除。signame
内でt->siggen = 1;
を設定し、signatlist
に型を追加するロジックを追加。- 空のインターフェース (
interface { }
) のシグネチャ名をsigi.empty
として特別に扱うロジックを追加。
-
src/cmd/gc/walk.c
:ascompat
関数でisnilinter(t1)
およびisnilinter(t2)
のチェックを追加。isandss
関数でisnilinter
のチェックを含む条件を修正。ifaceop
関数でsigname
の呼び出しから0
(block) 引数を削除。
-
src/runtime/iface.c
:- ハードコードされていた多くの
Sigi
およびSigt
定義(例:sigt·int8
,sigt·string
など)を削除。 Sigi sigi·inter[2]
をSigi sigi·empty[2]
に変更し、hashmap
の呼び出しもこれに合わせて修正。
- ハードコードされていた多くの
コアとなるコードの解説
このコミットの核心は、Goの型システムとランタイムの連携を強化し、特にインターフェースの扱いを改善することにあります。
DUPOK
フラグによる型シグネチャの重複排除:
src/cmd/6g/obj.c
の dumpsigt
および dumpsigi
関数は、Goの型情報をランタイムが利用できる形式(型シグネチャ)に変換し、オブジェクトファイルに出力する役割を担います。このコミットでは、これらの関数が生成する AGLOBL
命令に DUPOK
フラグが設定されるようになりました。
// src/cmd/6g/obj.c (dumpsigt, dumpsigi 内)
// set DUPOK to allow other .6s to contain
// the same signature. only one will be chosen.
p = pc;
gins(AGLOBL, N, N);
p->from = at;
p->from.scale = DUPOK; // ここで DUPOK フラグを設定
p->to = ac;
p->to.offset = ot;
この DUPOK
フラグは、リンカ (6l
) にとって重要な意味を持ちます。src/cmd/6l/obj.c
では、リンカがオブジェクトファイル内の命令を処理する際に、このフラグを認識します。
// src/cmd/6l/obj.c (loop 内)
case AGLOBL:
// ...
if(p->from.scale & DUPOK) // DUPOK フラグがセットされているかチェック
s->dupok = 1; // シンボルに dupok フラグをセット
goto loop;
case ADATA:
// Assume that AGLOBL comes after ADATA.
// If we've seen an AGLOBL that said this sym was DUPOK,
// ignore any more ADATA we see, which must be
// redefinitions.
if(p->from.sym != S && p->from.sym->dupok) // シンボルが dupok なら
goto loop; // この ADATA 定義をスキップ
// ...
このロジックにより、複数のオブジェクトファイルが同じ型シグネチャ(例えば sigt.string
)を定義していても、リンカは DUPOK
フラグがセットされたシンボルに対しては、最初に見つけた定義のみを採用し、後続の重複する定義を無視します。これにより、最終的な実行可能ファイルには各型シグネチャのコピーが1つだけ含まれることが保証され、バイナリサイズが削減されます。
nil
インターフェースの変換ロジック:
src/cmd/gc/walk.c
の ascompat
関数は、型変換の互換性をチェックするGoコンパイラの重要な部分です。このコミットでは、nil
インターフェースの変換に関するルールが緩和されました。
// src/cmd/gc/walk.c (ascompat 関数内)
if(isnilinter(t1)) // t1 が nil インターフェースなら
return 1; // 互換性ありと見なす
// ...
if(isnilinter(t2)) // t2 が nil インターフェースなら
return 1; // 互換性ありと見なす
この変更により、nil
インターフェースは、それが保持する具体的な型が何であれ(実際には nil
)、他のインターフェース型や具象型との間でより柔軟に変換できるようになりました。これは、Goのインターフェースが (type, value)
のペアとして表現されることを考えると、nil
インターフェースが (nil, nil)
であるため、型情報が nil
であっても互換性があると見なせるようになったことを意味します。これにより、Goプログラマは nil
インターフェースをより自然に扱うことができるようになります。
これらの変更は、Goのコンパイラ、リンカ、ランタイムが密接に連携し、言語のセマンティクスと実装の詳細がどのように進化していくかを示す良い例です。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやチュートリアル
- Goコンパイラとリンカの内部構造に関する資料(Goのソースコードリポジトリ内の
doc/
ディレクトリや、Goの設計に関するブログ記事など) - リンカにおける「弱いシンボル」や「重複シンボル解決」に関する一般的な情報
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/6g
,src/cmd/6l
,src/cmd/gc
,src/runtime
ディレクトリ) - Goの初期の設計に関するメーリングリストの議論や設計ドキュメント(Goの公式リポジトリやGoのブログで公開されているもの)
- リンカの動作に関する一般的なコンピュータサイエンスの資料