[Libguestfs] [PATCH libnbd v4] Add Go language bindings (golang) (RHBZ#1814538).

Richard W.M. Jones rjones at redhat.com
Wed Mar 25 09:42:20 UTC 2020


This adds a mostly complete implementation of libnbd bindings for
golang.  Synchronous calls and callbacks are fully supported.  There
is a reasonably complete test suite.

Asynchronous calls are also working, with the caveat that the AIO
buffer is not GC-safe (as in other languages).  At the moment the
caller has to ensure that the AIO buffer passed in when starting the
command remains live until the command has completed (like in C).  We
may fix this properly in future, hopefully without needing to change
the golang API.

Thanks: Daniel P. Berrangé for invaluable help with this feature.
---
 Makefile.am                                   |   2 +
 configure.ac                                  |  32 +
 generator/GoLang.ml                           | 727 ++++++++++++++++++
 generator/GoLang.mli                          |  22 +
 generator/Makefile.am                         |   2 +
 generator/generator.ml                        |   9 +
 golang/Makefile.am                            |  70 ++
 golang/config-test.go                         |  34 +
 golang/examples/LICENSE-FOR-EXAMPLES          |  38 +
 golang/examples/Makefile.am                   |  21 +
 golang/run-tests.sh                           |  24 +
 golang/src/libguestfs.org/libnbd/.gitignore   |   4 +
 .../src/libguestfs.org/libnbd/aio_buffer.go   |  66 ++
 golang/src/libguestfs.org/libnbd/callbacks.go | 102 +++
 golang/src/libguestfs.org/libnbd/handle.go    | 134 ++++
 .../libnbd/libnbd_010_load_test.go            |  25 +
 .../libnbd/libnbd_100_handle_test.go          |  29 +
 .../libnbd/libnbd_200_connect_command_test.go |  36 +
 .../libnbd/libnbd_300_get_size_test.go        |  47 ++
 .../libnbd/libnbd_400_pread_test.go           |  55 ++
 .../libnbd_405_pread_structured_test.go       |  84 ++
 .../libnbd/libnbd_410_pwrite_test.go          |  58 ++
 .../libnbd/libnbd_460_block_status_test.go    | 120 +++
 .../libnbd/libnbd_500_aio_pread_test.go       |  68 ++
 .../libnbd/libnbd_510_aio_pwrite_test.go      |  75 ++
 .../libnbd/libnbd_590_aio_copy_test.go        | 213 +++++
 .../libnbd/libnbd_600_debug_callback_test.go  |  56 ++
 .../libnbd/libnbd_610_error_test.go           |  41 +
 run.in                                        |  19 +
 29 files changed, 2213 insertions(+)

diff --git a/Makefile.am b/Makefile.am
index bf2db68..a9f13ca 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -43,6 +43,8 @@ SUBDIRS = \
 	ocaml \
 	ocaml/examples \
 	ocaml/tests \
+	golang \
+	golang/examples \
 	interop \
 	fuzzing \
 	bash \
diff --git a/configure.ac b/configure.ac
index 9fd284b..36617fc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -403,6 +403,34 @@ AS_IF([test "x$enable_python" != "xno"],[
 AM_CONDITIONAL([HAVE_PYTHON],
     [test "x$PYTHON" != "xno" && test "x$have_python_module" = "x1" ])
 
+dnl Golang.
+AC_ARG_ENABLE([golang],
+    AS_HELP_STRING([--disable-golang], [disable Go language bindings]),
+        [],
+        [enable_golang=yes])
+AS_IF([test "x$enable_golang" != "xno"],[
+    AC_CHECK_PROG([GOLANG],[go],[go],[no])
+    AS_IF([test "x$GOLANG" != "xno"],[
+        AC_MSG_CHECKING([if $GOLANG is usable])
+        AS_IF([$GOLANG run $srcdir/golang/config-test.go 2>&AS_MESSAGE_LOG_FD],[
+            AC_MSG_RESULT([yes])
+
+            # Substitute some golang environment.
+            GOOS=`$GOLANG env GOOS`
+            GOARCH=`$GOLANG env GOARCH`
+            GOROOT=`$GOLANG env GOROOT`
+            AC_SUBST([GOOS])
+            AC_SUBST([GOARCH])
+            AC_SUBST([GOROOT])
+        ],[
+            AC_MSG_RESULT([no])
+            AC_MSG_WARN([golang ($GOLANG) is installed but not usable])
+            GOLANG=no
+        ])
+    ])
+],[GOLANG=no])
+AM_CONDITIONAL([HAVE_GOLANG],[test "x$GOLANG" != "xno"])
+
 dnl Produce output files.
 AC_CONFIG_HEADERS([config.h])
 
@@ -423,6 +451,8 @@ AC_CONFIG_FILES([Makefile
                  fuse/Makefile
                  fuzzing/Makefile
                  generator/Makefile
+                 golang/Makefile
+                 golang/examples/Makefile
                  include/Makefile
                  interop/Makefile
                  lib/Makefile
@@ -472,6 +502,8 @@ feature "Bash tab completion .................... " \
 echo
 echo "Language bindings:"
 echo
+feature "Go ..................................... " \
+        test "x$HAVE_GOLANG_TRUE" = "x"
 feature "OCaml .................................. " \
         test "x$HAVE_OCAML_TRUE" = "x"
 feature "Python ................................. " \
diff --git a/generator/GoLang.ml b/generator/GoLang.ml
new file mode 100644
index 0000000..ce33fab
--- /dev/null
+++ b/generator/GoLang.ml
@@ -0,0 +1,727 @@
+(* nbd client library in userspace: generator
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *)
+
+(* Go language bindings. *)
+
+open Printf
+
+open API
+open Utils
+
+(* Convert C function name to camel-case name.
+ * Regular but somewhat hit and miss.
+ *)
+let camel_case name =
+  let xs = nsplit "_" name in
+  List.fold_left (
+    fun a x ->
+      a ^ String.uppercase_ascii (Str.first_chars x 1) ^
+          String.lowercase_ascii (Str.string_after x 1)
+  ) "" xs
+
+let go_name_of_arg = function
+  | Bool n -> n
+  | BytesIn (n, len) -> n
+  | BytesOut (n, len) -> n
+  | BytesPersistIn (n, len) -> n
+  | BytesPersistOut (n, len) -> n
+  | Closure { cbname } -> cbname
+  | Enum (n, _) -> n
+  | Fd n -> n
+  | Flags (n, _) -> n
+  | Int n -> n
+  | Int64 n -> n
+  | Path n -> n
+  | SockAddrAndLen (n, len) -> n
+  | String n -> n
+  | StringList n -> n
+  | UInt n -> n
+  | UInt32 n -> n
+  | UInt64 n -> n
+
+let go_arg_type = function
+  | Bool _ -> "bool"
+  | BytesIn _ -> "[]byte"
+  | BytesPersistIn _ -> "AioBuffer"
+  | BytesOut _ -> "[]byte"
+  | BytesPersistOut _ -> "AioBuffer"
+  | Closure { cbname } -> sprintf "%sCallback" (camel_case cbname)
+  | Enum (_, { enum_prefix }) -> camel_case enum_prefix
+  | Fd _ -> "int"
+  | Flags (_, { flag_prefix }) -> camel_case flag_prefix
+  | Int _ -> "int"
+  | Int64 _ -> "int64"
+  | Path _ -> "string"
+  | SockAddrAndLen _ -> "string"
+  | String _ -> "string"
+  | StringList _ -> "[]string"
+  | UInt _ -> "uint"
+  | UInt32 _ -> "uint32"
+  | UInt64 _ -> "uint64"
+
+let go_name_of_optarg = function
+  | OClosure { cbname } -> sprintf "%sCallback" (camel_case cbname)
+  | OFlags (n, _) -> String.capitalize_ascii n
+
+let go_ret_type = function
+  (* RErr returns only the error, with no return value. *)
+  | RErr -> None
+  | RBool -> Some "bool"
+  | RStaticString -> Some "*string"
+  | RFd -> Some "int"
+  | RInt -> Some "uint"
+  | RInt64 -> Some "uint64"
+  | RCookie -> Some "uint64"
+  | RString -> Some "*string"
+  (* RUInt returns (type, error) for consistency, but the error is
+   * always nil.
+   *)
+  | RUInt -> Some "uint"
+
+let go_ret_error = function
+  | RErr -> None
+  | RBool -> Some "false"
+  | RStaticString -> Some "nil"
+  | RFd -> Some "0"
+  | RInt -> Some "0"
+  | RInt64 -> Some "0"
+  | RCookie -> Some "0"
+  | RString -> Some "nil"
+  | RUInt -> Some "0"
+
+let go_ret_c_errcode = function
+  | RBool -> Some "-1"
+  | RStaticString -> Some "nil"
+  | RErr -> Some "-1"
+  | RFd -> Some "-1"
+  | RInt -> Some "-1"
+  | RInt64 -> Some "-1"
+  | RCookie -> Some "-1"
+  | RString -> Some "nil"
+  | RUInt -> None
+
+(* We need a wrapper around every function (except Close) to
+ * handle errors because cgo calls are sequence points and
+ * could result in us being rescheduled on another thread,
+ * but libnbd error handling means we must call nbd_get_error
+ * etc from the same thread.
+ *)
+let print_wrapper (name, { args; optargs; ret }) =
+  let ret_c_type = C.type_of_ret ret and errcode = C.errcode_of_ret ret in
+  pr "%s\n" ret_c_type;
+  pr "_nbd_%s_wrapper (struct error *err,\n" name;
+  pr "        ";
+  C.print_arg_list ~wrap:true ~handle:true ~parens:false args optargs;
+  pr ")\n";
+  pr "{\n";
+  pr "  %s ret;\n" ret_c_type;
+  pr "\n";
+  pr "  ret = nbd_%s " name;
+  C.print_arg_list ~wrap:true ~handle:true ~types:false args optargs;
+  pr ";\n";
+  (match errcode with
+   | None -> ()
+   | Some errcode ->
+      pr "  if (ret == %s)\n" errcode;
+      pr "    save_error (err);\n";
+  );
+  pr "  return ret;\n";
+  pr "}\n";
+  pr "\n"
+
+(* C wrappers around callbacks. *)
+let print_callback_wrapper { cbname; cbargs } =
+  pr "int\n";
+  pr "_nbd_%s_callback_wrapper " cbname;
+  C.print_cbarg_list ~wrap:true cbargs;
+  pr "\n";
+  pr "{\n";
+  pr "  return %s_callback ((long)" cbname;
+  C.print_cbarg_list ~types:false ~parens:false cbargs;
+  pr ");\n";
+  pr "}\n";
+  pr "\n";
+  pr "void\n";
+  pr "_nbd_%s_callback_free (void *user_data)\n" cbname;
+  pr "{\n";
+  pr "  extern void freeCallbackId (long);\n";
+  pr "  freeCallbackId ((long)user_data);\n";
+  pr "}\n";
+  pr "\n"
+
+let print_binding (name, { args; optargs; ret; shortdesc }) =
+  let cname = camel_case name in
+
+  (* Tedious method of passing optional arguments in golang. *)
+  if optargs <> [] then (
+    pr "/* Struct carrying optional arguments for %s. */\n" cname;
+    pr "type %sOptargs struct {\n" cname;
+    List.iter (
+      fun optarg ->
+        let fname = go_name_of_optarg optarg in
+        pr "  /* %s field is ignored unless %sSet == true. */\n"
+          fname fname;
+        pr "  %sSet bool\n" fname;
+        pr "  %s " fname;
+        (match optarg with
+         | OClosure { cbname } -> pr "%sCallback" (camel_case cbname)
+         | OFlags (_, {flag_prefix}) -> pr "%s" (camel_case flag_prefix)
+        );
+        pr "\n"
+    ) optargs;
+    pr "}\n";
+    pr "\n";
+  );
+
+  (* Define the golang function which calls the C wrapper. *)
+  pr "/* %s: %s */\n" cname shortdesc;
+  pr "func (h *Libnbd) %s (" cname;
+  let comma = ref false in
+  List.iter (
+    fun arg ->
+      if !comma then pr ", ";
+      comma := true;
+      pr "%s %s" (go_name_of_arg arg) (go_arg_type arg)
+  ) args;
+  if optargs <> [] then (
+    if !comma then pr ", ";
+    comma := true;
+    pr "optargs *%sOptargs" cname
+  );
+  pr ") ";
+  (match go_ret_type ret with
+   | None -> pr "error"
+   | Some t -> pr "(%s, error)" t
+  );
+  pr " {\n";
+  pr "    if h.h == nil {\n";
+  (match go_ret_error ret with
+   | None -> pr "        return closed_handle_error (\"%s\")\n" name
+   | Some v -> pr "        return %s, closed_handle_error (\"%s\")\n" v name
+  );
+  pr "    }\n";
+  pr "\n";
+  pr "    var c_err C.struct_error\n";
+  List.iter (
+    function
+    | Bool n ->
+       pr "    c_%s := C.bool (%s)\n" n n
+    | BytesIn (n, len) ->
+       pr "    c_%s := unsafe.Pointer (&%s[0])\n" n n;
+       pr "    c_%s := C.size_t (len (%s))\n" len n;
+    | BytesOut (n, len) ->
+       pr "    c_%s := unsafe.Pointer (&%s[0])\n" n n;
+       pr "    c_%s := C.size_t (len (%s))\n" len n;
+    | BytesPersistIn (n, len) ->
+       pr "    c_%s := %s.P\n" n n;
+       pr "    c_%s := C.size_t (%s.Size)\n" len n;
+    | BytesPersistOut (n, len) ->
+       pr "    c_%s := %s.P\n" n n;
+       pr "    c_%s := C.size_t (%s.Size)\n" len n;
+    | Closure { cbname } ->
+       pr "    var c_%s C.nbd_%s_callback\n" cbname cbname;
+       pr "    c_%s.callback = (*[0]byte)(C._nbd_%s_callback_wrapper)\n"
+         cbname cbname;
+       pr "    c_%s.free = (*[0]byte)(C._nbd_%s_callback_free)\n"
+         cbname cbname;
+       pr "    c_%s.user_data = unsafe.Pointer (C.long_to_vp (C.long (registerCallbackId (%s))))\n"
+         cbname cbname
+    | Enum (n, _) ->
+       pr "    c_%s := C.int (%s)\n" n n
+    | Fd n ->
+       pr "    c_%s := C.int (%s)\n" n n
+    | Flags (n, _) ->
+       pr "    c_%s := C.uint32_t (%s)\n" n n
+    | Int n ->
+       pr "    c_%s := C.int (%s)\n" n n
+    | Int64 n ->
+       pr "    c_%s := C.int64_t (%s)\n" n n
+    | Path n ->
+       pr "    c_%s := C.CString (%s)\n" n n;
+       pr "    defer C.free (unsafe.Pointer (c_%s))\n" n
+    | SockAddrAndLen (n, len) ->
+       pr "    panic (\"SockAddrAndLen not supported\")\n";
+       pr "    var c_%s *C.struct_sockaddr\n" n;
+       pr "    var c_%s C.uint\n" len
+    | String n ->
+       pr "    c_%s := C.CString (%s)\n" n n;
+       pr "    defer C.free (unsafe.Pointer (c_%s))\n" n
+    | StringList n ->
+       pr "    c_%s := arg_string_list (%s)\n" n n;
+       pr "    defer free_string_list (c_%s)\n" n
+    | UInt n ->
+       pr "    c_%s := C.uint (%s)\n" n n
+    | UInt32 n ->
+       pr "    c_%s := C.uint32_t (%s)\n" n n
+    | UInt64 n ->
+       pr "    c_%s := C.uint64_t (%s)\n" n n
+  ) args;
+  if optargs <> [] then (
+    List.iter (
+      function
+      | OClosure { cbname } -> pr "    var c_%s C.nbd_%s_callback\n"
+                                 cbname cbname
+      | OFlags (n, _) -> pr "    var c_%s C.uint32_t\n" n
+    ) optargs;
+    pr "    if optargs != nil {\n";
+    List.iter (
+      fun optarg ->
+         pr "        if optargs.%sSet {\n" (go_name_of_optarg optarg);
+         (match optarg with
+          | OClosure { cbname } ->
+             pr "            c_%s.callback = (*[0]byte)(C._nbd_%s_callback_wrapper)\n"
+               cbname cbname;
+             pr "            c_%s.free = (*[0]byte)(C._nbd_%s_callback_free)\n"
+               cbname cbname;
+             pr "            c_%s.user_data = unsafe.Pointer (C.long_to_vp (C.long (registerCallbackId (optargs.%s))))\n"
+               cbname (go_name_of_optarg optarg)
+          | OFlags (n, _) ->
+             pr "            c_%s = C.uint32_t (optargs.%s)\n"
+               n (go_name_of_optarg optarg);
+         );
+         pr "        }\n";
+    ) optargs;
+    pr "    }\n";
+  );
+  pr "\n";
+  pr "    ret := C._nbd_%s_wrapper (&c_err, h.h" name;
+  List.iter (
+    function
+    | Bool n -> pr ", c_%s" n
+    | BytesIn (n, len) -> pr ", c_%s, c_%s" n len
+    | BytesOut (n, len) ->  pr ", c_%s, c_%s" n len
+    | BytesPersistIn (n, len) ->  pr ", c_%s, c_%s" n len
+    | BytesPersistOut (n, len) ->  pr ", c_%s, c_%s" n len
+    | Closure { cbname } ->  pr ", c_%s" cbname
+    | Enum (n, _) -> pr ", c_%s" n
+    | Fd n -> pr ", c_%s" n
+    | Flags (n, _) -> pr ", c_%s" n
+    | Int n -> pr ", c_%s" n
+    | Int64 n -> pr ", c_%s" n
+    | Path n -> pr ", c_%s" n
+    | SockAddrAndLen (n, len) -> pr ", c_%s, c_%s" n len
+    | String n -> pr ", c_%s" n
+    | StringList n -> pr ", &c_%s[0]" n
+    | UInt n -> pr ", c_%s" n
+    | UInt32 n -> pr ", c_%s" n
+    | UInt64 n -> pr ", c_%s" n
+  ) args;
+  List.iter (
+    function
+    | OClosure { cbname} -> pr ", c_%s" cbname
+    | OFlags (n, _) -> pr ", c_%s" n
+  ) optargs;
+  pr ")\n";
+
+  (* This ensures that we keep the handle alive until the C
+   * function has completed, in case all other references
+   * to the handle have disappeared and the finalizer would run.
+   *)
+  pr "    runtime.KeepAlive (h.h)\n";
+
+  let errcode = go_ret_c_errcode ret in
+  (match errcode with
+   | None -> ()
+   | Some errcode ->
+      pr "    if ret == %s {\n" errcode;
+      pr "        err := get_error (\"%s\", c_err)\n" name;
+      pr "        C.free_error (&c_err)\n";
+      (match go_ret_error ret with
+       | None -> pr "        return err\n"
+       | Some v -> pr "        return %s, err\n" v
+      );
+      pr "    }\n";
+  );
+  (match ret with
+   | RErr ->
+      pr "    return nil\n"
+   | RBool ->
+      pr "    r := int (ret)\n";
+      pr "    if r != 0 { return true, nil } else { return false, nil }\n"
+   | RStaticString ->
+      pr "    /* ret is statically allocated, do not free it. */\n";
+      pr "    r := C.GoString (ret);\n";
+      pr "    return &r, nil\n"
+   | RFd ->
+      pr "    return int (ret), nil\n"
+   | RInt ->
+      pr "    return uint (ret), nil\n"
+   | RInt64 ->
+      pr "    return uint64 (ret), nil\n"
+   | RCookie ->
+      pr "    return uint64 (ret), nil\n"
+   | RString ->
+      pr "    r := C.GoString (ret)\n";
+      pr "    C.free (unsafe.Pointer (ret))\n";
+      pr "    return &r, nil\n"
+   | RUInt ->
+      pr "    return uint (ret), nil\n"
+  );
+  pr "}\n";
+  pr "\n"
+
+let generate_golang_bindings_go () =
+  generate_header CStyle;
+
+  pr "\
+package libnbd
+
+/*
+#cgo pkg-config: libnbd
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include \"libnbd.h\"
+#include \"wrappers.h\"
+
+// There must be no blank line between end comment and import!
+// https://github.com/golang/go/issues/9733
+*/
+import \"C\"
+
+import (
+    \"runtime\"
+    \"unsafe\"
+)
+
+/* Enums. */
+";
+  List.iter (
+    fun { enum_prefix; enums } ->
+      pr "type %s int\n" (camel_case enum_prefix);
+      pr "const (\n";
+      List.iter (
+        fun (enum, v) ->
+          pr "    %s_%s = %s(%d)\n" enum_prefix enum (camel_case enum_prefix) v
+      ) enums;
+      pr ")\n";
+      pr "\n"
+  ) all_enums;
+
+  pr "\
+/* Flags. */
+";
+  List.iter (
+    fun { flag_prefix; flags } ->
+      pr "type %s uint32\n" (camel_case flag_prefix);
+      pr "const (\n";
+      List.iter (
+        fun (flag, v) ->
+          pr "    %s_%s = %s(%d)\n" flag_prefix flag (camel_case flag_prefix) v
+      ) flags;
+      pr ")\n";
+      pr "\n"
+  ) all_flags;
+
+  pr "\
+/* Constants. */
+const (
+";
+  List.iter (
+    fun (n, v) -> pr "    %s uint32 = %d\n" n v
+  ) constants;
+  List.iter (
+    fun (ns, ctxts) ->
+      pr "    namespace_%s = \"%s:\"\n" ns ns;
+      List.iter (
+        fun (ctxt, consts) ->
+          pr "    context_%s_%s = \"%s:%s\"\n" ns ctxt ns ctxt;
+          List.iter (fun (n, v) ->
+              pr "    %s uint32 = %d\n" n v
+          ) consts
+      ) ctxts;
+  ) metadata_namespaces;
+
+  pr ")\n\n";
+
+  (* Bindings. *)
+  List.iter print_binding handle_calls
+
+let generate_golang_closures_go () =
+  generate_header CStyle;
+
+  pr "\
+package libnbd
+
+/*
+#cgo pkg-config: libnbd
+
+#include <config.h>
+
+#include <stdlib.h>
+
+#include \"libnbd.h\"
+#include \"wrappers.h\"
+*/
+import \"C\"
+
+import \"unsafe\"
+
+/* Closures. */
+
+func copy_uint32_array (entries *C.uint32_t, count C.size_t) []uint32 {
+    ret := make([]uint32, int (count))
+    for i := 0; i < int (count); i++ {
+       entry := (*C.uint32_t) (unsafe.Pointer(uintptr(unsafe.Pointer(entries)) + (unsafe.Sizeof(*entries) * uintptr(i))))
+       ret[i] = uint32 (*entry)
+    }
+    return ret
+}
+";
+
+  List.iter (
+    fun { cbname; cbargs } ->
+      let uname = camel_case cbname in
+      pr "type %sCallback func (" uname;
+      let comma = ref false in
+      List.iter (
+        fun cbarg ->
+          if !comma then pr ", "; comma := true;
+          match cbarg with
+          | CBArrayAndLen (UInt32 n, _) ->
+             pr "%s []uint32" n;
+          | CBBytesIn (n, len) ->
+             pr "%s []byte" n;
+          | CBInt n ->
+             pr "%s int" n
+          | CBUInt n ->
+             pr "%s uint" n
+          | CBInt64 n ->
+             pr "%s int64" n
+          | CBString n ->
+             pr "%s string" n
+          | CBUInt64 n ->
+             pr "%s uint64" n
+          | CBMutable (Int n) ->
+             pr "%s *int" n
+          | CBArrayAndLen _ | CBMutable _ -> assert false
+      ) cbargs;
+      pr ") int\n";
+      pr "\n";
+      pr "//export %s_callback\n" cbname;
+      pr "func %s_callback (callbackid C.long" cbname;
+      List.iter (
+        fun cbarg ->
+          pr ", ";
+          match cbarg with
+          | CBArrayAndLen (UInt32 n, count) ->
+             pr "%s *C.uint32_t, %s C.size_t" n count
+          | CBBytesIn (n, len) ->
+             pr "%s unsafe.Pointer, %s C.size_t" n len
+          | CBInt n ->
+             pr "%s C.int" n
+          | CBUInt n ->
+             pr "%s C.uint" n
+          | CBInt64 n ->
+             pr "%s C.int64_t" n
+          | CBString n ->
+             pr "%s *C.char" n
+          | CBUInt64 n ->
+             pr "%s C.uint64_t" n
+          | CBMutable (Int n) ->
+             pr "%s *C.int" n
+          | CBArrayAndLen _ | CBMutable _ -> assert false
+      ) cbargs;
+      pr ") C.int {\n";
+      pr "    callbackFunc := getCallbackId (int (callbackid));\n";
+      pr "    callback, ok := callbackFunc.(%sCallback);\n" uname;
+      pr "    if !ok {\n";
+      pr "        panic (\"inappropriate callback type\");\n";
+      pr "    }\n";
+
+      (* Deal with mutable int by creating a local variable
+       * and passing a pointer to it to the callback.
+       *)
+      List.iter (
+        fun cbarg ->
+          match cbarg with
+          | CBMutable (Int n) ->
+             pr "    go_%s := int (*%s)\n" n n
+          | _ -> ()
+      ) cbargs;
+
+      pr "    ret := callback (";
+      let comma = ref false in
+      List.iter (
+        fun cbarg ->
+          if !comma then pr ", "; comma := true;
+          match cbarg with
+          | CBArrayAndLen (UInt32 n, count) ->
+             pr "copy_uint32_array (%s, %s)" n count
+          | CBBytesIn (n, len) ->
+             pr "C.GoBytes (%s, C.int (%s))" n len
+          | CBInt n ->
+             pr "int (%s)" n
+          | CBUInt n ->
+             pr "uint (%s)" n
+          | CBInt64 n ->
+             pr "int64 (%s)" n
+          | CBString n ->
+             pr "C.GoString (%s)" n
+          | CBUInt64 n ->
+             pr "uint64 (%s)" n
+          | CBMutable (Int n) ->
+             pr "&go_%s" n
+          | CBArrayAndLen _ | CBMutable _ -> assert false
+      ) cbargs;
+      pr ")\n";
+
+      List.iter (
+        fun cbarg ->
+          match cbarg with
+          | CBMutable (Int n) ->
+             pr "    *%s = C.int (go_%s)\n" n n
+          | _ -> ()
+      ) cbargs;
+      pr "    return C.int (ret);\n";
+      pr "}\n";
+      pr "\n"
+  ) all_closures
+
+let generate_golang_wrappers_go () =
+  generate_header CStyle;
+
+  pr "\
+package libnbd
+
+/*
+#cgo pkg-config: libnbd
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include \"libnbd.h\"
+#include \"wrappers.h\"
+
+";
+
+  (* Wrappers. *)
+  List.iter print_wrapper handle_calls;
+
+  (* Callback wrappers. *)
+  List.iter print_callback_wrapper all_closures;
+
+  pr "\
+// There must be no blank line between end comment and import!
+// https://github.com/golang/go/issues/9733
+*/
+import \"C\"
+"
+
+let generate_golang_wrappers_h () =
+  generate_header CStyle;
+
+  pr "\
+#ifndef LIBNBD_GOLANG_WRAPPERS_H
+#define LIBNBD_GOLANG_WRAPPERS_H
+
+#include <string.h>
+
+#include \"libnbd.h\"
+
+/* When calling callbacks we pass the callback ID (an int) in
+ * the void *user_data field.  I couldn't work out how to do
+ * this in golang, so this helper function does the cast.
+ */
+static inline void *long_to_vp (long i) { return (void *)(intptr_t)i; }
+
+struct error {
+  char *error;
+  int errnum;
+};
+
+static inline void
+save_error (struct error *err)
+{
+  err->error = strdup (nbd_get_error ());
+  err->errnum = nbd_get_errno ();
+}
+
+static inline void
+free_error (struct error *err)
+{
+  free (err->error);
+}
+
+";
+
+  (* Function decl for each wrapper. *)
+  List.iter (
+    fun (name, { args; optargs; ret }) ->
+      let ret_c_type = C.type_of_ret ret in
+      pr "%s _nbd_%s_wrapper (struct error *err,\n" ret_c_type name;
+      pr "        ";
+      C.print_arg_list ~wrap:true ~handle:true ~parens:false args optargs;
+      pr ");\n";
+  ) handle_calls;
+  pr "\n";
+
+  (* Function decl for each callback wrapper. *)
+  List.iter (
+    fun { cbname; cbargs } ->
+      (*
+       * It would be nice to do this, but it basically means we have
+       * to guess the prototype that golang will generate for a
+       * golang exported function.  Also golang doesn't bother with
+       * const-correctness.
+       pr "extern int %s_callback (long callbackid" cbname;
+       List.iter (
+         fun cbarg ->
+           pr ", ";
+           match cbarg with
+           | CBArrayAndLen (UInt32 n, count) ->
+              pr "uint32_t *%s, size_t %s" n count
+           | CBBytesIn (n, len) ->
+              pr "void *%s, size_t %s" n len
+           | CBInt n ->
+              pr "int %s" n
+           | CBUInt n ->
+              pr "unsigned int %s" n
+           | CBInt64 n ->
+              pr "int64_t %s" n
+           | CBString n ->
+              pr "char *%s" n
+           | CBUInt64 n ->
+              pr "uint64_t *%s" n
+           | CBMutable (Int n) ->
+              pr "int *%s" n
+           | CBArrayAndLen _ | CBMutable _ -> assert false
+       ) cbargs;
+       pr ");\n";
+       pr "\n";
+       * So instead we do this:
+       *)
+      pr "extern int %s_callback ();\n" cbname;
+      pr "\n";
+      pr "int _nbd_%s_callback_wrapper " cbname;
+      C.print_cbarg_list ~wrap:true cbargs;
+      pr ";\n";
+      pr "void _nbd_%s_callback_free (void *user_data);\n" cbname;
+      pr "\n";
+  ) all_closures;
+
+  pr "\
+#endif /* LIBNBD_GOLANG_WRAPPERS_H */
+"
diff --git a/generator/GoLang.mli b/generator/GoLang.mli
new file mode 100644
index 0000000..fe1ad4e
--- /dev/null
+++ b/generator/GoLang.mli
@@ -0,0 +1,22 @@
+(* nbd client library in userspace: generator
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *)
+
+val generate_golang_bindings_go : unit -> unit
+val generate_golang_closures_go : unit -> unit
+val generate_golang_wrappers_go : unit -> unit
+val generate_golang_wrappers_h : unit -> unit
diff --git a/generator/Makefile.am b/generator/Makefile.am
index e499ca8..0389d70 100644
--- a/generator/Makefile.am
+++ b/generator/Makefile.am
@@ -57,6 +57,8 @@ sources = \
 	Python.ml \
 	OCaml.mli \
 	OCaml.ml \
+	GoLang.mli \
+	GoLang.ml \
 	generator.ml \
 	$(NULL)
 
diff --git a/generator/generator.ml b/generator/generator.ml
index 1b151c3..804d9ea 100644
--- a/generator/generator.ml
+++ b/generator/generator.ml
@@ -56,3 +56,12 @@ let () =
   output_to "ocaml/NBD.mli" OCaml.generate_ocaml_nbd_mli;
   output_to "ocaml/NBD.ml" OCaml.generate_ocaml_nbd_ml;
   output_to "ocaml/nbd-c.c" OCaml.generate_ocaml_nbd_c;
+
+  output_to "golang/src/libguestfs.org/libnbd/bindings.go"
+    GoLang.generate_golang_bindings_go;
+  output_to "golang/src/libguestfs.org/libnbd/closures.go"
+    GoLang.generate_golang_closures_go;
+  output_to "golang/src/libguestfs.org/libnbd/wrappers.go"
+    GoLang.generate_golang_wrappers_go;
+  output_to "golang/src/libguestfs.org/libnbd/wrappers.h"
+    GoLang.generate_golang_wrappers_h;
diff --git a/golang/Makefile.am b/golang/Makefile.am
new file mode 100644
index 0000000..38a8785
--- /dev/null
+++ b/golang/Makefile.am
@@ -0,0 +1,70 @@
+# nbd client library in userspace
+# Copyright (C) 2013-2020 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+include $(top_srcdir)/subdir-rules.mk
+
+# http://golang.org/doc/code.html#Organization
+pkg = libguestfs.org/libnbd
+
+source_files = \
+	src/$(pkg)/aio_buffer.go \
+	src/$(pkg)/bindings.go \
+	src/$(pkg)/callbacks.go \
+	src/$(pkg)/closures.go \
+	src/$(pkg)/handle.go \
+	src/$(pkg)/wrappers.go \
+	src/$(pkg)/wrappers.h \
+	src/$(pkg)/libnbd_*_test.go
+
+generator_built = \
+	src/$(pkg)/bindings.go \
+	src/$(pkg)/closures.go \
+	src/$(pkg)/wrappers.go \
+	src/$(pkg)/wrappers.h
+
+EXTRA_DIST = \
+	src/$(pkg)/.gitignore \
+	src/$(pkg)/aio_buffer.go \
+	src/$(pkg)/callbacks.go \
+	src/$(pkg)/handle.go \
+	$(generator_built) \
+	config-test.go \
+	run-tests.sh
+
+if HAVE_GOLANG
+
+golangpkgdir = $(GOROOT)/pkg/$(GOOS)_$(GOARCH)/$(pkg)
+golangsrcdir = $(GOROOT)/src/pkg/$(pkg)
+
+golangpkg_DATA = \
+	pkg/$(GOOS)_$(GOARCH)/$(pkg).a
+
+pkg/$(GOOS)_$(GOARCH)/$(pkg).a: $(source_files)
+	$(top_builddir)/run $(GOLANG) install $(pkg)
+
+golangsrc_DATA = $(source_files)
+
+TESTS_ENVIRONMENT = pkg=$(pkg) LIBNBD_DEBUG=1
+LOG_COMPILER = $(top_builddir)/run
+TESTS = run-tests.sh
+
+endif
+
+CLEANFILES += src/$(pkg)/*~
+
+clean-local:
+	rm -rf pkg
diff --git a/golang/config-test.go b/golang/config-test.go
new file mode 100644
index 0000000..e104c71
--- /dev/null
+++ b/golang/config-test.go
@@ -0,0 +1,34 @@
+/* libnbd Go configuration test
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/* This is called from ./configure to check that golang works
+ * and is above the minimum required version.
+ */
+
+package main
+
+func main() {
+	/* XXX Check for minimum runtime.Version() >= "go1.1.1"
+         * Unfortunately go version numbers are not easy to parse.
+         * They have the 3 formats "goX.Y.Z", "release.rN" or
+         * "weekly.YYYY-MM-DD".  The latter two formats are mostly
+         * useless, and the first one is hard to parse.  See also
+         * cmpGoVersion in
+         * http://web.archive.org/web/20130402235148/http://golang.org/src/cmd/go/get.go?m=text
+         */
+}
diff --git a/golang/examples/LICENSE-FOR-EXAMPLES b/golang/examples/LICENSE-FOR-EXAMPLES
new file mode 100644
index 0000000..4986466
--- /dev/null
+++ b/golang/examples/LICENSE-FOR-EXAMPLES
@@ -0,0 +1,38 @@
+The files in the golang/examples/ directory are licensed under this
+very permissive BSD license.  This means that you can copy, use, adapt
+and modify them without any significant restrictions.  You can also
+combine them with proprietary code or include them in code that is
+distributed under other open source licenses.
+
+----------------------------------------------------------------------
+
+libnbd examples
+Copyright (C) 2013-2020 Red Hat Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+* Neither the name of Red Hat nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/golang/examples/Makefile.am b/golang/examples/Makefile.am
new file mode 100644
index 0000000..90498ab
--- /dev/null
+++ b/golang/examples/Makefile.am
@@ -0,0 +1,21 @@
+# nbd client library in userspace
+# Copyright (C) 2013-2020 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+include $(top_srcdir)/subdir-rules.mk
+
+EXTRA_DIST = \
+	LICENSE-FOR-EXAMPLES
diff --git a/golang/run-tests.sh b/golang/run-tests.sh
new file mode 100755
index 0000000..a155819
--- /dev/null
+++ b/golang/run-tests.sh
@@ -0,0 +1,24 @@
+#!/bin/sh -
+# nbd client library in userspace
+# Copyright (C) 2013-2020 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+set -e
+
+# The -count=1 parameter is the "idiomatic way to bypass test caching".
+# https://golang.org/doc/go1.10#test
+# The -v option enables verbose output.
+$GOLANG test -count=1 -v $pkg
diff --git a/golang/src/libguestfs.org/libnbd/.gitignore b/golang/src/libguestfs.org/libnbd/.gitignore
new file mode 100644
index 0000000..653f279
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/.gitignore
@@ -0,0 +1,4 @@
+/bindings.go
+/closures.go
+/wrappers.go
+/wrappers.h
diff --git a/golang/src/libguestfs.org/libnbd/aio_buffer.go b/golang/src/libguestfs.org/libnbd/aio_buffer.go
new file mode 100644
index 0000000..325f781
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/aio_buffer.go
@@ -0,0 +1,66 @@
+/* libnbd golang handle.
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+/*
+#cgo pkg-config: libnbd
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "libnbd.h"
+#include "wrappers.h"
+
+*/
+import "C"
+
+import "unsafe"
+
+/* Asynchronous I/O buffer. */
+type AioBuffer struct {
+	P    unsafe.Pointer
+	Size uint
+}
+
+func MakeAioBuffer(size uint) AioBuffer {
+	return AioBuffer{C.malloc(C.ulong(size)), size}
+}
+
+func FromBytes(buf []byte) AioBuffer {
+	size := len(buf)
+	ret := MakeAioBuffer(uint(size))
+	for i := 0; i < len(buf); i++ {
+		*ret.Get(uint(i)) = buf[i]
+	}
+	return ret
+}
+
+func (b *AioBuffer) Free() {
+	C.free(b.P)
+}
+
+func (b *AioBuffer) Bytes() []byte {
+	return C.GoBytes(b.P, C.int(b.Size))
+}
+
+func (b *AioBuffer) Get(i uint) *byte {
+	return (*byte)(unsafe.Pointer(uintptr(b.P) + uintptr(i)))
+}
diff --git a/golang/src/libguestfs.org/libnbd/callbacks.go b/golang/src/libguestfs.org/libnbd/callbacks.go
new file mode 100644
index 0000000..1ae8d8e
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/callbacks.go
@@ -0,0 +1,102 @@
+/*
+ * This file is part of the libvirt-go project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Copyright (c) 2013 Alex Zorin
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ */
+
+package libnbd
+
+// Helpers functions to register a Go callback function to a C
+// function. For a simple example, look at how SetErrorFunc works in
+// error.go.
+//
+// - Create a struct that will contain at least the Go callback to
+//   invoke (errorContext).
+//
+// - Create an exported Golang function whose job will be to retrieve
+//   the context and execute the callback in it
+//   (connErrCallback). Such a function should receive a callback ID
+//   and will use it to retrive the context.
+//
+// - Create a CGO function similar to the above function but with the
+//   appropriate signature to be registered as a callback in C code
+//   (connErrCallbackHelper). Notably, it will have a void* argument
+//   that should be cast to long to retrieve the callback ID. It
+//   should be just a thin wrapper to transform the opaque argument to
+//   a callback ID.
+//
+// - Create a CGO function which will be a wrapper around the C
+//   function to register the callback (virConnSetErrorFuncWrapper). Its
+//   only role is to transform a callback ID (long) to an opaque (void*)
+//   and call the C function.
+//
+// - When setting up a callback (SetErrorFunc), register the struct from first step
+//   with registerCallbackId and invoke the CGO function from the
+//   previous step with the appropriate ID.
+//
+// - When unregistering the callback, don't forget to call freecallbackId.
+//
+// If you need to associate some additional data with the connection,
+// look at saveConnectionData, getConnectionData and
+// releaseConnectionData.
+
+import "C"
+
+import (
+	"sync"
+)
+
+const firstGoCallbackId int = 100 // help catch some additional errors during test
+var goCallbackLock sync.RWMutex
+var goCallbacks = make(map[int]interface{})
+var nextGoCallbackId int = firstGoCallbackId
+
+//export freeCallbackId
+func freeCallbackId(goCallbackId int) {
+	goCallbackLock.Lock()
+	delete(goCallbacks, goCallbackId)
+	goCallbackLock.Unlock()
+}
+
+func getCallbackId(goCallbackId int) interface{} {
+	goCallbackLock.RLock()
+	ctx := goCallbacks[goCallbackId]
+	goCallbackLock.RUnlock()
+	if ctx == nil {
+		// If this happens there must be a bug in libvirt
+		panic("Callback arrived after freeCallbackId was called")
+	}
+	return ctx
+}
+
+func registerCallbackId(ctx interface{}) int {
+	goCallbackLock.Lock()
+	goCallBackId := nextGoCallbackId
+	nextGoCallbackId++
+	for goCallbacks[nextGoCallbackId] != nil {
+		nextGoCallbackId++
+	}
+	goCallbacks[goCallBackId] = ctx
+	goCallbackLock.Unlock()
+	return goCallBackId
+}
diff --git a/golang/src/libguestfs.org/libnbd/handle.go b/golang/src/libguestfs.org/libnbd/handle.go
new file mode 100644
index 0000000..a227790
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/handle.go
@@ -0,0 +1,134 @@
+/* libnbd golang handle.
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+/*
+#cgo pkg-config: libnbd
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "libnbd.h"
+#include "wrappers.h"
+
+static struct nbd_handle *
+_nbd_create_wrapper (struct error *err)
+{
+  struct nbd_handle *ret;
+
+  ret = nbd_create ();
+  if (ret == NULL)
+    save_error (err);
+  return ret;
+}
+*/
+import "C"
+
+import (
+	"fmt"
+	"runtime"
+	"syscall"
+	"unsafe"
+)
+
+/* Handle. */
+type Libnbd struct {
+	h *C.struct_nbd_handle
+}
+
+/* Convert handle to string (just for debugging). */
+func (h *Libnbd) String() string {
+	return "&Libnbd{}"
+}
+
+/* All functions (except Close) return ([result,] LibnbdError). */
+type LibnbdError struct {
+	Op     string        // operation which failed
+	Errmsg string        // string (nbd_get_error)
+	Errno  syscall.Errno // errno (nbd_get_errno)
+}
+
+func (e *LibnbdError) String() string {
+	if e.Errno != 0 {
+		return fmt.Sprintf("%s: %s", e.Op, e.Errmsg)
+	} else {
+		return fmt.Sprintf("%s: %s: %s", e.Op, e.Errmsg, e.Errno)
+	}
+}
+
+/* Implement the error interface */
+func (e *LibnbdError) Error() string {
+	return e.String()
+}
+
+func get_error(op string, c_err C.struct_error) *LibnbdError {
+	errmsg := C.GoString(c_err.error)
+	errno := syscall.Errno(c_err.errnum)
+	return &LibnbdError{Op: op, Errmsg: errmsg, Errno: errno}
+}
+
+func closed_handle_error(op string) *LibnbdError {
+	return &LibnbdError{Op: op, Errmsg: "handle is closed",
+		Errno: syscall.Errno(0)}
+}
+
+/* Create a new handle. */
+func Create() (*Libnbd, error) {
+	var c_err C.struct_error
+	c_h := C._nbd_create_wrapper(&c_err)
+	if c_h == nil {
+		err := get_error("create", c_err)
+		C.free_error(&c_err)
+		return nil, err
+	}
+	h := &Libnbd{h: c_h}
+	// Finalizers aren't guaranteed to run, but try having one anyway ...
+	runtime.SetFinalizer(h, (*Libnbd).Close)
+	return h, nil
+}
+
+/* Close the handle. */
+func (h *Libnbd) Close() *LibnbdError {
+	if h.h == nil {
+		return closed_handle_error("close")
+	}
+	C.nbd_close(h.h)
+	h.h = nil
+	return nil
+}
+
+/* Functions for translating between NULL-terminated lists of
+ * C strings and golang []string.
+ */
+func arg_string_list(xs []string) []*C.char {
+	r := make([]*C.char, 1+len(xs))
+	for i, x := range xs {
+		r[i] = C.CString(x)
+	}
+	r[len(xs)] = nil
+	return r
+}
+
+func free_string_list(argv []*C.char) {
+	for i := 0; argv[i] != nil; i++ {
+		C.free(unsafe.Pointer(argv[i]))
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_010_load_test.go b/golang/src/libguestfs.org/libnbd/libnbd_010_load_test.go
new file mode 100644
index 0000000..1b7948c
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_010_load_test.go
@@ -0,0 +1,25 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "testing"
+
+func Test010Load(t *testing.T) {
+	/* Nothing - just test that the library can be linked to. */
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_100_handle_test.go b/golang/src/libguestfs.org/libnbd/libnbd_100_handle_test.go
new file mode 100644
index 0000000..c14c981
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_100_handle_test.go
@@ -0,0 +1,29 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "testing"
+
+func Test100Handle(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	h.Close()
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_200_connect_command_test.go b/golang/src/libguestfs.org/libnbd/libnbd_200_connect_command_test.go
new file mode 100644
index 0000000..cf2d639
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_200_connect_command_test.go
@@ -0,0 +1,36 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "testing"
+
+func Test200ConnectCommand(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v", "null",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_300_get_size_test.go b/golang/src/libguestfs.org/libnbd/libnbd_300_get_size_test.go
new file mode 100644
index 0000000..52383be
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_300_get_size_test.go
@@ -0,0 +1,47 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "fmt"
+import "testing"
+
+func Test300GetSize(t *testing.T) {
+	expected := uint64(1048576)
+
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v", "null",
+		fmt.Sprintf("size=%d", expected),
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+	actual, err := h.GetSize()
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if actual != expected {
+		t.Fatalf("actual %d != expected %d", actual, expected)
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_400_pread_test.go b/golang/src/libguestfs.org/libnbd/libnbd_400_pread_test.go
new file mode 100644
index 0000000..c9553d3
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_400_pread_test.go
@@ -0,0 +1,55 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "bytes"
+import "encoding/binary"
+import "testing"
+
+func Test400PRead(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"pattern", "size=512",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	buf := make([]byte, 512)
+	err = h.Pread(buf, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	// Expected data.
+	expected := make([]byte, 512)
+	for i := 0; i < 512; i += 8 {
+		binary.BigEndian.PutUint64(expected[i:i+8], uint64(i))
+	}
+
+	if !bytes.Equal(buf, expected) {
+		t.Fatalf("did not read expected data")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_405_pread_structured_test.go b/golang/src/libguestfs.org/libnbd/libnbd_405_pread_structured_test.go
new file mode 100644
index 0000000..4a4d1be
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_405_pread_structured_test.go
@@ -0,0 +1,84 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "bytes"
+import "encoding/binary"
+import "fmt"
+import "testing"
+
+var expected405 = make([]byte, 512)
+
+func psf(buf2 []byte, offset uint64, status uint, error *int) int {
+	if *error != 0 {
+		panic("expected *error == 0")
+	}
+	if offset != 0 {
+		panic("expected offset == 0")
+	}
+	if status != uint(READ_DATA) {
+		panic("expected status == READ_DATA")
+	}
+	if !bytes.Equal(buf2, expected405) {
+		fmt.Printf("buf2 = %s\nexpected = %s\n", buf2, expected405)
+		panic("expected sub-buffer == expected pattern")
+	}
+	return 0
+}
+
+func Test405PReadStructured(t *testing.T) {
+	// Expected data.
+	for i := 0; i < 512; i += 8 {
+		binary.BigEndian.PutUint64(expected405[i:i+8], uint64(i))
+	}
+
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"pattern", "size=512",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	buf := make([]byte, 512)
+	err = h.PreadStructured(buf, 0, psf, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if !bytes.Equal(buf, expected405) {
+		t.Fatalf("did not read expected data")
+	}
+
+	var optargs PreadStructuredOptargs
+	optargs.FlagsSet = true
+	optargs.Flags = CMD_FLAG_DF
+	err = h.PreadStructured(buf, 0, psf, &optargs)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if !bytes.Equal(buf, expected405) {
+		t.Fatalf("did not read expected data")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_410_pwrite_test.go b/golang/src/libguestfs.org/libnbd/libnbd_410_pwrite_test.go
new file mode 100644
index 0000000..bf7f440
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_410_pwrite_test.go
@@ -0,0 +1,58 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "bytes"
+import "testing"
+
+func Test410PWrite(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"memory", "size=512",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	/* Write a pattern and read it back. */
+	buf := make([]byte, 512)
+	for i := 0; i < 512; i += 2 {
+		buf[i] = 0x55
+		buf[i+1] = 0xAA
+	}
+	err = h.Pwrite(buf, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	buf2 := make([]byte, 512)
+	err = h.Pread(buf2, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	if !bytes.Equal(buf, buf2) {
+		t.Fatalf("did not read back same data as written")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_460_block_status_test.go b/golang/src/libguestfs.org/libnbd/libnbd_460_block_status_test.go
new file mode 100644
index 0000000..3bedcae
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_460_block_status_test.go
@@ -0,0 +1,120 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+)
+
+var srcdir = os.Getenv("srcdir")
+var script = srcdir + "/../../../../tests/meta-base-allocation.sh"
+
+var entries []uint32
+
+func mcf(metacontext string, offset uint64, e []uint32, error *int) int {
+	if *error != 0 {
+		panic("expected *error == 0")
+	}
+	if metacontext == "base:allocation" {
+		entries = e
+	}
+	return 0
+}
+
+// Seriously WTF?
+func mc_compare(a1 []uint32, a2 []uint32) bool {
+	if len(a1) != len(a2) {
+		return false
+	}
+	for i := 0; i < len(a1); i++ {
+		if a1[i] != a2[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// Like, WTF, again?
+func mc_to_string(a []uint32) string {
+	ss := make([]string, len(a))
+	for i := 0; i < len(a); i++ {
+		ss[i] = fmt.Sprint(a[i])
+	}
+	return strings.Join(ss, ", ")
+}
+
+func Test460BlockStatus(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.AddMetaContext("base:allocation")
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"sh", script,
+	})
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	err = h.BlockStatus(65536, 0, mcf, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if !mc_compare(entries, []uint32{
+		8192, 0,
+		8192, 1,
+		16384, 3,
+		16384, 2,
+		16384, 0,
+	}) {
+		t.Fatalf("unexpected entries (1): %s", mc_to_string(entries))
+	}
+
+	err = h.BlockStatus(1024, 32256, mcf, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if !mc_compare(entries, []uint32{
+		512, 3,
+		16384, 2,
+	}) {
+		t.Fatalf("unexpected entries (2): %s", mc_to_string(entries))
+	}
+
+	var optargs BlockStatusOptargs
+	optargs.FlagsSet = true
+	optargs.Flags = CMD_FLAG_REQ_ONE
+	err = h.BlockStatus(1024, 32256, mcf, &optargs)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	if !mc_compare(entries, []uint32{512, 3}) {
+		t.Fatalf("unexpected entries (3): %s", mc_to_string(entries))
+	}
+
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_500_aio_pread_test.go b/golang/src/libguestfs.org/libnbd/libnbd_500_aio_pread_test.go
new file mode 100644
index 0000000..fd5e1a2
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_500_aio_pread_test.go
@@ -0,0 +1,68 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "bytes"
+import "encoding/binary"
+import "testing"
+
+func Test500AioPRead(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"pattern", "size=512",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	buf := MakeAioBuffer(512)
+	defer buf.Free()
+	var cookie uint64
+	cookie, err = h.AioPread(buf, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	for {
+		var b bool
+		b, err = h.AioCommandCompleted(cookie)
+		if err != nil {
+			t.Fatalf("%s", err)
+		}
+		if b {
+			break
+		}
+		h.Poll(-1)
+	}
+
+	// Expected data.
+	expected := make([]byte, 512)
+	for i := 0; i < 512; i += 8 {
+		binary.BigEndian.PutUint64(expected[i:i+8], uint64(i))
+	}
+
+	if !bytes.Equal(buf.Bytes(), expected) {
+		t.Fatalf("did not read expected data")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_510_aio_pwrite_test.go b/golang/src/libguestfs.org/libnbd/libnbd_510_aio_pwrite_test.go
new file mode 100644
index 0000000..335cd1e
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_510_aio_pwrite_test.go
@@ -0,0 +1,75 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "bytes"
+import "testing"
+
+func Test510AioPWrite(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"memory", "size=512",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	/* Write a pattern and read it back. */
+	buf := MakeAioBuffer(512)
+	defer buf.Free()
+	for i := 0; i < 512; i += 2 {
+		*buf.Get(uint(i)) = 0x55
+		*buf.Get(uint(i + 1)) = 0xAA
+	}
+
+	var cookie uint64
+	cookie, err = h.AioPwrite(buf, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	for {
+		var b bool
+		b, err = h.AioCommandCompleted(cookie)
+		if err != nil {
+			t.Fatalf("%s", err)
+		}
+		if b {
+			break
+		}
+		h.Poll(-1)
+	}
+
+	/* We already tested aio_pread, let's just read the data
+	back in the regular synchronous way. */
+	buf2 := make([]byte, 512)
+	err = h.Pread(buf2, 0, nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	if !bytes.Equal(buf.Bytes(), buf2) {
+		t.Fatalf("did not read back same data as written")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_590_aio_copy_test.go b/golang/src/libguestfs.org/libnbd/libnbd_590_aio_copy_test.go
new file mode 100644
index 0000000..13e9102
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_590_aio_copy_test.go
@@ -0,0 +1,213 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "fmt"
+import "syscall"
+import "testing"
+
+var disk_size = uint(512 * 1024 * 1024)
+var bs = uint64(65536)
+var max_reads_in_flight = uint(16)
+var bytes_read = uint(0)
+var bytes_written = uint(0)
+
+/* Functions to handle FdSet.
+   XXX These probably only work on 64 bit platforms. */
+func fdset_set(set *syscall.FdSet, fd int) {
+	(*set).Bits[fd/64] |= 1 << (uintptr(fd) % 64)
+}
+
+func fdset_test(set *syscall.FdSet, fd int) bool {
+	return (*set).Bits[fd/64]&(1<<(uintptr(fd)%64)) != 0
+}
+
+/* Functions to test socket direction. */
+func dir_is_read(h *Libnbd) bool {
+	dir, _ := h.AioGetDirection()
+	return (uint32(dir) & AIO_DIRECTION_READ) != 0
+}
+func dir_is_write(h *Libnbd) bool {
+	dir, _ := h.AioGetDirection()
+	return (uint32(dir) & AIO_DIRECTION_WRITE) != 0
+}
+
+/* Queue of writes. */
+type wbuf struct {
+	buf    AioBuffer
+	offset uint64
+}
+
+var writes []wbuf
+
+/* Called whenever any asynchronous pread command from
+   the source has completed. */
+func read_completed(buf AioBuffer, offset uint64) int {
+	bytes_read += buf.Size
+	/* Move the AIO buffer to the write queue. */
+	writes = append(writes, wbuf{buf, offset})
+	/* Returning 1 means the command is automatically retired. */
+	return 1
+}
+
+/* Called whenever any asynchronous pwrite command to the
+   destination has completed. */
+func write_completed(buf AioBuffer) int {
+	bytes_written += buf.Size
+	/* Now we have to manually free the AIO buffer. */
+	buf.Free()
+	/* Returning 1 means the command is automatically retired. */
+	return 1
+}
+
+/* Copy between two libnbd handles using aynchronous I/O (AIO). */
+func asynch_copy(t *testing.T, src *Libnbd, dst *Libnbd) {
+	size, _ := dst.GetSize()
+
+	/* This is our reading position in the source. */
+	soff := uint64(0)
+
+	for {
+		/* Number of commands in flight on source and dest handles. */
+		src_in_flight, _ := src.AioInFlight()
+		dst_in_flight, _ := dst.AioInFlight()
+
+		/* We're finished when we've read everything from the
+		   source and there are no commands in flight. */
+		if soff >= size && src_in_flight == 0 &&
+			dst_in_flight == 0 {
+			break
+		}
+
+		/* If we're able to submit more reads from the
+		   source then do it now. */
+		if soff < size && src_in_flight < max_reads_in_flight {
+			n := bs
+			if n > size-soff {
+				n = size - soff
+			}
+			buf := MakeAioBuffer(uint(n))
+			var optargs AioPreadOptargs
+			optargs.CompletionCallbackSet = true
+			optargs.CompletionCallback = func(*int) int {
+				return read_completed(buf, soff)
+			}
+			src.AioPread(buf, soff, &optargs)
+			soff += n
+		}
+
+		/* If there are any write commands waiting to
+		   be issued, send them now. */
+		for _, wb := range writes {
+			var optargs AioPwriteOptargs
+			optargs.CompletionCallbackSet = true
+			optargs.CompletionCallback = func(*int) int {
+				return write_completed(wb.buf)
+			}
+			dst.AioPwrite(wb.buf, wb.offset, &optargs)
+		}
+		writes = writes[:0]
+
+		/* Now poll the file descriptors. */
+		nfd := 1
+		sfd, err := src.AioGetFd()
+		if err != nil {
+			t.Fatalf("src.AioGetFd: %s", err)
+		}
+		if sfd >= nfd {
+			nfd = sfd + 1
+		}
+		dfd, err := dst.AioGetFd()
+		if err != nil {
+			t.Fatalf("dst.AioGetFd: %s", err)
+		}
+		if dfd >= nfd {
+			nfd = dfd + 1
+		}
+		var rfds syscall.FdSet
+		if dir_is_read(src) {
+			fdset_set(&rfds, sfd)
+		}
+		if dir_is_read(dst) {
+			fdset_set(&rfds, dfd)
+		}
+		var wfds syscall.FdSet
+		if dir_is_write(src) {
+			fdset_set(&wfds, sfd)
+		}
+		if dir_is_write(dst) {
+			fdset_set(&wfds, dfd)
+		}
+		_, err = syscall.Select(nfd, &rfds, &wfds, nil, nil)
+		if err != nil {
+			t.Fatalf("select: %s", err)
+		}
+
+		if fdset_test(&rfds, sfd) && dir_is_read(src) {
+			src.AioNotifyRead()
+		} else if fdset_test(&wfds, sfd) && dir_is_write(src) {
+			src.AioNotifyWrite()
+		} else if fdset_test(&rfds, dfd) && dir_is_read(dst) {
+			dst.AioNotifyRead()
+		} else if fdset_test(&wfds, dfd) && dir_is_write(dst) {
+			dst.AioNotifyWrite()
+		}
+	}
+}
+
+func Test590AioCopy(t *testing.T) {
+	src, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer src.Close()
+	src.SetHandleName("src")
+
+	var dst *Libnbd
+	dst, err = Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer dst.Close()
+	dst.SetHandleName("dst")
+
+	err = src.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-r",
+		"pattern", fmt.Sprintf("size=%d", disk_size),
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	err = dst.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent",
+		"memory", fmt.Sprintf("size=%d", disk_size),
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	asynch_copy(t, src, dst)
+	if bytes_read != disk_size {
+		t.Fatalf("bytes_read != disk_size")
+	}
+	if bytes_written != disk_size {
+		t.Fatalf("bytes_written != disk_size")
+	}
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_600_debug_callback_test.go b/golang/src/libguestfs.org/libnbd/libnbd_600_debug_callback_test.go
new file mode 100644
index 0000000..a3673aa
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_600_debug_callback_test.go
@@ -0,0 +1,56 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "fmt"
+import "testing"
+
+var msgs []string
+
+func f(context string, msg string) int {
+	fmt.Printf("debug callback called: context=%s msg=%s\n",
+		context, msg)
+	msgs = append(msgs, context, msg)
+	return 0
+}
+
+func Test600DebugCallback(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.SetDebugCallback(f)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "null",
+	})
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	err = h.Shutdown(nil)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	fmt.Printf("msgs = %s\n", msgs)
+}
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_610_error_test.go b/golang/src/libguestfs.org/libnbd/libnbd_610_error_test.go
new file mode 100644
index 0000000..27540ce
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_610_error_test.go
@@ -0,0 +1,41 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import "fmt"
+import "testing"
+
+func Test610Error(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	/* This will always return an error because the handle is
+	   not connected. */
+	buf := make([]byte, 512)
+	err = h.Pread(buf, 0, nil)
+	if err == nil {
+		t.Fatalf("expected an error from operation")
+	}
+	fmt.Printf("error = %s\n", err)
+	/* XXX We expect the errno to be ENOTCONN, but I couldn't work
+	   out how to test it. */
+}
diff --git a/run.in b/run.in
index 0411c85..55a1ec8 100755
--- a/run.in
+++ b/run.in
@@ -78,6 +78,25 @@ export PYTHONPATH
 prepend CAML_LD_LIBRARY_PATH "$b/ocaml"
 export CAML_LD_LIBRARY_PATH
 
+# For golang.
+export GOLANG="@GOLANG@"
+prepend GOPATH "$b/golang"
+export GOPATH
+if [ -z "$CGO_CFLAGS" ]; then
+    CGO_CFLAGS="-I$s/include -I$b"
+else
+    CGO_CFLAGS="$CGO_CFLAGS -I$s/include -I$b"
+fi
+export CGO_CFLAGS
+if [ -z "$CGO_LDFLAGS" ]; then
+    CGO_LDFLAGS="-L$b/lib/.libs"
+else
+    CGO_LDFLAGS="$CGO_LDFLAGS -L$b/lib/.libs"
+fi
+export CGO_LDFLAGS
+# This implements complete pointer checking in cgo.
+export GODEBUG=cgocheck=2
+
 # This is a cheap way to find some use-after-free and uninitialized
 # read problems when using glibc.
 export MALLOC_CHECK_=1
-- 
2.25.0




More information about the Libguestfs mailing list