CGO with bitfield C structs
- ES
- EN
One of the things I worked on in a previous (golang) project was to interact with a C library which used bitfield structs, so it was a matter not only of interacting with C code, but also moving bits around to make sure that the data representation was not being corrupted between C bitfield structs and golang structs back and forth.
This post is a reminder for myself of how it was done, in case I forget in the future (which is really a matter of whether will I forget in 2 or 4 months).
The C part
A bitfield struct in C is like this:
// the size of struct content is 128 bits, of which
// the first 64 are used within the first-fifth fields
struct content {
uint64_t first: 4;
uint64_t second: 2;
uint64_t third: 10;
uint64_t fourth: 16;
uint64_t fifth: 32;
uint64_t sixth;
} __attribute__((packed));
In order to give an actual meaning of how the handling of the struct was done
between C <-> Go, I’ll define 2 simple operations: write the struct content
to a file and read the struct content
from a file:
int save_to_file(const char *filename, struct content *c);
int read_from_file(const char *filename, struct content *c);
At this point we’d have our library (let’s call it libbitfield
)
The Go part
I’ll start by defining the Go
counterparts as follows:
// each field is enclosed to the shortest type capable
// of holding the values represented in the C struct field
type content struct {
first uint8
second uint8
third uint16
fourth uint16
fifth uint32
sixth uint64
}
func saveToFile(filename string, c *content) error {}
func readFromFile(filename string) (*content, error) {}
Up to this point it all looks good, but here’s the trick:
There’s no direct handling of bitfield C structs with CGO, so we have to do the translation with an intermediate C struct that can be translated to Go.
The nitty-gritty
For this to work, we’ll have to define a intermediate CGO struct that is closest to the
types used in the content
go struct:
// this C struct will translate bitfields to full-size types
// which can be handled to a GO struct (and back)
struct cgo_content {
uint8_t first;
uint8_t second;
uint16_t third;
uint16_t fourth;
uint32_t fifth;
uint64_t sixth;
};
As well as the functions that will perform the translations:
struct cgo_content *to_cgo_content(const struct content *c); // bitfield -> full-size
struct content *to_content(const struct cgo_content *cgo_c); // full-size -> bitfield
At the end, the saveToFile
and readFromFile
GO functions will do the
dirty work for working the previously defined CGO structs, the libbitfield
structs and C pointers so that we can move data back and forth.
The flow
Basically we would split the GO part, the boilerplate C code we need to write and the bitfield part.
The GO binary would communicate via CGO to the C boilerplate, which would do the translation between C full-size types struct and the bitfield struct and back.
┌───────────────────┐
│ Go binary │
│ │
│ │
│ │
│ │ GO
│ │ }
│ │ }
│ GCO │ }
│ │ }
└───┬───────────────┘ }
│ ▲ } This part is the
▼ │ } middleware section
┌─────────────┴─────┐ } we must write to
│ │ } communicate with
│ C struct │ } each other
│ Boilerplate │ }
└───┬───────────────┘ }
│ ▲ }
▼ │
┌──────────────┴────┐
│ │
│ C struct │ libitfield
│ Bitfield │
│ │
└───────────────────┘
Detailed view
For the save to file flow it would go as follows:
- Transform the file name (go string) to a C string (
C.CString
) - Transform the go
content
struct to a Ccgo_content
struct - Transform the C
cgo_content
struct to C bitfieldcontent
struct - Call the C
save_to_file
function
For the read from file flow, it would do the following:
- Transform the file name (go string) to a C string (
C.CString
) - Define a C bitfield
content
struct pointer - Call the C
read_from_file
function - Transform the C bitfield
content
to a Ccgo_content
struct - Transform the C
cgo_content
struct to a gocontent
struct
Running the whole thing
In order to be able to actually call the library from GO, we would
need to link it and define the LD_LIBRARY_PATH
variable (assuming the library
is not properly installed).
To build it, we need to define the CFLAGS
and LDFLAGS
to include
the functions CGO would interact with. For this example we can add those
instructions as comments within our go code so that CGO grabs them without
having to defining them with environment variables every time we compile the
code:
package main
// #cgo CFLAGS: -I${SRCDIR}/libbitfield
// #cgo LDFLAGS: -L${SRCDIR}/libbitfield -lbitfield
// #include "cgo_bitfield.h"
import "C"
// ...
// more code below
In the previous snippet, SRCDIR
has been replaced by go as:
[…] the absolute path to the directory containing the source file.
More details in the CGO documentation.
Now we can actually go build
and verify that the resulting binary would be
linked against our libbitfield
:
# note that libbitfield is not installed
$ ldd bitfield
linux-vdso.so.1 (0x00007fff9c3db000)
libbitfield.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fd59b309000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd59b4ff000)
# but adding LD_LIBRARY_PATH allows us to find it
$ LD_LIBRARY_PATH=./libbitfield ldd bitfield
linux-vdso.so.1 (0x00007fff9c3db000)
libbitfield.so => ./libbitfield.so (0x00007fdcfa83f000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd59b309000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd59b4ff000)
Providing something small enough to exercise this example, we end up with a main function like the following
func main() {
filename := "file_from_go.bin"
c := &content{
first: 0x4,
second: 0x0,
third: 0x1BD,
fourth: 0x276E,
fifth: 0x61502074,
sixth: 0x323420202163696E,
}
fmt.Printf("Saving %#v into %v\n", c, filename)
if err := saveToFile(filename, c); err != nil {
fmt.Printf("error saving the file: %v\n", err)
}
new_c, err := readFromFile(filename)
if err != nil {
fmt.Printf("error reading from file: %v\n", err)
return
}
fmt.Printf("Read values from golang struct content: %#v\n", new_c)
}
Which would provide the following output when executing:
$ LD_LIBRARY_PATH=./libbitfield ./bitfield
Saving &main.content{first:0x4, second:0x0, third:0x1bd, fourth:0x276e, fifth:0x61502074, sixth:0x323420202163696e} into file_from_go.bin
C (save_to_file): saving struct to file file_from_go.bin
C (save_to_file): 0x4 0x0 0x1BD 0x276E 0x61502074 0x2163696E
C (read_from_file): reading struct from file file_from_go.bin
C (read_from_file): 0x4 0x0 0x1BD 0x276E 0x61502074 0x2163696E
Read values from golang struct content: &main.content{first:0x4, second:0x0, third:0x1bd, fourth:0x276e, fifth:0x61502074, sixth:0x323420202163696e}
The output with C(save_to_file/read_from_file)
are just debugging messages
from the C
bitfield part of the code, so we visualize the values are correct
Finally (and just for fun), dumping the actual content from the file_from_go.bin
file, we get:
$ hexdump -C file_from_go.bin
00000000 44 6f 6e 27 74 20 50 61 6e 69 63 21 20 20 34 32 |Don't Panic! 42|
00000010
The funky values defined in the content
struct were not so random at all ;-)
And that’s all. For a detailed and browsable code example see the repository