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

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

このコミットは、Go言語のcgoツールにおけるバグ修正に関するものです。具体的には、#cgoディレクティブがCコードの行番号に影響を与え、エラーメッセージの行番号がずれてしまう問題を解決します。これにより、cgoを介してGoとCを連携させる際に、Cコード内で発生したエラーのデバッグがより正確に行えるようになります。

コミット

commit f68c23e2bba617d8f9bbe1cb53a920aeaf8901ad
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue Sep 3 21:15:15 2013 -0700

    cmd/cgo: don't let #cgo directives mess up line numbering
    
    Fixes #5272.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/13498046
---
 misc/cgo/errors/err1.go   | 14 ++++++++++++++
 misc/cgo/errors/test.bash | 19 +++++++++++++++++++
 src/cmd/cgo/gcc.go        |  2 ++\
 src/run.bash              |  6 ++++++\
 4 files changed, 41 insertions(+)

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

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

元コミット内容

cmd/cgo: don't let #cgo directives mess up line numbering

このコミットは、cmd/cgoツールが#cgoディレクティブによって行番号を狂わせないようにするものです。Issue 5272を修正します。

変更の背景

このコミットは、GoのIssue 5272「cmd/cgo: #cgo directives mess up line numbering」を修正するために作成されました。

Go言語のcgoツールは、GoプログラムからCコードを呼び出すためのメカニズムを提供します。cgoを使用する際、Goのソースファイル内にCコードを埋め込んだり、Cのライブラリにリンクするためのディレクティブ(#cgo)を記述したりします。

この問題の背景には、cgoがGoのソースファイルを処理し、Cコードを抽出してコンパイル可能な形式に変換する過程があります。以前の実装では、#cgoディレクティブを含む行がCコードのプリプロセッサに渡される際に、これらのディレクティブが削除されることで、元のGoファイルと生成されたCファイルの間で行番号のずれが生じていました。

具体的には、Goファイル内でCコードブロックの前に#cgoディレクティブが存在すると、cgoツールがそのディレクティブを処理して削除した後、Cコンパイラに渡されるCコードの行番号が、元のGoファイルでのCコードの開始行番号と一致しなくなっていました。これにより、Cコード内でコンパイルエラーが発生した場合、Cコンパイラが出力するエラーメッセージの行番号が、開発者が参照しているGoソースファイル内の実際の行番号と異なるため、デバッグが非常に困難になるという問題がありました。

このコミットは、この行番号のずれを解消し、CコンパイラからのエラーメッセージがGoソースファイル内の正しい行番号を指すようにすることで、cgoを利用した開発のデバッグ体験を向上させることを目的としています。

前提知識の解説

Go言語のcgo

cgoは、GoプログラムからC言語のコードを呼び出すためのGoツールチェーンの一部です。Goは通常、純粋なGoコードで記述されますが、既存のCライブラリを利用したい場合や、特定のパフォーマンス要件のためにCコードを使用したい場合にcgoが役立ちます。

cgoの基本的な仕組みは以下の通りです。

  1. Goファイル内のCコード: Goのソースファイル(例: main.go)内に、特別なコメントブロックを使ってCコードを記述できます。このブロックはimport "C"の直前に配置されます。

    package main
    
    /*
    #include <stdio.h>
    
    void my_c_function() {
        printf("Hello from C!\n");
    }
    */
    import "C" // この行の直前にCコードブロックを置く
    
    func main() {
        C.my_c_function() // C関数を呼び出す
    }
    
  2. #cgoディレクティブ: Cコードブロック内、またはGoファイルのどこかに#cgoディレクティブを記述することで、Cコンパイラやリンカに渡すフラグを指定できます。これにより、外部のCライブラリへのリンクや、コンパイル時のオプションを設定できます。

    /*
    #cgo LDFLAGS: -L/usr/local/lib -lmyc
    #cgo CFLAGS: -I/usr/local/include
    void my_c_function();
    */
    import "C"
    
    • LDFLAGS: リンカに渡すフラグ(例: ライブラリのパスやライブラリ名)
    • CFLAGS: Cコンパイラに渡すフラグ(例: インクルードパスやコンパイルオプション)
    • CPPFLAGS: Cプリプロセッサに渡すフラグ
    • CXXFLAGS: C++コンパイラに渡すフラグ
    • FFLAGS: Fortranコンパイラに渡すフラグ
    • PKG_CONFIG: pkg-configツールを使用してライブラリのフラグを取得
  3. cgoツールの処理: GoコンパイラがGoファイルを処理する前に、cgoツールが介入します。cgoは以下の処理を行います。

    • GoファイルからCコードブロックを抽出し、Cコンパイラが処理できる形式の.cファイルや.hファイルを生成します。
    • #cgoディレクティブを解析し、それらの情報を基にCコンパイラとリンカを呼び出します。
    • GoとCの間でデータをやり取りするためのスタブコードを生成します。
    • 最終的に、Goコンパイラは生成されたGoコードとCコンパイラによってコンパイルされたオブジェクトファイルをリンクして実行可能ファイルを生成します。

行番号の重要性

プログラミングにおいて、行番号はデバッグやエラー報告において非常に重要です。コンパイラやインタプリタがエラーを報告する際、通常はエラーが発生したファイル名と行番号を提示します。これにより、開発者は問題の箇所を迅速に特定し、修正することができます。

cgoのような異言語間連携ツールでは、元のソースファイル(Goファイル)と、ツールによって生成されコンパイラに渡される中間ファイル(Cファイル)の間で行番号の対応関係が正確であることが不可欠です。もしこの対応関係がずれてしまうと、Cコンパイラが「X行目でエラー」と報告しても、そのX行目が元のGoファイルでは全く別のコードを指している、という混乱が生じます。

技術的詳細

このコミットの技術的な核心は、cgoツールがGoソースファイル内の#cgoディレクティブを処理する際に、Cコンパイラに渡すコードの行番号の整合性を保つ方法にあります。

以前のcgoの実装では、Goファイル内のCコードブロックを抽出する際、#cgoディレクティブを含む行は単に削除されていました。例えば、以下のようなGoファイルがあったとします。

// main.go
package main

/*
#cgo LDFLAGS: -c // Line 4
void test() {    // Line 5
    xxx;         // Line 6 (original)
}
*/
import "C"

func main() {
    C.test()
}

この場合、cgoがCコードを抽出してCコンパイラに渡す際、#cgo LDFLAGS: -cの行が削除されると、Cコンパイラに渡されるコードは以下のようになります。

void test() {    // Line 1 (generated)
    xxx;         // Line 2 (generated)
}

もしxxx;の行でコンパイルエラーが発生した場合、Cコンパイラは「Line 2でエラー」と報告します。しかし、元のmain.goファイルではxxx;はLine 6にあります。このように、行番号が4行ずれてしまうため、開発者はエラーの場所を特定するのに苦労していました。

このコミットの修正は、src/cmd/cgo/gcc.goDiscardCgoDirectives関数にあります。この関数は、Goファイル内のCコードブロックから#cgoディレクティブを「破棄」する役割を担っています。

修正前は、#cgoディレクティブの行はlinesOut(出力される行のリスト)に追加されませんでした。つまり、完全に削除されていました。

修正後は、#cgoディレクティブの行が見つかった場合でも、その行を完全に削除するのではなく、空文字列 ("") をlinesOutに追加するように変更されました。これにより、#cgoディレクティブがあった場所には空行が挿入されることになります。

上記の例で考えると、修正後のcgoがCコードを抽出してCコンパイラに渡すコードは以下のようになります。

             // Line 1 (empty line for #cgo LDFLAGS: -c)
void test() {    // Line 2 (original Line 5)
    xxx;         // Line 3 (original Line 6)
}

この結果、xxx;の行でエラーが発生した場合、Cコンパイラは「Line 3でエラー」と報告します。元のmain.goファイルではxxx;はLine 6にあります。まだずれはありますが、これはCプリプロセッサが#lineディレクティブを処理する際に、元のファイル名と行番号を正確に伝えるための標準的な方法です。

このコミットの目的は、#cgoディレクティブがCコードの行番号に影響を与えないようにすることです。空行を挿入することで、Cコンパイラに渡されるコードの行数が元のGoファイル内のCコードブロックの行数と一致するようになり、Cコンパイラが生成するエラーメッセージの行番号が、Goソースファイル内の対応する行番号とより正確に一致するようになります。

これは、Cプリプロセッサが#lineディレクティブを挿入する際に、正確な行番号情報を提供するための準備とも言えます。#lineディレクティブは、コンパイラに対して、現在のソースコードの行番号とファイル名を指定されたものに変更するよう指示するもので、プリプロセッサによって生成されたコードのデバッグを容易にするために広く使用されます。cgoは内部的にこのようなメカニズムを利用して、Goファイル内のCコードと生成されたCコードの間の行番号の対応を維持しています。このコミットは、その対応をより正確にするための基盤となる変更です。

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

src/cmd/cgo/gcc.go

--- a/src/cmd/cgo/gcc.go
+++ b/src/cmd/cgo/gcc.go
@@ -76,6 +76,8 @@ func (f *File) DiscardCgoDirectives() {
 	\tl := strings.TrimSpace(line)
 	\tif len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
 	\t\tlinesOut = append(linesOut, line)
+\t\t} else {
+\t\t\tlinesOut = append(linesOut, "")
 	\t}\
 	}\
 	f.Preamble = strings.Join(linesOut, "\\n")

