Skip to content

Cgo Call Practice

About 1581 wordsAbout 5 min

Go

2024-12-19

Cgo calls are mainly divided into two types:

  1. Put C language code into the Go language file in the form of comments.
  2. Provide C language code to the Go source code in the form of a shared library.

This article will mention both methods, but mainly talk about the second method. When talking about the second method, we will take Go language calling C++ library-- Armadillo as an example.

The article will first describe the principles of Cgo, and then describe the two methods of calling. If you only want to see the sample code, you can jump to the back.

Cgo usage scenarios

In some of the following scenarios, we may have to use Cgo.

But we must understand that the use of Cgo requires a certain cost, and its complexity is extremely high and difficult to control, so it needs to be used with caution.

  1. In order to improve the performance of local code, use C code to replace some Go code. In terms of performance, C code is to Go like assembly code is to C.
  2. Sensitive to the latency of Go memory GC, you need to manually manage memory (allocation and release).
  3. Make Go bindings (binding) or wrappers for some C language-specific libraries that have no Go alternatives.
  4. With legacy C that is difficult to rewrite or replace code to interact.

Principle Of Cgo

Let's take a look at an example of Cgo call, and use this example to explain the principle of Cgo.

First, let's take a look at a demo code of Cgo call.

how_cgo_works.go
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// void print(char *str){
//	printf("%s\n", str)
// }
import "C"

import "unsafe"

func main(){
    s := "Hello, Cgo"
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.print(cs)  //Hello, Cgo
}

We can see that it is different from the regular Go There are several differences compared to the Go file:

  1. The C code appears directly in the Go file, but in the form of comments.
  2. After the comment, we import a C package import "C".
  3. The main function calls a function print defined in the C code through the C package.

::: There is no blank line between import "C" and the comment. Only in this way can the compiler recognize it. :::

After writing the code, you should compile it, and the compilation of this file is no different from the normal one:

go build -x -v how_cgo_works.go

-x -v two parameters can output the compilation details of the Go file with Cgo code.

The main operations during the actual compilation:

  1. go build calls a tool called Cgo.
  2. Cgo will recognize and read the C code in the Go source file (how_cgo_works.go), extract it and hand it over to the external C Compiler (such as gcc) for compilation.
  3. Finally, link with the target file compiled from the Go source code to become an executable program.

Because of this, the C code in the Go source file should be wrapped with comments and placed under the C pseudo-package. These special syntaxes can be recognized by Cgo.

Cgo Code Call

Go File Contains C

We can directly write C++ language into the .go file, and the compiler can also successfully recognize it when compiling. This is the simplest way, but it is not often used because it is not friendly to code management.

how_cgo_works.go
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// void print(char *str){
//	printf("%s\n", str)
// }
import "C"

import "unsafe"

func main(){
    s := "Hello, Cgo"
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.print(cs)  //Hello, Cgo
}

You can compile directly using the normal go build command:

go build -x -v how_cgo_works.go

Linking External C Libraries In Go

From the code structure point of view, writing a lot of C code in Go source files is not a common practice recommended by Go, so the following will show how to define C functions and variables from Go. Separate from the source code and define it separately.

I recommend using static build for Cgo calls. The so-called static build means that all the symbols, instructions and data required for the built application to run are included in its own binary file, without any external dynamic shared library dependencies.

Next, I will use Cgo to call the C++ library--Armadillo as an example to show the whole process. We mainly need to prepare 2 parts:

Static File

If you want to do a static build, we need to compile the C++ library into a binary file for Go language to call.

  1. Download Armadillo.

    Download website: Armadillo official website.

    It is recommended to use Stable Version.

  2. Download Lapack and Blas libraries.

    These two libraries are optimizations for matrix operations. If you want Armadillo has better performance, and users are recommended to download and install it.

  3. Cmake installation.

    For specific steps, please refer to Use CMake to install Armadillo library under Windows, including Lapack and Blas support library.

Code

We need to write two parts of code, one for Go language space and one for C language space.

Here we implement a function to calculate log: logTransform().

Recommended project structure

  • example
    • example_test.go
    • logTransform.cpp
    • logTransform.hpp
    • main.go
  • pkg
    • include
      • armadillo_bits
      • armadillo
    • libarmadillo.dll.a
Go Language
main.go
package main

// #cgo CXXFLAGS: -I../pkg/include -std=c++11
// #cgo LDFLAGS: -L../pkg/ -larmadillo
// #include "logTransform.hpp"
import "C"
import (
	"fmt"
	"time"
	"unsafe"
)

func main() {
	data := make([]float64, 0)
	for i := -10; i < 1000; i++ {
		data = append(data, float64(i))
	}
	fmt.Println(data)

	// 将 Go 切片转换为 C 数组
	dataPtr := (*C.double)(unsafe.Pointer(&data[0]))

	// 创建用于接收结果的 C 数组
	result := make([]float64, len(data))
	resultPtr := (*C.double)(unsafe.Pointer(&result[0]))
	st := time.Now()
	// 调用 C 的包装函数
	C.logTransform(dataPtr, resultPtr, C.int(len(data)))
	dur := time.Since(st)
	fmt.Println(result)
	// 打印结果
	fmt.Println("耗时: ", dur.Seconds(), "")
}
C Language
logTransform.hpp
#ifdef __cplusplus
extern "C" {
#endif
    void logTransform(const double* data, double* result, int size);
#ifdef __cplusplus
}
#endif

Info

Here I share a library that I encapsulated myself, which uses Cgo to implement many basic calculation functions. clone This project is convenient for viewing the structure and function call relationship between each file.

Overhead Of Using Cgo

  1. Call overhead.

    Benchmark tests show that the cost of calling C functions through Cgo is nearly 30 times that of calling Go functions.

  2. Increase the possibility of a surge in the number of threads.

    Go is famous for its lightweight goroutine to cope with high concurrency. Go will optimize some system calls that would otherwise cause thread blocking. However, since Go cannot control the C space, it is easy to write code that causes thread blocking in the C space during daily development, which increases the possibility of a surge in the number of threads in the Go application process. This deviates from the lightweight concurrency promised by Go.

  3. Loss of cross-platform cross-building capabilities.

  4. Other overhead.

    • Memory management. The Go space uses a garbage collection mechanism, and C The memory space is managed manually.
    • The powerful tool chain that Go has cannot be used in C. For example, performance analysis tools, test coverage tools, etc.
    • Debugging is difficult.

When using Cgo, you must pay attention to memory management and need to manually release it in time.




Changelog

Last Updated: View All Changelog
  • feat(docs): add a new english article

    On 12/19/24

Keep It Simple