Suppose we have a C lib where we have defined various data structures and methods. Due to some reason or constraint, there needs to be a Golang process which has to reuse the structures mentioned in the C lib.

Apart from accessing the C defined elements from the Go code, another topic which is more important is to understand the difference between both the C and Go runtimes.

Go’s runtime does all the memory management tasks like memory allocation and memory freeing for the processes and deciding which object’s memory should escape to the heap. In C, we have to explicitly allocate and free memory allocated on the heap.

Naturally, when C objects are used in the context of Go runtime, the Go runtime has to disable accounting and management of those memory segments. The unsafe package helps to do that as well.

The unsafe package, is one of the Go packages which helps in accessing system internals and performing complex operations when necessary like accessing the object memory, modifying the object memory across data types which are not supported in Golang, firing system calls, etc. In this post, we will be mainly focussing on how the Unsafe package helps in providing a bridge between C and Go objects.

The main reason that we require unsafe package is because the pointers provided by Go doesn’t allow pointer arithmetic, type casting across different data types, etc to make the language safer for application programmers with lesser experience in the system programming side. Pointers normally in Go, are mainly to be used as references for objects. However, by using unsafe, we can leverage the power of a language which is able to properly utilize system constructs.

Let us take the example of a short C snippet where we are trying to take a message object and call the receiver method where we are just printing it for now, but can potentially be a network call later as well.

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

typedef struct Message {
    uint8_t     m_type;
    uint32_t    buff_size;
} message_t;

char * gl_msg = "message_in_c_global";

void receiver(message_t* msg) {
    printf("%d \t %d \n", msg->m_type, msg->buff_size);
}

message_t* getMessage() {
    message_t *msg;
    msg = (message_t*)malloc(sizeof(message_t));
    msg->m_type = 1;
    msg->buff_size = 1024;
    return msg;
}

void hello() {
    printf("hello from C\n");
}

In order to call the C code in Go, take the C snippet and paste it in the Go code as shown below. There should not be any line space between the C block and the import C line.

package main

/*
# include <stdio.h>
# include <stdlib.h>
# include <stdint.h>
typedef struct Message {
    uint8_t     m_type;
    uint32_t    buff_size;
} message_t;
char * gl_msg = "message_in_c_global";
void receiver(message_t* msg) {
    printf("%d \t %d \n", msg->m_type, msg->buff_size);
}
message_t* getMessage() {
    message_t *msg;
    msg = (message_t*)malloc(sizeof(message_t));
    msg->m_type = 1;
    msg->buff_size = 1024;
    return msg;
}
void hello() {
    printf("hello from C\n");
}
*/
import "C"

import (
    "fmt"
    "unsafe"
)

type (
    Message struct {
        MType    uint8
        BuffSize uint32
    }
)

func main() {

    var (
        msg string

        msgC  *C.message_t
        msgGo *Message
    )

    C.hello()

    // Convert C char pointer to Go String
    msg = C.GoString(C.gl_msg)
    fmt.Println("Converted C string to Go string", msg)

    // Fetch C struct of same data type
    msgC = C.getMessage()

    // Convert the C pointer using unsafe and convert it back
    // to the Go Message struct pointer
    msgGo = (*Message)(unsafe.Pointer(msgC))

    fmt.Println("Converted C struct to Go struct of similar data types",
        msgGo.MType, msgGo.BuffSize)

    // Change the struct variable and cast it back to C struct
    msgGo.BuffSize = 4096

    // Convert the msgGo pointer to unsafe pointer
    // and pass it back to the C receiver method
    C.receiver((*C.message_t)(unsafe.Pointer(msgGo)))

}

In the first method inside the main function, we are converting C string which is a char pointer to a Go string.

In the second method, we initialize a *C.message_t type and assign the C data type to it. Using unsafe.Pointer, we convert it to a pointer which is then casted to a Go struct which contains the similar data types.

The structures are all padded to keep in mind the cache alignment. That’s why if you print the msgC variable, we get the output

&{1 [145 84 1] 1024}

Here, 1 and 1024 were initialized in the C code. 145, 84, 1 are uint8 size elements padded so that the total size comes up to 64.

Once we have the unsafe pointer to the C structure, we can cast it to any data type which contains similar alignment. The same can be done for the sk_buff packet structure which received from the network interfaces and used in Go accordingly.

In the third example, we take the go structure pointer, convert it to unsafe pointer again and then reconvert it to the C message_t pointer and pass it as an argument to the C receiver method.

A very important thing to remember here is that msgGo is a Go pointer and becomes a candidate for garbage collection. But since we are passing that value to the C method, garbage collection may happen before the data is read which will lead to segmentation faults. To avoid this, we need to call unsafe.Pointer while passing it as the argument to the function itself. When the go runtime sees the unsafe pointer being passed in the same function call, it doesn’t garbage collect the pointer.

This is one of the uses of using the unsafe package. Using such techniques, the Go runtime can have access to other system and hardware level abstractions like device drivers, etc.

References