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: cgo 和 cgo 命令。
我们将从一些简单的事情开始,包装 Go 标准库的方法,用于平方根和 n 次方,导出它们并从 C++ 中调用它们。
第一步是创建 lib.go
和 main.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.Sqrt
和 math.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,此处不再赘述。