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

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

このコミットは、Go言語のcgoツールにおけるC言語の関数ポインタのサポートを追加するものです。具体的には、以下のファイルが変更されています。

  • misc/cgo/test/cgo_test.go: cgoのテストスイートに新しいテストケースTestFpVarが追加されました。
  • misc/cgo/test/fpvar.go: C言語の関数ポインタ変数を扱うcgoのテストケースが新規に作成されました。このファイルには、Cのtypedefで定義された関数ポインタ型intFunc、その関数ポインタを呼び出すC関数bridge_int_func、およびテスト用のC関数fortytwoが含まれています。Go側からはこれらのC関数を呼び出し、関数ポインタの挙動を確認しています。
  • src/cmd/cgo/doc.go: cgoのドキュメントが更新され、C言語の関数ポインタ変数をGoとCの間で受け渡し、Cコードから呼び出す方法に関する説明とサンプルコードが追加されました。
  • src/cmd/cgo/gcc.go: cgoの主要な処理ロジックが含まれるファイルで、関数ポインタ変数のサポートのために大幅な変更が加えられました。特に、rewriteRef関数内で関数ポインタが式として使用される場合の処理が追加され、新しいfpvarというNameKindが導入されました。また、式としてのみ使用される関数に対してはブリッジコードの生成を避けるロジックが追加されています。
  • src/cmd/cgo/main.go: cgoツールのエントリポイントおよび主要なデータ構造が定義されているファイルです。Name構造体に新しいKindとして"fpvar"が追加され、関数ポインタ変数を識別できるようになりました。また、IsVarヘルパー関数が追加され、"var""fpvar"の両方を変数として扱えるようになりました。
  • src/cmd/cgo/out.go: cgoがGoとCの間のブリッジコードを生成する部分が含まれるファイルです。writeDefs関数がfpvar型の変数を適切に処理するように修正され、関数ポインタ変数のGo側での宣言方法が調整されました。

コミット

cmd/cgo: Add support for C function pointers

* Add a new kind of Name, "fpvar" which stands for function pointer variable
* When walking the AST, find functions used as expressions and create a new Name object for them
* Track functions which are only used in expr contexts, and avoid generating bridge code for them

R=golang-dev, minux.ma, fullung, rsc, iant
CC=golang-dev
https://golang.org/cl/9835047

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

https://github.com/golang/go/commit/c18dc11ef210ca91b251996ff2a2546d0bcde848

元コミット内容

cmd/cgo: Add support for C function pointers

このコミットは、Go言語のcgoツールにC言語の関数ポインタのサポートを追加します。

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

  • Name構造体に新しい種類として「fpvar」(関数ポインタ変数)を追加。
  • AST(抽象構文木)を走査する際に、式として使用される関数を見つけ、それらに対して新しいNameオブジェクトを作成。
  • 式コンテキストでのみ使用される関数を追跡し、それらに対するブリッジコードの生成を回避。

変更の背景

Go言語は、cgoツールを通じてC言語のコードと相互運用する機能を提供しています。しかし、このコミット以前は、C言語の関数ポインタをGoコード内で直接的かつ柔軟に扱うための完全なサポートが不足していました。特に、Cの関数ポインタをGoの変数に代入したり、GoからC関数に引数として渡したりする際に、cgoが適切に型変換やブリッジコードの生成を行う必要がありました。

従来のcgoでは、Cの関数をGoから呼び出すためのブリッジコードは生成されていましたが、Cの関数ポインタをGoの式の中で利用する(例えば、Cの関数ポインタをGoの変数に代入して、それを別のC関数に渡す)といったシナリオが十分に考慮されていませんでした。このような場合、cgoは関数ポインタを通常の関数呼び出しと誤解釈し、不必要なブリッジコードを生成したり、コンパイルエラーを引き起こしたりする可能性がありました。

このコミットの目的は、Cの関数ポインタをGoのコードベースでより自然に、かつ安全に扱えるようにすることです。具体的には、Cの関数ポインタをGoの変数として宣言し、GoとCの間で受け渡し、Cコードからその関数ポインタを呼び出すといったユースケースをサポートするために、cgoの内部処理を強化することが背景にあります。これにより、Goプログラムが既存のCライブラリと連携する際の柔軟性と表現力が向上します。

