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

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

このコミットは、Go言語のcmd/cgoツールにおける、生成されるCコードの行番号アノテーションの不正確さを修正するものです。具体的には、go/astパッケージのCommentGroup.Textメソッドがコメントの先頭の空白行を削除してしまうために発生していた、行番号のずれを解消します。

コミット

commit f20e3a10654e71823d58fb5d2deefce24d44a76f
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 1 08:34:08 2013 -0800

    cmd/cgo: fix line number annotations in generated C code
    
    The old version was using go/ast's CommentGroup.Text method,
    but that method drops leading blank lines from the result, so that
    if the comment looked like one of
    
    //
    // syntax error
    import "C"
    
    /*
    syntax error
    */
    import "C"
    
    then the line numbers for the syntax error would be off by the
    number of leading blank lines (1 in each of the above cases).
    
    The new text extractor preserves blank lines.
    
    Fixes #4019.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/7232071

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

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

元コミット内容

cmd/cgo: fix line number annotations in generated C code

このコミットは、cmd/cgoが生成するCコードにおける行番号のアノテーションを修正します。以前のバージョンでは、go/astパッケージのCommentGroup.Textメソッドを使用していましたが、このメソッドは結果から先頭の空白行を削除していました。そのため、コメントが以下のような形式の場合、

//
// syntax error
import "C"

または

/*
syntax error
*/
import "C"

構文エラーの行番号が、先頭の空白行の数(上記の各ケースでは1行)だけずれていました。新しいテキスト抽出器は空白行を保持します。

Issue #4019を修正します。

変更の背景

Go言語のcgoツールは、GoコードからC言語の関数を呼び出したり、C言語のコードをGoプログラムに組み込んだりするための重要なツールです。cgoはGoのソースコードを解析し、C言語のコードを生成する際に、元のGoコードの行番号情報をCコードに埋め込みます。これは、Cコンパイラがエラーや警告を報告する際に、元のGoソースコードの正確な位置を示すために不可欠です。

しかし、以前のcgoの実装では、Goの抽象構文木(AST)を扱うgo/astパッケージのCommentGroup.Textメソッドを使用して、コメントブロックからテキストを抽出していました。このTextメソッドには、抽出されたテキストの先頭にある空白行を自動的に削除するという挙動がありました。

この挙動が問題を引き起こしたのは、cgoがGoコード内のimport "C"ディレクティブの前に記述されたコメントを処理する際でした。例えば、以下のようなコードがあったとします。

//
// This is a comment with a blank line above.
import "C"

CommentGroup.Textメソッドがこのコメントを処理すると、先頭の空白行が削除され、コメントのテキストが実際よりも上の行にあるかのように扱われてしまいました。その結果、cgoが生成するCコードに埋め込まれる#lineディレクティブ(Cプリプロセッサに行番号情報を提供する)が不正確になり、Cコンパイラが報告するエラーメッセージの行番号が元のGoソースコードと一致しないという問題が発生していました。これは、特にデバッグ作業において開発者を混乱させる原因となっていました。

このコミットは、この行番号のずれを修正し、cgoがより正確なデバッグ情報を提供できるようにすることを目的としています。

前提知識の解説

cgo

cgoは、GoプログラムからC言語のコードを呼び出すためのGoツールです。Goのソースファイルにimport "C"という特別なインポート文を記述することで、C言語の関数や変数、型をGoコードから利用できるようになります。cgoは、GoとCの間のデータ変換や関数呼び出しの橋渡しをするためのコードを自動生成します。この生成されたCコードには、元のGoソースコードの行番号情報が#lineディレクティブとして埋め込まれます。

Goの抽象構文木 (AST) と go/ast パッケージ

