>> +let hidden_handle_calls : NameSt.t = + NameSet.of_list + [ >> "get_debug"; "set_debug"; "set_debug_callback"; >> "clear_debug_callback" ] > > So, hmm. Isn't it better to track this in the API.ml file?Perhaps, but it is only specific to Rust, so I thought it was best placed in the Rust.ml file.>> +(* Convert snake-case to upper camel-case. *) > > In this comment, an example would help. eg: > > (* Convert snake-case to upper camel-case. * For example: [get_debug > => GetDebug] *) > > Doesn't golang have a similar function? It does: > > generator/Golang.ml: let camel_case name > > It would be better to extract this function into common code (eg. > into generator/utils.ml), as a separate patch.Yes, I've made a new patch with the camel_case-function moved to utils.ml. I think I fixed most of your other comments as well. Best regards, Tage
Tage Johansson
2023-Jun-26 08:02 UTC
[Libguestfs] [PATCH v2 1/3] generator: Move [camel_case] function from GoLang.ml to utils.ml as it will be used in Rust.ml as well
From: Tage Johansson <frans.tage at gmail.com> --- generator/GoLang.ml | 11 ----------- generator/utils.ml | 8 ++++++++ generator/utils.mli | 3 +++ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/generator/GoLang.ml b/generator/GoLang.ml index 4ab6b26..50ed722 100644 --- a/generator/GoLang.ml +++ b/generator/GoLang.ml @@ -30,17 +30,6 @@ 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 diff --git a/generator/utils.ml b/generator/utils.ml index 6d77e51..3a96929 100644 --- a/generator/utils.ml +++ b/generator/utils.ml @@ -500,3 +500,11 @@ let pod2text longdesc Hashtbl.add cache key lines; save_cache (); lines + +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 diff --git a/generator/utils.mli b/generator/utils.mli index e622e31..b4a2525 100644 --- a/generator/utils.mli +++ b/generator/utils.mli @@ -65,3 +65,6 @@ val line_directive_of_location : location -> string type cache_key = string type cache_value = string list val pod2text : cache_key -> cache_value + +(* Convert C function name to upper-camel-case name. *) +val camel_case : string -> string -- 2.41.0
Tage Johansson
2023-Jun-26 08:02 UTC
[Libguestfs] [PATCH v2 2/3] rust: create basic Rust bindings
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 | 529 +++++++++++++++++++++++++++++++++++++ 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 | 60 +++++ 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 ++ rustfmt.toml | 18 ++ 27 files changed, 1109 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..b8e93ad 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..2493467 --- /dev/null +++ b/generator/Rust.ml @@ -0,0 +1,529 @@ +(* hey emacs, this is OCaml code: -*- tuareg -*- *) +(* nbd client library in userspace: generator + * Copyright Tage Johansson + * + * 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" ] + +let print_rust_constant (name, value) + pr "pub const %s: u32 = %d;\n" name value + +let print_rust_enum (enum : enum) + pr "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n"; + pr "#[repr(isize)]"; + pr "pub enum %s {\n" (camel_case enum.enum_prefix); + List.iter + (fun (name, num) -> pr " %s = %d,\n" (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" (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 }) -> + camel_case name + | Fd _ -> "OwnedFd" + | BytesIn _ -> "&[u8]" + | BytesOut _ -> "&mut [u8]" + (* XXX: 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 } -> + 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 { " cbname; + pr "callback: None, "; + pr "free: None, "; + pr "user_data: ptr::null_mut() "; + pr "},\n"; + pr "};\n" + | OFlags (_, { flag_prefix }, _) -> + let flags_type = 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 <I>th variable + will be named `<NAME>_ffi_<I>` where `<NAME>` is the name of the argument. + Keep in mind that this function may create multiple variables. *) +let rust_cbarg_to_ffi 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 + let ffi_names = ffi_cbarg_names cbarg in + pr "let %s: %s = " (rust_cbarg_name cbarg) (rust_cbarg_type cbarg); + (match (cbarg, ffi_names) with + | (CBInt _ | CBUInt _ | CBInt64 _ | CBUInt64 _), [ ffi_name ] -> + pr "%s" ffi_name + | CBString _, [ ffi_name ] -> pr "CStr::from_ptr(%s)" ffi_name + | CBBytesIn _, [ ffi_buf_name; ffi_len_name ] -> + pr "slice::from_raw_parts(%s as *const u8, %s)" ffi_buf_name ffi_len_name + | CBArrayAndLen (UInt32 _, _), [ ffi_arr_name; ffi_len_name ] -> + 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 _), [ ffi_name ] -> pr "%s.as_mut().unwrap()" ffi_name + | CBMutable _, [ _ ] -> + failwith + "generator/Rust.ml: in ffi_cbargs_to_rust: Unsupported type of mutable \ + argument." + | _, _ -> + failwith + "generator/Rust.ml: In ffi_cbargs_to_rust: bad number of ffi arguments."); + 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)); + + (* Print 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..450e4ca --- /dev/null +++ b/generator/Rust.mli @@ -0,0 +1,20 @@ +(* nbd client library in userspace: generator + * Copyright Tage Johansson + * + * 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 3a96929..3a1cc9f 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 b4a2525..7489fe0 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..0a934e7 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,48 @@ +# nbd client library in userspace +# Copyright Tage Johansson +# +# 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..cb8d7c9 --- /dev/null +++ b/rust/Makefile.am @@ -0,0 +1,60 @@ +# nbd client library in userspace +# Copyright Tage Johansson +# +# 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/lib.rs \ + src/error.rs \ + src/handle.rs \ + src/types.rs \ + src/utils.rs \ + libnbd-sys/Cargo.toml \ + libnbd-sys/build.rs \ + libnbd-sys/wrapper.h \ + libnbd-sys/src/lib.rs \ + $(NULL) + +if HAVE_RUST + +all-local: $(source_files) + rm -f libnbd-sys/libnbd_version.t + $(abs_top_builddir)/run echo $(VERSION) > libnbd-sys/libnbd_version.t + mv libnbd-sys/libnbd_version.t 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 += 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..9f9d478 --- /dev/null +++ b/rust/cargo_test/Cargo.toml @@ -0,0 +1,23 @@ +# nbd client library in userspace +# Copyright Tage Johansson +# +# 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..a5cbb84 --- /dev/null +++ b/rust/cargo_test/src/lib.rs @@ -0,0 +1,31 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..7eadd0a --- /dev/null +++ b/rust/libnbd-sys/Cargo.toml @@ -0,0 +1,30 @@ +# nbd client library in userspace +# Copyright Tage Johansson +# +# 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..3dcf8cc --- /dev/null +++ b/rust/libnbd-sys/build.rs @@ -0,0 +1,60 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..f93317c --- /dev/null +++ b/rust/libnbd-sys/src/lib.rs @@ -0,0 +1,24 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..c25f751 --- /dev/null +++ b/rust/libnbd-sys/wrapper.h @@ -0,0 +1,18 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..5c1780a --- /dev/null +++ b/rust/src/error.rs @@ -0,0 +1,47 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..48b84d9 --- /dev/null +++ b/rust/src/handle.rs @@ -0,0 +1,53 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..4a4e74a --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,28 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..eb2df06 --- /dev/null +++ b/rust/src/types.rs @@ -0,0 +1,18 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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..b8200c1 --- /dev/null +++ b/rust/src/utils.rs @@ -0,0 +1,23 @@ +// nbd client library in userspace +// Copyright Tage Johansson +// +// 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)) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..249e923 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,18 @@ +# nbd client library in userspace +# Copyright Tage Johansson +# +# 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 + +max_width = 80 -- 2.41.0
Tage Johansson
2023-Jun-26 08:02 UTC
[Libguestfs] [PATCH v2 3/3] rust: add some (but not yet all) tests
From: Tage Johansson <frans.tage at gmail.com> --- rust/Cargo.toml | 3 + rust/Makefile.am | 1 + rust/run-tests.sh | 6 +- rust/tests/test_100_handle.rs | 25 +++++++ rust/tests/test_110_defaults.rs | 33 ++++++++++ rust/tests/test_120_set_non_defaults.rs | 56 ++++++++++++++++ rust/tests/test_130_private_data.rs | 28 ++++++++ rust/tests/test_140_explicit_close.rs | 31 +++++++++ rust/tests/test_200_connect_command.rs | 33 ++++++++++ rust/tests/test_210_opt_abort.rs | 39 +++++++++++ rust/tests/test_220_opt_list.rs | 85 ++++++++++++++++++++++++ rust/tests/test_log/mod.rs | 86 +++++++++++++++++++++++++ 12 files changed, 424 insertions(+), 2 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0a934e7..2ce7e06 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -46,3 +46,6 @@ log = { version = "0.4.19", optional = true } [features] default = ["log"] + +[dev-dependencies] +byte-strings = "0.3.1" diff --git a/rust/Makefile.am b/rust/Makefile.am index cb8d7c9..9ff0779 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -49,6 +49,7 @@ TESTS_ENVIRONMENT = \ LIBNBD_DEBUG=1 \ $(MALLOC_CHECKS) \ abs_top_srcdir=$(abs_top_srcdir) \ + CARGO=$(CARGO) \ $(NULL) LOG_COMPILER = $(top_builddir)/run TESTS = run-tests.sh diff --git a/rust/run-tests.sh b/rust/run-tests.sh index 7a0bc85..005000e 100755 --- a/rust/run-tests.sh +++ b/rust/run-tests.sh @@ -1,6 +1,6 @@ #!/bin/bash - # nbd client library in userspace -# Copyright Red Hat +# Copyright Tage Johansson # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -21,4 +21,6 @@ set -e set -x -cargo test +requires nbdkit --version + +$CARGO test -- --nocapture diff --git a/rust/tests/test_100_handle.rs b/rust/tests/test_100_handle.rs new file mode 100644 index 0000000..3a281ca --- /dev/null +++ b/rust/tests/test_100_handle.rs @@ -0,0 +1,25 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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 + +//! Just check that we can link with libnbd and create a handle. + +#![deny(warnings)] + +#[test] +fn test_nbd_handle_new() { + let _ = libnbd::NbdHandle::new().unwrap(); +} diff --git a/rust/tests/test_110_defaults.rs b/rust/tests/test_110_defaults.rs new file mode 100644 index 0000000..8960ef9 --- /dev/null +++ b/rust/tests/test_110_defaults.rs @@ -0,0 +1,33 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +#[test] +fn test_defaults() { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + + assert!(nbd.get_export_name().unwrap().to_str().unwrap().is_empty()); + assert!(!nbd.get_full_info().unwrap()); + assert_eq!(nbd.get_tls(), libnbd::Tls::Disable); + assert!(nbd.get_request_structured_replies()); + assert!(nbd.get_request_meta_context().unwrap()); + assert!(nbd.get_request_block_size().unwrap()); + assert!(nbd.get_pread_initialize()); + assert!(nbd.get_handshake_flags().is_all()); + assert!(!nbd.get_opt_mode()); +} diff --git a/rust/tests/test_120_set_non_defaults.rs b/rust/tests/test_120_set_non_defaults.rs new file mode 100644 index 0000000..88dd2c8 --- /dev/null +++ b/rust/tests/test_120_set_non_defaults.rs @@ -0,0 +1,56 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +use std::ffi::CString; + +#[test] +fn test_set_non_defaults() { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + + let name = CString::new("name").unwrap(); + nbd.set_export_name(&name).unwrap(); + assert_eq!(nbd.get_export_name().unwrap(), name); + + nbd.set_full_info(true).unwrap(); + assert!(nbd.get_full_info().unwrap()); + + if nbd.supports_tls() { + nbd.set_tls(libnbd::Tls::Allow).unwrap(); + assert_eq!(nbd.get_tls(), libnbd::Tls::Allow); + } + + nbd.set_request_structured_replies(false).unwrap(); + assert!(!nbd.get_request_structured_replies()); + + nbd.set_request_meta_context(false).unwrap(); + assert!(!nbd.get_request_meta_context().unwrap()); + + nbd.set_request_block_size(false).unwrap(); + assert!(!nbd.get_request_block_size().unwrap()); + + nbd.set_pread_initialize(false).unwrap(); + assert!(!nbd.get_pread_initialize()); + + nbd.set_handshake_flags(libnbd::HandshakeFlag::empty()) + .unwrap(); + assert!(nbd.get_handshake_flags().is_empty()); + + nbd.set_opt_mode(true).unwrap(); + assert!(nbd.get_opt_mode()); +} diff --git a/rust/tests/test_130_private_data.rs b/rust/tests/test_130_private_data.rs new file mode 100644 index 0000000..62db160 --- /dev/null +++ b/rust/tests/test_130_private_data.rs @@ -0,0 +1,28 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +#[test] +fn test_private_data() { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + + assert_eq!(nbd.get_private_data(), 0); + assert_eq!(nbd.set_private_data(42), 0); + assert_eq!(nbd.set_private_data(314), 42); + assert_eq!(nbd.get_private_data(), 314); +} diff --git a/rust/tests/test_140_explicit_close.rs b/rust/tests/test_140_explicit_close.rs new file mode 100644 index 0000000..19f4f85 --- /dev/null +++ b/rust/tests/test_140_explicit_close.rs @@ -0,0 +1,31 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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 test_log; + +use test_log::DEBUG_LOGGER; + +#[test] +fn test_private_data() { + DEBUG_LOGGER.init(); + + let nbd = libnbd::NbdHandle::new().unwrap(); + drop(nbd); + assert!(DEBUG_LOGGER.contains("closing handle")); +} diff --git a/rust/tests/test_200_connect_command.rs b/rust/tests/test_200_connect_command.rs new file mode 100644 index 0000000..ad23d39 --- /dev/null +++ b/rust/tests/test_200_connect_command.rs @@ -0,0 +1,33 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +use byte_strings::c_str; + +#[test] +fn test_connect_command() { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + ]) + .unwrap(); +} diff --git a/rust/tests/test_210_opt_abort.rs b/rust/tests/test_210_opt_abort.rs new file mode 100644 index 0000000..c5109d5 --- /dev/null +++ b/rust/tests/test_210_opt_abort.rs @@ -0,0 +1,39 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +use byte_strings::c_str; + +#[test] +fn test_opt_abort() { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + ]) + .unwrap(); + assert_eq!(nbd.get_protocol().unwrap(), c_str!("newstyle-fixed")); + assert!(nbd.get_structured_replies_negotiated().unwrap()); + + nbd.opt_abort().unwrap(); + assert!(nbd.aio_is_closed()); +} diff --git a/rust/tests/test_220_opt_list.rs b/rust/tests/test_220_opt_list.rs new file mode 100644 index 0000000..d2357de --- /dev/null +++ b/rust/tests/test_220_opt_list.rs @@ -0,0 +1,85 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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)] + +use byte_strings::c_str; +use std::cell::RefCell; +use std::env; +use std::ffi::{CStr, CString}; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; +use std::rc::Rc; + +/// Test different types of connections. +struct ConnTester { + script_path: CString, +} + +impl ConnTester { + fn new() -> Self { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/opt-list.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + Self { script_path } + } + + fn connect( + &self, + mode: u8, + expected_exports: &[&CStr], + ) -> libnbd::Result<()> { + let mut nbd = libnbd::NbdHandle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("sh"), + &self.script_path, + &CString::new(format!("mode={mode}")).unwrap(), + ]) + .unwrap(); + + // Collect all exports in this list. + let exports = Rc::new(RefCell::new(Vec::new())); + let exports_clone = exports.clone(); + let count = nbd.opt_list(move |name, _| { + exports_clone.borrow_mut().push(name.to_owned()); + Ok(()) + })?; + let exports = exports.borrow(); + assert_eq!(exports.len(), count as usize); + assert_eq!(exports.len(), expected_exports.len()); + for (export, &expected) in exports.iter().zip(expected_exports) { + assert_eq!(export.as_c_str(), expected); + } + Ok(()) + } +} + +#[test] +fn test_opt_list() { + let conn_tester = ConnTester::new(); + assert!(conn_tester.connect(0, &[]).is_err()); + assert!(conn_tester.connect(1, &[c_str!("a"), c_str!("b")]).is_ok()); + assert!(conn_tester.connect(2, &[]).is_ok()); + assert!(conn_tester.connect(3, &[c_str!("a")]).is_ok()); +} diff --git a/rust/tests/test_log/mod.rs b/rust/tests/test_log/mod.rs new file mode 100644 index 0000000..8dbcd79 --- /dev/null +++ b/rust/tests/test_log/mod.rs @@ -0,0 +1,86 @@ +// libnbd Rust test case +// Copyright Tage Johansson +// +// 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 + +//! This module provides facilities for capturing log output and asserting that +//! it does or does not contain certain messages. The primary use of this module +//! is to assert that certain libnbd operations are or are not performed. + +#![allow(unused)] + +use std::sync::Mutex; + +/// Logger that stores all debug messages in a list. +pub struct DebugLogger { + /// All targets and messages logged. Wrapped in a mutex so that it can be + /// updated with an imutable reference to self. + entries: Mutex<Vec<(String, String)>>, + is_initialized: Mutex<bool>, +} + +impl DebugLogger { + const fn new() -> Self { + Self { + entries: Mutex::new(Vec::new()), + is_initialized: Mutex::new(false), + } + } + + /// Set this logger as the global logger. + pub fn init(&'static self) { + let mut is_initialized = self.is_initialized.lock().unwrap(); + if !*is_initialized { + log::set_logger(self).unwrap(); + log::set_max_level(log::LevelFilter::Debug); + *is_initialized = true; + } + } + + /// Check wether a specific message has been logged. + pub fn contains(&self, msg: &str) -> bool { + self.entries.lock().unwrap().iter().any(|(_, x)| x == msg) + } + + /// Print all logged messages, in no particular order. + /// + /// Only for debug purposes. Remember to run cargo test with the `-- + /// --nocapture` arguments. That is, from the rust directory run: + /// `./../run cargo test -- --nocapture` + pub fn print_messages(&self) { + for (target, msg) in self.entries.lock().unwrap().iter() { + eprintln!("{target}: {msg}"); + } + } +} + +/// A static global `DebugLogger`. Just call `.init()` on this to set it as the +/// global logger. +pub static DEBUG_LOGGER: DebugLogger = DebugLogger::new(); + +impl log::Log for DebugLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.level() == log::Level::Debug + } + + fn log(&self, record: &log::Record<'_>) { + self.entries + .lock() + .unwrap() + .push((record.target().to_string(), record.args().to_string())); + } + + fn flush(&self) {} +} -- 2.41.0