[インデックス 13572] ファイルの概要
このコミットは、Go言語のツールチェインの一部である cmd/cc において、C言語のコードからGo言語のネストされたパッケージ内の関数を呼び出す際のシンボル解決を改善するものです。具体的には、Goのパッケージパス区切り文字である / や、構造体・メソッドの区切り文字である . を、C言語側で扱いにくい特殊なUnicode文字(∕ および ·)にマッピングし、cmd/cc がこれらの特殊文字を通常の / や . に変換することで、CコードからのGo関数の呼び出しを可能にします。これにより、sync/atomic.LoadInt32() のような関数を sync∕atomic·LoadInt32() の形式でCコードから呼び出せるようになります。
コミット
- コミットハッシュ:
8be5e8a419d12185b410e881271875a2612dd2d5 - 作者: Dmitriy Vyukov dvyukov@google.com
- コミット日時: 2012年8月4日 土曜日 16:11:53 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8be5e8a419d12185b410e881271875a2612dd2d5
元コミット内容
cmd/cc: allow to call nested packages from within C code
E.g. sync/atomic.LoadInt32() can be called as sync»atomic·LoadInt32()
R=rsc
CC=golang-dev
https://golang.org/cl/6448057
変更の背景
Go言語は、C言語との相互運用性(Cgo)を提供しており、GoコードからC関数を呼び出したり、CコードからGo関数を呼び出したりすることが可能です。しかし、Go言語のパッケージ構造や関数名の命名規則は、C言語のシンボル命名規則と直接互換性がない場合があります。特に、Go言語ではパッケージの階層を / で表現し、パッケージ内の関数や変数、構造体などを . で区切ります(例: sync/atomic.LoadInt32)。
C言語のリンカやコンパイラは、通常、このような特殊文字を含むシンボル名を直接扱うことができません。このコミットの背景には、Cgoを通じてCコードからGoのネストされたパッケージ内の関数を呼び出す際に、Goの命名規則をCのシンボルとして表現するための課題がありました。
この変更は、Goの内部ツールである cmd/cc(GoのCコンパイラ)が、Cコード内でGoの関数名を参照する際に、Goの命名規則をCのシンボルとして「エンコード」し、それをGo側で「デコード」するメカニズムを導入することで、この問題を解決しようとしています。具体的には、Cコード側でGoの / をUnicode文字の ∕ (Division Slash, U+2215) に、Goの . をUnicode文字の · (Middle Dot, U+00B7) に置き換えることで、Cのシンボルとして有効な名前を生成し、cmd/cc がこれらの特殊文字を元の / や . に戻すことで、正しいGoの関数を解決できるようにします。
これにより、開発者はCコードから sync∕atomic·LoadInt32() のように記述することで、Goの sync/atomic.LoadInt32() 関数を呼び出すことが可能になります。
前提知識の解説
Go言語のCgo
Cgoは、GoプログラムがC言語のコードを呼び出したり、C言語のコードからGo関数を呼び出したりするためのGo言語の機能です。Goのソースファイル内に import "C" を記述し、Cのコードをコメントブロック内に記述することで、GoとCの間の相互運用が可能になります。Cgoは、GoとCの間のデータ型変換や関数呼び出しの橋渡しを行います。
cmd/cc
cmd/cc は、Go言語のツールチェインの一部であり、Goのソースコードに埋め込まれたCコードをコンパイルするために使用されるCコンパイラです。Cgoのビルドプロセスにおいて重要な役割を担っています。このコンパイラは、Goのビルドシステムと連携し、Cgoで記述されたCコードをGoの実行可能ファイルにリンクできるようにします。
Unicode文字とUTF-8エンコーディング
- Unicode: 世界中の文字を統一的に扱うための文字コード標準です。各文字には一意のコードポイント(例: U+00B7)が割り当てられています。
- UTF-8: Unicode文字をバイト列にエンコードするための可変長エンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイト以上で表現されます。
·(Middle Dot, U+00B7): UTF-8では0xC2 0xB7の2バイトで表現されます。∕(Division Slash, U+2215): UTF-8では0xE2 0x88 0x95の3バイトで表現されます。
シンボル解決
プログラミングにおいて、シンボル解決とは、プログラム内で使用される関数名や変数名などのシンボルを、それらが実際に定義されているメモリ上のアドレスや、ライブラリ内の具体的な実装と結びつけるプロセスです。コンパイラやリンカがこの役割を担います。C言語では、シンボル名に特定の文字(例: /, .)を使用できない、または特殊な意味を持つ場合があります。
技術的詳細
このコミットの核心は、cmd/cc の字句解析器(lexer)が、特定のUnicode文字のバイトシーケンスを、Go言語の命名規則で使用される通常のASCII文字に変換する点にあります。
lookup 関数は、シンボル名を処理する際に呼び出されます。この関数内で、入力されたシンボル名がバイト単位で走査され、以下の変換が行われます。
-
·(Middle Dot, U+00B7) から.への変換:·はUTF-8で0xC2 0xB7の2バイトでエンコードされます。- コードは、現在のバイト
*rが0xc2であり、次のバイト*(r+1)が0xb7であるかをチェックします。 - このパターンが一致した場合、出力バッファ
wにASCIIの.を書き込み、入力ポインタrを2バイト分進めます。
-
∕(Division Slash, U+2215) から/への変換:∕はUTF-8で0xE2 0x88 0x95の3バイトでエンコードされます。- コードは、現在のバイト
*rが0xe2、次のバイト*(r+1)が0x88、さらに次のバイト*(r+2)が0x95であるかをチェックします。 - このパターンが一致した場合、出力バッファ
wにASCIIの/を書き込み、入力ポインタrを3バイト分進めます。
これらの変換により、Cコード側で sync∕atomic·LoadInt32() のように記述されたシンボル名が、cmd/cc によって sync/atomic.LoadInt32() というGoの正規の関数名に変換され、Goランタイムがその関数を正しく解決できるようになります。
このアプローチの利点は、C言語のコンパイラやリンカが特殊なUnicode文字を含むシンボル名をそのまま扱えるため、Cgoのインターフェースをより柔軟に設計できる点です。また、Goの命名規則をCのシンボル名に「エンコード」する際に、既存のASCII文字と衝突しないUnicode文字を選択することで、曖昧さを避けています。
コアとなるコードの変更箇所
src/cmd/cc/lex.c および src/cmd/cc/lexbody の lookup 関数が変更されています。
--- a/src/cmd/cc/lex.c
+++ b/src/cmd/cc/lex.c
@@ -377,11 +377,16 @@ lookup(void)
symb[1] = '"';
}
- // turn · into .
for(r=w=symb; *r; r++) {
+ // turn · (U+00B7) into .
+ // turn ∕ (U+2215) into /
if((uchar)*r == 0xc2 && (uchar)*(r+1) == 0xb7) {
*w++ = '.';
r++;
+ }else if((uchar)*r == 0xe2 && (uchar)*(r+1) == 0x88 && (uchar)*(r+2) == 0x95) {
+ *w++ = '/';
+ r++;
+ r++;
}else
*w++ = *r;
}
--- a/src/cmd/cc/lexbody
+++ b/src/cmd/cc/lexbody
@@ -251,11 +251,16 @@ lookup(void)
symb[1] = '"';
}
- // turn · into .
for(r=w=symb; *r; r++) {
+ // turn · (U+00B7) into .
+ // turn ∕ (U+2215) into /
if((uchar)*r == 0xc2 && (uchar)*(r+1) == 0xb7) {
*w++ = '.';
r++;
+ }else if((uchar)*r == 0xe2 && (uchar)*(r+1) == 0x88 && (uchar)*(r+2) == 0x95) {
+ *w++ = '/';
+ r++;
+ r++;
}else
*w++ = *r;
}
コアとなるコードの解説
上記のコードスニペットは、lookup 関数内のループを示しています。このループは、入力シンボル名 symb をバイト単位で走査し、変換後のシンボル名を w ポインタが指す位置に書き込んでいます。
for(r=w=symb; *r; r++):rは入力シンボル名を走査するポインタ、wは変換後のシンボル名を書き込むポインタです。*rがヌル終端文字に達するまでループします。if((uchar)*r == 0xc2 && (uchar)*(r+1) == 0xb7): これは、現在のバイト*rと次のバイト*(r+1)が、Unicodeの·(Middle Dot) のUTF-8エンコーディングである0xC2 0xB7に一致するかどうかをチェックしています。- 一致した場合、
*w++ = '.';で出力にASCIIの.を書き込み、r++;で入力ポインタを1バイト(合計2バイト)進めて、·の2バイトをスキップします。
- 一致した場合、
else if((uchar)*r == 0xe2 && (uchar)*(r+1) == 0x88 && (uchar)*(r+2) == 0x95): これは、現在のバイト*rとそれに続く2バイトが、Unicodeの∕(Division Slash) のUTF-8エンコーディングである0xE2 0x88 0x95に一致するかどうかをチェックしています。- 一致した場合、
*w++ = '/';で出力にASCIIの/を書き込み、r++; r++;で入力ポインタを2バイト(合計3バイト)進めて、∕の3バイトをスキップします。
- 一致した場合、
else *w++ = *r;: 上記の特殊なUnicode文字に一致しない場合は、現在のバイトをそのまま出力にコピーし、入力ポインタを1バイト進めます。
このロジックにより、Cコードから渡されたシンボル名に含まれる特殊なUnicode文字が、Goの命名規則に合致するASCII文字に変換され、Goの関数が正しく参照できるようになります。
関連リンク
- Go CL 6448057: https://golang.org/cl/6448057
参考にした情報源リンク
- Go Programming Language: Cgo: https://go.dev/blog/cgo
- UTF-8 - Wikipedia: https://ja.wikipedia.org/wiki/UTF-8
- Unicode Character 'MIDDLE DOT' (U+00B7): https://www.compart.com/en/unicode/U+00B7
- Unicode Character 'DIVISION SLASH' (U+2215): https://www.compart.com/en/unicode/U+2215
- Go source code (cmd/cc): https://github.com/golang/go/tree/master/src/cmd/cc
- Go source code (src/cmd/cc/lex.c): https://github.com/golang/go/blob/master/src/cmd/cc/lex.c
- Go source code (src/cmd/cc/lexbody): https://github.com/golang/go/blob/master/src/cmd/cc/lexbody