Your team has decided on Go after months of months of developing modules and roadmapping for a large project, but
since there are three external dependencies in C/C++, your entire project will now have to be done in C/C++. What
does this mean? Now, at least half of your development time will be spent correcting memory accesses bugs and
invalid cast errors — and not many developers can afford to allocate this time. What can be done to expedite this
Maxim Kupriianov, a Senior Go Engineer at Sphere Software, first formulated an assistant tool called C-for-Go used to create CGo bindings of C libraries to use in his
applications. “The idea was to scan the provided C header files for structs, types, and function definitions, and
then convert their names into idiomatic Go names, preparing boilerplate code for CGo calls, and so on,” he said.
After discovering the czinc/cc package, Maxim’s project would soon become
a full-featured binding generator, allowing developers who would like to use any C library in their code to do so in
less than one hour — all without writing a single line of code. The sole purpose of C-for-Go is to bring a
substantial boost in a developer’s productivity and overall system performance.
C-for-Go Process Overview
C-for-Go allows to reuse existing C/C++ libraries in your Go applications by automatically generating c-go bindings
for a given set of C headers and the manifest file. Below is an overview of this process:
The only component required to produce a Go package that will wrap the source C/C++ code is the YAML manifest file
that defines parsing, translation, and generation rules. The manifest only requires a few lines, however, in order
to match Go’s naming conventions and provide enough tips for type conversions, it usually contains about 100 lines.
Overall, this is much less time consuming than producing tens-of-thousands of lines of Go code by hand.
The resulting bindings are as low-level as C code. It would require knowledge of memory management to carefully use
the resulting code, however, no more C code is needed to finish this process. Eventually, some functions can be
replaced manually with pure-Go analogs. Also, high-level wrappers are usually created by hand to introduce Object
Oriented Design into API, manage inner state, and memory — increasing safety and lifting the mental overhead.
Architecture of C-for-Go
C-for-Go consists of three parts: The translator, generator, and parser — in order of importance.
Translator – Responsible for name conversions. The translator does a bulk of the C-for-Go work — with
parts of its engine including name substitution, rule patterns, hints/tips for type conversions.
Generator – The generator is based on fmt.Sprintf mechanism and that helped a lot to avoid code bloat.
Also, the generator is very flexible. For example, when Go 1.6 announced pointer rules, the required changes
included switching all allocations to C.malloc calls, having a reference count object, implementing pointer
borrowing, automating frees with finalizers, and nesting allocations while unpacking slices. This was all added
to the generator in a few days.
Parser – The parser is actually a wrapper for cznic/cc C99 compiler front-end. By coincidence, it was
under active development at the same time as C-for-Go. Parser is configured partly from the configuration
manifest and partly from predefined options in predefined.go.
The main executable provides the glue for all of three modules. It does config parsing, runs external utils like
in-house pkg-config lightweight clone to discover more headers, manages the output buffers, formats the code using
imports, process after generation is done, and so on.
Consideration for Bindings
What would motivate a developer to use bindings instead of writing a pure-Go package with the same functionality?
Here are a few arguments:
- Rewriting Difficulty with Old Tech
- Even many of today’s technologies, including C/C++, have roots in old technologies. Let’s consider how a
developer would rewrite the Qt Framework? The libvpx from the WebM project? Among technologies that difficult to
rewrite, there are many with nothing to rewrite at all. Native adapters for various system drivers like
PortAudio or PortMIDI that are meant to be portable and support dozens of OSes and environment configurations
are some examples. If a good C library is worth a pure-Go rewrite, consider getting a quick binding set, write
your tests, and then rewrite the library piece-by-piece.
- Rewriting Difficulty with Modern Tech
- What about modern APIs being auto-generated from language-agnostic XML specs like Vulkan Graphics API? There’s
nothing to rewrite in Go, because this API provides no implementation in user space either. Let’s face it,
nobody will be able to “rewrite” reentrant wrappers for old heritage libraries because the main action goes in
the heritage code — which acts like an anchor. If this thin wrapper allows you to run that code on Android/iOS
platforms, why not bring that into Go?
- Performance Increase
- Since Go 1.5, there have been significant penalties for calling CGo too frequent. However, the amount of calls
needed to get the job done vary from domain to domain. Some C libraries maxed out the amount of tricks used to
increase performance like custom allocators, zero-copy buffers, efficient data types, and so on. With the new
SSA additions and runtime improvements, Go may perform even better than C — especially with all the not-so-thin
CGo wrappers and validators around the C API.
C-for-Go Demo and More
To see how to utilize C-for-Go, filling the generator, adding the parser and translator preferences, and more,
visit Sphere Software’s Github/c-for-go. C-for-Go is free to use, free to
distribute, and free to fork under the MIT license. However, Sphere Software also provides professional services to
help companies reuse C/C++ components in their Go applications. This consists of composing the manifest, consulting
about the best workflows with C-Go bindings, verifying, and improving the resulting Go packages.