序文
C言語の遺産を受け継ぐためにGo言語からC言語のコードを動かすcgoなるパッケージがあると聞いて、自然にこう思ったわけ。「じゃあ、逆にC言語からGo言語を動かせないの?」って。
やはり対称性こそが嬉しい 1 ので。
というわけで、Let’s start cooking!
料理名(成果)
虚無2。Goの簡単なコード3をCから動かせるようになるだけ。実務で使うことなんて一生ない(はず)。でもね、こういうのがおいしい4ってわけ。
材料
- Go言語の知識
- C言語の知識
- cgo
- make
つくり方
- Goで関数
my_atoi(cstr *C.char, errMsg **C.char) C.int
を書く- 文字列を整数へ変換する関数
- 類似した関数
- Cで関数
char *itoa(int value, char *buffer)
を書く- 整数を文字列へ変換する関数
- 類似した関数
- Go
strconv.Itoa
https://pkg.go.dev/strconv#Itoa - C
itoa
https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/itoa-itow?view=msvc-170- Cでは標準ライブラリ関数ではないことに注意
- Go
- Cでmain.cにmain関数を書く
- Makefileを書く
-
キモは
go build -buildmode=c-archive
である。 - このオプションをつけてコンパイルすることで、C用のヘッダーファイルとライブラリを生成できるので、これを用いてCから動かす。
- 今回は静的ライブラリ(
.a
)を生成してコンパイルしたが、動的ライブラリ(.so
)も可能である。
-
キモは
- コンパイルする
make
- 実行する
./main
ソースコード
- 全体構成は以下
.
├── Makefile
├── main.c
├── c_lib
│ ├── my_itoa.h
│ └── my_itoa.c
└── go
└── my_atoi.go
Makefile
GO := go
GO_DIR := go
GO_SRC := $(GO_DIR)/my_atoi.go
GO_BASENAME := libatoi
GO_HEADER := $(GO_DIR)/$(GO_BASENAME).h
GO_LIB := $(GO_DIR)/$(GO_BASENAME).a
C_DIR := c_lib
C_BASENAME := my_itoa
C_HEADER := $(C_DIR)/$(C_BASENAME).h
C_SRC := $(C_DIR)/$(C_BASENAME).c
C_OBJ := $(C_DIR)/$(C_BASENAME).o
all: $(GO_LIB) main
$(GO_LIB): $(GO_SRC)
$(GO) build -buildmode=c-archive -o $(GO_LIB) $(GO_SRC)
$(C_OBJ): $(C_SRC)
gcc -c $(C_SRC) -o $(C_OBJ)
main: main.c $(C_OBJ) $(GO_HEADER) $(GO_LIB)
gcc main.c $(C_OBJ) $(GO_LIB) -o main -I$(GO_DIR) -pthread
clean:
rm -f $(GO_HEADER) $(GO_LIB) $(C_OBJ) main
re: clean all
.PHONY: all clean
main.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include "go/libatoi.h"
#include "c_lib/my_itoa.h"
static void test_my_atoi(char *num_str);
int main(int argc, char **argv) {
if (argc < 2);
printf("--- boundary cases ---\n");
char int_min[20] = {};
itoa(INT_MIN, int_min);
test_my_atoi(int_min);
char minus_one[] = " -1";
test_my_atoi(minus_one);
char zero[] = " 0";
test_my_atoi(zero);
char plus_one[] = " 1";
test_my_atoi(plus_one);
char int_max[20] = {};
itoa(INT_MAX, int_max);
test_my_atoi(int_max);
printf("--- under/overfolw case ---\n");
char underflow[] = " -2147483649";
test_my_atoi(underflow);
char overflow[] = " 2147483648";
test_my_atoi(overflow);
return 0;
}
static void test_my_atoi(char *num_str) {
char *err = NULL;
int num = my_atoi(num_str, &err);
if (err) {
printf("error: %s\n", err);
free(err);
} else {
printf("value: %d\n", num);
}
}
my_atoi.go
空白のmain()を書かなければならないことに注意6
package main
/*
#include <stdlib.h>
*/
import "C"
// This function does not detect errors about over/underflow.
//
//export my_atoi
func my_atoi(cstr *C.char, errMsg **C.char) C.int {
s := C.GoString(cstr)
i, n := 0, len(s)
for i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\v' || s[i] == '\f' || s[i] == '\r') {
i++
}
if i >= n {
*errMsg = C.CString("invalid syntax")
return 0
}
sign := 1
if s[i] == '+' {
i++
} else if s[i] == '-' {
sign = -1
i++
}
num, start := 0, i
for i < n {
c := s[i]
if c < '0' || c > '9' {
break
}
num = num*10 + int(c-'0')
i++
}
if i == start {
*errMsg = C.CString("invalid syntax")
return 0
}
return C.int(sign * num)
}
func main() {}
my_itoa.h
#ifndef MY_ITOA
#define MY_ITOA
char *itoa(int value, char *buffer);
#endif // MY_ITOA
my_itoa.c
#include "my_itoa.h"
static char *write_digits(unsigned int u, char *p) {
if (u >= 10) {
p = write_digits(u / 10, p);
}
*p++ = '0' + (u % 10);
return p;
}
char *itoa(int value, char *buffer) {
char *p = buffer;
unsigned int u;
if (value < 0) {
*p++ = '-';
u = -(unsigned int)value;
} else {
u = (unsigned int)value;
}
p = write_digits(u, p);
*p = '\0';
return buffer;
}
- 実行結果
$ ./main
--- boundary cases ---
value: -2147483648
value: -1
value: 0
value: 1
value: 2147483647
--- under/overfolw case ---
value: 2147483647
value: -2147483648
-
「美しい」とは言っていないことに注意。時代の影響を受けやすい美的感覚よりも、普遍性を求める知的感覚側でより強く惹かれるという意味で使用している。例えば、代数方程式の可解性を特徴づけることに成功したGalois理論は、対称性から自然に定義可能な「群」なる数学的構築物を道具として用いて理論が展開されている。ちなみに対称性が破れても面白いらしい。(僕は物理を知らないのでここまで。) ↩
-
成果物が役に立たない、無意味であるときにその成果物を「虚無」と呼ぶことにした。ただし、その過程で得た知的な満足感、経験は虚無ではないと著者は信じている。 ↩
-
本当に簡単なコードのみを動かすことを推奨する。例えばポインタに関する操作に関して
「Go 管理領域のポインタを C に渡せない」
「C 側で確保したメモリを Go 側で勝手に解放できない」
などの困難が存在する。またCを使用するような場合では実行速度が求められると考えられるが、
「Goの関数が実行される際に、プログラムの初期化が走り時間がかかる5」
「GoでGC(ガベージコレクション)が走る際にすべてのgoroutineが止まる」
というデメリットが存在する。また、致命的なのは
「GoのPanicはCへエラーとして伝播せず、プロセスが強制停止する」
ということが起きるからだ。 ↩ -
「面白い」の意味で用いている。本記事はあくまで料理レシピを模しているので、表現を捻じ曲げている。 ↩
-
注釈6で説明するように「mainパッケージにはmain関数が必須」である。これと、下記の2点よりプログラムの初期化に時間がかかりそうなことが想像される。
1.「プログラムの初期化後に、main関数が呼び出される」(Goの仕様( https://go.dev/ref/spec#Program_execution )より)
2.プログラムの初期化はパッケージの初期化の連鎖からなる(Goの仕様( https://go.dev/ref/spec#Program_initialization )の内容全体より)
関連して、ソースコード読解によってGoのプログラムの実行の仕組みに迫っている以下の記事は興味深い。
https://medium.com/voicy-engineering/go%E3%81%A7main%E3%81%8C%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%82%8B%E6%A7%98%E5%AD%90%E3%82%92%E8%BF%BD%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B-d6d5a64b42d2 ↩ -
もしmain関数なしでビルド
go build -buildmode=c-archive -o go/libatoi.a go/my_atoi.go
を行うと、以下のようなコンパイルエラーが発生する。
# command-line-arguments
runtime.main_main·f: function main is undeclared in the main package
これはGoの仕様の下記の2点から要求される「ビルド可能なコードにはmain関数を含むmainパッケージが必須である」ことに反したためにおこることである。
1.「mainパッケージと、mainパッケージにimportされたすべてのパッケージをビルドする」( https://pkg.go.dev/cmd/go#hdr-Build_modes のc-archiveの説明より)
2.「mainパッケージにはmain関数が必須である」(Goの仕様( https://go.dev/ref/spec#Program_execution )より) ↩