C++调用Go函数

创建于 2024年7月27日修改于 2024年7月27日
C++Go

作为程序员,有时我们需要做一些奇怪的事情,或者让两个不应该互操作的系统一起工作。比如在 C++ 中调用 Go 编写的函数。

Contents

项目设置

我们项目的流程是使用 Go 工具链将 Go 文件编译成一个库文件。然后我们将使用 GNU C++ 编译器链接我们的 C++ 代码,该代码调用 Go 库中的函数,并与编译后的库文件链接。

将 Go 文件编译成归档文件:

go build -buildmode=c-archive -o lib.a lib.go

编译 C++ 文件并链接之前编译的库文件:

g++ -Wall main.cpp lib.a -o main

为了加快我们的迭代流程,我们将使用 make 并将生成的二进制文件的调用插入到我们的 example 目标中:

example:
	go build -buildmode=c-archive -o lib.a lib.go
	g++ -Wall main.cpp lib.a -o main
	./main

简单函数调用

有关如何从 C 或 C++ 交互 Go 运行时的文档,请查看 Go Wiki: cgocgo 命令

我们将从一些简单的事情开始,包装 Go 标准库的方法,用于平方根和 n 次方,导出它们并从 C++ 中调用它们。

第一步是创建 lib.gomain.cpp 文件,并填充以下样板代码:

// lib.go
package main

import "C"
import ()

func main() {}
// main.cpp
#include "lib.h"
int main() {}

我们现在可以检查我们的项目设置是否有效:

$ make
go build -buildmode=c-archive -o lib.a lib.go
g++ -Wall main.cpp lib.a -o main
./main

现在让我们在 Go 函数中包装 math.Sqrtmath.Pow,将它们导出并从 C++ 调用它们,以了解交互逻辑。从 lib.go 中的定义开始:

package main

import "C"
import (
	"math"
)

//export Sqrt
func Sqrt(n float64) float64 {
	return math.Sqrt(n)
}

//export Pow
func Pow(x float64, y float64) float64 {
	return math.Pow(x, y)
}

func main() {}

所有导出的函数都需要以大写字母开头(如 Go 规范中定义的),并且需要在它们上方有一个注释,指定从 C++ 上下文中调用它们的函数名。

我们现在可以修改我们的 C++ 并创建一个小函数来调用我们上面定义的函数:

#include "lib.h"
#include <iostream>

void maths() {
  GoFloat64 sqrt = Sqrt(25.0);
  std::cout << "sqrt(25.0)=" << sqrt << std::endl;
  GoFloat64 pow = Pow(5, 3);
  std::cout << "power(5, 3)=" << pow << std::endl;
}

int main() {
  maths();
}

C 中的 Go 数据类型

我们使用 GoFloat64 与调用的 Go 函数的结果进行交互。GoFloat64 定义为:

typedef double GoFloat64;

其他类型包括:

// ...
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
// ...

这些定义由 Go 工具链为库归档构建生成,在首次编译归档时生成在 lib.h 文件中。在 70 行样板代码之后,我们可以看到为 lib.go 文件生成的输出:

#ifdef __cplusplus
extern "C" {
#endif

extern GoFloat64 Sqrt(GoFloat64 n);
extern GoFloat64 Pow(GoFloat64 x, GoFloat64 y);

#ifdef __cplusplus
}
#endif

通过 make 运行我们的示例:

$ make
go build -buildmode=c-archive -o lib.a lib.go
g++ -Wall main.cpp lib.a -o main
./main
sqrt(25.0)=5
power(5, 3)=125

Web 请求和传递字符串到 Go

让我们看看发起对 URL 的 GET 请求的可能性。为此,我们在 lib.go 中实现 Request 方法:

package main

import "C"
import (
	"math"
	"net/http"
	"time"
)

// ...

//export Request
func Request(str string) int {
	client := http.Client{
		Timeout: time.Millisecond * 200,
	}
	resp, _ := client.Get(str)
	return resp.StatusCode
}

// ...

然而,Go 运行时中使用的常规字符串与 C++ 中使用的字符串不同。为了帮助我们处理这一问题,工具链在 lib.h 中生成了一个类型定义:

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif
// ...
#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif

我们可以创建这个结构的一个实例,并将其传递给我们的 Request 函数:

#include "lib.h"
#include <cstring>

// ...

void request(const char *s) {
  GoString str;
  str.p = s;
  str.n = strlen(str.p);
  int r = Request(str);
  std::cout << "http.Get(" << str.p << ")=" << r << std::endl;
}

int main() {
  // ...
  request("https://xnacly.me/5");
  request("https://xnacly.me/about");
}

GoString.n 是字符串的长度,GoString.p 是指向字符串的指针。让我们测试对无效页面和有效页面的请求:

$ make
go build -buildmode=c-archive -o lib.a lib.go
g++ -Wall main.cpp lib.a -o main
./main
http.Get(https://xnacly.me/5)=404
http.Get(https://xnacly.me/about)=200

Go 协程

可能与其他广泛使用的编程语言相比,Go 的最独特特性是其并发模型。因此,让我们添加在 C++ 中生成 Go 协程的能力。

package main

import "C"
import (
	"fmt"
	"math"
	"net/http"
	"sync"
	"time"
)

// ...

//export Routines
func Routines(n int) {
	wg := sync.WaitGroup{}
	for i := range n {
		wg.Add(1)
		fmt.Printf("Spawning go routine %d\n", i)
		go func() {
			defer wg.Done()
			time.Sleep(time.Millisecond * 10)
		}()
	}
	wg.Wait()
}

// ...

让我们将 Routines 调用添加到 C++ 的 main 函数中,并生成 1024 个 Go 协程:

// ...
int main() {
  // ...
  Routines(1024);
}

构建并执行二进制文件后,我们得到:

$ make
go build -buildmode=c-archive -o lib.a lib.go
g++ -Wall main.cpp lib.a -o main
./main
Spawning go routine 0
...
Spawning go routine 1023

可以看到非常多的 Go 协程。

结语

C++调用Go函数就是这样简单。同样的操作应该也适用于 C,此处不再赘述。

原文:https://xnacly.me/posts/2024/go-cpp-interop/