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

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

このコミットは、Goコンパイラ(cmd/gc)における、無名型(特に埋め込みフィールドを持つ構造体)のラッパーメソッドの扱いに関するバグ修正です。コンパイラがオンザフライで生成するこれらのメソッドが、異なるパッケージで同一の型に対して生成された場合に、リンカが多重定義エラーを発生させる問題を解決します。具体的には、リンカがこれらの多重定義を受け入れるように、ラッパーメソッドをDUPOK(Duplicate OK)としてマークする変更が加えられています。

コミット

commit 20c76f7f3f9a8e353cedf57fd633afdc00c09d6f
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed Jan 2 21:42:26 2013 +0100

    cmd/gc: mark wrapper methods for unnamed types as DUPOK.
    
    Unnamed types like structs with embedded fields can have methods.
    These methods are generated on-the-fly by the compiler and
    it may happen for identical types in different packages.
    The linker must accept these multiple definitions.
    
    Fixes #4590.
    
    R=golang-dev, rsc
    CC=golang-dev, remy
    https://golang.org/cl/7030051

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

https://github.com/golang/go/commit/20c76f7f3f9a8e353cedf57fd633afdc00c09d6f

元コミット内容

cmd/gc: 無名型のラッパーメソッドをDUPOKとしてマークする。

埋め込みフィールドを持つ構造体のような無名型はメソッドを持つことができる。 これらのメソッドはコンパイラによってオンザフライで生成され、 異なるパッケージで同一の型に対して発生する可能性がある。 リンカはこれらの多重定義を受け入れなければならない。

Issue #4590 を修正。

変更の背景

Go言語では、構造体に他の構造体を埋め込む(embedding)ことで、埋め込まれた構造体のメソッドを外側の構造体が「昇格(promote)」して利用できるようになります。この際、コンパイラは必要に応じて、外側の構造体から埋め込まれた構造体のメソッドを呼び出すための「ラッパーメソッド」を自動的に生成します。

問題は、このような無名型(例えば struct{ MyType } のような型)が異なるパッケージで定義され、それぞれが同じ基底型(MyType)を埋め込んでいる場合です。Goの型システムでは、異なるパッケージで定義された無名型であっても、その構造が完全に同一であれば、コンパイラは同じラッパーメソッドを生成する可能性があります。

通常、リンカは同じ名前の関数やシンボルが複数定義されているとエラーとします(多重定義エラー)。しかし、コンパイラが自動生成するラッパーメソッドの場合、異なるパッケージで独立して生成された同一のラッパーメソッドが、リンカにとっては「多重定義」と見なされてしまい、ビルドが失敗するという問題が発生していました。このコミットは、このリンカエラーを回避するための修正です。