この変更は、DiscardCgoDirectives関数内で、#cgoディレクティブの行を処理する方法を変更しています。以前は#cgoディレクティブの行はlinesOutに追加されませんでしたが、変更後は空文字列""が追加されるようになりました。

misc/cgo/errors/err1.go (新規ファイル)

package main

/*
#cgo LDFLAGS: -c

void test() {
	xxx;		// This is line 7.
}
*/
import "C"

func main() {
	C.test()
}

このファイルは、#cgoディレクティブとCコードを含むGoプログラムのテストケースです。意図的にxxx;という未定義のシンボルを含めることで、Cコンパイラがエラーを発生させるようにしています。コメントで// This is line 7.と明記されており、エラーがこの行で報告されることを期待しています。

misc/cgo/errors/test.bash (新規ファイル)

# Copyright 2013 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.

if go tool cgo err1.go >errs 2>&1; then
  echo 1>&2 misc/cgo/errors/test.bash: BUG: expected cgo to fail but it succeeded
  exit 1
fi
if ! test -s errs; then
  echo 1>&2 misc/cgo/errors/test.bash: BUG: expected error output but saw none
  exit 1
fi
if ! fgrep err1.go:7 errs >/dev/null 2>&1; then
  echo 1>&2 misc/cgo/errors/test.bash: BUG: expected error on line 7 but saw:
  cat 1>&2 errs
  exit 1
