Preface
The libvirt-go-module project provides Go bindings to use libvirt’s library. It wraps libvirt’s C types and APIs into Go types and methods. The wrapping involves writing a bit of Cgo code that looks like the following:
|
|
In this example, we are calling libvirt’s virConnectOpen() to stablish connection with the
underlying hypervisor. The virConnectOpenWrapper()
is a helper function, to be consumed by the
NewConnect()
API, which returns a pointer of Go struct type Connect
to the caller or sets the
error if unsuccessful. The actual code:
|
|
The problem
Libvirt is a big project created in 2005, currently with over 1.1M lines of code1 with an
average of 3800 commits per year2. New APIs are constantly being added to handle new features
which raises the need of libvirt-go-module
to be patched constantly with Cgo to wrap around
libvirt’s new additions plus the actual Go code that will expose it to Go applications.
Reducing this work as much as possible would help with the maintenance of libvirt-go-module
while
making it easier to keep go bindings up to date with libvirt.
Luckily, libvirt’s API is thoroughly documented in a format that we can take advantage of in order to generate almost all of the Cgo code, leaving developers able to focus on how to expose libvirt’s new features.
The input
Libvirt’s scripts/apibuild.py
parses the source code to gather all the metadata it can to build
its API documentation which produces four XMLs, one per module. The <api>
section has two
subsections, <files>
and <symbols>
. The symbols part is what we are mostly interested in order
to generate those Cgo wrappers.
Taking the virDomainSetLaunchSecurityState()
function as example. We have know when it was
introduced, what are the return and arguments type and the correct argument order.
<function name='virDomainSetLaunchSecurityState'
file='libvirt-domain'
module='libvirt-domain'
version='8.0.0'>
<info><![CDATA[
Set a launch security secret in the guest's memory. The guest must be
in a paused state, e.g. in state VIR_DOMIAN_PAUSED as reported by
virDomainGetState. On success, the guest can be transitioned to a
running state. On failure, the guest should be destroyed.]]></info>
<return type='int'
info='-1 in case of failure, 0 in case of success.'/>
<arg name='domain'
type='virDomainPtr'
info='a domain object'/>
<arg name='params'
type='virTypedParameterPtr'
info='pointer to launch security parameter objects'/>
<arg name='nparams'
type='int'
info='number of launch security parameters'/>
<arg name='flags'
type='unsigned int'
info='currently used, set to 0.'/>
</function>
Adding version metadata
As one of libvirt-go-module
’s goals is to be able to compile and run in platforms supported by
libvirt itself. This means we need to be aware of libvirt’s version at compile and running time. At
compile time, we might need libvirt symbols that are not included in older versions of its headers;
At run time, to avoid calling functions that are not yet implement, like SetIdentity
below:
|
|
Starting in libvirt’s v8.3.0, the version information was included in all the symbols exported in
XML. This involved patching scripts/apibuild.py
to parse a Since:
tag from that symbol’s
comments plus adding the Since: $version
to all exported symbols. This last bit alone made me
learn a few new git
and regular expressions
tricks.
The code generator
The logic of code generator is relatively simple:
- Locate the path of XMLs with
pkg-config --variable
- Loading the XMLs into Go Structus
- Preparing the loaded data (e.g: sorting, do some calculations)
- Opening output files
- Generate code by running the templates
The encoding/xml
Go module makes it very easy to Unmarshal XML into structs and I decided to
use text/template
to write Cgo templates for each symbol. The base API struct
below is used to
load the XML and as argument for each of the templates.
|
|
Just to give an example, the template for enums has less than 20 lines of code to generate more than 3400 lines of C header with proper version checking.
|
|
Enums are generated in blocks like:
|
|
Compiling without libvirt headers
The title of the actual merge request was Generate Wrapper code that uses dlopen/dlsym
but
after a few exchanges, we decided to take the feature of loading libvirt at runtime as a compile
time option and disabled by default.
There are two major benefit of using dlopen/dlsym.
- Build the application consuming libvirt-go-module without worrying about libvirt as build-time dependency. For example, this can benefit KubeVirt to reduce the amount of rpm’s they need to track for their Bazel builds.
- Run the application consuming libvirt-go-module on any system and only install and load libvirt if needed. This can benefit applications where the binary offers libvirt as an option between other technologies.
To use the dlopen/dlsym
feature, one needs to build libvirt-go-module
with libvirt_dlopen
tag
(e.g: go build -tags libvirt_dlopen
).
Let’s take a look at the generated functions of virDomainSetLaunchSecurityState()
that we put the
XML in The input
section.
First, the static version:
|
|
At build time we check if libvirt’s version is new enough to contain that function. At runtime we either call it or set the error appropriately. This is basically how it was working before but now it is generated.
The dlopen version:
|
|
Here we have to actually define the function type and then load the function into
virDomainSetLaunchSecurityStateSymbol
variable, before we can call it.
The libvirtSymbol()
function is the helper to dlopen()
the library once and dlsym()
its
symbol. Let’s take a peek at it.
|
|
It is more error handling than anything else :)
This process is similar for lxc
, qemu
and admin
drivers with all this code being generated.
Tokei
↩︎Language Files Lines Code Comments Blanks Alex 9 8118 6710 0 1408 Autoconf 86 8080 5190 1886 1004 BASH 1 94 81 1 12 C 635 658853 474652 69777 114424 C Header 484 157396 106061 22425 28910 C++ 1 40 22 8 10 CSS 5 890 757 6 127 D 2 101 84 0 17 Dockerfile 34 3673 3358 170 145 JavaScript 1 141 116 1 24 JSON 304 46250 46095 0 155 Makefile 3 2057 1378 405 274 Meson 74 9318 7767 429 1122 Perl 4 3620 2915 283 422 Python 41 13427 10000 1272 2155 ReStructuredText 159 59502 44465 0 15037 Rust 1 28 22 0 6 Shell 61 5871 4822 513 536 SVG 18 7223 6915 301 7 Plain Text 12 173 0 162 11 TOML 1 10 7 1 2 XSL 3 1116 1029 25 62 XML 3464 180883 180134 452 297 YAML 10 2151 1655 170 326 Total 5413 1169015 904235 98287 166493 Commits in the last 5 Years by gitstats
↩︎Year Commits (% of all) Lines added Lines removed 2022 2891 (6.11%) 628760 755641 2021 4005 (8.46%) 1364990 1232564 2020 4357 (9.20%) 1914844 393243 2019 4299 (9.08%) 563890 273553 2018 3567 (7.54%) 2329146 5983747