Share
Embed
意外と知らないcgoの世界
この発表では普段意識することが少ない「cgo」パッケージについて解説します!cgo とはC言語とGo言語の橋渡しをするパッケージであり、Go言語側でC言語で定義された機能を使うことができます。このスライドではCとGoの連携方法から、cgo パッケージの内部コンパイル順序について解説します✍️
Hinata
May 08, 2025
Transcript
45 slides
-
© DMM © DMM 意外と知らない cgoの世界 菊地 ひなた
開発統括本部 データアプリケーショングループ ML基盤チーム 2025-05-15 -
© DMM 自己紹介 名前 : 菊地 ひなた 経緯 : 24新卒でDMMに入社
ML基盤チームに所属 趣味 : 料理とお酒が大好きです 最近は肝臓のためにお酒控えてますられませんでした シェアハウスにお引越ししました 2 -
© DMM お品書き 1. 自己紹介 2. cgo package発表の動機 3. 使い方
a. Go側でCの関数を定義して呼び出す b. Go側からCの定義済み関数を呼び出す c. C側から定義済みGo関数を呼び出す 4. CからGoパッケージを呼び出す 5. コンパイル順序 6. まとめ 3 -
© DMM cgo packageの発表動機 - Pythonの機械学習フレームワークの裏ではCが動いてることが多い ⇒ 調べてみるとGoも同じ ⇒ cgo
package 登場 4 [1] https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/ops/array_grad.cc [2] https://github.com/tensorflow/tensorflow/blob/master/tensorflow/go/graph.go tensorflow for pythonで内部利用 される配列の微分用計算core [1] tensorflow for Goにおける 配列の微分用計算core [2] ☝cgoパッケージが 使われている ☝.ccで記述されてる でもどうやってGoとCが連携して動くんだろう…? 💡調べてみることにしました! -
© DMM cgoって何? - C言語とGo言語の橋渡しをする機能 - C言語で定義された機能をGo言語側で呼び出して使うことができる たとえばこんなときに便利: • どうしてもCで提供されているライブラリを使いたい
(例) - デバイスドライバの操作[3][4]パッケージを使った機能の実装 - マイコンなどのデバイスで独自関数が提供されている時[5] 5 [3] ioctlのgoのラッパーhttps://pkg.go.dev/acln.ro/ioctl のlatestは2019, amd64, armのみ対応。実装はcgoを使用 [4] goでusb接続イベントを拾いたい ではmac OSの提供するIOKit Frameworkをcgoで呼び出している [5] 組み込みLinuxでGolangのススメ - Qiita tinygoからcgoでC関数を呼び出す組み込みの勧め -
© DMM お品書き 1. 自己紹介 2. cgo package発表の動機 3. 使い方
a. Go側でCの関数を定義して呼び出す b. Go側からCの定義済み関数を呼び出す c. C側から定義済みGo関数を呼び出す 4. CからGoパッケージを呼び出す 5. コンパイル順序 6. まとめ 6 -
© DMM 1. Go側でCの関数を定義して呼び出す (例)普通の計算式を定義する場合 7 package main /*
#include <stdio.h> int myadd(int a, int b) { return a + b; } */ import "C" import "fmt" func main() { // C の関数を呼び出す c := C.myadd(1, 2) fmt.Printf("result value = %d, result type = %T", c, c) } - 使い方 1. Go側でCの関数を定義して呼び出す 2. Go側からCの定義済み関数を呼び出す 3. C側から定義済みGo関数を呼び出す -
© DMM (例)普通の計算式を定義する場合 8 package main /* #include <stdio.h>
int myadd(int a, int b) { return a + b; } */ import "C" import "fmt" func main() { // C の関数を呼び出す c := C.myadd(1, 2) fmt.Printf("result value = %d, result type = %T", c, c) } 1. コメントアウトで C の関数を定義 1. Go側でCの関数を定義して呼び出す 2. 直後でcgo パッケージをimport 3. C.”関数名” として定義さ れた関数を呼び出す - 使い方 1. Go側でCの関数を定義して呼び出す 2. Go側からCの定義済み関数を呼び出す 3. C側から定義済みGo関数を呼び出す -
© DMM 2. Go側からCの定義済み関数を呼び出す (例) 9 - 使い方 1. Go側でCの関数を定義して呼び出す
2. Go側からCの定義済み関数を呼び出す 3. C側から定義済みGo関数を呼び出す package main /* // CFLAGS: Cコンパイラへのオプション // (例: インクルードパス指定 ) // #cgo CFLAGS: -I/opt/local/include // LDFLAGS: リンカへのオプション // (例: ライブラリパス、リンクするライブラリ名指定 ) // #cgo LDFLAGS: -L/opt/local/lib -lcustommath // 標準のmath.hを使用。多くの Unix系システムでは libmのリンクが必要。 #cgo LDFLAGS: -lm #include <math.h> // C 標準数学ライブラリ */ import "C" // cgoを有効化 import "fmt" func main() { inputValue := 49.0 fmt.Printf("Go: Calculating square root of %.2f using C's math library. \n", inputValue ) // Goのfloat64をCのdoubleに変換してC関数を呼び出す cInput := C.double(inputValue ) cResult := C.sqrt(cInput) fmt.Printf("Go: Result from C's sqrt( %.2f) = %.2f\n", cInput, cResult) } コンパイルオプションの指定 CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS and LDFLAGSがある[6] [6] https://pkg.go.dev/cmd/cgo#hdr-Using_cgo_with_the_go_command -
© DMM 2. Go側からCの定義済み関数を呼び出す (例) 10 package main /* //
CFLAGS: C コンパイラへのオプション // (例: インクルードパス指定 ) // #cgo CFLAGS: -I/opt/local/include // LDFLAGS: リンカへのオプション // (例: ライブラリパス、リンクするライブラリ名指定 ) // #cgo LDFLAGS: -L/opt/local/lib -lcustommath // 標準の math.hを使用。多くの Unix系システムでは libmのリンクが必 要。 #cgo LDFLAGS: -lm #include <math.h> // C標準数学ライブラリ */ import "C" // cgoを有効化 import "fmt" func main() { inputValue := 49.0 fmt.Printf("Go: Calculating square root of %.2f using C's math library. \n", inputValue ) // Goのfloat64をCのdoubleに変換してC関数を呼び出す cInput := C.double(inputValue ) cResult := C.sqrt(cInput) fmt.Printf("Go: Result from C's sqrt( %.2f) = %.2f\n", cInput, cResult) } 1. 今回はmath.hをライブラリとして使用するの で-lm (library math)オプションが必要 2. math.hで定義済みの関数が呼び出せる - 使い方 1. Go側でCの関数を定義して呼び出す 2. Go側からCの定義済み関数を呼び出す 3. C側から定義済みGo関数を呼び出す -
© DMM 3. C側から定義済みGo関数を呼び出す (例) 11 - 使い方 1. Go側でCの関数を定義して呼び出す
2. Go側からCの定義ずみ関数を呼び出す 3. C側から定義済みGo関数を呼び出す 4. package main /* #include <stdio.h> // Goからエクスポートされる関数の宣言 extern void GoCallbackHandler(int id); // C側からGoの関数を呼び出す関数 (これをGoのmainから呼び出す ) static void triggerGoCallbacks() { printf("C: Triggering Go callback...\n"); GoCallbackHandler(123); // Go 関数を呼び出し printf("C: Triggering Go workers...\n"); } */ import "C" … … import "C" import ( "fmt" ) //export GoCallbackHandler <= C で呼び出す関数を宣言 func GoCallbackHandler (id C.int) { fmt.Printf("Go: Callback handler received ID: %d\n", int(id)) } func main() { C.triggerGoCallbacks () // C関数を呼び出し、そこから Go関数が呼ばれる } -
© DMM 3. C側から定義済みGo関数を呼び出す (例) 12 package main /* #include
<stdio.h> // Goからエクスポートされる関数の宣言 extern void GoCallbackHandler(int id); // C側から Goの関数を呼び出す関数 (これを Goのmainから呼び出す ) static void triggerGoCallbacks() { printf("C: Triggering Go callback...\n"); GoCallbackHandler(123); // Go関数を呼び出し printf("C: Triggering Go workers...\n"); } */ import "C" … … import "C" import ( "fmt" ) //export GoCallbackHandler <= Cで呼び出す関数を宣言 func GoCallbackHandler (id C.int) { fmt.Printf("Go: Callback handler received ID: %d\n", int(id)) } func main() { C.triggerGoCallbacks () // C関数を呼び出し、そこから Go関数が呼ばれる } 1. externでGo関数の宣言 3. Go関数の呼び出し 2. C側で呼び出す関数を定義 4. C関数からGo関数を呼ぶ - 使い方 1. Go側でCの関数を定義して呼び出す 2. Go側からCの定義ずみ関数を呼び出す 3. C側から定義済みGo関数を呼び出す -
© DMM - 使い方 1. GO側でCの関数を定義して呼び出す 2. GO側からCの定義ずみ関数を呼び出す
3. C側から定義済みGO関数を呼び出す 4. 3. C側から定義済みGo関数を呼び出す (例) 13 package main /* #include <stdio.h> // Goからエクスポートされる関数の宣言 extern void GoCallbackHandler(int id); // C側からGoの関数を呼び出す関数 (これをGoのmainから呼び出す ) static void triggerGoCallbacks() { printf("C: Triggering Go callback...\n"); GoCallbackHandler(123); // Go 関数を呼び出し printf("C: Triggering Go workers...\n"); } */ import "C" … … import "C" import ( "fmt" ) //export GoCallbackHandler <= C で呼び出す関数を宣言 func GoCallbackHandler (id C.int) { fmt.Printf("Go: Callback handler received ID: %d\n", int(id)) } func main() { C.triggerGoCallbacks () // C関数を呼び出し、そこから Go関数が呼ばれる } 1. externでgo関数の宣言 3. go関数の呼び出し 2. C側で呼び出す関数を定義 4. C関数からGo関数を呼ぶ a. Go側でCの関数を定義 して呼び出す b. Go側からCの定義済み関数 を呼び出す c. C側から定義済みGo関数 を呼び出す -
© DMM - 使い方 1. GO側でCの関数を定義して呼び出す 2. GO側からCの定義ずみ関数を呼び出す
3. C側から定義済みGO関数を呼び出す 4. 3. C側から定義済みGo関数を呼び出す (例) 14 package main /* #include <stdio.h> // Goからエクスポートされる関数の宣言 extern void GoCallbackHandler(int id); // C側からGoの関数を呼び出す関数 (これをGoのmainから呼び出す ) static void triggerGoCallbacks() { printf("C: Triggering Go callback...\n"); GoCallbackHandler(123); // Go 関数を呼び出し printf("C: Triggering Go workers...\n"); } */ import "C" … … import "C" import ( "fmt" ) //export GoCallbackHandler <= C で呼び出す関数を宣言 func GoCallbackHandler (id C.int) { fmt.Printf("Go: Callback handler received ID: %d\n", int(id)) } func main() { C.triggerGoCallbacks () // C関数を呼び出し、そこから Go関数が呼ばれる } 1. externでgo関数の宣言 3. go関数の呼び出し 2. C側で呼び出す関数を定義 4. C関数からGo関数を呼ぶ Goのパッケージは呼び出せないの? a. Go側でCの関数を定義 して呼び出す b. Go側からCの定義済み関数 を呼び出す c. C側から定義済みGo関数 を呼び出す -
© DMM お品書き 1. 自己紹介 2. cgo package発表の動機 3. 使い方
a. Go側でCの関数を定義して呼び出す b. Go側からCの定義済み関数を呼び出す c. C側から定義済みGo関数を呼び出す 4. CからGoパッケージを呼び出す 5. コンパイル順序 6. まとめ 15 -
© DMM CからGoパッケージを呼び出す 16 (例) 1. Goを.so(共有ライブラリ)としてビルド 2. Cをコンパイルする際1をライブラリとしてリンク 3.
a.out(実行ファイル)が生成される // main.c #include <stdio.h> #include "library.h" // go build -buildmode=c-shared などで生成されたヘッ ダーファイル int main() { printf("C: Calling Go function... \n"); // Goでエクスポートされた関数を呼び出す PrintMessage ("Hello from C!" ); printf("C: Back from Go function.\n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage (message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:" , goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 -
© DMM CからGoパッケージを呼び出す 17 (例) 1. Goを.so(共有ライブラリ)としてビルド 2. Cをコンパイルする際1をライブラリとしてリンク
3. a.out(実行ファイル)が生成される // main.c #include <stdio.h> #include "library.h" // go build -buildmode=c-shared などで生成されたヘッ ダーファイル int main() { printf("C: Calling Go function... \n"); // Goでエクスポートされた関数を呼び出す PrintMessage ("Hello from C!" ); printf("C: Back from Go function.\n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage (message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:" , goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 -
© DMM CからGoパッケージを呼び出す 18 (例) 1. Goを.so(共有ライブラリ)としてビルド ⇒ liblibrary.so, liblibrary.h
2. Cをコンパイルする際1をライブラリとしてリンク 3. a.out(実行ファイル)が生成される // main.c #include <stdio.h> #include "library.h" // go build -buildmode=c-shared などで生成されたヘッ ダーファイル int main() { printf("C: Calling Go function...\n"); // Goでエクスポートされた関数を呼び出す PrintMessage("Hello from C!"); printf("C: Back from Go function.\n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage (message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:" , goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 go build -buildmode=c-shared -o liblibrary.so wrapper.go # linux go build -ldflags="-w -linkmode external -extldflags '-Wl,-install_name,@rpath/liblibrary.so'" wrapper.go # mac -
© DMM CからGoパッケージを呼び出す 19 (例) 1. Goを.so(共有ライブラリ)としてビルド ⇒ liblibrary.so, liblibrary.h
2. Cをコンパイルする際1をライブラリとしてリンク 3. a.out(実行ファイル)が生成される // main.c #include <stdio.h> #include "library.h" // go build -buildmode=c-shared などで生成されたヘッ ダーファイル int main() { printf("C: Calling Go function...\n"); // Goでエクスポートされた関数を呼び出す PrintMessage("Hello from C!"); printf("C: Back from Go function.\n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage (message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:" , goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 go build -buildmode=c-shared -o liblibrary.so wrapper.go # linux go build -ldflags="-w -linkmode external -extldflags '-Wl,-install_name,@rpath/liblibrary.so'" wrapper.go # mac パッケージに含まれる関数定義 ビルドのためのmain(空) 共有ライブラリの ビルド用オプション 共有ライブラリの命名 (libから始まる必要がある)[7] [7] 命名規約 (リンカーとライブラリ ) - 共有オブジェクトは -
© DMM CからGoパッケージを呼び出す 20 (例) 2. Cをコンパイルする際に共有ライブラリをリンク // main.c
#include <stdio.h> #include "liblibrary.h" // go build -buildmode=c-shared などで生成されたヘッダー ファイル int main() { printf("C: Calling Go function... \n"); // Goでエクスポートされた関数を呼び出す PrintMessage ("Hello from C!" ); printf("C: Back from Go function. \n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage( message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:", goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 gcc main.c -o main_app -L. -llibrary # linuxの場合 gcc main.c -o main_app -L. -llibrary -Wl,-rpath,@executable_path/ # macの場合 -
© DMM CからGoパッケージを呼び出す 21 (例) 2. Cをコンパイルする際に共有ライブラリをリンク // main.c
#include <stdio.h> #include "liblibrary.h" // go build -buildmode=c-shared などで生成されたヘッダー ファイル int main() { printf("C: Calling Go function... \n"); // Goでエクスポートされた関数を呼び出す PrintMessage ("Hello from C!" ); printf("C: Back from Go function. \n"); return 0; } // wrapper.go package main import "C" import "fmt" func PrintMessage( message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:", goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 gcc main.c -o main_app -L. -llibrary # linuxの場合 gcc main.c -o main_app -L. -llibrary -Wl,-rpath,@executable_path/ # macの場合 -L でライブラリのディレクトリを指定 -l はlib配下の名称を指定 (例) libxxx.soの場合 -lxxx -
© DMM CからGoパッケージを呼び出す 22 (例) 3. 生成された実行ファイルを利用 // main.c #include
<stdio.h> #include "library.h" // go build -buildmode=c-shared などで生成されたヘッ ダーファイル int main() { printf("C: Calling Go function...\n"); // Goでエクスポートされた関数を呼び出す PrintMessage("Hello from C!"); printf("C: Back from Go function.\n"); return 0; } // wrapper.go package main import "C" import "fmt" //export PrintMessage func PrintMessage( message *C.char) { // Cの文字列 (*C.char) をGoの文字列 (string) に変換 goMessage := C.GoString(message) // Goの fmt パッケージの関数を呼び出す fmt.Println("Message from C:", goMessage) } func main() {} // ビルドのために空で定義 呼び出されるパッケージ側 パッケージを呼び出す側 # 3. 実行 export LD_LIBRARY_PATH =. # mac/linux 共通 (ライブラリのパスを認識させる必要がある場合がある ) ./main_app -
© DMM お品書き 1. 自己紹介 2. cgo package発表の動機 3. 使い方
a. Go側でCの関数を定義して呼び出す b. C側から定義済みGo関数を呼び出す c. Go側からCの定義ずみ関数を呼び出す 4. コンパイル順序 5. まとめ 25 -
© DMM cgo packageのコンパイルの順序 1. CとGoの中間ファイル生成 (cgo) 2. Cコードのコンパイル 3.
Goコードのコンパイル 26 package main /* #include <stdio.h> int myadd(int a, int b) { return a + b; } */ import "C" import "fmt" func main() { // C の関数を呼び出す c := C.myadd(1, 2) fmt.Printf("[result] value = %d, type = %T", c, c) } go build -a \ # -a : 強制再ビルド -x \ # -x : 出力内容を全表示 main.go > output.txt 対象のソースコード (既出) ビルドコマンド 4. まとめる 5. リンク -
© DMM cgo packageのコンパイルの順序 1. CとGoの中間ファイル生成 (cgo) 2. Cコードのコンパイル 3.
Goコードのコンパイル 27 package main /* #include <stdio.h> int myadd(int a, int b) { return a + b; } */ import "C" import "fmt" func main() { // C の関数を呼び出す c := C.myadd(1, 2) fmt.Printf("[result] value = %d, type = %T", c, c) } go build -a \ # -a : 強制再ビルド -x \ # -x : 出力内容を全表示 main.go > output.txt 対象のソースコード (既出) ビルドコマンド 4. まとめる 5. リンク -
© DMM 4. まとめる 5. リンク cgo packageのコンパイルの順序 1. CとGoの中間ファイル生成
(cgo) 2. Cコードのコンパイル 3. Goコードのコンパイル 28 go build -a \ # -a : 強制再ビルド -x \ # -x : 出力内容を全表示 main.go > output.txt ビルドコマンド package main /* #include <stdio.h> int myadd(int a, int b) { return a + b; } */ import "C" import "fmt" func main() { // C の関数を呼び出す c := C.myadd(1, 2) fmt.Printf("[result] value = %d, type = %T", c, c) } 対象のソースコード (既出) -
© DMM 1. CとGoの中間ファイル生成(cgo) cgoがmain.goに対して中間ファイルを生成する 29 … TERM='dumb' CGO_LDFLAGS='' \
/opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/cgo \ -objdir $WORK/b001 -importpath command-line-arguments "-ldflags=\"-O2\" \"-g\"" \ -- -I $WORK/b001/ -O2 -g ./main.go … - _cgo_export.c GoからC言語側へエクスポートされる関数の実体や、GoとCの 間の関数呼び出しに必要なヘルパー関数 - _cgo_export.h GoからC言語側へエクスポートされる関数や型のプロトタイプ 宣言 - _cgo_main.c main() のエントリーポイントをまとめるCのコード - main.cgo2.c main.go の import "C" ブロック内に書かれたC関数 (myadd) の定義と、GoからそのC関数を呼び出すためのブ リッジとなるC関数の実体 - _cgo_gotypes.go C側で使うGo型を記述したGoファイル - main.cgo1.go main.go のC関数呼び出し部分 (C.myadd(1, 2)) を、_cgo_gotypes.go で定義されたラッパー関数 (_Cfunc_myadd) を使う形に変換した Goコード -
© DMM 2. Cコードのコンパイル 中間Cファイルと他のアセンブラ等からオブジェクトファイル (.o)を生成 30 // _cgo_export.c から
_x001.o を生成 TERM='dumb' cc ... -o $WORK/b001/_x001.o -c _cgo_export.c // main.cgo2.c から _x002.o を生成 TERM='dumb' cc ... -o $WORK/b001/_x002.o -c main.cgo2.c // _cgo_main.c から _cgo_main.o を生成 TERM='dumb' cc ... -o $WORK/b001/_cgo_main.o -c _cgo_main.c … // gcc_arm64.S: ARM64アーキテクチャ固有のアセンブリコード TERM='dumb' cc -I . ... -o $WORK/b056/_x010.o -c gcc_arm64.S // Appendix _x0NN.o -
© DMM 3. Goコードのコンパイル 31 中間Goファイルをアーカイブファイル (_pkg_.a)にまとめる // ログ抜粋 (.goのコンパイル
) /opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/compile \ -o $WORK/b001/_pkg_.a \ -trimpath "$WORK/b001=>" \ -p main -lang=go1.24 \ -buildid mt8PWloQVyET0ZBQuKBs /mt8PWloQVyET0ZBQuKBs \ -goversion go1.24.2 \ -c=4 -shared -nolocalimports \ -importcfg $WORK/b001/importcfg \ -pack $WORK/b001/_cgo_gotypes.go $WORK/b001/main.cgo1.go $WORK/b001/_cgo_import.go _pkg_.a -
© DMM 3. Goコードのコンパイル 32 中間Goファイルをアーカイブファイル (_pkg_.a)にまとめる // ログ抜粋 (.goのコンパイル
) /opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/compile \ -o $WORK/b001/_pkg_.a \ -trimpath "$WORK/b001=>" \ -p main -lang=go1.24 \ -buildid mt8PWloQVyET0ZBQuKBs /mt8PWloQVyET0ZBQuKBs \ -goversion go1.24.2 \ -c=4 -shared -nolocalimports \ -importcfg $WORK/b001/importcfg \ -pack $WORK/b001/_cgo_gotypes.go $WORK/b001/main.cgo1.go $WORK/b001/_cgo_import.go _pkg_.a 中間生成ファイル 生成する静的アーカイブファイル -
© DMM 4. まとめる 33 アーカイブファイル (.a: Go由来)にオブジェクトファイル (.o: C由来)をまと
める // ログ抜粋 (オブジェクトファイルのアーカイブへの追加 ) /opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/pack r \ $WORK/b056/_pkg_.a \ $WORK/b056/_x001.o \ $WORK/b056/_x002.o \ … $WORK/b056/_x010.o _x0NN.o _pkg_.a 中間生成物(Go)から生成した静的アーカイブファイル 中間生成物(C)から生成したオブジェクトファイル -
© DMM 5. リンク 34 まとめた_pkg_.aをGoのリンカと Cのリンカでリンク GOROOT='/opt/homebrew/Cellar/go/1.24.2/libexec' \ /opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/link
\ -o $WORK/b001/exe/a.out \ -importcfg $WORK/b001/importcfg.link \ -buildmode=pie \ -buildid=UhZEhrBWBaa_sTRe6ewo/mt8PWloQVyET0ZBQuKBs/u8r6iV97MhPiAOxJs9eq/UhZEhrBWBaa_sTRe6ewo \ -extld=cc \ $WORK/b001/_pkg_.a _pkg_.a a.out -
© DMM 5. リンク 35 まとめた_pkg_.aをGoのリンカと Cのリンカでリンク GOROOT='/opt/homebrew/Cellar/go/1.24.2/libexec' \ /opt/homebrew/Cellar/go/1.24.2/libexec/pkg/tool/darwin_arm64/link
\ -o $WORK/b001/exe/a.out \ -importcfg $WORK/b001/importcfg.link \ -buildmode=pie \ -buildid=UhZEhrBWBaa_sTRe6ewo/mt8PWloQVyET0ZBQuKBs/u8r6iV97MhPiAOxJs9eq/UhZEhrBWBaa_sTRe6ewo \ -extld=cc \ $WORK/b001/_pkg_.a _pkg_.a a.out 出力する実行ファイル 4まででまとめた_pkg_.a -
© DMM cgo packageのコンパイルの順序 1. CとGoの中間ファイル生成 (cgo) 2. Cコードのコンパイル 3.
Goコードのコンパイル 4. まとめる 5. リンク 36 - _cgo_export.c - _cgo_export.h - _cgo_main.c - main.cgo2.c - _cgo_gotypes.go - main.cgo1.go _x0NN.o _pkg_.a _x0NN.o _pkg_.a _pkg_.a a.out -
© DMM 38 cgoでコンパイルする場合 C ⇒ .o(オブジェクトファイル) , Go ⇒
.a(静的ライブラリファイル) .oと.aを連携させて実行ファイルを生成していた -
© DMM まとめ - cgo パッケージの仕組みについて調べた - オブジェクトファイルや静的ライブラリを介して『2言語のコードをまとめた実行ファ イル』を作っていた -
まだまだ深掘りしたい点が出てくる面白いパッケージ - この他の深掘りポイント - ランタイム問題 - メモリ管理とGCの動き 40 -
© DMM Cコードのコンパイル[8] 1. cgo フェーズ cgo ツールが走って、 GoコードとCコードを橋渡しするための中間ファイルを生成する。 主に以下のファイルがこの段階で作られる:
_cgo_export.h: Go側の関数を Cから呼び出すためのヘッダ ファイル _cgo_gotypes.go: C側で使う Go型を記述した Goファイル _cgo_main.c: main() のエントリーポイントをまとめる プロトタイプ宣言が列挙された Cのコード main.cgo1.go, main.cgo2.c: CとGoの相互呼び出しに必要なコード 2. Cコードのコンパイル .c ファイル(例:main.cgo2.c, _cgo_main.c)がコンパイルされて .o(オブジェクトファイル)になる。 例:main.cgo2.c → _x001.o, _cgo_main.c → _cgo_main.o 3. Goコードのコンパイル main.go や main.cgo1.go が .a ライブラリ(静的アーカイブ)にコンパイルされる。 例:_pkg_.a 4. リンク 全ての .o ファイルと .a ファイルをまとめてリンクして、最終的な実行ファイル(例: exe)ができる。 5. ./mainへ exe/a.outをあたかもgoがビルドした./mainかのように移動させる 42 TERM='dumb' cc -I . ... -o $WORK/b056/_x003.o -c gcc_context.c TERM='dumb' cc -I . ... -o $WORK/b056/_x004.o -c gcc_darwin_arm64.c TERM='dumb' cc -I . ... -o $WORK/b056/_x005.o -c gcc_libinit.c TERM='dumb' cc -I . ... -o $WORK/b056/_x006.o -c gcc_setenv.c TERM='dumb' cc -I . ... -o $WORK/b056/_x007.o -c gcc_stack_darwin.c TERM='dumb' cc -I . ... -o $WORK/b056/_x008.o -c gcc_traceback.c TERM='dumb' cc -I . ... -o $WORK/b056/_x009.o -c gcc_util.c TERM='dumb' cc -I . ... -o $WORK/b056/_x010.o -c gcc_arm64.S gcc_context.c: スレッドやゴルーチンのコンテキスト(状態)の切り替えに関連する処理 gcc_darwin_arm64.c: macOS (darwin) の ARM64 環境固有の処理 gcc_libinit.c: Cライブラリ側の初期化に関連する処理 gcc_setenv.c: 環境変数の設定に関連する処理 gcc_stack_darwin.c: C言語側のスタック管理に関連する処理 (macOS固有) gcc_traceback.c: C言語関数呼び出しを含むスタックトレース(エラー発生時の呼び出し履歴)の取得に関連する処理 gcc_util.c: その他のユーティリティ関数 gcc_arm64.S: ARM64アーキテクチャ固有のアセンブリコード [11] https://go.dev/src/runtime/cgo/ -
© DMM .a, .o, .so, .Sの違い 43 - .o =
オブジェクトファイル 単一のソースコードファイルから生成される機械コードとリンク用の関数名や変数名を含むファイル - .a = アーカイブファイル 一つ以上のオブジェクトファイル (.o)をまとめたファイル、中身は.oと同様にリンク用の変数名や機械語を含 む - .so = 共有オブジェクト プログラムの実行時にメモリに読み込まれ、複数のプログラムから共有して利用されることを目的とするファ イル - .S = アセンブリ コンピュータのCPUが直接理解できる機械語に近い、低レベルな命令. gcc_arm64.Sは条件コンパイルにプリ プロセッサが必要なので大文字(#if define) [8][9] [8] file "gcc_arm64.S" [9] 玄箱PRO、Linux ZaurusでARMアセンブリプログラミング -
© DMM a.outはGoの実行ファイルじゃなくない? - 実は裏でa.outをmainに変換している - あたかも通常のビルドかのように見せている 44 a.out mv
$WORK/b001/exe/a.out main rm -rf $WORK/b001/ main -
© DMM cgo packageのコンパイルの順序列挙 1. cgo フェーズ cgo ツールが走って、 GoコードとCコードを橋渡しするための中間ファイルを生成する。
主に以下のファイルがこの段階で作られる: _cgo_export.h: Go側の関数をCから呼び出すためのヘッダファイル。 _cgo_gotypes.go: C側で使うGo型を記述したGoファイル。 _cgo_main.c: main() のエントリーポイントをまとめる Cのコード。 main.cgo1.go, main.cgo2.c: CとGoの相互呼び出しに必要なコード。 2. Cコードのコンパイル .c ファイル(例:main.cgo2.c, _cgo_main.c)がコンパイルされて .o(オブジェクトファイル)になる。 例:main.cgo2.c → _x001.o, _cgo_main.c → _cgo_main.o 3. Goコードのコンパイル main.go や main.cgo1.go が .a ライブラリ(静的アーカイブ)にコンパイルされる。 例:_pkg_.a 4. リンク 全ての .o ファイルと .a ファイルをまとめてリンクして、最終的な実行ファイル(例: exe)ができる。 5. ./mainへ exe/a.outをあたかもgoがビルドした./mainかのように移動させる 45