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:

759
760
761
762
763
764
765
766
767
768
769
/* connect_wrapper.go from libvirt-go-module version v1.8009.0 */
virConnectPtr
virConnectOpenWrapper(const char *name,
                      virErrorPtr err)
{
    virConnectPtr ret = virConnectOpen(name);
    if (!ret) {
        virCopyLastError(err);
    }
    return ret;
}

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:

328
329
330
331
332
333
334
335
336
337
338
339
340
341
/* connect.go from libvirt-go-module version v1.8009.0 */
func NewConnect(uri string) (*Connect, error) {
	var cUri *C.char
	if uri != "" {
		cUri = C.CString(uri)
		defer C.free(unsafe.Pointer(cUri))
	}
	var err C.virError
	ptr := C.virConnectOpenWrapper(cUri, &err)
	if ptr == nil {
		return nil, makeError(&err)
	}
	return &Connect{ptr: ptr}, nil
}

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:

582
583
584
585
586
587
// See also https://libvirt.org/html/libvirt-libvirt-host.html#virConnectSetIdentity
func (c *Connect) SetIdentity(ident *ConnectIdentity, flags uint32) error {
    if C.LIBVIR_VERSION_NUMBER < 5008000 {
        return makeNotImplementedError("virConnectSetIdentity")
    }
    /* implements the function  */

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:

  1. Locate the path of XMLs with pkg-config --variable
  2. Loading the XMLs into Go Structus
  3. Preparing the loaded data (e.g: sorting, do some calculations)
  4. Opening output files
  5. 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.

43
44
45
46
47
48
49
50
51
52
53
54
type API struct {
	XMLName   xml.Name      `xml:"api"`
	Name      string        `xml:"name,attr"`
	Files     []APIFile     `xml:"files>file"`
	Macros    []APIMacro    `xml:"symbols>macro"`
	Typedefs  []APITypedef  `xml:"symbols>typedef"`
	Enums     []APIEnum     `xml:"symbols>enum"`
	Structs   []APIStruct   `xml:"symbols>struct"`
	Functions []APIFunction `xml:"symbols>function"`
	Functypes []APIFunctype `xml:"symbols>functype"`
	Variables []APIVariable `xml:"symbols>variable"`
}

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.

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#pragma once

{{- with .Enums }}
    {{- $lastTypeDefined := "" }}
    {{- range . }}
        {{- if ne $lastTypeDefined .Type }}
            {{- $lastTypeDefined = .Type }}

/* enum {{ .Type }} */
        {{- end }}
#  if !LIBVIR_CHECK_VERSION({{ getVersionMajor .Version }}, {{ getVersionMinor .Version }}, {{ getVersionMicro .Version }})
#    define {{ .Name }} {{ getEnumString . }}
#  endif
    {{- end }}
{{ end }}

Enums are generated in blocks like:

68
69
70
71
72
73
74
/* enum virConnectBaselineCPUFlags */
#  if !LIBVIR_CHECK_VERSION(1, 1, 2)
#    define VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES (1 << 0)
#  endif
#  if !LIBVIR_CHECK_VERSION(1, 2, 14)
#    define VIR_CONNECT_BASELINE_CPU_MIGRATABLE (1 << 1)
#  endif

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.

  1. 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.
  2. 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:

3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
int
virDomainSetLaunchSecurityStateWrapper(virDomainPtr domain,
                                       virTypedParameterPtr params,
                                       int nparams,
                                       unsigned int flags,
                                       virErrorPtr err)
{
    int ret = -1;
#if !LIBVIR_CHECK_VERSION(8, 0, 0)
    setVirError(err, "Function virDomainSetLaunchSecurityState not available prior to libvirt version 8.0.0");
#else
    ret = virDomainSetLaunchSecurityState(domain,
                                          params,
                                          nparams,
                                          flags);
    if (ret < 0) {
        virCopyLastError(err);
    }
#endif
    return ret;
}

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:

5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
typedef int
(*virDomainSetLaunchSecurityStateType)(virDomainPtr domain,
                                       virTypedParameterPtr params,
                                       int nparams,
                                       unsigned int flags);

int
virDomainSetLaunchSecurityStateWrapper(virDomainPtr domain,
                                       virTypedParameterPtr params,
                                       int nparams,
                                       unsigned int flags,
                                       virErrorPtr err)
{
    int ret = -1;
    static virDomainSetLaunchSecurityStateType virDomainSetLaunchSecurityStateSymbol;
    static bool once;
    static bool success;

    if (!libvirtSymbol("virDomainSetLaunchSecurityState",
                       (void**)&virDomainSetLaunchSecurityStateSymbol,
                       &once,
                       &success,
                       err)) {
        return ret;
    }
    ret = virDomainSetLaunchSecurityStateSymbol(domain,
                                                params,
                                                nparams,
                                                flags);
    if (ret < 0) {
        virCopyLastErrorWrapper(err);
    }
    return ret;
}

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.

 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
bool
libvirtSymbol(const char *name,
              void **symbol,
              bool *once,
              bool *success,
              virErrorPtr err)
{
    char *errMsg;

    if (!libvirtLoad(err)) {
        return *success;
    }

    if (*once) {
        if (!*success) {
            // Set error for successive calls
            char msg[100];
            snprintf(msg, 100, "Failed to load %s", name);
            setVirError(err, msg);
        }
        return *success;
    }

    // Documentation of dlsym says we should use dlerror() to check for failure
    // in dlsym() as a NULL might be the right address for a given symbol.
    // This is also the reason to have the @success argument.
    *symbol = dlsym(handle, name);
    if ((errMsg = dlerror()) != NULL) {
        setVirError(err, errMsg);
        *once = true;
        return *success;
    }
    *once = true;
    *success = true;
    return *success;
}

It is more error handling than anything else :)

This process is similar for lxc, qemu and admin drivers with all this code being generated.


  1. Tokei

    LanguageFilesLinesCodeCommentsBlanks
    Alex98118671001408
    Autoconf868080519018861004
    BASH19481112
    C63565885347465269777114424
    C Header4841573961060612242528910
    C++14022810
    CSS58907576127
    D210184017
    Dockerfile3436733358170145
    JavaScript1141116124
    JSON30446250460950155
    Makefile320571378405274
    Meson74931877674291122
    Perl436202915283422
    Python41134271000012722155
    ReStructuredText1595950244465015037
    Rust1282206
    Shell6158714822513536
    SVG18722369153017
    Plain Text12173016211
    TOML110712
    XSL3111610292562
    XML3464180883180134452297
    YAML1021511655170326
    Total5413116901590423598287166493
     ↩︎
  2. Commits in the last 5 Years by gitstats

    YearCommits (% of all)Lines addedLines removed
    20222891 (6.11%)628760755641
    20214005 (8.46%)13649901232564
    20204357 (9.20%)1914844393243
    20194299 (9.08%)563890273553
    20183567 (7.54%)23291465983747
     ↩︎