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.