跳至主要內容

Go 调用 c/c++

逸尘.Lycodx大约 4 分钟后端goc/c++

Go 调用 c/c++

调用原理

要在 Go 中调用 C++ 代码,通常需要使用 Go 的 CGO 功能。CGO 允许 Go 代码调用 C 代码,而由于 C++ 是 C 的超集,因此也可以用于调用 C++ 代码。首先,编写 C++ 代码并确保使用extern "C"将其导出,然后编译为共享库文件。接着,编写 Go 代码并使用import "C"导入 C 代码,通过 CGO 调用 C++ 函数。最后,通过构建和运行 Go 代码来执行调用。

一、 链接选项调用 .so 文件

1.1 目录结构

├── cpp
│   ├── hello.h
│   ├── libhello.so
│   └── hello.cpp
└── demo
    └── main.go

1.2 cpp 代码内容示例

hello.h 文件内容

#ifndef HELLO_H
#define HELLO_H

#ifdef __cplusplus
extern "C" {
#endif

void printHelloWorld();

#ifdef __cplusplus
}
#endif

hello.cpp 文件内容

#include <iostream>

extern "C" {
    void printHelloWorld() {
        std::cout << "Hello World" << std::endl;
    }
}

1.3 编译

g++ -shared -o libhello.so -fPIC hello.cpp

1.4 go 代码内容示例

package main

// #cgo LDFLAGS: -L./cpp -lhello
// #include <stdlib.h>
// extern void printHelloWorld();
import "C"

func main() {
    // 调用 C 函数
    C.printHelloWorld()
}

解释说明

  • // #cgo LDFLAGS: -L./cpp -lhello:这是一个 CGO 的指令,用于指定在链接 Go 代码时需要使用的选项。在这里,LDFLAGS 指定了链接器的选项。-L./cpp 指定了要搜索库文件的路径(如果在当前目录下也要加点),-lhello 指定了要链接的库文件名称为 libhello.solibhello.a(根据具体情况而定)。这样,编译器就可以找到并链接名为 libhello 的库文件,使得 Go 代码能够调用其中的函数。
  • // #include :这是一个 C 语言的 #include 指令,它告诉编译器在编译时将标准库头文件 stdlib.h 包含进来。这样,在 Go 代码中通过 CGO 调用 C 函数时,就可以使用 stdlib.h 中定义的函数和数据类型。
  • extern void printHelloWorld();:这是一个 C 函数的声明,声明了一个名为 printHelloWorld 的外部函数。extern 关键字表明该函数是在别处定义的,这里只是声明了它的存在。这样,Go 代码就可以通过 CGO 调用这个 C 函数,而函数的具体实现可以在 C/C++ 文件中定义

1.5 编译运行

go build -o hello main.go && ./hello 

注意

这里会报错:error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory,这是因为: 如果共享库文件未放置在系统预定义的路径中,例如 /usr/local/lib/usr/lib,那么系统默认的动态链接器可能无法找到它

解决办法1:

# 调用方法改为:
LD_LIBRARY_PATH=. ./hello

二、 动态加载调用 .so 文件

2.1 目录结构

├── hello.cpp
├── hello.h
├── libhello.so
└── main.go

2.2 cpp 代码说明

这部分代码和 1.2 的代码示例一样

2.3 go 代码内容示例

package main

/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -ldl

#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>

#include "./hello.h"

typedef void (*printHelloWorld_type)();

void printHelloWorld_wrapper(void* f){
        ((printHelloWorld_type) f)();
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // 加载动态链接库
    libName := C.CString("./libhello.so")
    defer C.free(unsafe.Pointer(libName))

    handle := C.dlopen(libName, C.RTLD_LAZY)
    defer C.dlclose(handle)
    if handle == nil {
       fmt.Println(C.GoString(C.dlerror()))
       return
    }
    proc := C.dlsym(handle, C.CString("printHelloWorld"))
    C.printHelloWorld_wrapper(proc)
}

这里的复杂之处就是将头文件的函数方法,写到go的注释中,要包装一层进行调用

三、强化训练

上面的动态加载调用示例比较简单,这里再提供一个复杂一点的调用代码示例

c++ 头文件内容

#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
// 结构体定义
typedef struct {
    int  identify_type;
    int  recognition_logic;
    const char *field_name;
    const char *data_content;
} identify_rule_t;

  void *identifyRule_New(identify_rule_t rule);
  void identifyRule_Delete(void *instance);

  const char *identifyRule_Rewrite(void *instance, const char *data);

#ifdef __cplusplus
}
#endif

Go 代码内容

package main

/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -ldl

#include <stdlib.h>
#include <dlfcn.h>
#include <stdint.h>

#include "./replace.h"

typedef void* (*IdentifyRule_New_type)(identify_rule_t);

void* IdentifyRule_New_wrapper(void* f, identify_rule_t rule) {
        return ((IdentifyRule_New_type) f)(rule);
}

typedef void (*IdentifyRule_Delete_type)(void *);

void IdentifyRule_Delete_wrapper(void* f, void* instance) {
        return ((IdentifyRule_Delete_type) f)(instance);
}

typedef char* (*IdentifyRule_Rewrite_type)(void*, char*);

char* IdentifyRule_Rewrite_wrapper(void* f,void* instance,char *data) {
        return ((IdentifyRule_Rewrite_type) f)(instance, data);
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // 加载动态链接库
    libName := C.CString("./libreplace.so")
    defer C.free(unsafe.Pointer(libName))

    handle := C.dlopen(libName, C.RTLD_LAZY)
    defer C.dlclose(handle)
    if handle == nil {
       fmt.Println(C.GoString(C.dlerror()))
       return
    }
    newIdentifyRule := C.dlsym(handle, C.CString("identifyRule_New"))

    instance := (*C.void)(C.IdentifyRule_New_wrapper(newIdentifyRule, C.identify_rule_t{
       identify_type:     3,
       recognition_logic: 2,
       field_name:        C.CString("IPv4,IPv6"),
       data_content:      C.CString("134.231.87.27"),
    }))

    jsonStr := `{"IPv4":"134.231.87.27","IPv6":"3e79:a603:d35b:f3c1:9ccb:19db:36aa:a9c2","MAC 地址":"41:2a:12:2e:98:d4",}`
    cStr := C.CString(jsonStr)
    defer C.free(unsafe.Pointer(cStr))

    rewrite := C.dlsym(handle, C.CString("identifyRule_Rewrite"))

    res := C.IdentifyRule_Rewrite_wrapper(rewrite, unsafe.Pointer(instance), cStr)
    value := C.GoString(res)
    fmt.Println(value)
}

四、注意及帮助

  • 在动态加载调用中,c++ 的头文件函数名如果是大写字母开头可能在 go 这里出现找不到的情况,所以最好要保证函数名小写字母开头。
  • 使用 nm 命令可以查询你需要调用的函数在 .so 文件中是否存在。
  • 使用 ldd 命令可以检查 .so 文件是否缺少依赖,找不到的情况如下图所示:
上次编辑于: