Interoperability between Go and C language

Posted by youngloopy on Thu, 20 Jan 2022 18:09:31 +0100

1. Interoperability between go and C language

Go has a strong c background. In addition to the inheritance of syntax, its designers and design goals are inextricably linked with C language. In terms of interoperability between go and C language, go provides powerful support. Especially when using C in go, you can even write c code directly in the go source file, which is unmatched by other languages.

In the following scenarios, the interoperability between Go and C may be involved:

  1. When improving the performance of local code, replace some Go code with C. C is to Go what assembly is to C.
  2. The GC performance of Go memory is insufficient, so you manage the application memory manually.
  3. Implement some library Go wrappers. For example, Oracle provides the C version of OCI, but Oracle does not provide the Go version and the protocol details of connecting to DB. Therefore, it can only be used by Go developers by packaging the C OCI version.
  4. Go export functions for use by C developers (this requirement should be rare at present).
  5. Maybe more...

1.1. Principle of go calling C code

Here is a short example:

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)
    C.print(cs)
    C.free(unsafe.Pointer(cs))
}

Compared with the "normal" Go code, the above code has several "special" places:

  1. The words include of the C header file appear in the comments at the beginning
  2. The C function print is defined in the comment
  3. import a "package" named C
  4. The above C function - print was called in the main function

Yes, this is the procedure of calling C code in Go source code. We can see that we can write C code directly in the Go source file.

First, the C code in the Go source file needs to be wrapped with comments, like the include header file and the print function definition above;
Secondly, the import "C" statement is required, and it cannot be separated from the above C code by empty lines, but must be closely connected. "C" here is not a package name, but a concept similar to namespace, or can be understood as a pseudo package. All syntax elements of C language are under the pseudo package;
Finally, when accessing C syntax elements, pseudo package prefixes should be added before them, such as C.uint and C.print and C.free in the above code.

How do we compile this go source file? In fact, it is no different from the "normal" go source file. It can still be compiled and executed directly through go build or go run. However, in the actual compilation process, go calls a tool called cgo. cgo will recognize and read the C elements in the go source file, extract them and submit them to the C compiler for compilation. Finally, it will be linked with the target file compiled by the go source code into an executable program. In this way, it is not difficult for us to understand why the C code in the go source file should be wrapped with comments. These special syntax can be recognized and used by cgo.

1.2. Types of C language used in Go

1.2.1. primitive type

1.2.1.1. value type

In Go, you can access C's native numeric types in the following ways:

C.char,
C.schar (signed char),
C.uchar (unsigned char),
C.short,
C.ushort (unsigned short),
C.int, C.uint (unsigned int),
C.long,
C.ulong (unsigned long),
C.longlong (long long),
C.ulonglong (unsigned long long),
C.float,
C.double

The value type of Go does not correspond to the value type in C one by one. Therefore, explicit transformation is indispensable when using opposite type variables, such as the example in Go doc:

func Random() int {
    return int(C.random())//C. int of long - > go
}
 
func Seed(i int) {
    C.srandom(C.uint(i))//Uint of Go - > uint of C
}

1.2.1.2. Pointer type

The pointer type of the native numeric type can be preceded by * according to Go syntax, such as var p *C.int. void * is special. Use unsafe in Go Pointer indicates. Any type of pointer value can be converted to unsafe Pointer type, and unsafe Pointer type values can also be converted to pointer values of any type. unsafe.Pointer can also be converted to uintptr. Because unsafe The pointer type of pointer cannot be used for arithmetic operation. It can be used for arithmetic operation after being converted to uintptr.

1.2.1.3. String type

There is no formal string type in C language. In C, the string is represented by a character array with an end '\ 0'; In Go, the string type is a native type, so it is necessary to convert the string type in the interoperability between the two languages.

Through the C.CString function, we can convert the string type of Go to the "string" type of C, and then pass it to the C function for use. As we used in the example at the beginning of this article:

s := "Hello Cgo\n"
cs := C.CString(s)
C.print(cs)

However, the C string cs obtained after this transformation can not be managed by Go's GC. We must manually release the memory occupied by cs, which is why the last call C.free in the example released cs. The GC in go cannot perceive the memory allocated inside C, so remember to release it.

C.GoString can convert the string (* C.char) of C to the string type of Go, for example:

// #include <stdio.h>
// #include <stdlib.h>
// char *foo = "hellofoo";
import "C"
 
import "fmt"
 