前提知識の解説

  • Go言語の型システムと無名型: Go言語では、struct{} のように名前を持たない構造体や、interface{} のように名前を持たないインターフェース型を定義できます。これらは「無名型」と呼ばれます。特に構造体の埋め込みは、Goのコンポジション(合成)の強力な機能であり、埋め込まれた型のメソッドが外側の型に「昇格」して利用可能になります。

    type MyWriter struct{}
    func (mw MyWriter) Write(p []byte) (n int, err error) { /* ... */ }
    
    type MyContainer struct {
        MyWriter // MyWriterを埋め込む
    }
    // MyContainerのインスタンスから mw.Write() のように直接Writeメソッドを呼び出せる
    

    このとき、MyContainerWriteメソッドを直接持っているわけではなく、コンパイラが内部的にMyContainerからMyWriterWriteメソッドを呼び出すためのラッパーコードを生成します。

  • コンパイラとリンカ:

    • コンパイラ(cmd/gc: Goのソースコード(.goファイル)を機械語に近い中間表現やオブジェクトファイルに変換するツールです。この過程で、Goの言語仕様に基づいた最適化や、埋め込みによるメソッド昇格のような機能のためのコード生成を行います。
    • リンカ: コンパイラによって生成された複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルを作成するツールです。リンカの主な役割の一つは、プログラム内のすべてのシンボル(関数、変数など)の参照を解決し、それぞれの定義に結びつけることです。
  • シンボルの多重定義: プログラム内で同じ名前の関数やグローバル変数が複数回定義されている状態を指します。通常、これはリンカエラーの原因となります。なぜなら、リンカはどの定義を使用すべきか判断できないためです。しかし、特定の状況下では、リンカが多重定義を許容する場合があります。

  • DUPOK (Duplicate OK): コンパイラやリンカの文脈で使われる用語で、特定のシンボル(関数や変数)が複数回定義されていても、リンカがエラーとせず、その多重定義を許容することを示すフラグや属性です。これは、特にコンパイラが自動生成するコードや、異なるモジュール間で重複するが機能的に同一であると保証されるコードに対して使用されます。Goのコンパイラ内部では、生成されるシンボルに対してこのフラグを設定することで、リンカの多重定義チェックを回避します。

技術的詳細

このコミットの技術的な核心は、Goコンパイラ(cmd/gc)がラッパーメソッドを生成する際に、特定の条件を満たす場合にそのメソッドをDUPOKとしてマークすることです。

Goコンパイラのgenwrapper関数は、埋め込みフィールドを持つ構造体など、特定の型に対するラッパーメソッドを生成する役割を担っています。この関数は、元のメソッドを呼び出すための新しい関数(シンボル)を作成します。

変更前は、このgenwrapper関数が生成するシンボルにはDUPOKフラグが設定されていませんでした。そのため、異なるパッケージで、構造が同一の無名型(例: type T struct { io.Writer }type U struct { io.Writer } が異なるパッケージで定義されている場合)に対して、コンパイラが同じシグネチャを持つラッパーメソッドを生成すると、リンカはこれらを別々のシンボルとして扱い、多重定義エラーを報告していました。

このコミットでは、src/cmd/gc/subr.c内のgenwrapper関数に以下の行が追加されました。

	// wrappers where T is anonymous (struct{ NamedType }) can be duplicated.
	if(rcvr->etype == TSTRUCT || isptr[rcvr->etype] && rcvr->type->etype == TSTRUCT)
		fn->dupok = 1;

このコードは、生成されるラッパーメソッドのレシーバ型(rcvr)が構造体(TSTRUCT)であるか、または構造体へのポインタである場合に、そのラッパーメソッドのシンボル(fn)のdupokフラグを1に設定しています。

  • rcvr->etype == TSTRUCT: レシーバが直接構造体型である場合。
  • isptr[rcvr->etype] && rcvr->type->etype == TSTRUCT: レシーバがポインタ型であり、そのポインタが指す型が構造体である場合(例: *struct{})。

これにより、コンパイラは、無名型(特に埋め込みフィールドを持つ構造体)のために生成されたラッパーメソッドが、リンカによって多重定義されても問題ないことを明示的にリンカに伝えます。リンカはこのdupokフラグを認識し、これらのシンボルが複数存在してもエラーとせず、適切に処理を進めることができるようになります。

この修正は、Goのコンパイルとリンクのプロセスにおける、自動生成コードの特殊なケースを適切に扱うための重要な変更です。

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

変更は主にsrc/cmd/gc/subr.cファイルに集中しています。

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2521,6 +2521,9 @@ genwrapper(Type *rcvr, Type *method, Sym *newnam, int iface)\n \n 	funcbody(fn);\
 	curfn = fn;\
+\t// wrappers where T is anonymous (struct{ NamedType }) can be duplicated.\
+\tif(rcvr->etype == TSTRUCT || isptr[rcvr->etype] && rcvr->type->etype == TSTRUCT)\
+\t\tfn->dupok = 1;\
 	typecheck(&fn, Etop);\
 	typechecklist(fn->nbody, Etop);\
 	curfn = nil;\

また、この修正を検証するためのテストケースがtest/fixedbugs/issue4590.dir/以下に複数追加されています。

  • test/fixedbugs/issue4590.dir/pkg1.go
  • test/fixedbugs/issue4590.dir/pkg2.go
  • test/fixedbugs/issue4590.dir/prog.go
  • test/fixedbugs/issue4590.go

これらのテストファイルは、異なるパッケージで同一の無名型を定義し、それらが持つメソッドがリンカによって正しく処理されることを確認しています。

コアとなるコードの解説

追加された3行のコードは、genwrapper関数内で、生成される関数シンボルfnに対してdupokフラグを設定する条件分岐です。

	if(rcvr->etype == TSTRUCT || isptr[rcvr->etype] && rcvr->type->etype == TSTRUCT)
		fn->dupok = 1;
  • rcvr: 現在処理しているラッパーメソッドのレシーバの型を表すポインタ。
  • rcvr->etype: レシーバの基本型(TSTRUCTは構造体型を示す)。
  • isptr[rcvr->etype]: レシーバの基本型がポインタ型であるかどうかをチェックする配列。
  • rcvr->type->etype: レシーバがポインタ型の場合、そのポインタが指す先の基本型。

この条件式は、レシーバが「構造体型」であるか、または「構造体型へのポインタ」である場合に真となります。これは、埋め込みフィールドを持つ構造体(無名型を含む)のメソッド昇格によって生成されるラッパーメソッドが、まさにこのようなレシーバを持つためです。

fn->dupok = 1; は、生成された関数シンボルfndupokフィールドを1に設定します。このdupokフィールドは、リンカに対して「このシンボルは多重定義されても問題ない」という指示を与える役割を果たします。これにより、異なるパッケージで同一のラッパーメソッドが生成されても、リンカはエラーを報告せず、ビルドが成功するようになります。

関連リンク

  • Go言語の埋め込み(Embedding)に関する公式ドキュメントやチュートリアル: Goの構造体埋め込みの概念を理解する上で役立ちます。
  • Goコンパイラの内部構造に関するドキュメント: cmd/gcの動作原理や、シンボル管理についてより深く知りたい場合に参照できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/cmd/gcディレクトリ)
  • Go言語のIssueトラッカー(Issue #4590に関する詳細情報があれば)
  • Go言語のChange List (CL) 7030051 (ただし、直接アクセスはできませんでした)