Use Go to write C Library bindings

I’ve never tried to interface a C library with Go, so today I decided to do just that.

For this purpose, I want to try using the SaxonC library to apply modern XSLT 3.0 XML stylesheets from inside Go.

TL;DR

I managed to achieve my goal and sucessfully applied some of the sample transformations while learning how to work with cgo.

The learning curve for cgo is quite steep and required a lot of researching which compiler and linker flags need to be configured.

What is XSLT?

XSLT (eXtensible Stylesheet Language Transformation) is part of XSL and is a Turing-complete programming language to define transformations for XML documents.

A popular example for this is the DocBook document format, which is a popular XML language used for creating books. These “books” can then be transformed into other output formats, e.g. a single HTML file containing everything or multiple HTML files split by chapter.

The underlying principle is the same: prepare the documentation in a single format and apply transformations to achieve the desired output.

Why use a dedicated library?

The Go standard library doesn’t have a XSLT package. The only XML-related package is encoding/xml, which allows to encode structs as XML documents.

How to interact with a C library in Go?

cgo can be used to interface with programs and libraries written in C. Since a while now, cgo is used automatically when Go code includes a import "C" statement. Above this statement, special instructions can be used to configure the flags used in the build process, e.g. CFLAGS and LDFLAGS.

For example, see the following snippet:

// #cgo LDFLAGS: -L ${SRCDIR}/lib/libsaxon-HEC-linux-amd64-v12.5.0/libs/nix -lsaxon-hec-12.5.0 -Wl,-rpath,${SRCDIR}/lib/libsaxon-HEC-linux-amd64-v12.5.0/libs/nix
// #cgo CFLAGS: -I ${SRCDIR}/lib/libsaxon-HEC-linux-amd64-v12.5.0/Saxon.C.API
// #cgo CFLAGS: -I ${SRCDIR}/lib/libsaxon-HEC-linux-amd64-v12.5.0/Saxon.C.API/graalvm
// #include <SaxonCProcessor.h>
import "C"

This snippet does the following:

  • configure the linker flags to point to the actual shared library including instructions where to find that library in a non-standard directory
  • configure the compiler flags to point to the API directory and the GraalVM directory
  • include the header file containing the functions I intend to use

With this out of the way, I can start to use the library.

Caveats

As per the rather sparse cgo documentation, the functions imported like this are available as part of the virtual C package that is available when cgo is used. Types like unsigned int are C.uint for Go. C-style strings need to be cast to a Go compatible type with C.GoString(const_char_pointer_with_string).

Adapting the C example code

This is my current version to apply a fixed stylesheet to a fixed XML file:

func main() {
	var saxon_params *C.sxnc_parameter
	var saxon_env *C.sxnc_environment
	var saxon_processor *C.sxnc_processor
	var saxon_property *C.sxnc_property

	C.initSaxonc(&saxon_env, &saxon_processor, &saxon_params, &saxon_property, 10, 10)
	defer C.freeSaxonc(&saxon_env, &saxon_processor, &saxon_params, &saxon_property)

	C.create_graalvm_isolate(saxon_env)
	defer C.graal_tear_down(saxon_env.thread)

    // TODO: error handling!
	_ = C.c_createSaxonProcessor(saxon_env, saxon_processor, 0)
	productAndVersion := C.getProductVariantAndVersion(saxon_env, saxon_processor)
	fmt.Println(C.GoString(productAndVersion))

    // TODO: error handling!
	result := C.xsltApplyStylesheet(
		saxon_env,
		saxon_processor,
		C.CString("./"),
		C.CString("./data/cat.xml"),
		C.CString("./data/test.xsl"),
		saxon_params,
		saxon_property,
		0,
		0,
	)
	fmt.Println(C.GoString(result))
}

This is very similar to the C example code given in the SaxonC documentation. The only differences are the wrapping of strings in C.CString() to make them available as the required const char *. Additionally, saxon_params and saxon_property were both given as 0 in the example code, probably as some C shortcut for a Null pointer. The Go compiler did not like that very much and insisted that the parameters are handled correctly, much to my delight!

The output is as follows:

--- git/go-saxonc-he » ./go-saxonc-he       
SaxonC-HE 12.5 from Saxonica
Testing message2
<?xml version="1.0" encoding="UTF-8"?>
<output>
   <out>text1</out>
   <out>text2</out>
   <out>text3</out>
   <out>4</out>
</output>

This was the original file:

<out>
<person>text1</person>
<person>text2</person>
<person>text3</person>
</out>

Where to go from here?

As the comments in the code state, the most critical thing missing is proper error handling. After that, some refactoring and extracting code into a dedicated package would be next, allowing this package to be used wherever I might need to apply advanced XML transformations!

References