Tuesday, 4 October 2011

Experimenting with Cgo - Part 2

In this second cgo article, I am going to demonstrate how to handle strings. There is already an excellent post by Andrew Gerrand, one of the Go developers at Google, on using strings in cgo. My own post will provide a slightly more complete example.

The first thing to understand is that C has no concept of a string. Unlike in Go, where a string is an actual type, a string in C is just a pointer to an array of characters. Thankfully, the "C" package has two simple functions to convert between the two types. However, there are two problems: One, converting from a Go string to a C string (character array) allocates memory to the string which is NOT garbage collected by Go. This is important! The reason for this is because Go has no way of knowing when the C library you're creating a language binding for will be done with the string. Once your function exists, any local variables will go out of scope and Go's garbage collector will at some point free the allocated memory even though the C library involved may not be finished with the string. Therefore, you have to free the memory when you know the C library is finished with it. Second, you still need a means of storing characters into a buffer and there isn't an immediately obvious way to do that.

This article assumes you've read my previous post and already know how to start a basic cgo project and have a working Go development environment. I am going to provide a very simple wrapper for the following functions from stdio.h: fopen, fclose, fputs, fgets and frewind.

Important: Absolutely no attempt was made to provide error handling in this code in order to provide absolute clarity for the code itself. While I believe proper examples should always contain error handling, handling errno and NULL return values is not easily handled in Go.

1. Include any C headers and import any libraries you will need:


// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

Note that, when converting to a C string, you will need to free the allocated memory. Free() from stdlib.h is required to do this. You will also need to the "unsafe" library to handle C pointers.


2. In order to make stdio feel like Go, type the FILE structure:

type File C.FILE

While embedding the FILE type in a struct would allow us to create much clearer code because we could remove all the casting to/from the C and Go types Go does not allow C types to be embedded into Go structs. So, we must do things the hard way.

3. Implement the fopen() function:


func Open(path, mode string) *File {
    cpath, cmode := C.CString(path), C.CString(mode)
    defer C.free(unsafe.Pointer(cpath))
    defer C.free(unsafe.Pointer(cmode))
    
    return (*File)(C.fopen(cpath, cmode))
}

Notice first that, like the original C code, we accept strings as arguments to indicate the path and mode. The CString() function from the "C" package allocates enough memory for the Go string and then converts it to a C string. You must make sure you free the memory after using it, and the defer statement is the ideal way to accomplish this. Not only does it guarantee to free up the memory it does so after we return the value from fopen(). free() take a void pointer as an argument, so variables passed to the function must be cast to an unsafe.Pointer().


4. Implement the fgets() function:


func (f *File) Get(n int) string {
    cbuf := make([]C.char, n)
    return C.GoString(C.fgets(&cbuf[0], C.int(n), (*C.FILE)(f)))
}

I found this to be tricky to figure out until I thought about it. You need to provide a pointer to a C character array but Go provides no obvious ways to do so. Use Go's make() function to build an array of characters. You could also use a static array size but the dynamic version provides a lot more flexibility and is no more complex to implement. Use C.GoString() to convert the C string to a Go string.

5. Implement the rest of the functions. See below for the complete code.

6. Compile and install your library.

7. Compile and run a test program to complete your proof of concept.


The complete code follows:

godev/src/gostdio/gostdio.h:

package gostdio

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

type File C.FILE

func Open(path, mode string) *File {
    cpath, cmode := C.CString(path), C.CString(mode)
    defer C.free(unsafe.Pointer(cpath))
    defer C.free(unsafe.Pointer(cmode))
    
    return (*File)(C.fopen(cpath, cmode))
}

func (f *File) Close () {
    C.fclose((*C.FILE)(f))
    return
}

func (f *File) Get(n int) string {
    cbuf := make([]C.char, n)
    return C.GoString(C.fgets(&cbuf[0], C.int(n), (*C.FILE)(f)))
}

func (f *File) Put(str string) {
    cstr := C.CString(str)
    defer C.free(unsafe.Pointer(cstr))
    
    C.fputs(cstr, (*C.FILE)(f))
    return
}

func (f *File) Rewind() {
    C.rewind((*C.FILE)(f))
}


godev/src/cgoexample/cgoexample.go:

package main

import "gostdio"
import "fmt"

func main() {
    f := gostdio.Open("test.txt", "w+")    
    defer f.Close()
    
    f.Put("Some example text\n")
    f.Rewind()
    
    fmt.Println(f.Get(13))
}



No comments:

Post a Comment