前提知識の解説

このコミットの理解には、以下の概念に関する前提知識が役立ちます。

1. Go言語のcgoツール

cgoは、GoプログラムがC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Goのソースファイル内に特別なコメントブロック(import "C"の直前)でCのコードを記述することで、cgoがGoとCの間の相互運用に必要なブリッジコードを自動生成します。

  • GoからCの呼び出し: Goコード内でC.funcName()のように記述することで、Cの関数を呼び出すことができます。cgoは、Goの型とCの型の間で適切な変換を行い、Goの呼び出し規約とCの呼び出し規約の間のギャップを埋めるコードを生成します。
  • CからGoの呼び出し(コールバック): Goの関数に//export FuncNameというディレクティブを付与することで、そのGo関数をCから呼び出せるようにエクスポートできます。cgoは、Cから呼び出し可能なラッパー関数を生成し、CのコードがGoの関数をコールバックとして利用できるようにします。

2. C言語の関数ポインタ

C言語において、関数ポインタは関数を指すポインタ変数です。これにより、関数を他の関数の引数として渡したり、データ構造に格納したり、実行時に呼び出す関数を動的に決定したりすることが可能になります。

例:

typedef int (*IntFunc)(int, int); // intを2つ引数にとり、intを返す関数へのポインタ型

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    IntFunc op;
    op = add; // 関数ポインタにadd関数を代入
    int result = op(5, 3); // opを通じてadd関数を呼び出す (結果は8)

    op = subtract; // 関数ポインタにsubtract関数を代入
    result = op(5, 3); // opを通じてsubtract関数を呼び出す (結果は2)
    return 0;
}

3. GoとCの間の相互運用における課題

GoとCは異なるメモリモデル、型システム、呼び出し規約を持っています。cgoはこれらの違いを吸収しますが、特にポインタやメモリ管理に関しては注意が必要です。

  • ポインタの安全性: Goのガベージコレクタはメモリを移動させる可能性がありますが、Cは安定したメモリアドレスを期待します。GoのポインタをCに渡す際には、Goのポインタが他のGoのポインタを含まないこと、C関数がGoのポインタのコピーを保持しないことなど、厳格なルールがあります。runtime/cgo.Handleのようなメカニズムは、Goの値を安全にCに渡し、CからGoに返すためのIDとして機能します。
  • 型変換: GoのstringsliceはCの文字列や配列とは異なる内部表現を持っています。cgoはこれらの変換を支援しますが、手動でのメモリ解放(C.freeなど)が必要になる場合があります。
  • 関数ポインタの直接呼び出しの制限: GoからCの関数ポインタを直接呼び出すことは、このコミット以前は困難でした。GoのランタイムとガベージコレクタがGoの関数を管理するため、Goの関数のアドレスは安定しておらず、Cの関数ポインタと直接互換性がありませんでした。このため、Cのラッパー関数を介して呼び出すなどの回避策が必要でした。

このコミットは、特にCの関数ポインタをGoの式として扱う際のcgoの内部的な処理を改善し、よりシームレスな相互運用を可能にすることを目指しています。

技術的詳細

