[Yiwentong] C/C++ and Go language mixed programming entry-level tutorial (completed on Windows platform)

insert image description here

I. Overview

Go language can perform C+GO mixed programming through the built-in cgo tool. This tool is placed under pkg\tool in the go installation directory, and its source code is in src\runtime\cgo. Of course, this article does not intend to use cgo as an introductory tutorial. In-depth research on the implementation principle of cgo, only from the perspective of Hello World to actually experience cgo (at the end of the article, there are various resources I collected for in-depth study).


2. Start with the simplest (inline C code)

The default Go compiler is to turn off the cross-compilation function, because turning on cgo will make the portability of Go programs worse and the deployment will become troublesome. Pure Go code is certainly better, but sometimes when the required software library cannot find the Go version, this is the only way to deal with it. On the positive side, without cgo, Go would not be where it is today, because it can inherit nearly half a century of C/C++ software heritage, and cgo is also the key to running Go programs on Android and iOS.

Opening cgo is very simple, just set the environment variable CGO_ENABLED to 1:
Windows: set CGO_ENABLED=1
Linux-like platform: export CGO_ENABLED=1

Enter this program:

main.go:

package main

/*
int Add(int a, int b){
    return a+b;
}
*/
import "C"

import "fmt"

func main() {
    
    
	a := C.int(10)
	b := C.int(20)
	c := C.Add(a, b)
	fmt.Println(c) // 30
}

In the above code, there is a "comment" at the top, and the next line is import "C". Note that this comment is not a real comment, but a C language code. In the Go language specification, the import "C" previous comment lines are called preambles. , and in fact the Go language does not have the "C" package. This is a virtual package. Its function can be understood as a dividing lineimport "C" to separate the C and Go codes. is plain Go code. Note that there can be no blank lines between C code and , and the package "C" cannot be referenced by regular import statements, and can only exist as an exclusive line. After that, in the normal Go code part, we can use C.Add to refer to this C language function. This method of referencing C code inline is very simple, and there is no difference in the compilation process compared with ordinary pure Go, just input or directly .import "C" go build main.go go run main.go


3. Reference the library written in C language

In most cases, our purpose is to refer to third-party software libraries through cgo. Regardless of open source or closed source, they basically exist in the form of libraries. Let's talk about how to refer to static code compiled in C language in Go code. Link functions in the library. For the sake of simplicity, we first simulate a C language static link library and create the following two files:

hello.c:

#include <stdio.h>
#include "hello.h"

void SayHello()
{
    
    
    printf("Hello, world!\n");
}

hello.h:

void SayHello();

Use gcc to compile them into a static link library (the suffix is ​​.a):
$ gcc -c hello.c
$ ar -crv libhello.a hello.o
After entering the above two lines of commands, we get a static link library of libhello.a.

PS: Regarding the gcc and ar commands, they are included in the MinGW software. I will not discuss their installation here. You can read my previous blog post. The process is also very simple.

Create another Go file:
main.go:

package main

/*
#cgo CFLAGS: -I${SRCDIR}
#cgo LDFLAGS: -L${SRCDIR} -lhello
#include "hello.h"
*/
import "C"

import (
	"fmt"
)

func main() {
    
    
	C.SayHello()
	fmt.Println("Succeed!")
}

CFLAGS and LDFLAGS are two C language compile and link switches, where CFLAGS specifies the path of the header file, and LDFLAGS specifies the library file path and library file name.

PS: The header file refers to the file with the suffix .h following #include, and the library file refers to the file ending with .a or .so. Under Windows, it exists in the form of .lib or .dll. ${SRCDIR} represents the current directory, which is the "." we usually use. It is a historical issue of C/C++ that library files cannot use relative paths. Through ${SRCDIR}, relative paths can be used in disguise. For example, if there is an absolute path c:\test\hello, ${SRCDIR}\lib will automatically Expands to c:\test\hello\lib.

CFLAGS via -I will set the current directory as the header file (.h) search path. LDFLAGS uses -L${SRCDIR} to set the library file (.a) search path in the current directory, and -lhello means that the specific link is the library libhello.a .

Note: -lhello means to link the library libhello.a, which is actually abbreviated as hello after removing "lib" and the suffix ".a", which is a routine of C language. In addition, the linking method of the dynamic link library (.so file) is the same, assuming that the library file we provide is a dynamic link library libhello.so, the settings here are exactly the same as -lhello.

Enter go build main.goor go run main.gorun to display:

Hello, world!
Succeed!

Although only two lines are printed, the "Hello, world!" here is actually the result of calling the SayHello() function in the libhello.a library of the C language, while "Succeed!" is the result of calling the normal Go standard The result of the library fmt, there is an essential difference between the two.


Fourth, refer to the C++ library

Relatively speaking, the cross-coding between C++ and Go seems to be more troublesome. cgo is a bridge between C language and Go language, but in principle it cannot directly support C++ classes, and can only add a set of C language function interfaces as C++ classes A bridge between Go and CGO, allowing Go and C++ to connect in a roundabout way. This is why we often see "xxx-bridge" in open source Go projects. As long as this happens, most of the projects refer to C++ libraries.

First, we create a C++ library, create a myLib directory, and then create two C++ files in it:
$ mkdir myLib
$cd myLib

hello.cpp:

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

void hello() {
    
    
    std::cout << "Hello, World!\nThis message comes from a CPP function!" << std::endl;
}

hello.h:

void hello();

As in the previous example, create a static link library:
$ g++ -c hello.cpp (Note that g++ is used instead of gcc this time)
$ ar crs libhello.a hello.o
Then return to the root directory of the project and create two C++ files as "bridges":
$cd ..

hellobridge.cpp :

#include "hellobridge.h"
#include "mylib/hello.h"

void CallHello()
{
    
    
    hello(); // 调用库中的hello()函数
}

hellobridge.h :

#ifdef __cplusplus
extern "C" {
    
    
#endif

void CallHello();

#ifdef __cplusplus
}
#endif

Because this file is used for CGO, the name decoration rules of the C language specification must be adopted, and statements must be used to describe it when it is included in the C++ source file extern "C" .

Finally create the Go main program:

hello.go :

package main

/*
package main

/*
#cgo CXXFLAGS: -std=c++0x
#cgo LDFLAGS: -L${SRCDIR}/mylib -lhello
#cgo CPPFLAGS: -Wno-unused-result
#include "hellobridge.h"
*/
import "C"

func main() {
    
    
	C.CallHello()
}

This time we set the CXXFLAGS compiler switch to tell cgo that it is now C++ code.

Note: go env environment variables are divided into CC and CXX, which correspond to the executable compilers of C and C++ (need to be placed in the PATH command to be executed anywhere), when we set CFLAGS, cgo will automatically open The C language compiler works (default is gcc), and when the CXXFLAGS switch is set, cgo will automatically choose to use the C++ compiler (default is g++). And we don't have to worry about how to "command" what compiler cgo uses to work. Its logic is very simple, that is, it is judged by two compilation switches. We don't need to set CC to g++ to force cgo to use C++ for compilation. This will be self-defeating.

About these three switches:
CFLAGS : C language compilation parameters
CXXFLAGS : C++ unique compilation parameters
CPPFLAGS : C and C++ common compilation parameters
In this example, we found a parameter -Wno-unused-resultthat tells the compiler to cancel "unused" variables Warning, this parameter is common to C and C++, so it is placed in CPPFLAG .

After all the above files are created, enter go build -o hello.exe or go run .

Hello, World!
This message comes from a CPP function!

Note: Because this time we not only have the hello.go file to participate in the compilation, but also two other bridge files: hellobridge.cpp and hellobridge.h also participate in the compilation, so we cannot compile hello separately like the above example go build hello.go. go files instead need to compile the entire folder.


5. Use pkg-config

This section is not the focus of this article, but since the tool pkg-config is widely used and cgo also has corresponding docking parameters with it, it will be reported here.

pkg-config is a tool under native Linux (there is also a For Win version). Its main function is to simplify the reference operation of library files, because we must first know where its header files and library files are stored when we refer to any third-party library. Then splicing parameters like CFLAGS or LDFLAGS is a very troublesome thing. This operation can be simplified to some extent by pkg-config. Its operating logic is actually very simple, and it can be simply understood as an automatic expander. For example, now there is a library hello, its header files are stored in /usr/local/include, and the library files are in /usr/local/lib , we used to do this when compiling it:
gcc hello.cpp -I/usr/local/include -L/usr/local/lib -lhello -o hello.exe

We need to find the actual storage location of this library through various methods (for example whereis, findthis type of instruction), which is very troublesome. After using pkg-config, it becomes like this:

gcc hello.cpp `pkg-config -cflags -libs hello` -o hello.exe 

Among them, the content wrapped with `` will be automatically expanded to

-I/usr/local/include -L/usr/local/lib -lhello

In this way, we only need to know the name of the library, and do not need to care about where the library is stored, saving time and worry.

Each library will pre-create a file with a suffix of .pc, which contains preset records such as CFLAGS and LDFLAGS. When pkg-config retrieves this file, it will automatically expand the matched content, and these .pc files The storage place is called PKG_CONFIG_PATH, let's use our hello library to demonstrate the usage of pkg-config.

First of all, although this is a Linux tool, we don't actually have to look hard to find its so-called Windows version, because it comes with this tool when installing MSYS2 or MinGW. We only need to enter MSYS2 to use it directly. Use the command:
$ echo $PKG_CONFIG_PATH
to know that all .pc files are placed in these two directories:
/mingw64/lib/pkgconfig:/mingw64/share/pkgconfig
enter the command:
$ cd /mingw64/lib/pkgconfig
Create a file hello.pc :

Name: Hello
Description: Hello World Cgo Test.
Version: 1.0.0
Libs: -Lc:/test/myLib -lhello
Cflags: -Ic:/test/myLib

Name and Description can be entered casually, Libs indicates the actual storage location of our hello library, and Cflags indicates the header file storage location of the hello library.
Save and exit, enter;
#The pkg-config --list-all
screen will display a bunch of package names, look carefully, our Hello library should also be in it.
Input:
#Display pkg-config --cflags --libs hello
:
-Ic:/test/myLib -Lc:/test/myLib -lhello
It means that pkg-config has detected our hello library and can automatically expand it.

Now, when we compile with the gcc or g++ command, we can do this:

gcc hello.cpp `pkg-config -cflags -libs hello` -o hello.exe

It will automatically expand to this:

gcc hello.cpp -Ic:/test/myLib -Lc:/test/myLib -lhello -o hello.exe

Pit Tip: The two shells under Windows cannot recognize the format `pkg-config -cflags -libs hello`, whether it is cmd or powershell, I also tried to use $(pkg-config -cflags -libs hello) like this formats, cannot be expanded. But for a system that has already installed MSYS2, this is not a big problem, it's just a difference in which Shell to enter.

Back to our Go side, after adopting pkg-config, it is no longer necessary to specify the paths of header files and library files. The modified code is as follows:

package main

/*
#cgo pkg-config: hello
#cgo CXXFLAGS: -std=c++0x
#cgo CPPFLAGS: -Wno-unused-result
#include "hellobridge.h"
*/
import "C"

func main() {
    
    
	C.CallHello()
}

The old code is: #cgo LDFLAGS: -L${SRCDIR}/mylib -lhello
and now we only need: pkg-config: hellothat’s fine, it’s nothing to us, because the library is written by ourselves, of course we know where it is stored, but from another perspective, this library is supposed to be provided to others Use it, it will save him a lot of trouble.

6. Postscript
The cgo doorway is very deep. This article is only for introductory purposes, and only talks about how go references c, but does not mention how c references go, as well as various conversion issues such as variables, arrays, structures, and pointers. I have collected some learning materials for you to study in depth:

Official manual:
https://pkg.go.dev/cmd/cgo
https://go.dev/blog/cgo

CGO:
https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-01-hello-cgo.html
https://www.cntofu.com/book/19/0.13.md
https://www.cnblogs.com/lidabo/p/6068448.html
https://bastengao.com/blog/2017/12/go-cgo-cpp.html
https://fasionchan.com/golang/practices/call-c/

C/C++:
https://blog.51cto.com/u_15091053/2652800
https://www.cnblogs.com/52php/p/5681711.html

Guess you like

Origin blog.csdn.net/rockage/article/details/131357305