CGO 从入门到放弃

1. 写在最前面

作为一只弱小可怜又无助的 go 开发,在接到要调用 c++ 的接口需求的时候,我的内心是忐忑的。但是作为一个种成熟的语言,go 的 GGO 特性已经支持了调用 C/C++,撒花。

注: CGO 特性支持在 go 语言中调用 C 语言,这一特性使得 go 能够站在 C 的肩膀上,直接可以使用 C 沉淀多年的库。

调用 C++ 的库可以采用通过 C 语言包装的方式

2. CGO 调用方式

CGO 在调用 C 的时候主要有两种方式:

  • 引入 C 源码的方式
  • 引入动态或者静态链接库的方式

2.1 引入 C 源码

2.1.1 将 C 源码嵌入 go 文件
  • hello.go

    package main
    
    /*
    #include <stdio.h>
    static void SayHello(const char* s) {
    	puts(s);
    }
    */
    import "C"
    
    func main() {
    	C.SayHello(C.CString("hello, CGO \n"))
    }
    

    注:

    • import “C” 这句话要紧跟在注释之后,不能换行,否则报错
    • 运行 go 文件的方式为 go run hello.go
2.1.2 将 C 的源文件嵌入到 go 项目

go 项目包括以下文件

  • hello.h

    void SayHello(const char* s);
    
  • hello.c

    #include "hello.h"
    #include <stdio.h>
    
    void SayHello(const char* s) {
    	puts(s);
    }
    
  • hello.go

    package main
    
    //#include "hello.h"
    import "C"
    
    func main() {
    	C.SayHello(C.CString("hello, CGO \n"))
    }
    

    • 将 C 的源码模块化,在 go 中通过引入头文件的方式进行函数调用
    • 此处须使用go run "your/package"go build "your/package"才可以。若本就在包路径下的话,也可以直接运行go run .go build

2.2 引入链接库的方式

此处以动态链接库为例子,go 项目下包括以下文件 — number.h、number.c、main.go。

2.2.1 链接库源文件的定义
  • number.h 的定义

    int number_add_mod(int a, int b, int mod);
    
  • number.c 的定义

    #include "number.h"
    
    int number_add_mod(int a, int b, int mod){
    	return (a+b)%mod;
    }
    

注:编译 number 的动态库 gcc -shared -o libnumber.so number.c

2.2.2 在 go 源文件中引用动态链接库
  • mian.go 的定义

    package main
    
    //#cgo CFLAGS: -I./
    //#cgo LDFLAGS: -L./ -lnumber
    //
    //#include "number.h"
    import "C"
    import "fmt"
    
    func main() {
    	fmt.Println("number_add_mod", C.number_add_mod(10, 5, 12))
    }
    

注:在本包路基下,直接运行go run .go build

3. CGO 调用 C++

CGO 是 C 语言和 go 语言之间的桥梁,无法直接支持 C++ 的类。但 C++ 是兼容 C 语言的,所以可通过增加一组 C 函数作为 C++ 类和 CGO 之间的桥梁,间接的支持 C++ 和 go 之间的互联。

3.1 demo

demo 包括以下内容:

  • C++ 部分:student.h、student.cpp、interface.h、interface.cpp
  • C 部分:mian.c
  • go 部分:main.go
