[Libguestfs] [PATCH 1/2] rust: create basic Rust bindings

Tage Johansson
2023-06-20T09:31:36Z

From: Tage Johansson <frans.tage at gmail.com>

This commit creates basic Rust bindings in the rust directory.
The bindings are generated by generator/Rust.ml and generator/Rust.mli.

No tests are created so far.

In rust/libnbd-sys, [rust-bindgen](https://github.com/rust-lang/rust-bindgen)
is used to generate low level Rust bindings to libnbd.h. This requires Clang,
see [this
link](https://rust-lang.github.io/rust-bindgen/requirements.html#clang).
Ultimately, we shall generate the low level bindings without rust-bindgen in
the future so that Clang would not be required.

Apart from Clang, you would need Cargo with Rustdoc and Rustfmt to build
the Rust bindings. See [here](https://www.rust-lang.org/tools/install)
for installation instructions.
---
 Makefile.am                |   1 +
 configure.ac               |  28 ++
 generator/Makefile.am      |   2 +
 generator/Rust.ml          | 547 +++++++++++++++++++++++++++++++++++++
 generator/Rust.mli         |  20 ++
 generator/generator.ml     |   2 +
 generator/utils.ml         |   9 +-
 generator/utils.mli        |   3 +-
 rust/.gitignore            |   3 +
 rust/Cargo.toml            |  48 ++++
 rust/Makefile.am           |  54 ++++
 rust/cargo_test/.gitignore |   2 +
 rust/cargo_test/Cargo.toml |  23 ++
 rust/cargo_test/README.md  |   3 +
 rust/cargo_test/src/lib.rs |  31 +++
 rust/libnbd-sys/.gitignore |   4 +
 rust/libnbd-sys/Cargo.toml |  30 ++
 rust/libnbd-sys/build.rs   |  60 ++++
 rust/libnbd-sys/src/lib.rs |  24 ++
 rust/libnbd-sys/wrapper.h  |  18 ++
 rust/run-tests.sh          |  24 ++
 rust/src/error.rs          |  47 ++++
 rust/src/handle.rs         |  53 ++++
 rust/src/lib.rs            |  28 ++
 rust/src/types.rs          |  18 ++
 rust/src/utils.rs          |  23 ++
 26 files changed, 1103 insertions(+), 2 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index 243fabd..9e1790b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -55,6 +55,7 @@ SUBDIRS = \
 	ocaml/tests \
 	golang \
 	golang/examples \
+	rust \
 	interop \
 	fuzzing \
 	bash-completion \
diff --git a/configure.ac b/configure.ac
index 6ac2862..ac520f1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -613,6 +613,32 @@ AS_IF([test "x$enable_golang" !=
"xno"],[
 ],[GOLANG=no])
 AM_CONDITIONAL([HAVE_GOLANG],[test "x$GOLANG" != "xno"])

+dnl Rust.
+AC_ARG_ENABLE([rust],
+    AS_HELP_STRING([--disable-rust], [disable Rust language bindings]),
+        [],
+        [enable_rust=yes])
+AS_IF([test "x$enable_rust" != "xno"],[
+    AC_CHECK_PROG([CARGO],[cargo],[cargo],[no])
+    AC_CHECK_PROG([RUSTFMT],[rustfmt],[rustfmt],[no])
+    AS_IF([test "x$CARGO" != "xno"],[
+        AC_MSG_CHECKING([if $CARGO is usable])
+        AS_IF([ (
+              cd $srcdir/rust/cargo_test &&
+               $CARGO test 2>&AS_MESSAGE_LOG_FD 1>&2 &&
+               $CARGO doc 2>&AS_MESSAGE_LOG_FD 1>&2 &&
+               $CARGO fmt 2>&AS_MESSAGE_LOG_FD 1>&2
+               ) ],[
+            AC_MSG_RESULT([yes])
+        ],[
+            AC_MSG_RESULT([no])
+            AC_MSG_WARN([Rust ($CARGO) is installed but not usable])
+            CARGO=no
+        ])
+    ])
+],[CARGO=no])
+AM_CONDITIONAL([HAVE_RUST],[test "x$CARGO" != "xno" -a
"x$RUSTFMT" != "xno"])
+
 AC_MSG_CHECKING([for how to mark DSO non-deletable at runtime])
 NODELETE `$LD --help 2>&1 | grep -- "-z nodelete"
>/dev/null` && \
@@ -657,6 +683,7 @@ AC_CONFIG_FILES([Makefile
                  generator/Makefile
                  golang/Makefile
                  golang/examples/Makefile
+                 rust/Makefile
                  include/Makefile
                  info/Makefile
                  interop/Makefile
@@ -717,6 +744,7 @@ echo
 echo "Language bindings:"
 echo
 feature "Go"                    test "x$HAVE_GOLANG_TRUE" =
"x"
+feature "Rust"                    test "x$HAVE_RUST_TRUE" =
"x"
 feature "OCaml"                 test "x$HAVE_OCAML_TRUE" =
"x"
 feature "Python"                test "x$HAVE_PYTHON_TRUE" =
"x"

diff --git a/generator/Makefile.am b/generator/Makefile.am
index 91dbde5..cc1f9a4 100644
--- a/generator/Makefile.am
+++ b/generator/Makefile.am
@@ -60,6 +60,8 @@ sources = \
 	OCaml.ml \
 	GoLang.mli \
 	GoLang.ml \
+	Rust.mli \
+	Rust.ml \
 	generator.ml \
 	$(NULL)

diff --git a/generator/Rust.ml b/generator/Rust.ml
new file mode 100644
index 0000000..2bf2873
--- /dev/null
+++ b/generator/Rust.ml
@@ -0,0 +1,547 @@
+(* hey emacs, this is OCaml code: -*- tuareg -*- *)
+(* nbd client library in userspace: generator
+ * Copyright Red Hat
+ *
+ * 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
+ *)
+
+(* Rust language bindings. *)
+
+open Printf
+open API
+open Utils
+
+(* The type for a set of names. *)
+module NameSet = Set.Make (String)
+
+(* List of handle calls which should not be part of the public API. This could
+ * for instance be `set_debug` and `set_debug_callback` which are handled
separately
+ * by the log crate
+ *)
+let hidden_handle_calls : NameSet.t +  NameSet.of_list
+    [ "get_debug"; "set_debug";
"set_debug_callback"; "clear_debug_callback" ]
+
+(* Convert snake-case to upper camel-case. *)
+let to_upper_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 print_rust_constant (name, value) +  let typ = if value < 0 then
"i32" else "u32" in
+  pr "pub const %s: %s = %d;\n" name typ value
+
+let print_rust_enum (enum : enum) +  pr "#[derive(Debug, Clone, Copy,
PartialEq, Eq, Hash)]\n";
+  pr "#[repr(isize)]";
+  pr "pub enum %s {\n" (to_upper_camel_case enum.enum_prefix);
+  List.iter
+    (fun (name, num) -> pr "    %s = %d,\n" (to_upper_camel_case
name) num)
+    enum.enums;
+  pr "}\n\n"
+
+(* Print a Rust struct for a set of flags. *)
+let print_rust_flags ({ flag_prefix; flags } : flags) +  pr "bitflags!
{\n";
+  pr "    #[repr(C)]\n";
+  pr "    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n";
+  pr "    pub struct %s: u32 {\n" (to_upper_camel_case flag_prefix);
+  List.iter
+    (fun (name, value) -> pr "        const %s = %d;\n" name
value)
+    flags;
+  pr "    }\n";
+  pr "}\n\n"
+
+(* Get the name of a rust argument. *)
+let rust_arg_name : arg -> string = function
+  | Bool n
+  | Int n
+  | UInt n
+  | UIntPtr n
+  | UInt32 n
+  | Int64 n
+  | UInt64 n
+  | SizeT n
+  | String n
+  | StringList n
+  | Path n
+  | Fd n
+  | Enum (n, _)
+  | Flags (n, _)
+  | SockAddrAndLen (n, _)
+  | BytesIn (n, _)
+  | BytesPersistIn (n, _)
+  | BytesOut (n, _)
+  | BytesPersistOut (n, _)
+  | Closure { cbname = n } ->
+      n
+
+(* Get the name of a rust optional argument. *)
+let rust_optarg_name : optarg -> string = function
+  | OClosure { cbname = n } | OFlags (n, _, _) -> n
+
+(* Get the name of a Rust closure argument. *)
+let rust_cbarg_name : cbarg -> string = function
+  | CBInt n | CBUInt n | CBInt64 n | CBUInt64 n | CBString n | CBBytesIn (n, _)
+    ->
+      n
+  | CBArrayAndLen (arg, _) | CBMutable arg -> rust_arg_name arg
+
+(* Get the Rust type for an argument. *)
+let rec rust_arg_type : arg -> string = function
+  | Bool _ -> "bool"
+  | Int _ -> "c_int"
+  | UInt _ -> "c_uint"
+  | UIntPtr _ -> "usize"
+  | UInt32 _ -> "u32"
+  | Int64 _ -> "i64"
+  | UInt64 _ -> "u64"
+  | SizeT _ -> "usize"
+  | String _ -> "&CStr"
+  | SockAddrAndLen _ -> "SocketAddr"
+  | StringList _ -> "&[&CStr]"
+  | Path _ -> "&PathBuf"
+  | Enum (_, { enum_prefix = name }) | Flags (_, { flag_prefix = name }) ->
+      to_upper_camel_case name
+  | Fd _ -> "OwnedFd"
+  | BytesIn _ -> "&[u8]"
+  | BytesOut _ -> "&mut [u8]"
+  (* TODO: These should probably be changed to something but static slices. *)
+  | BytesPersistIn _ -> "&'static [u8]"
+  | BytesPersistOut _ -> "&'static mut [u8]"
+  | Closure { cbargs } -> "impl " ^ rust_closure_trait cbargs
+
+(* Get the Rust closure trait for a callback, That is `FnMut(...) -> ...)`.
*)
+and rust_closure_trait (cbargs : cbarg list) : string +  let rust_cbargs =
String.concat ", " (List.map rust_cbarg_type cbargs) in
+  "FnMut(" ^ rust_cbargs
+  ^ ") -> Result<(), ()>"
+
+(* Get the Rust type for a callback argument. *)
+and rust_cbarg_type : cbarg -> string = function
+  | CBInt n -> rust_arg_type (Int n)
+  | CBUInt n -> rust_arg_type (UInt n)
+  | CBInt64 n -> rust_arg_type (Int64 n)
+  | CBUInt64 n -> rust_arg_type (UInt64 n)
+  | CBString n -> rust_arg_type (String n)
+  | CBBytesIn (n1, n2) -> rust_arg_type (BytesIn (n1, n2))
+  | CBArrayAndLen (elem, _) -> "&[" ^ rust_arg_type elem ^
"]"
+  | CBMutable arg -> "&mut " ^ rust_arg_type arg
+
+(* Get the type of a rust optional argument. *)
+let rust_optarg_type : optarg -> string = function
+  | OClosure x -> sprintf "Option<%s>" (rust_arg_type
(Closure x))
+  | OFlags (name, flags, _) ->
+      sprintf "Option<%s>" (rust_arg_type (Flags (name,
flags)))
+
+(* Given an argument, produce a list of names for arguments in FFI functions
corresponding
+ * to that argument. Most arguments will just produce one name for one FFI
argument,
+ * but for example `BytesIn` requires two separate FFI arguments hence a list
is produced.
+ *)
+let ffi_arg_names : arg -> string list = function
+  | Bool n
+  | Int n
+  | UInt n
+  | UIntPtr n
+  | UInt32 n
+  | Int64 n
+  | UInt64 n
+  | SizeT n
+  | String n
+  | StringList n
+  | Path n
+  | Fd n
+  | Enum (n, _)
+  | Flags (n, _)
+  | Closure { cbname = n } ->
+      [ n ^ "_ffi" ]
+  | SockAddrAndLen (n1, n2)
+  | BytesIn (n1, n2)
+  | BytesPersistIn (n1, n2)
+  | BytesOut (n1, n2)
+  | BytesPersistOut (n1, n2) ->
+      [ n1 ^ "_ffi"; n2 ^ "_ffi" ]
+
+let ffi_optarg_name : optarg -> string = function
+  | OClosure { cbname = name } | OFlags (name, _, _) -> name ^
"_ffi"
+
+(* Given a closure argument, produce a list of names used by FFI functions for
that
+ * particular argument. Most closure arguments will just produce one FFI
argument, but for instance
+ * `CBArrayAndLen` will produce two, hence we return a list.
+ *)
+let ffi_cbarg_names : cbarg -> string list = function
+  | CBInt n | CBUInt n | CBInt64 n | CBUInt64 n | CBString n -> [ n ^
"_ffi" ]
+  | CBBytesIn (n1, n2) -> [ n1 ^ "_ffi"; n2 ^ "_ffi" ]
+  | CBArrayAndLen (arg, len) -> [ rust_arg_name arg ^ "_ffi"; len
^ "_ffi" ]
+  | CBMutable arg -> [ rust_arg_name arg ^ "_ffi" ]
+
+(* Given a closure argument, produce a list of types used by FFI functions for
that
+ * particular argument. Most closure arguments will just produce one FFI
argument, but for instance
+ * `CBArrayAndLen` will produce two, hence we return a list.
+ *)
+let ffi_cbarg_types : cbarg -> string list = function
+  | CBInt _ -> [ "c_int" ]
+  | CBUInt _ -> [ "c_uint" ]
+  | CBInt64 _ -> [ "i64" ]
+  | CBUInt64 _ -> [ "u64" ]
+  | CBString _ -> [ "*const c_char" ]
+  | CBBytesIn _ -> [ "*const c_void"; "usize" ]
+  | CBArrayAndLen (UInt32 _, _) -> [ "*mut u32"; "usize"
]
+  | CBArrayAndLen _ ->
+      failwith
+        "generator/Rust.ml: in ffi_cbarg_types: Unsupported type of array
\
+         element."
+  | CBMutable (Int _) -> [ "*mut c_int" ]
+  | CBMutable _ ->
+      failwith
+        "generator/Rust.ml: in ffi_cbarg_types: Unsupported type of
mutable \
+         argument."
+
+(* Return type for a Rust function. *)
+let rust_ret_type (call : call) : string +  let core_type +    match call.ret
with
+    | RBool -> "bool"
+    | RStaticString -> "&'static CStr"
+    | RErr -> "()"
+    | RFd -> "RawFd"
+    | RInt -> "c_uint"
+    | RInt64 -> "u64"
+    | RCookie -> "Cookie"
+    | RSizeT -> "usize"
+    | RString -> "CString"
+    | RUInt -> "c_uint"
+    | RUIntPtr -> "usize"
+    | RUInt64 -> "u64"
+    | REnum { enum_prefix = name } | RFlags { flag_prefix = name } ->
+        to_upper_camel_case name
+  in
+  if call.may_set_error then sprintf "Result<%s>" core_type
else core_type
+
+(* Given an argument `(arg : arg)`, print Rust code for variable declarations
for
+ * all FFI arguments corresponding to `arg`. That is, for each
`<FFI_NAME>` in `ffi_arg_names arg`,
+ * print `let <FFI_NAME> = <XXX>;`. Assuming that a variable with
name `rust_arg_name arg`
+ * and type `rust_arg_type arg` exists in scope.
+ *)
+let rust_arg_to_ffi (arg : arg) +  let rust_name = rust_arg_name arg in
+  let ffi_names = ffi_arg_names arg in
+  match arg with
+  | Bool _ | Int _ | UInt _ | UIntPtr _ | UInt32 _ | Int64 _ | UInt64 _
+  | SizeT _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s;\n" ffi_name rust_name
+  | Enum _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s as c_int;\n" ffi_name rust_name
+  | Flags _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s.bits();\n" ffi_name rust_name
+  | String _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s.as_ptr();\n" ffi_name rust_name
+  | SockAddrAndLen _ ->
+      let ffi_addr_name, ffi_len_name +        match ffi_names with [ x; y ]
-> (x, y) | _ -> assert false
+      in
+      pr "let %s_os = OsSocketAddr::from(%s);\n" rust_name rust_name;
+      (* We assume here that the bindgen generated `sys::sockaddr` is
equivalent to `libc::sockaddr`. *)
+      pr "let %s = %s_os.as_ptr() as *const sys::sockaddr;\n"
ffi_addr_name
+        rust_name;
+      pr "let %s = %s_os.len();\n" ffi_len_name rust_name
+  | StringList _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      (* Convert a slice to a null terminated slice.
+         For some reason the C functions have not marked the pointers as const,
+         so we use `cast_mut` and `as_mut_ptr` here even though the strings
shouldn't be modified. *)
+      pr
+        "let mut %s_vec: Vec<*mut c_char> = %s.iter().map(|x| \
+         x.as_ptr().cast_mut()).collect();\n"
+        ffi_name rust_name;
+      pr "%s_vec.push(ptr::null_mut());\n" ffi_name;
+      pr "let %s = %s_vec.as_mut_ptr();\n" ffi_name ffi_name
+  | Path _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr
+        "let %s_buf = \
+
CString::new(%s.as_os_str().to_owned().into_raw_vec()).unwrap();\n"
+        rust_name rust_name;
+      pr "let %s = %s_buf.as_ptr();\n" ffi_name rust_name
+  | BytesIn _ | BytesPersistIn _ ->
+      let ffi_buf_name, ffi_len_name +        match ffi_names with [ x; y ]
-> (x, y) | _ -> assert false
+      in
+      pr "let %s = %s.as_ptr() as *const c_void;\n" ffi_buf_name
rust_name;
+      pr "let %s = %s.len();\n" ffi_len_name rust_name
+  | BytesOut _ | BytesPersistOut _ ->
+      let ffi_buf_name, ffi_len_name +        match ffi_names with [ x; y ]
-> (x, y) | _ -> assert false
+      in
+      pr "let %s = %s.as_mut_ptr() as *mut c_void;\n" ffi_buf_name
rust_name;
+      pr "let %s = %s.len();\n" ffi_len_name rust_name
+  | Fd _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s.as_raw_fd();\n" ffi_name rust_name
+  | Closure _ ->
+      let ffi_name = match ffi_names with [ x ] -> x | _ -> assert false
in
+      pr "let %s = %s_to_raw(%s);\n" ffi_name rust_name rust_name
+
+(* Same as `rust_arg_to_ffi` but for optional arguments. *)
+let rust_optarg_to_ffi (arg : optarg) +  let rust_name = rust_optarg_name arg
in
+  let ffi_name = ffi_optarg_name arg in
+  match arg with
+  | OClosure { cbname } ->
+      pr "let %s = match %s {\n" ffi_name rust_name;
+      pr "    Some(f) => %s_to_raw(f),\n" rust_name;
+      pr
+        "    None => sys::nbd_%s_callback { callback: None, free: None,
\
+         user_data: ptr::null_mut() },\n"
+        cbname;
+      pr "};\n"
+  | OFlags (_, { flag_prefix }, _) ->
+      let flags_type = to_upper_camel_case flag_prefix in
+      pr "let %s = %s.unwrap_or(%s::empty()).bits();\n" ffi_name
rust_name
+        flags_type
+
+(* Same as `rust_arg_to_ffi` but for closure args instead.
+ * The th variable will be named `<NAME>_ffi_` where
`<NAME>` is the name of the argument.
+ * Keep in mind that this function may create multiple variables.
+ *)
+let rust_cbarg_to_ffi (cbarg : cbarg) = function
+  | CBInt n -> rust_arg_to_ffi (Int n)
+  | CBUInt n -> rust_arg_to_ffi (UInt n)
+  | CBInt64 n -> rust_arg_to_ffi (Int64 n)
+  | CBUInt64 n -> rust_arg_to_ffi (UInt64 n)
+  | CBString n -> rust_arg_to_ffi (String n)
+  | CBBytesIn (n1, n2) -> rust_arg_to_ffi (BytesIn (n1, n2))
+  | CBArrayAndLen (UInt32 _, _) ->
+      let ffi_arr_name, ffi_len_name +        match ffi_cbarg_names cbarg with
+        | [ x; y ] -> (x, y)
+        | _ -> assert false
+      in
+      let rust_name = rust_cbarg_name cbarg in
+      pr "let %s = %s.as_ptr();\n" ffi_arr_name rust_name;
+      pr "let %s = %s.len();\n" ffi_len_name rust_name
+  | CBArrayAndLen _ ->
+      failwith
+        "generator/Rust.ml: in rust_cbarg_to_ffi: Unsupported type of
array \
+         element."
+  | CBMutable (Int _) ->
+      let ffi_name +        match ffi_cbarg_names cbarg with [ x ] -> x | _
-> assert false
+      in
+      let rust_name = rust_cbarg_name cbarg in
+      pr "let %s = %s.as_mut_ptr();\n" ffi_name rust_name
+  | CBMutable _ ->
+      failwith
+        "generator/Rust.ml: in rust_cbarg_to_ffi: Unsupported type of
mutable \
+         argument."
+
+(* Given a closure argument (`x : cbarg`), print Rust code to create a variable
with
+ * name `rust_cbarg_name x` of type `rust_cbarg_type x`. Assuming that
variables with
+ * names from `ffi_cbarg_names x` exists in scope`.
+ *)
+let ffi_cbargs_to_rust (cbarg : cbarg) +  let ffi_name = ffi_cbarg_names cbarg
in
+  pr "let %s: %s = " (rust_cbarg_name cbarg) (rust_cbarg_type cbarg);
+  (match cbarg with
+  | CBInt _ | CBUInt _ | CBInt64 _ | CBUInt64 _ -> (
+      match ffi_name with [ x ] -> pr "%s" x | _ -> assert
false)
+  | CBString _ ->
+      let ffi_name = match ffi_name with [ x ] -> x | _ -> assert false
in
+      pr "CStr::from_ptr(%s)" ffi_name
+  | CBBytesIn _ ->
+      let ffi_buf_name, ffi_len_name +        match ffi_name with [ x; y ]
-> (x, y) | _ -> assert false
+      in
+      pr "slice::from_raw_parts(%s as *const u8, %s)" ffi_buf_name
ffi_len_name
+  | CBArrayAndLen (UInt32 _, _) ->
+      let ffi_arr_name, ffi_len_name +        match ffi_name with [ x; y ]
-> (x, y) | _ -> assert false
+      in
+      pr "slice::from_raw_parts(%s, %s)" ffi_arr_name ffi_len_name
+  | CBArrayAndLen _ ->
+      failwith
+        "generator/Rust.ml: in ffi_cbargs_to_rust: Unsupported type of
array \
+         element."
+  | CBMutable (Int _) ->
+      let ffi_name = match ffi_name with [ x ] -> x | _ -> assert false
in
+      pr "%s.as_mut().unwrap()" ffi_name
+  | CBMutable _ ->
+      failwith
+        "generator/Rust.ml: in ffi_cbargs_to_rust: Unsupported type of
mutable \
+         argument.");
+  pr ";\n"
+
+(* Print Rust code for converting a return value from an FFI call to a Rusty
return value.
+ * In other words, given `x : ret`, this functions print a Rust expression with
+ * type `rust_ret_type x`, with a free variable `ffi_ret` with the return value
from the FFI call.
+ *)
+let ffi_ret_to_rust (call : call) +  let ret_type = rust_ret_type call in
+  let pure_expr +    match call.ret with
+    | RBool -> "ffi_ret != 0"
+    | RErr -> "()"
+    | RInt -> "ffi_ret.try_into().unwrap()"
+    | RInt64 -> "ffi_ret.try_into().unwrap()"
+    | RSizeT -> "ffi_ret.try_into().unwrap()"
+    | RCookie -> "Cookie(ffi_ret.try_into().unwrap())"
+    | RFd -> "ffi_ret as RawFd"
+    | RStaticString -> "unsafe { CStr::from_ptr(ffi_ret) }"
+    | RString -> "unsafe { CStr::from_ptr(ffi_ret) }.to_owned()"
+    | RFlags { flag_prefix } ->
+        sprintf "%s::from_bits(ffi_ret).unwrap()" ret_type
+    | RUInt | RUIntPtr | RUInt64 -> sprintf "ffi_ret as %s"
ret_type
+    | REnum _ ->
+        (* We know that each enum is represented by an isize, hence this
transmute is safe. *)
+        sprintf "unsafe { mem::transmute::<isize, %s>(ffi_ret as
isize) }"
+          ret_type
+  in
+  if call.may_set_error then (
+    (match call.ret with
+    | RBool | RErr | RInt | RFd | RInt64 | RCookie | RSizeT ->
+        pr "if ffi_ret < 0 { Err(unsafe { Error::get_error() })
}\n"
+    | RStaticString | RString ->
+        pr "if ffi_ret.is_null() { Err(unsafe { Error::get_error() })
}\n"
+    | RUInt | RUIntPtr | RUInt64 | REnum _ | RFlags _ ->
+        failwith "In ffi_ret_to_rust: Return type cannot be an
error.");
+    pr "else { Ok(%s) }\n" pure_expr)
+  else pr "%s\n" pure_expr
+
+(* This function prints a rust function which converts a rust closure to a
+ * (`repr(C)`) struct containing the function pointer, a `*mut c_void` for the
closure data,
+ * and a free function for the closure data. This struct is what will be sent
to a C function
+ * taking the closure as an argument. In fact, the struct itself is generated
by rust-bindgen.
+ *)
+let print_rust_closure_to_raw_fn ({ cbname; cbargs } : closure) +  let
closure_trait = rust_closure_trait cbargs in
+  let ffi_cbargs_names = List.flatten (List.map ffi_cbarg_names cbargs) in
+  let ffi_cbargs_types = List.flatten (List.map ffi_cbarg_types cbargs) in
+  let rust_cbargs_names = List.map rust_cbarg_name cbargs in
+  pr "fn %s_to_raw<F>(f: F) -> sys::nbd_%s_callback\n"
cbname cbname;
+  pr "  where F: %s\n" closure_trait;
+  pr "{\n";
+  pr
+    "    unsafe extern \"C\" fn call_closure<F>(data: *mut
c_void, %s) -> c_int\n"
+    (String.concat ", "
+       (List.map2
+          (fun name typ -> name ^ ": " ^ typ)
+          ffi_cbargs_names ffi_cbargs_types));
+  pr "      where F: %s\n" closure_trait;
+  pr "    {\n";
+  pr "        let callback_ptr = data as *mut F;\n";
+  pr "        let callback = &mut *callback_ptr;\n";
+  List.iter ffi_cbargs_to_rust cbargs;
+  pr "        let res = callback(%s);\n" (String.concat ",
" rust_cbargs_names);
+  pr "        res.map(|()| 0).unwrap_or(-1)\n";
+  pr "    }\n";
+  pr "    let callback_data = Box::into_raw(Box::new(f));\n";
+  pr "    sys::nbd_%s_callback {\n" cbname;
+  pr "        callback: Some(call_closure::<F>),\n";
+  pr "        user_data: callback_data as *mut _,\n";
+  pr "        free: Some(utils::drop_data::<F>),\n";
+  pr "    }\n";
+  pr "}\n";
+  pr "\n"
+
+(* Print the Rust function for a handle call. Note that this is a
"method" on the
+ * `NbdHandle` struct. So the printed Rust function should be in a `impl
NbdHandle {` block.
+ *)
+let print_rust_handle_method ((name, call) : string * call) +  let
rust_args_names +    List.map rust_arg_name call.args @ List.map
rust_optarg_name call.optargs
+  in
+  let rust_args_types +    List.map rust_arg_type call.args @ List.map
rust_optarg_type call.optargs
+  in
+  let ffi_args_names +    List.flatten (List.map ffi_arg_names call.args)
+    @ List.map ffi_optarg_name call.optargs
+  in
+  (* Print comments. *)
+  if call.shortdesc <> String.empty then
+    pr "/// %s\n"
+      (String.concat "\n/// " (String.split_on_char '\n'
call.shortdesc));
+  if call.longdesc <> String.empty then (
+    (* If a short comment was printed, print a blank comment line befor the
long description. *)
+    if call.shortdesc <> String.empty then pr "/// \n";
+    (* Print all lines of the long description. Since Rust comments are
supposed
+       to be Markdown, all indented lines will be treated as code blocks. Hence
we
+       trim all lines. Also brackets ("[" and "]") must be
escaped. *)
+    List.iter
+      (fun line ->
+        let unindented = String.trim line in
+        let escaped +          Str.global_replace (Str.regexp {|\(\[\|\]\)|})
{|\\\1|} unindented
+        in
+        pr "/// %s\n" escaped)
+      (pod2text call.longdesc));
+
+  (* Frint visibility modifier. *)
+  if NameSet.mem name hidden_handle_calls then (
+    (* If this is hidden to the public API, it might be used only if some
feature
+     * is active, and we don't want a unused-warning. *)
+    pr "#[allow(unused)]\n";
+    pr "pub(crate) ")
+  else pr "pub ";
+
+  pr "fn %s(&mut self, %s) -> %s {\n" name
+    (String.concat ", "
+       (List.map2
+          (fun name typ -> name ^ ": " ^ typ)
+          rust_args_names rust_args_types))
+    (rust_ret_type call);
+  List.iter rust_arg_to_ffi call.args;
+  List.iter rust_optarg_to_ffi call.optargs;
+  pr "    let ffi_ret = unsafe { sys::nbd_%s(self.0, %s) };\n" name
+    (String.concat ", " ffi_args_names);
+  ffi_ret_to_rust call;
+  pr "}\n";
+  pr "\n"
+
+let print_rust_imports () +  pr "use bitflags::bitflags;\n";
+  pr "use crate::{sys, types::*, utils, Error, Result,
NbdHandle};\n";
+  pr "use os_str_bytes::OsStringBytes as _;\n";
+  pr "use os_socketaddr::OsSocketAddr;\n";
+  pr "use std::ffi::*;\n";
+  pr "use std::mem;\n";
+  pr "use std::net::SocketAddr;\n";
+  pr "use std::os::fd::{AsRawFd, OwnedFd, RawFd};\n";
+  pr "use std::path::PathBuf;\n";
+  pr "use std::ptr;\n";
+  pr "use std::slice;\n";
+  pr "\n"
+
+let generate_rust_bindings () +  generate_header CStyle;
+  pr "\n";
+  print_rust_imports ();
+  List.iter print_rust_constant constants;
+  pr "\n";
+  List.iter print_rust_enum all_enums;
+  List.iter print_rust_flags all_flags;
+  List.iter print_rust_closure_to_raw_fn all_closures;
+  pr "impl NbdHandle {\n";
+  List.iter print_rust_handle_method handle_calls;
+  pr "}\n\n"
diff --git a/generator/Rust.mli b/generator/Rust.mli
new file mode 100644
index 0000000..86fea8c
--- /dev/null
+++ b/generator/Rust.mli
@@ -0,0 +1,20 @@
+(* nbd client library in userspace: generator
+ * Copyright Red Hat
+ *
+ * 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
+ *)
+
+(* Print all flag-structs, enums, constants and handle calls in Rust code. *)
+val generate_rust_bindings : unit -> unit
diff --git a/generator/generator.ml b/generator/generator.ml
index c73824e..18856f5 100644
--- a/generator/generator.ml
+++ b/generator/generator.ml
@@ -61,3 +61,5 @@ let ()    output_to "golang/closures.go"
GoLang.generate_golang_closures_go;
   output_to "golang/wrappers.go" GoLang.generate_golang_wrappers_go;
   output_to "golang/wrappers.h" GoLang.generate_golang_wrappers_h;
+
+  output_to ~rustfmt:true "rust/src/bindings.rs"
Rust.generate_rust_bindings;
diff --git a/generator/utils.ml b/generator/utils.ml
index 6d77e51..3d4714e 100644
--- a/generator/utils.ml
+++ b/generator/utils.ml
@@ -413,7 +413,7 @@ let files_equal n1 n2    | 1 -> false
   | i -> failwithf "%s: failed with error code %d" cmd i

-let output_to filename k +let output_to ?(rustfmt = false) filename k    lineno
:= 1; col := 0;
   let filename_new = filename ^ ".new" in
   let c = open_out filename_new in
@@ -422,6 +422,13 @@ let output_to filename k    close_out c;
   chan := NoOutput;

+  if rustfmt then
+    (match system (sprintf "rustfmt %s" filename_new) with
+      | WEXITED 0 -> ()
+      | WEXITED i -> failwith (sprintf "Rustfmt failed with exit code
%d" i)
+      | _ -> failwith "Rustfmt was killed or stopped by a
signal.");
+
+
   (* Is the new file different from the current file? *)
   if Sys.file_exists filename && files_equal filename filename_new then
     unlink filename_new                 (* same, so skip it *)
diff --git a/generator/utils.mli b/generator/utils.mli
index e622e31..a70d332 100644
--- a/generator/utils.mli
+++ b/generator/utils.mli
@@ -50,7 +50,8 @@ val files_equal : string -> string -> bool

 val generate_header : ?extra_sources:string list -> comment_style -> unit

-val output_to : string -> (unit -> 'a) -> unit
+(** Redirect stdout to a file. If `rustfmt` is true, will format the text with
rustfmt. *)
+val output_to : ?rustfmt:bool -> string -> (unit -> 'a) -> unit
 val pr : ('a, unit, string, unit) format4 -> 'a
 val pr_wrap : ?maxcol:int -> char -> (unit -> 'a) -> unit
 val pr_wrap_cstr : ?maxcol:int -> (unit -> 'a) -> unit
diff --git a/rust/.gitignore b/rust/.gitignore
new file mode 100644
index 0000000..da001fd
--- /dev/null
+++ b/rust/.gitignore
@@ -0,0 +1,3 @@
+/target
+/Cargo.lock
+/src/bindings.rs
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..f4fe8fb
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,48 @@
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# 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
+
+[workspace]
+
+[workspace.package]
+# TODO: Add authors.
+version = "0.1.0"
+edition = "2021"
+description = "Rust bindings for libnbd, a client library for controlling
block devices over a network."
+license = "LGPL-2.1-only"
+keywords = ["libnbd", "block-device", "network"]
+categories = ["api-bindings", "emulators",
"virtualization"]
+
+[package]
+name = "libnbd"
+version.workspace = true
+edition.workspace = true
+description.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+
+[dependencies]
+libnbd-sys = { path = "libnbd-sys" }
+bitflags = "2.3.1"
+errno = "0.3.1"
+os_socketaddr = "0.2.4"
+os_str_bytes = { version = "6.5.0", default-features = false }
+thiserror = "1.0.40"
+log = { version = "0.4.19", optional = true }
+
+[features]
+default = ["log"]
diff --git a/rust/Makefile.am b/rust/Makefile.am
new file mode 100644
index 0000000..5fc26e2
--- /dev/null
+++ b/rust/Makefile.am
@@ -0,0 +1,54 @@
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# 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
+
+generator_built = \
+  src/bindings.rs \
+	$(NULL)
+
+EXTRA_DIST = \
+	$(generator_built) \
+	.gitignore \
+	Cargo.toml \
+	src/*.rs \
+	libnbd-sys/Cargo.toml \
+	libnbd-sys/build.rs \
+	libnbd-sys/src/*.rs \
+	$(NULL)
+
+if HAVE_RUST
+
+all-local: $(source_files)
+	$(abs_top_builddir)/run echo $(VERSION) > libnbd-sys/libnbd_version
+	$(abs_top_builddir)/run $(CARGO) build
+	$(abs_top_builddir)/run $(CARGO) doc
+
+TESTS_ENVIRONMENT = \
+	LIBNBD_DEBUG=1 \
+	$(MALLOC_CHECKS) \
+	abs_top_srcdir=$(abs_top_srcdir) \
+	$(NULL)
+LOG_COMPILER = $(top_builddir)/run
+TESTS = run-tests.sh
+
+endif
+
+clean-local:
+	cargo clean
+CLEANFILES += $(generator_built)
+CLEANFILES += libnbd-sys/libnbd_version
diff --git a/rust/cargo_test/.gitignore b/rust/cargo_test/.gitignore
new file mode 100644
index 0000000..4fffb2f
--- /dev/null
+++ b/rust/cargo_test/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/rust/cargo_test/Cargo.toml b/rust/cargo_test/Cargo.toml
new file mode 100644
index 0000000..3283602
--- /dev/null
+++ b/rust/cargo_test/Cargo.toml
@@ -0,0 +1,23 @@
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# 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
+
+[workspace]
+
+[package]
+name = "cargo_test"
+edition = "2021"
+version = "0.1.0"
diff --git a/rust/cargo_test/README.md b/rust/cargo_test/README.md
new file mode 100644
index 0000000..f80646b
--- /dev/null
+++ b/rust/cargo_test/README.md
@@ -0,0 +1,3 @@
+The solely purpose of this directory is to serve as a test crate for checking
if Cargo is useable.
+`cargo test`, `cargo doc` and `cargo fmt` are run in the Autoconf script in
this directory. If any of the commands failes,
+Cargo is assumed not to be useable and the Rust bindings will be disabled.
diff --git a/rust/cargo_test/src/lib.rs b/rust/cargo_test/src/lib.rs
new file mode 100644
index 0000000..02c4323
--- /dev/null
+++ b/rust/cargo_test/src/lib.rs
@@ -0,0 +1,31 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+/// A dummy test function which adds one to an 32-bit integer.
+pub fn add_one(i: i32) -> i32 {
+    i + 1
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_add_one() {
+        assert_eq!(add_one(42), 43);
+    }
+}
diff --git a/rust/libnbd-sys/.gitignore b/rust/libnbd-sys/.gitignore
new file mode 100644
index 0000000..a662d0c
--- /dev/null
+++ b/rust/libnbd-sys/.gitignore
@@ -0,0 +1,4 @@
+/target
+/Cargo.lock
+/src/bindings.rs
+/libnbd_version
diff --git a/rust/libnbd-sys/Cargo.toml b/rust/libnbd-sys/Cargo.toml
new file mode 100644
index 0000000..adb644c
--- /dev/null
+++ b/rust/libnbd-sys/Cargo.toml
@@ -0,0 +1,30 @@
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# 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
+
+[package]
+name = "libnbd-sys"
+version.workspace = true
+edition.workspace = true
+description.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+links = "nbd"
+
+[build-dependencies]
+bindgen = "0.65.1"
+pkg-config = "0.3.27"
diff --git a/rust/libnbd-sys/build.rs b/rust/libnbd-sys/build.rs
new file mode 100644
index 0000000..cd7068e
--- /dev/null
+++ b/rust/libnbd-sys/build.rs
@@ -0,0 +1,60 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+extern crate bindgen;
+
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+
+fn main() {
+    // If the environment variable LIBNBD_DIR is set, use it as the path for
the libnbd shared
+    // library.  Else, use pkg-config.
+    let libnbd_version =
fs::read_to_string("libnbd_version").unwrap();
+    pkg_config::Config::new()
+        .atleast_version(libnbd_version.trim())
+        .probe("libnbd")
+        .unwrap();
+
+    // Tell cargo to tell rustc to link the system libnbd
+    // shared library.
+    //println!("cargo:rustc-link-lib=nbd");
+
+    // Tell cargo to invalidate the built crate whenever the wrapper changes
+    println!("cargo:rerun-if-changed=wrapper.h");
+
+    // The bindgen::Builder is the main entry point
+    // to bindgen, and lets you build up options for
+    // the resulting bindings.
+    let bindings = bindgen::Builder::default()
+        // The input header we would like to generate
+        // bindings for.
+        .header("wrapper.h")
+        // Tell cargo to invalidate the built crate whenever any of the
+        // included header files changed.
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        // Finish the builder and generate the bindings.
+        .generate()
+        // Unwrap the Result and panic on failure.
+        .expect("Unable to generate bindings");
+
+    // Write the bindings to the $OUT_DIR/bindings.rs file.
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
diff --git a/rust/libnbd-sys/src/lib.rs b/rust/libnbd-sys/src/lib.rs
new file mode 100644
index 0000000..fd2a1ba
--- /dev/null
+++ b/rust/libnbd-sys/src/lib.rs
@@ -0,0 +1,24 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+//! All functions from libnbd.h generated by bindgen.
+
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/rust/libnbd-sys/wrapper.h b/rust/libnbd-sys/wrapper.h
new file mode 100644
index 0000000..308d940
--- /dev/null
+++ b/rust/libnbd-sys/wrapper.h
@@ -0,0 +1,18 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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 "../../include/libnbd.h"
diff --git a/rust/run-tests.sh b/rust/run-tests.sh
new file mode 100755
index 0000000..7a0bc85
--- /dev/null
+++ b/rust/run-tests.sh
@@ -0,0 +1,24 @@
+#!/bin/bash -
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# 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
+
+. ../tests/functions.sh
+
+set -e
+set -x
+
+cargo test
diff --git a/rust/src/error.rs b/rust/src/error.rs
new file mode 100644
index 0000000..a2ae702
--- /dev/null
+++ b/rust/src/error.rs
@@ -0,0 +1,47 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+use crate::sys;
+use errno::Errno;
+use std::ffi::CStr;
+
+/// A general error type for libnbd.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("Errno: {errno}: {description}")]
+    WithErrno { errno: Errno, description: String },
+    #[error("{description}")]
+    WithoutErrno { description: String },
+}
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+impl Error {
+    /// Retrieve the last error from libnbd in the current thread.
+    pub(crate) unsafe fn get_error() -> Self {
+        let description = CStr::from_ptr(sys::nbd_get_error())
+            .to_string_lossy()
+            .to_string();
+        match sys::nbd_get_errno() {
+            0 => Self::WithoutErrno { description },
+            e => Self::WithErrno {
+                description,
+                errno: Errno(e),
+            },
+        }
+    }
+}
diff --git a/rust/src/handle.rs b/rust/src/handle.rs
new file mode 100644
index 0000000..de88a39
--- /dev/null
+++ b/rust/src/handle.rs
@@ -0,0 +1,53 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+use crate::sys;
+use crate::{Error, Result};
+
+/// An NBD client handle.
+pub struct NbdHandle(pub(crate) *mut sys::nbd_handle);
+
+impl NbdHandle {
+    pub fn new() -> Result<Self> {
+        let handle = unsafe { sys::nbd_create() };
+        if handle.is_null() {
+            Err(unsafe { Error::get_error() })
+        } else {
+            #[allow(unused_mut)]
+            let mut nbd = NbdHandle(handle);
+            #[cfg(feature = "log")]
+            {
+                nbd.set_debug_callback(|func_name, msg| {
+                    log::debug!(
+                        target: func_name.to_string_lossy().as_ref(),
+                        "{}",
+                        msg.to_string_lossy()
+                    );
+                    Ok(())
+                })?;
+                nbd.set_debug(true)?;
+            }
+            Ok(nbd)
+        }
+    }
+}
+
+impl Drop for NbdHandle {
+    fn drop(&mut self) {
+        unsafe { sys::nbd_close(self.0) }
+    }
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644
index 0000000..e0d00de
--- /dev/null
+++ b/rust/src/lib.rs
@@ -0,0 +1,28 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+#![deny(warnings)]
+
+mod bindings;
+mod error;
+mod handle;
+pub mod types;
+mod utils;
+pub use bindings::*;
+pub use error::{Error, Result};
+pub use handle::NbdHandle;
+pub(crate) use libnbd_sys as sys;
diff --git a/rust/src/types.rs b/rust/src/types.rs
new file mode 100644
index 0000000..fec5a2a
--- /dev/null
+++ b/rust/src/types.rs
@@ -0,0 +1,18 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+pub struct Cookie(pub(crate) u64);
diff --git a/rust/src/utils.rs b/rust/src/utils.rs
new file mode 100644
index 0000000..8db7b6b
--- /dev/null
+++ b/rust/src/utils.rs
@@ -0,0 +1,23 @@
+// nbd client library in userspace
+// Copyright Red Hat
+//
+// 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
+
+use std::ffi::c_void;
+
+/// Take a C pointer to some rust data of type `T` on the heap and drop it.
+pub unsafe extern "C" fn drop_data<T>(data: *mut c_void) {
+    drop(Box::from_raw(data as *mut T))
+}
--
2.41.0