Goコンパイラは、Goのソースコードを解析して抽象構文木(AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。go/astパッケージは、このASTをプログラム的に操作するための機能を提供します。

ast.CommentGroup

go/astパッケージにおいて、ast.CommentGroupは、ソースコード内の連続するコメントのグループを表す構造体です。例えば、関数定義の前に複数行のコメントがある場合、それらは一つのCommentGroupとして扱われます。

ast.CommentGroup.Text() メソッド

ast.CommentGroup構造体には、そのコメントグループのテキスト内容を文字列として返すText()メソッドがあります。このメソッドは、コメントマーカー(///* ... */)を取り除き、整形されたコメントテキストを返しますが、先頭の空白行を削除するという特定の挙動を持っていました。この挙動が、本コミットで修正される問題の根本原因でした。

#line ディレクティブ

C言語のプリプロセッサディレクティブの一つである#lineは、コンパイラに対して、現在のソースファイルの行番号とファイル名を指定された値に変更するよう指示します。これは、特にコード生成ツール(cgoのような)が、生成されたコードのエラーメッセージを元のソースコードの行番号にマッピングするために使用されます。例えば、#line 10 "original.go"というディレクティブは、それ以降のコードがoriginal.goファイルの10行目から始まったものとして扱われることを意味します。

技術的詳細

このコミットの技術的な核心は、go/astパッケージのCommentGroup.Text()メソッドの代わりに、空白行を保持する新しいコメントテキスト抽出関数commentTextを導入した点にあります。

以前のcmd/cgo/ast.goでは、f.Preamble += cg.Text() + "\\n"という行で、コメントグループcgのテキストをcg.Text()メソッドで取得し、それをf.Preamble(生成されるCコードのプレアンブル部分)に追加していました。このcg.Text()が先頭の空白行を削除するため、#lineディレクティブで指定された行番号と、実際にCコードに埋め込まれるコメントの行番号がずれていました。

この修正では、cg.Text()の呼び出しをcommentText(cg)に置き換えています。そして、commentTextという新しいヘルパー関数がsrc/cmd/cgo/ast.goに追加されました。

commentText関数の実装は以下の通りです。

  1. g *ast.CommentGroupを受け取る: コメントグループへのポインタを引数として受け取ります。
  2. nilチェック: gnilの場合は空文字列を返します。
  3. piecesスライス: コメントの各行(またはブロック)のテキストを格納するための文字列スライスpiecesを初期化します。
  4. コメントのイテレーション: g.List*ast.Commentのリスト)をループ処理します。*ast.Commentは個々のコメント要素を表します。
  5. コメントマーカーの除去: 各コメントcomcom.Textフィールド(コメントマーカーを含む生のテキスト)を取得し、switch文を使ってコメントの種類(//スタイルか/* ... */スタイルか)を判別し、適切なスライス操作でマーカーを除去します。
    • //スタイルのコメントの場合 (c[1] == '/'):c[2:] + "\\n"とすることで、//を除去し、末尾に改行を追加します。これにより、元のコメントの改行が保持されます。
    • /* ... */スタイルのコメントの場合 (c[1] == '*'):c[2 : len(c)-2]とすることで、/**/を除去します。
  6. piecesに追加: マーカーを除去したコメントテキストをpiecesスライスに追加します。
  7. 結合: 最後に、strings.Join(pieces, "")を使用して、piecesスライス内のすべての文字列を結合し、単一の文字列として返します。この結合時に区切り文字として空文字列""を使用することで、元のコメントの改行がそのまま保持され、空白行も失われることなく結合されます。

このcommentText関数は、CommentGroup.Text()が持っていた「先頭の空白行を削除する」という挙動を回避し、コメントの元の構造(空白行を含む)を正確に保持することで、#lineディレクティブの精度を向上させています。

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

--- a/src/cmd/cgo/ast.go
+++ b/src/cmd/cgo/ast.go
@@ -78,7 +78,7 @@ func (f *File) ReadGo(name string) {
 			}
 			if cg != nil {
 				f.Preamble += fmt.Sprintf("#line %d %q\\n", sourceLine(cg), name)
-				f.Preamble += cg.Text() + "\\n"
+				f.Preamble += commentText(cg) + "\\n"
 			}
 		}
 	}
@@ -131,6 +131,30 @@ func (f *File) ReadGo(name string) {
 	f.AST = ast2
 }
 
+// Like ast.CommentGroup's Text method but preserves
+// leading blank lines, so that line numbers line up.
+func commentText(g *ast.CommentGroup) string {
+	if g == nil {
+		return ""
+	}
+	var pieces []string
+	for _, com := range g.List {
+		c := string(com.Text)
+		// Remove comment markers.
+		// The parser has given us exactly the comment text.
+		switch c[1] {
+		case '/':
+			//-style comment (no newline at the end)
+			c = c[2:] + "\\n"
+		case '*':
+			/*-style comment */
+			c = c[2 : len(c)-2]
+		}
+		pieces = append(pieces, c)
+	}
+	return strings.Join(pieces, "")
+}
+
 // Save references to C.xxx for later processing.
 func (f *File) saveRef(x interface{}, context string) {
 	n, ok := x.(*ast.Expr)

コアとなるコードの解説

このコミットの主要な変更は、src/cmd/cgo/ast.goファイル内の2箇所です。

  1. ReadGo関数内の変更: ReadGo関数は、Goソースファイルを読み込み、そのASTを処理する役割を担っています。この関数内で、コメントグループcgのテキストをプレアンブルに追加する部分が変更されました。 - f.Preamble += cg.Text() + "\\n" + f.Preamble += commentText(cg) + "\\n" ここで、既存のcg.Text()メソッドの呼び出しが、新しく定義されたcommentText(cg)関数に置き換えられています。これにより、コメントテキストの抽出方法が変更され、先頭の空白行が保持されるようになります。

  2. commentText関数の新規追加: この関数は、ast.CommentGroupTextメソッドと同様の目的を持ちますが、先頭の空白行を保持するという重要な違いがあります。

    func commentText(g *ast.CommentGroup) string {
        if g == nil {
            return ""
        }
        var pieces []string
        for _, com := range g.List {
            c := string(com.Text)
            // Remove comment markers.
            // The parser has given us exactly the comment text.
            switch c[1] {
            case '/':
                //-style comment (no newline at the end)
                c = c[2:] + "\\n"
            case '*':
                /*-style comment */
                c = c[2 : len(c)-2]
            }
            pieces = append(pieces, c)
        }
        return strings.Join(pieces, "")
    }
    
    • if g == nil: コメントグループがnilの場合(コメントが存在しない場合)は、空文字列を返します。
    • var pieces []string: 各コメント行から抽出されたテキストを一時的に保持するための文字列スライスを宣言します。
    • for _, com := range g.List: CommentGroupは複数のComment(個々のコメント要素)を含むことができるため、それらをループで処理します。
    • c := string(com.Text): 各Commentの生のテキスト(///* */などのマーカーを含む)を取得します。
    • switch c[1]: コメントの2文字目(////**)をチェックして、コメントのスタイルを判別します。
      • case '/': //スタイルのコメントの場合、c[2:]//を除去し、末尾に\nを追加します。これにより、元のコメントの改行が保持されます。
      • case '*': /* ... */スタイルのコメントの場合、c[2 : len(c)-2]/**/を除去します。
    • pieces = append(pieces, c): 処理されたコメントテキストをpiecesスライスに追加します。
    • return strings.Join(pieces, ""): 最後に、piecesスライス内のすべての文字列を空文字列を区切り文字として結合し、単一の文字列として返します。この結合方法により、元のコメントに含まれる空白行がそのまま保持され、行番号のずれが解消されます。

この変更により、cgoが生成するCコードの#lineディレクティブが、Goソースコードのコメントの正確な行番号を反映するようになり、デバッグ時の利便性が向上します。

関連リンク

参考にした情報源リンク

  • コミットメッセージに記載されているIssue #4019 (ただし、Web検索ではGo言語の公式なIssueトラッカーやコミュニティで広く認識されている問題としての情報は見つかりませんでした。コミットメッセージに記載されているため、内部的なトラッキング番号である可能性があります。)