3.1.1 C++ 部分定义
  • student.h

    #include <iostream>
    using namespace std;
    
    class Student {
    public:
        Student(){}
        ~Student(){}
        void Operation();
        void SetName(string name);
        string name;
    };
    
  • student.cpp

    using namespace std;
    void Student::Operation()
    {
        cout << "Hi my name is " << name <<endl;
    }
    void Student::SetName(string name1)
    {
            name = name1;
    
    
  • interface.h

    #ifdef __cplusplus
    extern "C"{
    #endif
     
    void *stuCreate();
    void initName(void *, char* name);
    void getStuName(void *);
    void getName();
     
    #ifdef __cplusplus
    }
    #endif
    
  • interface.cpp

    #include "student.h"
    #include "interface.h"
     
    #ifdef __cplusplus
    extern "C"{
    #endif
     
    void *stuCreate()
    {
            return new Student();
    }
    
    void getStuName(void *p)
    {
        static_cast<Student *>(p)->Operation();
    }
    
    void initName(void *p, char* name1)
    {
        static_cast<Student *>(p)->SetName(name1);
    }
    
    void getName()
    {
            Student obj;
            obj.Operation();
    }
     
    #ifdef __cplusplus
    }
    #endif
    

注:生成动态库的命令

g++ student.cpp interface.cpp -fPIC -shared -o libstu.so
3.1.2 C 部分定义
  • mian.c

    #include "interface.h"
     
    int main()
    {
        void *p = stuCreate();
        char *name = "test";
        initName(p, name);
        getStuName(p);
        getName();
        return 0;
    }
    

注:

  • 编译 main.c gcc main.c -L. -lstu

  • 运行 a.out,将会得到以下内容

     $ ./a.out
    Hi my name is test
    Hi my name is
    
3.1.3 go 部分定义
  • main.go

    // +build linux
    // +build amd64
    // +build !noptlogin
    
    package main
    
    /*
    #cgo CFLAGS: -I./
    #cgo LDFLAGS: -L./ -lstu
    #include <stdlib.h>
    #include <stdio.h>
    #include "interface.h" //非标准c头文件,所以用引号
    */
    import "C"
    
    import (
    	"unsafe"
    )
    
    func main() {
    	name := "test!"
    	cStr := C.CString(name)
    	defer C.free(unsafe.Pointer(cStr))
    	obj := C.stuCreate()
    	C.initName(obj, cStr)
    	C.getStuName(obj)
    }
    

注:

  • 运行 go 代码 go run main.go,得到以下结果

    $ go run main.go
    Hi my name is test!
    

4. 坑点

4.1 内存

  • C 的内存需要用户控制申请和释放的时机
  • go 中用户申请内存后,由 GC 机制控制内存释放的策略

所以,在 C、go 互相调用的时候,如果涉及指针传递一定要注意内存申请/释放的问题。

Memory allocations made by C code are not known to Go's memory manager. When you create a C string with C.CString (or any C memory allocation) you must remember to free the memory when you're done with it by calling C.free.

释放内存的例子:

func Print(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.fputs(cs, (*C.FILE)(C.stdout))
}

4.2 栈

  • C 栈是固定大小(ps 使用 pthread_create() 创建线程的时候,如果不指定分配堆栈大小,则会直接使用系统提供默认值,通过 ulimit -a 命令可以查看
  • go 栈能够动态调整(ps go 1.3 之前使用 分段栈,1.4 之后使用 连续栈

4.3 线程模型

  • go 采用 GMP 的调度模型,会控制使用的内核线程的数量

  • CGO 在并发调用 c 语言的阻塞函数时,会导致线程数不受 GMP 控制,进而导致线程数暴增,例子 code 🌰

    package main
    
    //#include<unistd.h>
    import "C"
    import (
    	"flag"
    	"log"
    	"net/http"
    	_ "net/http/pprof"
    	"runtime/debug"
    	"sync"
    	"time"
    )
    
    const sleepTime = 60
    
    func init() {
    	debug.SetMaxThreads(10) //设置 go 程序允许开启的最大线程数量
    	go func() {
    		log.Println(http.ListenAndServe("localhost:6060", nil))
    	}()
    }
    
    func cSleep() {
    	C.sleep(sleepTime)
    	println("完成 c-sleep 睡眠")
    }
    
    func goSleep() {
    	time.Sleep(sleepTime * time.Second)
    	println("完成 go-sleep 睡眠")
    }
    
    func initGoroutines(isCSleep bool) {
    	var wg sync.WaitGroup
    	for i := 0; i < 20; i++ {
    		wg.Add(1)
    		go func(isCSleep bool) {
    			defer wg.Done()
    			if isCSleep {
    				cSleep()
    			} else {
    				goSleep()
    			}
    		}(isCSleep)
    	}
    	wg.Wait()
    }
    
    func main() {
    	isCSleep := flag.Bool("isCSleep", true, "确认是否调用 c 提供 sleep 函数")
    	flag.Parse()
    	initGoroutines(*isCSleep)
    }
    

    注:

    • 此处开启 go 程序使用的线程最大数量限制。
    • 在调用 cgo 阻塞 sleep 函数的时候,会提示 runtime: program exceeds 10-thread limit fatal error: thread exhaustion
    • 在调用 go 的 sleep 函数的时候,则不会提示该问题

5. 碎碎念

啦啦啦,踩着尾巴写好啦。

  • 尽全力在不愉快的日子里搜刮生活藏下的所有温柔
  • 别气馁呀,你的好运正在披荆斩棘的向你跑过来哦。

6. 参考资料

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值