Artem Krylysov


Handling C++ exceptions in Go

Cgo is a mechanism that allows Go packages call C code. The Go compiler enables cgo for every .go source file that imports a special pseudo package "C". The text in the comment before the import "C" line is treated as a C code. You can include headers, define functions, types and variables - everything a normal C code can do:

package main

/*
#include <stdio.h>

void foo(int x) {
    printf("x: %d\n", x);
}
 */
import "C"

func main() {
    C.foo(C.int(123)) // x: 123
}

The identifiers declared in the embedded C code can be accessed using the "C" package - e.g. write C.foo(C.int(123)) to call the function foo. Pretty straightforward, right? This opens the door to a huge amount of code that was written in C or was written in any other language and provides C bindings.

Almost all libraries play by the rules, but some libraries that are written in C++ may throw exceptions. C doesn't support exceptions, therefore there is no way to catch them in C or Go/cgo. C function should never throw an exception, but nothing stops a developer from writing code that does it. A good example of such a library is a machine learning project Vowpal Wabbit - it's written in C++ and provides a C interface, which may throw an exception from time to time:

package main

// #cgo LDFLAGS: -lvw_c_wrapper
// #include <stdlib.h>
// #include <vowpalwabbit/vwdll.h>
import "C"
import "unsafe"

func main() {
    cArgs := C.CString("invalid args")
    defer C.free(unsafe.Pointer(cArgs))
    C.VW_InitializeA(cArgs) // exception
}

Unhandled C++ exception on VW_InitializeA call just crashes the program:

SIGABRT: abort
PC=0x7efc793a7428 m=0 sigcode=18446744073709551610
signal arrived during cgo execution

goroutine 1 [syscall, locked to thread]:
runtime.cgocall(0x4500b0, 0xc420049f58, 0xc420049f58)
    /usr/lib/go-1.8/src/runtime/cgocall.go:131 +0xe2 fp=0xc420049f28 sp=0xc420049ee8
main._Cfunc_VW_InitializeA(0x193c620, 0x0)

While cgo lets us call only C code, we still can link any C++ file to our program. This gives us an ability to create a C wrapper for VW_InitializeA that handles the exceptions. We can use a structure to return both the original return value and a pointer to the exception message (good old C doesn't support tuples or multiple return values):

// vw_wrapper.h
#ifdef __cplusplus
extern "C" {
#endif

#include <stdlib.h>
#include <vowpalwabbit/vwdll.h>

typedef struct VWW_HANDLE_ERR {
    VW_HANDLE handle;
    const char *pstrErr;
} VWW_HANDLE_ERR;

VW_DLL_MEMBER VWW_HANDLE_ERR VW_CALLING_CONV VWW_InitializeA(const char *pstrArgs);

#ifdef __cplusplus
}
#endif

The wrapper code is only a few lines, save it in the project directory as a .cpp file:

#include <string.h>
#include <exception>
#include "vw_wrapper.h"

VW_DLL_MEMBER VWW_HANDLE_ERR VW_CALLING_CONV VWW_InitializeA(const char *pstrArgs) {
    VWW_HANDLE_ERR result = {0};
    try {
        result.handle = VW_InitializeA(pstrArgs);
    } catch(std::exception &e) {
        result.pstrErr = strdup(e.what());
    }
    return result;
}

The Go compiler will compile and link the C++ source with the program. Don't forget to explicitly free the strErr pointer if its value is not nil - Go garbage collector doesn't know how to deal with C pointers:

package main

// #cgo LDFLAGS: -lvw_c_wrapper
// #include <stdlib.h>
// #include "vw_wrapper.h"
import "C"

import (
    "log"
    "unsafe"
)

func main() {
    cArgs := C.CString("invalid args")
    defer C.free(unsafe.Pointer(cArgs))
    ret := C.VWW_InitializeA(cArgs)
    if ret.pstrErr != nil {
        defer C.free(unsafe.Pointer(ret.pstrErr))
        log.Println("VW error: ", C.GoString(ret.pstrErr))
    }
}

Now, whenever the original VW_InitializeA throws an exception, the C++ wrapper will catch it and return the error message as a part of the VWW_HANDLE_ERR structure.

I'm not a native English speaker, and I'm trying to improve my language skills. Feel free to correct me if you spot any spelling or grammatical errors!