func main() {
... ...
    fmt.Printf("%s\n", C.GoString(C.foo))
}

1.2.1.4. Array type

The array in C language is quite different from the array in Go language. The latter is a value type, while the former and the pointer in C can be converted at will on most occasions. At present, it seems that there is no direct and explicit transformation between the two, and the official documents do not explain it. However, we can convert the array of C to Slice of Go by writing a conversion function (because the array in Go is a value type and its size is static, it is more common to convert to Slice). The following is an example of integer array conversion:

// int cArray[] = {1, 2, 3, 4, 5, 6, 7};
 
func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) {
    p := uintptr(cArray)
    for i :=0; i < size; i++ {
        j := *(*int)(unsafe.Pointer(p))
        goArray = append(goArray, j)
        p += unsafe.Sizeof(j)
    }
 
    return
}
 
func main() {
    ... ...
    goArray := CArrayToGoArray(unsafe.Pointer(&C.cArray[0]), 7)
    fmt.Println(goArray)
}

Execution result output: [1 2 3 4 5 6 7]

It should be noted here that the Go compiler cannot automatically convert the C array into the address of the array, so it cannot directly pass the array variable to the function like using the array in C, but pass the address of the first element of the array to the function.

1.2.1.5. Custom type

In addition to native types, we can also access custom types in C.

1.2.1.5.1. Enum (enum)
// enum color {
//    RED,
//    BLUE,
//    YELLOW
// };
 
var e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOW
fmt.Println(e, f, g)

Output: 0 1 2

For named C enumeration types, we can use C.enum_xx to access the type. If it is an anonymous enumeration, it seems that only its fields can be accessed.

1.2.1.5.2. Structure (struct)
// struct employee {
//     char *id;
//     int  age;
// };
 
id := C.CString("1247")
var employee C.struct_employee = C.struct_employee{id, 21}
fmt.Println(C.GoString(employee.id))
fmt.Println(employee.age)
C.free(unsafe.Pointer(id))

Output:

1247
21

Similar to enum, we can use C.struct_xx to access the structure type defined in C.

1.2.1.5.3. Consortium

Here, I try to access a C union in the same way as struct:

// #include <stdio.h>
// union bar {
//        char   c;
//        int    i;
//        double d;
// };
import "C"
 
func main() {
    var b *C.union_bar = new(C.union_bar)
    b.c = 4
    fmt.Println(b)
}

However, when compiling, go reports an error: b.c undefined (type *[8]byte has no field or method c). From the error information, go treats union differently from other types. It seems to treat Union as [N]byte, where N is the size of the largest field in the Union (rounded), so we can deal with c.union as follows_ bar:

func main() {
    var b *C.union_bar = new(C.union_bar)
    b[0] = 13
    b[1] = 17
    fmt.Println(b)
}

Output: & [13 17 0 0 0]

1.2.1.5.4. typedef

When accessing the alias type defined with typedef in Go, the access method is the same as that of the original actual type. For example:

// typedef int myint;
 
var a C.myint = 5
fmt.Println(a)
 
// typedef struct employee myemployee;
 
var m C.struct_myemployee

As can be seen from the example, for the alias of the native type, you can directly access the new type name. For the alias of composite type, you need to access the new alias according to the access method of the original composite type. For example, if the actual type of myemployee is struct, struct should also be added when using myemployee_ Prefix.

1.3. Accessing C variables and functions in go

In fact, in the above example, we have demonstrated how to access C variables and functions in Go. The general method is to add C prefix, especially for functions in C standard library. However, although we can directly define C variables and C functions in the Go source code file, from the perspective of code structure, a large number of C codes written in the Go source code do not seem to be so "professional". How to separate the C function and variable definitions from the Go source code and define them separately? It's easy to think of providing C code to Go source code in the form of shared library.

cgo provides #cgo indicators to specify which shared libraries Go source code links to after compilation. Let's take an example:

package main
 
// #cgo LDFLAGS: -L ./ -lfoo
// #include <stdio.h>
// #include <stdlib.h>
// #include "foo.h"
import "C"
import "fmt"
 
func main() {
    fmt.Println(C.count)
    C.foo()
}

We see that in the above example, the go compiler is told to link the libfoo shared library in the current directory through #cgo indicator. C. The count variable and the C.foo function are defined in the libfoo shared library. Let's create this shared library:

// foo.h
 
int count;
void foo();
 
//foo.c
#include "foo.h"
 
int count = 6;
void foo() {
    printf("I am foo!\n");
}
$> gcc -c foo.c
$> ar rv libfoo.a foo.o

We first create a static shared library libfoo a. However, we encountered a problem when compiling the Go source file:

$> go build foo.go
# command-line-arguments
/tmp/go-build565913544/command-line-arguments.a(foo.cgo2.)(.text): foo: not defined
foo(0): not defined

Prompt foo function is not defined. The specific compilation details are printed through the - x option, and the problem is not found. But I found an issue in Go's list of questions( http://code.google.com/p/Go/issues/detail?id=3755 ), as mentioned above, the current version of Go does not support linking static shared libraries.

Let's try to create a dynamic shared library:

$> gcc -c foo.c
$> gcc -shared -Wl,-soname,libfoo.so -o libfoo.so  foo.o

Recompile foo Go, you can really succeed. Execute foo.

$> go build foo.go && go
6
I am foo!

It is also worth noting that Go supports multiple return values, which is not supported in C. Therefore, when the C function is used in the call with multiple return values, the errno of C will be returned as the err return value. The following is an example:

package main
 
// #include <stdlib.h>
// #include <stdio.h>
// #include <errno.h>
// int foo(int i) {
//    errno = 0;
//    if (i > 5) {
//        errno = 8;
//        return i – 5;
//    } else {
//        return i;
//    }
//}
import "C"
import "fmt"
 
func main() {
    i, err := C.foo(C.int(8))
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(i)
    }
}
$> go run foo.go
exec format error

Errno is 8, which means errno H:

#define ENOEXEC      8  /* Exec format error */

It is indeed "exec format error".

1.4. Use Go function in C

Compared with using C source code in Go, there are fewer occasions to use Go function in C. In Go, you can use "export + function name" to export the Go function used by C. see a simple example:

package main
 
/*
#include <stdio.h>
 
extern void GoExportedFunc();
 
void bar() {
        printf("I am bar!\n");
        GoExportedFunc();
}
*/
import "C"
 
import "fmt"
 
//export GoExportedFunc
func GoExportedFunc() {
        fmt.Println("I am a GoExportedFunc!")
}
 
func main() {
        C.bar()
}

However, when we compile the Go file, we get the following error message:

# command-line-arguments
/tmp/go-build163255970/command-line-arguments/_obj/bar.cgo2.o: In function `bar':
./bar.go:7: multiple definition of `bar'
/tmp/go-build163255970/command-line-arguments/_obj/_cgo_export.o:/home/tonybai/test/go/bar.go:7: first defined here
collect2: ld returned 1 exit status

There seems to be no problem with the code, but it can't be compiled. It always prompts "multiple definitions". Check Cgo's documents and find some clues. original

There is a limitation: if your program uses any //export directives, then the C code in the comment may only include declarations (extern int f();), not definitions (int f() { return 1; }).

It seems that / / extern int f() and / / export f cannot be placed in one go source file. We put bar Split go into bar1 Go and bar2 Go two files:

// bar1.go
 
package main
 
/*
#include <stdio.h>
 
extern void GoExportedFunc();
 
void bar() {
        printf("I am bar!\n");
        GoExportedFunc();
}
*/
import "C"
 
func main() {
        C.bar()
}
 
// bar2.go
 
package main
 
import "C"
import "fmt"
 
//export GoExportedFunc
func GoExportedFunc() {
        fmt.Println("I am a GoExportedFunc!")
}

Compilation execution:

$> go build -o bar bar1.go bar2.go
$> bar
I am bar!
I am a GoExportedFunc!

Personally, I think that at present, the functions of Go for exporting functions for C are still very limited. The calling conventions of the two languages are different, and the types cannot correspond one by one. As well as the advanced functions such as Gc in Go, it is difficult to perfectly realize the function of exporting Go functions. The exported functions still cannot be completely separated from the Go environment, so the practicability seems to be discounted.

1.5. other

Although Go provides powerful interoperability with C, it is still imperfect. For example, it does not support directly calling functions with variable parameters (issue975), such as printf (therefore, fputs is used in documents).

The suggestion here is to minimize the scope of interoperability between Go and C.

What do you mean? If you use C code in Go, try calling C function in C code. Go is best if you only use a C function encapsulated by you. Do not code like this:

C.fputs(...)
C.atoi(..)
C.malloc(..)

Instead, these C function calls are encapsulated into a C function, and Go only knows the C function.

C.foo(..)

On the contrary, the same is true for functions exported using Go in C.

Topics: C Go