このコミットの技術的詳細は、cgoがC言語の関数ポインタをGoのコードベースでどのように扱うかを改善した点に集約されます。

  1. 新しいNameKind「fpvar」の導入: cgoは、Cのシンボル(変数、関数、型など)をGoのコードにマッピングする際に、内部的にNameという構造体を使用します。このName構造体には、シンボルの種類を示すKindフィールドがあります。このコミットでは、Cの関数ポインタ変数を識別するために、新たに"fpvar"というKindが追加されました。これにより、cgoは通常の変数("var")と関数ポインタ変数("fpvar")を区別して処理できるようになります。

  2. AST走査時の関数ポインタ変数の識別とNameオブジェクトの生成: cgoはGoのソースコードを解析し、抽象構文木(AST)を構築します。このASTを走査する際に、Cの関数が式(expression)として使用されているケースを検出するロジックがsrc/cmd/cgo/gcc.gorewriteRef関数に追加されました。 以前は、C.funcNameが式として現れた場合、cgoは「C.funcNameは呼び出す必要があります」というエラーを発生させていました。これは、cgoC.funcNameを常にC関数への呼び出しとして解釈していたためです。 しかし、Cの関数ポインタを扱う場合、C.funcNameが関数ポインタ変数として扱われることがあります(例: f := C.intFunc(C.fortytwo)のように、Cの関数fortytwoのアドレスをGoの変数fに代入するケース)。 このコミットでは、C.funcNameが式コンテキストで現れた場合、それが関数ポインタ変数として扱われるべきであると判断し、その関数ポインタ変数に対応する新しいNameオブジェクト(Kind"fpvar")を動的に作成するようになりました。この新しいNameオブジェクトは、Go側ではunsafe.Pointer型として表現され、C側ではvoid*として扱われます。

  3. 式コンテキストでのみ使用される関数のブリッジコード生成の回避: cgoは、GoからC関数を呼び出すために、GoとCの間の呼び出し規約を変換する「ブリッジコード」を生成します。しかし、Cの関数がGoのコード内で関数ポインタ変数としてのみ使用され、直接呼び出されない場合、その関数に対するブリッジコードは不要です。 このコミットでは、rewriteRef関数内で、各C関数が実際にGoから呼び出されているかどうかを追跡するメカニズムが導入されました。具体的には、functionsというマップを使用して、C関数がcallコンテキスト(つまり、Goから実際に呼び出されている)で使用された場合にフラグを立てます。 rewriteRef関数の最後に、このfunctionsマップを走査し、callコンテキストで一度も使用されなかったC関数(つまり、式コンテキストでのみ使用された関数ポインタ変数)については、f.NameマップからそのNameオブジェクトを削除します。これにより、cgoは不要なブリッジコードの生成を回避し、生成されるコードの効率性を向上させます。

  4. out.goにおけるfpvarの適切な処理: src/cmd/cgo/out.gowriteDefs関数は、Go側でCの変数や関数ポインタに対応する定義を生成する役割を担っています。この関数が、新しい"fpvar"型のNameオブジェクトを適切に処理するように修正されました。 具体的には、"var"型の変数と"fpvar"型の関数ポインタ変数の両方をn.IsVar()ヘルパー関数で識別し、それぞれに応じたGoの型("var"*n.Type.Go"fpvar"n.Type.Go)で宣言されるように調整されました。これにより、Go側でCの関数ポインタ変数がunsafe.Pointerとして正しく表現されるようになります。また、C側でこれらの変数へのポインタを取得する際に、"fpvar"の場合は&演算子を付けないように修正され、Cの関数ポインタが直接void*として扱われるようになりました。

これらの変更により、cgoはCの関数ポインタをGoのコード内でより柔軟に、かつ効率的に扱えるようになり、GoとCの間の相互運用性が向上しました。

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

このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルと関数に集中しています。

  1. src/cmd/cgo/main.go:

    • Name構造体のKindフィールドのコメントに"fpvar"が追加されました。
    • 新しいメソッドIsVar() boolName構造体に追加されました。これは、Kind"var"または"fpvar"の場合にtrueを返します。
    // src/cmd/cgo/main.go
    type Name struct {
        // ...
        Kind     string // "const", "type", "var", "fpvar", "func", "not-type"
        // ...
    }
    
    // IsVar returns true if Kind is either "var" or "fpvar"
    func (n *Name) IsVar() bool {
        return n.Kind == "var" || n.Kind == "fpvar"
    }
    
  2. src/cmd/cgo/gcc.go:

    • rewriteRef関数内で、C関数が式として使用される場合の処理が大幅に変更されました。
    • functionsマップが導入され、Goから実際に呼び出されたC関数を追跡するようになりました。
    • r.Name.Kind == "func"かつr.Context == "expr"の場合、つまりC関数が式として使用されている場合(関数ポインタとして扱われる場合)、新しいNameオブジェクト(Kind: "fpvar")が動的に作成され、r.Nameがこの新しいNameオブジェクトに置き換えられます。
    • rewriteRef関数の最後に、functionsマップを走査し、Goから一度も呼び出されなかったC関数(式としてのみ使用された関数ポインタ変数)のNameオブジェクトをf.Nameから削除するロジックが追加されました。
    // src/cmd/cgo/gcc.go - rewriteRef function (抜粋)
    func (p *Package) rewriteRef(f *File) {
        // Keep a list of all the functions, to remove the ones
        // only used as expressions and avoid generating bridge
        // code for them.
        functions := make(map[string]bool)
    
        // ... (既存のコード)
    
        for _, r := range f.Ref {
            // ...
            case "call":
                // ...
                functions[r.Name.Go] = true // 関数が呼び出されたことを記録
                // ...
            case "expr":
                if r.Name.Kind == "func" {
                    // Function is being used in an expression, to e.g. pass around a C function pointer.
                    // Create a new Name for this Ref which causes the variable to be declared in Go land.
                    fpName := "fp_" + r.Name.Go
                    name := f.Name[fpName]
                    if name == nil {
                        name = &Name{
                            Go:   fpName,
                            C:    r.Name.C,
                            Kind: "fpvar", // 新しいKind "fpvar" を設定
                            Type: &Type{Size: p.PtrSize, Align: p.PtrSize, C: c("void*"), Go: ast.NewIdent("unsafe.Pointer")},
                        }
                        p.mangleName(name)
                        f.Name[fpName] = name
                    }
                    r.Name = name
                    expr = ast.NewIdent(name.Mangle)
                } else if r.Name.Kind == "type" {
                    // ...
                } else if r.Name.Kind == "var" {
                    // ...
                }
            // ...
        }
    
        // Remove functions only used as expressions, so their respective
        // bridge functions are not generated.
        for name, used := range functions {
            if !used {
                delete(f.Name, name) // 呼び出されなかった関数は削除
            }
        }
    }
    
  3. src/cmd/cgo/out.go:

    • writeDefs関数内で、Nameオブジェクトが変数であるかどうかのチェックにn.IsVar()が使用されるようになりました。
    • "var""fpvar"Kindに応じて、Go側で生成される型宣言が分岐するようになりました。"var"の場合はポインタ型(&*)が、"fpvar"の場合は直接の型が使用されます。
    // src/cmd/cgo/out.go - writeDefs function (抜粋)
    func (p *Package) writeDefs() {
        // ...
        for _, key := range nameKeys(p.Name) {
            n := p.Name[key]
            if !n.IsVar() { // n.IsVar() を使用
                continue
            }
    
            // ...
    
            var amp string
            var node ast.Node
            if n.Kind == "var" {
                amp = "&"
                node = &ast.StarExpr{X: n.Type.Go}
            } else if n.Kind == "fpvar" {
                node = n.Type.Go
            } else {
                panic(fmt.Errorf("invalid var kind %q", n.Kind))
            }
    
            if *gccgo {
                fmt.Fprintf(fc, `extern void *%s __asm__("%s.%s");`, n.Mangle, gccgoSymbolPrefix, n.Mangle)
                fmt.Fprintf(&gccgoInit, "\\t%s = %s%s;\\n", n.Mangle, amp, n.C) // amp を使用
            } else {
                fmt.Fprintf(fc, "void *·%s = %s%s;\\n", n.Mangle, amp, n.C) // amp を使用
            }
            fmt.Fprintf(fc, "\\n")
    
            fmt.Fprintf(fgo2, "var %s ", n.Mangle)
            conf.Fprint(fgo2, fset, node) // node を使用
            fmt.Fprintf(fgo2, "\\n")
        }
        // ...
    }
    

これらの変更は、cgoがCの関数ポインタをGoの型システムにマッピングし、不要なブリッジコードの生成を回避するための基盤を構築しています。

コアとなるコードの解説

このコミットのコアとなるコードの変更は、cgoがC言語の関数ポインタをGoのコード内でより適切に扱うための内部的なメカニズムを確立しています。

