Go 支持调用 C 代码, 这种能力来自 runtime 中对调用 C 函数的封装, cgocall.

调用前提的是将相关的函数暴露给 Go 代码. 来自官方的一个例子:

package main

/*
#include <stdlib.h>
*/
import "C"
import (
    "fmt"
    "time"
)

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

func main() {
    Seed(time.Now().Second())
    fmt.Println(Random())
}

在编译时增加相关参数 go build -x -work -a 可以看到具体的过程和现场. 不难发现, 其首先通过 go tool cgo 生成了相关的代码, 随后通过 clang 编译这些 C 代码.

go build -x -work -a 2>&1 | grep -A 8 "main.go$"
TERM='dumb' CGO_LDFLAGS='"-O2" "-g"' /Users/j2gg0s/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.4.darwin-arm64/pkg/tool/darwin_arm64/cgo -objdir $WORK/b001/ -importpath github.com/j2gg0s/gist/cgo -- -I $WORK/b001/ -O2 -g ./main.go
cd $WORK/b001
TERM='dumb' clang -I /Users/j2gg0s/go/src/github.com/j2gg0s/gist/cgo -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b001=/tmp/go-build -gno-record-gcc-switches -fno-common -I $WORK/b001/ -O2 -g -frandom-seed=nbhFK3v3q8WmHrRATnrf -o $WORK/b001/_x001.o -c _cgo_export.c
TERM='dumb' clang -I /Users/j2gg0s/go/src/github.com/j2gg0s/gist/cgo -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b001=/tmp/go-build -gno-record-gcc-switches -fno-common -I $WORK/b001/ -O2 -g -frandom-seed=nbhFK3v3q8WmHrRATnrf -o $WORK/b001/_x002.o -c main.cgo2.c
TERM='dumb' clang -I /Users/j2gg0s/go/src/github.com/j2gg0s/gist/cgo -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b001=/tmp/go-build -gno-record-gcc-switches -fno-common -I $WORK/b001/ -O2 -g -frandom-seed=nbhFK3v3q8WmHrRATnrf -o $WORK/b001/_cgo_main.o -c _cgo_main.c
cd /Users/j2gg0s/go/src/github.com/j2gg0s/gist/cgo
TERM='dumb' clang -I . -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b001=/tmp/go-build -gno-record-gcc-switches -fno-common -o $WORK/b001/_cgo_.o $WORK/b001/_cgo_main.o $WORK/b001/_x001.o $WORK/b001/_x002.o -O2 -g
TERM='dumb' /Users/j2gg0s/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.4.darwin-arm64/pkg/tool/darwin_arm64/cgo -dynpackage main -dynimport $WORK/b001/_cgo_.o -dynout $WORK/b001/_cgo_import.go
cat >/var/folders/3g/291pfdd54f11rlb15_hbq1dr0000gn/T/go-build3932226782/b001/importcfg << 'EOF' # internal

Go 并不支持调用 C++ 代码,但是我们可以通过使用 C 桥接二者. 以利用 pokerstove 来计算德州扑克胜率为例. 我们首先需要将对对象方法的调用包装成 C 兼容的函数调用.

cat bridge.cpp
#include <string>
#include <vector>

#include "pokerstove/penum/ShowdownEnumerator.h"

#include "bridge.h"

void calculateEquity(const char** _hands, int numHands, const char* _board, EquityResult* _results) {
  std::string board(_board);
  std::vector<pokerstove::CardDistribution> handsDist;

  for (int i = 0; i < numHands; i++) {
    pokerstove::CardDistribution dist;
    dist.parse(_hands[i]);
    handsDist.push_back(dist);
  }

  std::shared_ptr<pokerstove::PokerHandEvaluator> evaulator = pokerstove::PokerHandEvaluator::alloc("h");

  pokerstove::ShowdownEnumerator showdown;
  std::vector<pokerstove::EquityResult> results = showdown.calculateEquity(handsDist, pokerstove::CardSet(board), evaulator);

  for (int i = 0; i < results.size(); i++) {
    _results[i].winShares = results[i].winShares;
    _results[i].tieShares = results[i].tieShares;
  }
};
cat bridge.h
#pragma once

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
  double winShares;
  double tieShares;
} EquityResult;

void calculateEquity(const char**, int , const char*, EquityResult*);

#ifdef __cplusplus
}
#endif

随后在 Go 中, 我们就可以将其视作对 C 的调用.

cat bridge.go | grep "func calculateEquity" -A 25
func calculateEquity(hands []string, board string) ([]Equity, error) {
        cHands := make([]*C.char, len(hands))
        for i, hand := range hands {
                cHands[i] = C.CString(hand)
                defer C.free(unsafe.Pointer(cHands[i]))
        }

        cBoard := C.CString(board)
        defer C.free(unsafe.Pointer(cBoard))

        cResults := make([]C.EquityResult, len(hands))
        C.calculateEquity((**C.char)(unsafe.Pointer(&cHands[0])), C.int(len(hands)), cBoard, &cResults[0])

        results := make([]Equity, len(cResults))
        total := 0.0
        for i, cresult := range cResults {
                results[i].WinShares = float64(cresult.winShares)
                results[i].TieShares = float64(cresult.tieShares)
                total += results[i].WinShares + results[i].TieShares
        }
        for i, result := range results {
                results[i].Equity = (result.WinShares + result.TieShares) / total
                results[i].Total = total
        }
        return results, nil
}

Go 允许我们通过 import "C" 前的注释来控制要引入的 C 代码, 编译和链接的选项.

cat bridge.go | grep 'import "C"' -B 10
package pokerstove

// #cgo CXXFLAGS: --std=c++14 -I/Users/j2gg0s/cpp/pokerstove/src/lib
// #cgo darwin CXXFLAGS: -I/opt/homebrew/opt/boost/include
// #cgo LDFLAGS: -L/Users/j2gg0s/cpp/pokerstove/build/lib -lpeval -lpenum
// #include <bridge.h>
// #include <stdlib.h>
import "C"
go build -x -work -a ./cmd/pfai 2>&1 | grep clang | grep -E "bridge.cpp|penum"
TERM='dumb' clang++ -I . -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b054=/tmp/go-build -gno-record-gcc-switches -fno-common -I $WORK/b054/ -O2 -g --std=c++14 -I/Users/j2gg0s/cpp/pokerstove/src/lib -I/opt/homebrew/opt/boost/include -frandom-seed=7TpMI2L1HE_JeueLc6WL -o $WORK/b054/_x003.o -c bridge.cpp
TERM='dumb' clang++ -I ./pokerstove -fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=$WORK/b054=/tmp/go-build -gno-record-gcc-switches -fno-common -o $WORK/b054/_cgo_.o $WORK/b054/_cgo_main.o $WORK/b054/_x001.o $WORK/b054/_x002.o $WORK/b054/_x003.o -O2 -g -L/Users/j2gg0s/cpp/pokerstove/build/lib -lpeval -lpenum

对于写惯了 Go/Python/Java 的同学来说, 在 Go 中调用 C/C++ 代码的入门难点可能还是在于不习惯 C/C++ 本身的复杂的编译体系.

感谢 GPT, 我们快速简单的了解 C++ 项目的编译分为四个步骤:

  • 预处理(preprocessing), 处理代码中的指令, 以 # 开头, 如 #include, #define 等
  • 编译(compilation), 将预处理的代码转为汇编
  • 汇编(assembly), 将汇编代码转为目标机器的代码
  • 链接(linking), 将一个或多个目标文件以及所需的库链接在一起, 生成最终可执行的文件