fi
rm -rf errs _obj
exit 0

このシェルスクリプトは、err1.gocgoツールでコンパイルし、エラーが発生すること、そしてそのエラーメッセージがerr1.go:7(7行目)を指していることを検証するテストです。

src/run.bash

--- a/src/run.bash
+++ b/src/run.bash
@@ -145,6 +145,12 @@ esac
 go run main.go || exit 1
 ) || exit $?\
 
+[ "$CGO_ENABLED" != 1 ] ||
+[ "$GOHOSTOS" == windows ] ||
+(xcd ../misc/cgo/errors
+./test.bash || exit 1
+) || exit $?\
+\
 (xcd ../doc/progs
 time ./run || exit 1
 ) || exit $?\

この変更は、Goのテストスイートを実行するrun.bashスクリプトに、新しく追加されたmisc/cgo/errors/test.bashテストケースの実行を追加しています。CGO_ENABLEDが1でない場合(cgoが無効な場合)や、Windows環境の場合はテストをスキップします。

コアとなるコードの解説

このコミットの核心は、src/cmd/cgo/gcc.go内のDiscardCgoDirectives関数における変更です。

func (f *File) DiscardCgoDirectives() {
	linesOut := make([]string, 0, len(f.Preamble))
	for _, line := range strings.Split(f.Preamble, "\n") {
		l := strings.TrimSpace(line)
		// Check if the line starts with "#cgo" followed by a space.
		// This is a simple check to identify cgo directives.
		if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
			// If it's not a #cgo directive, keep the original line.
			linesOut = append(linesOut, line)
		} else {
			// If it is a #cgo directive, replace it with an empty string.
			// This preserves the line numbering for the C compiler.
			linesOut = append(linesOut, "")
		}
	}
	f.Preamble = strings.Join(linesOut, "\n")
}

この関数は、Goソースファイル内のCコードブロック(f.Preamble)を処理し、#cgoディレクティブを「破棄」します。

  • f.Preambleは、Goファイル内のimport "C"の直前にあるCコードの文字列です。
  • この関数は、f.Preambleを改行で分割し、各行をループ処理します。
  • 各行について、strings.TrimSpace(line)で前後の空白を削除した文字列lを取得します。
  • if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4]))の条件で、その行が#cgoディレクティブであるかどうかを判定します。
    • len(l) < 5: #cgoは4文字なので、最低でも5文字(#cgo + スペース)必要です。
    • l[:4] != "#cgo": 行の先頭が#cgoであるか。
    • !unicode.IsSpace(rune(l[4])): #cgoの直後に空白文字が続くか。
  • 変更点:
    • もしその行が#cgoディレクティブではない場合 (ifブロック)、元の行linelinesOutスライスに追加します。
    • もしその行が#cgoディレクティブである場合 (elseブロック)、以前は何も追加されませんでしたが、このコミットにより**空文字列""**がlinesOutスライスに追加されるようになりました。

この変更の意図は、#cgoディレクティブの行を完全に削除するのではなく、その場所に空行を挿入することで、Cコンパイラに渡されるコードの行数を元のGoファイル内のCコードブロックの行数と一致させることです。これにより、Cコンパイラがエラーを報告する際の行番号が、Goソースファイル内の実際の行番号とより正確に同期されるようになります。

新しく追加されたテストケースmisc/cgo/errors/err1.gomisc/cgo/errors/test.bashは、この修正が正しく機能することを検証します。err1.goは意図的にコンパイルエラーを発生させるCコードを含み、test.bashcgoを実行し、エラーメッセージがerr1.go:7を指していることをfgrepコマンドで確認します。このテストの成功は、#cgoディレクティブによる行番号のずれが解消されたことを意味します。

関連リンク

参考にした情報源リンク

  • Go Issue 5272の議論内容
  • Go言語のcgoに関する公式ドキュメントやチュートリアル
  • C言語のプリプロセッサと#lineディレクティブに関する一般的な情報