1. Name構造体とIsVar()メソッド (src/cmd/cgo/main.go)

  • Name構造体のKindフィールドへの"fpvar"追加: cgoは、Cのシンボル(関数、変数、型など)をGoのコードに変換する際に、それらの情報をName構造体で管理します。Kindフィールドは、そのシンボルが何であるかを示します。このコミット以前は、Cの関数ポインタ変数を明確に区別するKindがありませんでした。"fpvar"(function pointer variable)の追加により、cgoはCの関数ポインタ変数を特別な種類として認識し、それに応じた処理を適用できるようになりました。

  • IsVar()メソッドの導入: このヘルパーメソッドは、Nameオブジェクトが通常の変数("var")であるか、関数ポインタ変数("fpvar")であるかを簡潔にチェックするために導入されました。これにより、cgoの他の部分で、これら両方の種類の変数を一括して処理する必要がある場合に、コードの可読性と保守性が向上します。例えば、Go側でCの変数に対応する宣言を生成する際に、通常の変数と関数ポインタ変数の両方を対象とすることができます。

2. rewriteRef関数における関数ポインタの処理 (src/cmd/cgo/gcc.go)

rewriteRef関数は、GoのASTを走査し、C.xxx形式の参照をGoの等価な表現に書き換えるcgoの核心部分です。

  • 式コンテキストでのC関数の検出とfpvarへの変換: 最も重要な変更点の一つは、C.funcNamecallコンテキスト(関数呼び出し)ではなく、exprコンテキスト(式)で現れた場合の処理です。 以前は、C.funcNameが式として使われるとエラーになっていました。しかし、Cの関数ポインタをGoの変数に代入するような場合(例: f := C.intFunc(C.fortytwo))、C.fortytwoは関数ポインタとして扱われるべきであり、直接呼び出されるわけではありません。 この変更により、cgoC.funcNameexprコンテキストで使用されていることを検出すると、そのC関数に対応する新しいNameオブジェクトを動的に作成し、そのKind"fpvar"に設定します。この新しいNameオブジェクトは、Go側ではunsafe.Pointerとして表現され、C側ではvoid*として扱われます。これにより、GoのコードがCの関数ポインタをGoの変数として安全に保持し、他のC関数に渡すことが可能になります。

  • 不要なブリッジコード生成の回避: cgoは、GoからC関数を呼び出すためにブリッジコードを生成します。しかし、Cの関数がGoのコード内で関数ポインタとしてのみ使用され、直接呼び出されない場合、その関数に対するブリッジコードは不要です。 functionsマップは、Goから実際に呼び出されたC関数を追跡するために使用されます。rewriteRef関数の処理が完了した後、このマップをチェックし、一度もcallコンテキストで使用されなかったC関数(つまり、関数ポインタ変数としてのみ参照された関数)のNameオブジェクトをf.Nameから削除します。これにより、cgoはこれらの関数に対するブリッジコードの生成をスキップし、生成されるコードのサイズと複雑さを削減します。これは、コンパイル時間と最終的なバイナリサイズの両方に良い影響を与えます。

3. writeDefs関数におけるfpvarの出力 (src/cmd/cgo/out.go)

writeDefs関数は、cgoがGoとCの間の相互運用に必要な定義(変数宣言など)を生成する場所です。

  • "var""fpvar"の型宣言の区別: この関数は、n.IsVar()を使用して、処理中のNameオブジェクトが通常の変数か関数ポインタ変数かを判断します。
    • 通常の変数(n.Kind == "var")の場合、Go側ではそのC変数のポインタ型(例: var _Cvar_myVar *C.int)として宣言されます。これは、Cの変数のアドレスをGoのポインタとして扱うためです。C側では、その変数のアドレスを取得するために&演算子が付加されます(例: void *·_Cvar_myVar = &myVar;)。
    • 関数ポインタ変数(n.Kind == "fpvar")の場合、Go側ではunsafe.Pointer型(例: var _Cfpvar_fortytwo unsafe.Pointer)として宣言されます。これは、Cの関数ポインタがGoのunsafe.Pointerにマッピングされるためです。C側では、関数ポインタ自体がアドレスであるため、&演算子は不要です(例: void *·_Cfpvar_fortytwo = fortytwo;)。

この変更により、cgoはCの関数ポインタ変数をGoの型システムに正しくマッピングし、GoとCの間で安全かつ効率的に受け渡しできるようになりました。全体として、このコミットはcgoのC言語関数ポインタサポートを大幅に強化し、より複雑なCライブラリとの連携を可能にしています。

関連リンク

参考にした情報源リンク