Hello again, In patch v2, two tests were not compiling. This update fixes that. Best regards, Tage Tage Johansson (10): rust: create basic Rust bindings generator: Add information about asynchronous handle calls generator: Add information about the lifetime of closures rust: Use more specific closure traits rust: Add a couple of integration tests rust: Make it possible to run tests with Valgrind rust: async: Create an async friendly handle type rust: async: Add a couple of integration tests generator: Add `modifies_fd` flag to the [call] structure rust: async: Use the `modifies_fd` flag to exclude calls .gitignore | 8 + .ocamlformat | 4 + Makefile.am | 1 + configure.ac | 13 + generator/API.ml | 84 ++ generator/API.mli | 35 + generator/Makefile.am | 2 + generator/Rust.ml | 814 ++++++++++++++++++ generator/Rust.mli | 22 + generator/generator.ml | 3 + rust/Cargo.toml | 55 ++ rust/Makefile.am | 76 ++ 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 | 30 + rust/src/async_handle.rs | 222 +++++ rust/src/error.rs | 118 +++ rust/src/handle.rs | 67 ++ rust/src/lib.rs | 32 + rust/src/types.rs | 20 + rust/src/utils.rs | 23 + rust/tests/nbdkit_pattern/mod.rs | 28 + 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_230_opt_info.rs | 124 +++ rust/tests/test_240_opt_list_meta.rs | 151 ++++ rust/tests/test_245_opt_list_meta_queries.rs | 98 +++ rust/tests/test_250_opt_set_meta.rs | 124 +++ rust/tests/test_255_opt_set_meta_queries.rs | 111 +++ rust/tests/test_300_get_size.rs | 36 + rust/tests/test_400_pread.rs | 40 + rust/tests/test_405_pread_structured.rs | 80 ++ rust/tests/test_410_pwrite.rs | 62 ++ rust/tests/test_460_block_status.rs | 96 +++ rust/tests/test_620_stats.rs | 76 ++ rust/tests/test_async_100_handle.rs | 25 + rust/tests/test_async_200_connect_command.rs | 34 + rust/tests/test_async_210_opt_abort.rs | 40 + rust/tests/test_async_220_opt_list.rs | 86 ++ rust/tests/test_async_230_opt_info.rs | 126 +++ rust/tests/test_async_235memleak.rs | 57 ++ rust/tests/test_async_240_opt_list_meta.rs | 151 ++++ .../test_async_245_opt_list_meta_queries.rs | 96 +++ rust/tests/test_async_250_opt_set_meta.rs | 123 +++ .../test_async_255_opt_set_meta_queries.rs | 111 +++ rust/tests/test_async_400_pread.rs | 41 + rust/tests/test_async_405_pread_structured.rs | 85 ++ rust/tests/test_async_410_pwrite.rs | 63 ++ rust/tests/test_async_460_block_status.rs | 96 +++ rust/tests/test_async_620_stats.rs | 77 ++ rust/tests/test_log/mod.rs | 86 ++ rustfmt.toml | 19 + 60 files changed, 4433 insertions(+) create mode 100644 .ocamlformat create mode 100644 generator/Rust.ml create mode 100644 generator/Rust.mli create mode 100644 rust/Cargo.toml create mode 100644 rust/Makefile.am create mode 100644 rust/libnbd-sys/Cargo.toml create mode 100644 rust/libnbd-sys/build.rs create mode 100644 rust/libnbd-sys/src/lib.rs create mode 100644 rust/libnbd-sys/wrapper.h create mode 100755 rust/run-tests.sh create mode 100644 rust/src/async_handle.rs create mode 100644 rust/src/error.rs create mode 100644 rust/src/handle.rs create mode 100644 rust/src/lib.rs create mode 100644 rust/src/types.rs create mode 100644 rust/src/utils.rs create mode 100644 rust/tests/nbdkit_pattern/mod.rs create mode 100644 rust/tests/test_100_handle.rs create mode 100644 rust/tests/test_110_defaults.rs create mode 100644 rust/tests/test_120_set_non_defaults.rs create mode 100644 rust/tests/test_130_private_data.rs create mode 100644 rust/tests/test_140_explicit_close.rs create mode 100644 rust/tests/test_200_connect_command.rs create mode 100644 rust/tests/test_210_opt_abort.rs create mode 100644 rust/tests/test_220_opt_list.rs create mode 100644 rust/tests/test_230_opt_info.rs create mode 100644 rust/tests/test_240_opt_list_meta.rs create mode 100644 rust/tests/test_245_opt_list_meta_queries.rs create mode 100644 rust/tests/test_250_opt_set_meta.rs create mode 100644 rust/tests/test_255_opt_set_meta_queries.rs create mode 100644 rust/tests/test_300_get_size.rs create mode 100644 rust/tests/test_400_pread.rs create mode 100644 rust/tests/test_405_pread_structured.rs create mode 100644 rust/tests/test_410_pwrite.rs create mode 100644 rust/tests/test_460_block_status.rs create mode 100644 rust/tests/test_620_stats.rs create mode 100644 rust/tests/test_async_100_handle.rs create mode 100644 rust/tests/test_async_200_connect_command.rs create mode 100644 rust/tests/test_async_210_opt_abort.rs create mode 100644 rust/tests/test_async_220_opt_list.rs create mode 100644 rust/tests/test_async_230_opt_info.rs create mode 100644 rust/tests/test_async_235memleak.rs create mode 100644 rust/tests/test_async_240_opt_list_meta.rs create mode 100644 rust/tests/test_async_245_opt_list_meta_queries.rs create mode 100644 rust/tests/test_async_250_opt_set_meta.rs create mode 100644 rust/tests/test_async_255_opt_set_meta_queries.rs create mode 100644 rust/tests/test_async_400_pread.rs create mode 100644 rust/tests/test_async_405_pread_structured.rs create mode 100644 rust/tests/test_async_410_pwrite.rs create mode 100644 rust/tests/test_async_460_block_status.rs create mode 100644 rust/tests/test_async_620_stats.rs create mode 100644 rust/tests/test_log/mod.rs create mode 100644 rustfmt.toml base-commit: fbde5974fd5c8a3bcb081db0b1074c4a3e723e76 -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 01/10] rust: create basic Rust bindings
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. --- .gitignore | 8 + .ocamlformat | 4 + Makefile.am | 1 + configure.ac | 13 + generator/Makefile.am | 2 + generator/Rust.ml | 578 +++++++++++++++++++++++++++++++++++++ generator/Rust.mli | 20 ++ generator/generator.ml | 2 + rust/Cargo.toml | 50 ++++ rust/Makefile.am | 71 +++++ 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 | 111 +++++++ rust/src/handle.rs | 65 +++++ rust/src/lib.rs | 28 ++ rust/src/types.rs | 18 ++ rust/src/utils.rs | 23 ++ rustfmt.toml | 19 ++ 21 files changed, 1169 insertions(+) create mode 100644 .ocamlformat create mode 100644 generator/Rust.ml create mode 100644 generator/Rust.mli create mode 100644 rust/Cargo.toml create mode 100644 rust/Makefile.am create mode 100644 rust/libnbd-sys/Cargo.toml create mode 100644 rust/libnbd-sys/build.rs create mode 100644 rust/libnbd-sys/src/lib.rs create mode 100644 rust/libnbd-sys/wrapper.h create mode 100755 rust/run-tests.sh create mode 100644 rust/src/error.rs create mode 100644 rust/src/handle.rs create mode 100644 rust/src/lib.rs create mode 100644 rust/src/types.rs create mode 100644 rust/src/utils.rs create mode 100644 rustfmt.toml diff --git a/.gitignore b/.gitignore index efe3080..098e004 100644 --- a/.gitignore +++ b/.gitignore @@ -174,6 +174,14 @@ Makefile.in /python/nbd.py /python/run-python-tests /run +/rust/Cargo.lock +/rust/libnbd-sys/Cargo.lock +/rust/libnbd-sys/libnbd_version +/rust/libnbd-sys/src/bindings.rs +/rust/libnbd-sys/target +/rust/src/async_bindings.rs +/rust/src/bindings.rs +/rust/target /sh/nbdsh /sh/nbdsh.1 /stamp-h1 diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..7bfe155 --- /dev/null +++ b/.ocamlformat @@ -0,0 +1,4 @@ +profile = default +version = 0.25.1 +wrap-comments = true +margin = 78 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 0b94f5e..0004f25 100644 --- a/configure.ac +++ b/configure.ac @@ -613,6 +613,17 @@ 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]) +],[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 +668,7 @@ AC_CONFIG_FILES([Makefile generator/Makefile golang/Makefile golang/examples/Makefile + rust/Makefile include/Makefile info/Makefile interop/Makefile @@ -717,6 +729,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 c3d53b2..5b93734 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..8e2ca13 --- /dev/null +++ b/generator/Rust.ml @@ -0,0 +1,578 @@ +(* 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" + +(* Print metadata namespaces. *) +let print_metadata_namespace (ns, ctxts) + pr "/// The string \"%s:\" as a &[CStr].\n" ns; + pr "pub const NAMESPACE_%s: &CStr = c_str!(\"%s:\");\n" + (String.uppercase_ascii ns) + ns; + List.iter + (fun (ctxt, consts) -> + let s = ns ^ ":" ^ ctxt in + pr "/// The string \"%s\" as a &[CStr].\n" s; + pr "pub const CONTEXT_%s_%s: &CStr = c_str!(\"%s\");\n" + (String.uppercase_ascii ns) + (String.uppercase_ascii ctxt) + s; + List.iter + (fun (n, i) -> + pr "pub const %s: u32 = %d;\n" (String.uppercase_ascii n) i) + consts) + ctxts + +(* 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]" + | BytesPersistIn _ -> "&'static [u8]" + | BytesPersistOut _ -> "&'static mut [u8]" + | Closure { cbargs } -> "impl " ^ rust_closure_trait cbargs + +(* Get the Rust closure trait for a callback, That is `Fn*(...) -> ...)`. *) +and rust_closure_trait ?(lifetime = Some "'static") cbargs : string + let rust_cbargs = String.concat ", " (List.map rust_cbarg_type cbargs) + and lifetime_constraint + match lifetime with None -> "" | Some x -> " + " ^ x + in + "FnMut(" ^ rust_cbargs ^ ") -> c_int + Send + Sync" ^ lifetime_constraint + +(* 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> = <...>;`. + 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 = unsafe { crate::bindings::%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) => unsafe { crate::bindings::%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 -> "TryInto::<u32>::try_into(ffi_ret).unwrap()" + | RInt64 -> "TryInto::<u64>::try_into(ffi_ret).unwrap()" + | RSizeT -> "TryInto::<usize>::try_into(ffi_ret).unwrap()" + | RCookie -> "Cookie(ffi_ret.try_into().unwrap())" + | RFd -> "ffi_ret as RawFd" + | RStaticString -> "unsafe { CStr::from_ptr(ffi_ret) }" + | RString -> + "{ let res = unsafe { CStr::from_ptr(ffi_ret) }.to_owned();\n" + ^ "unsafe { libc::free(ffi_ret.cast()); }\n" ^ "res}" + | 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 {\n"; + pr " Err(unsafe { Error::get_error(self.raw_handle()) })\n"; + pr "}\n" + | RStaticString | RString -> + pr "if ffi_ret.is_null() {\n"; + pr " Err(unsafe { Error::get_error(self.raw_handle()) })\n"; + pr "}\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 ~lifetime:None 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 "pub(crate) unsafe 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 (sprintf "%s: %s") 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 " callback(%s)\n" (String.concat ", " rust_cbargs_names); + 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 ,comment for a rust function for a handle call. *) +let print_rust_handle_call_comment call + (* 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 a Rust expression which converts Rust like arguments to FFI like + arguments, makes a call on the raw FFI handle, and converts the return + value to a Rusty type. The expression assumes that variables with name + `rust_arg_name arg` for all `arg` in `call.args` exists in scope. *) +let print_ffi_call name handle call + let ffi_args_names + List.flatten (List.map ffi_arg_names call.args) + @ List.map ffi_optarg_name call.optargs + in + pr "{\n"; + pr " // Convert all arguments to FFI-like types.\n"; + List.iter rust_arg_to_ffi call.args; + List.iter rust_optarg_to_ffi call.optargs; + pr "\n"; + pr " // Call the FFI-function.\n"; + pr " let ffi_ret = unsafe { sys::nbd_%s(%s, %s) };\n" name handle + (String.concat ", " ffi_args_names); + pr "\n"; + pr " // Convert the result to something more rusty.\n"; + ffi_ret_to_rust call; + pr "}\n" + +(* Print the Rust function for a handle call. Note that this is a "method" on + the `Handle` struct. So the printed Rust function should be in an `impl + Handle {` 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 + and rust_args_types + List.map rust_arg_type call.args @ List.map rust_optarg_type call.optargs + in + let rust_args + String.concat ", " + (List.map2 (sprintf "%s: %s") rust_args_names rust_args_types) + in + print_rust_handle_call_comment call; + (* 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(&self, %s) -> %s\n" name rust_args (rust_ret_type call); + print_ffi_call name "self.handle" call; + pr "\n" + +let print_rust_imports () + pr "use bitflags::bitflags;\n"; + pr "use byte_strings::const_::c_str;\n"; + pr "use crate::{*, types::*};\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 ~copyright:"Tage Johansson"; + 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_metadata_namespace metadata_namespaces; + List.iter print_rust_closure_to_raw_fn all_closures; + pr "impl Handle {\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..67b9502 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 ~formatter:(Some Rustfmt) "rust/src/bindings.rs" Rust.generate_rust_bindings; diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..e81360b --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,50 @@ +# 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 } +libc = "0.2.147" +byte-strings = "0.3.1" + +[features] +default = ["log"] diff --git a/rust/Makefile.am b/rust/Makefile.am new file mode 100644 index 0000000..e8a916f --- /dev/null +++ b/rust/Makefile.am @@ -0,0 +1,71 @@ +# 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) + +source_files = \ + $(generator_built) \ + Cargo.toml \ + src/lib.rs \ + src/error.rs \ + src/handle.rs \ + src/types.rs \ + src/utils.rs \ + src/async_handle.rs \ + libnbd-sys/Cargo.toml \ + libnbd-sys/build.rs \ + libnbd-sys/wrapper.h \ + libnbd-sys/src/lib.rs \ + $(NULL) + +EXTRA_DIST = \ + $(source_files) \ + $(NULL) + +if HAVE_RUST + +all-local: libnbd-sys/libnbd_version target/debug/liblibnbd.rlib \ + target/doc/libnbd/index.html + +libnbd-sys/libnbd_version: Makefile + 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 + +target/debug/liblibnbd.rlib: $(source_files) + $(abs_top_builddir)/run $(CARGO) build + +target/doc/libnbd/index.html: $(source_files) + $(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/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..615a178 --- /dev/null +++ b/rust/src/error.rs @@ -0,0 +1,111 @@ +// 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; +use std::io; + +/// A general error type for libnbd. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Recoverable(ErrorKind), + #[error("Fatal: NBD handle is dead: {0}")] + Fatal(FatalErrorKind), +} + +#[derive(Debug, thiserror::Error)] +pub enum ErrorKind { + #[error("Errno: {errno}: {description}")] + WithErrno { errno: Errno, description: String }, + #[error("{description}")] + WithoutErrno { description: String }, + #[error(transparent)] + Errno(#[from] Errno), +} + +#[derive(Debug, thiserror::Error)] +pub enum FatalErrorKind { + #[error(transparent)] + Kind(#[from] ErrorKind), + #[error(transparent)] + Io(#[from] io::Error), +} + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +impl ErrorKind { + /// 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), + }, + } + } + + /// Create an error from an errno value without any additional description. + pub fn from_errno(val: i32) -> Self { + Self::Errno(Errno(val)) + } + + /// Get the errno value if any. + pub fn errno(&self) -> Option<i32> { + match self { + Self::WithErrno { + errno: Errno(x), .. + } + | Self::Errno(Errno(x)) => Some(*x), + Self::WithoutErrno { .. } => None, + } + } +} + +impl Error { + /// Retrieve the last error from libnbd in the current thread and check if + /// the handle is dead to determine if the error is fatal or not. + pub(crate) unsafe fn get_error(handle: *mut sys::nbd_handle) -> Self { + let kind = ErrorKind::get_error(); + if sys::nbd_aio_is_dead(handle) != 0 { + Self::Fatal(FatalErrorKind::Kind(kind)) + } else { + Self::Recoverable(kind) + } + } + + /// Get the errno value if any. + pub fn errno(&self) -> Option<i32> { + match self { + Self::Recoverable(e) | Self::Fatal(FatalErrorKind::Kind(e)) => { + e.errno() + } + Self::Fatal(FatalErrorKind::Io(e)) => e.raw_os_error(), + } + } +} + +impl From<io::Error> for Error { + fn from(err: io::Error) -> Self { + Self::Fatal(err.into()) + } +} diff --git a/rust/src/handle.rs b/rust/src/handle.rs new file mode 100644 index 0000000..477faa4 --- /dev/null +++ b/rust/src/handle.rs @@ -0,0 +1,65 @@ +// 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, ErrorKind, Result}; + +/// An NBD client handle. +#[derive(Debug)] +pub struct Handle { + /// A pointer to the raw handle. + pub(crate) handle: *mut sys::nbd_handle, +} + +impl Handle { + pub fn new() -> Result<Self> { + let handle = unsafe { sys::nbd_create() }; + if handle.is_null() { + Err(unsafe { Error::Fatal(ErrorKind::get_error().into()) }) + } else { + #[allow(unused_mut)] + let mut nbd = Handle { 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() + ); + 0 + })?; + nbd.set_debug(true)?; + } + Ok(nbd) + } + } + + /// Get the underliing C pointer to the handle. + pub(crate) fn raw_handle(&self) -> *mut sys::nbd_handle { + self.handle + } +} + +impl Drop for Handle { + fn drop(&mut self) { + unsafe { sys::nbd_close(self.handle) } + } +} + +unsafe impl Send for Handle {} +unsafe impl Sync for Handle {} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..a6f3131 --- /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, ErrorKind, FatalErrorKind, Result}; +pub use handle::Handle; +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..e6250dd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,19 @@ +# 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 + +edition = "2021" +max_width = 80 -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 02/10] generator: Add information about asynchronous handle calls
A new field `async_kind` is added to the `call` data type in generator/API.ml*. The purpose is to tell if a certain handle call is an asynchronous command and if so how one can know when it is completed. The motivation for this is that all asynchronous commands on the `AsyncHandle` in the Rust bindings makes use of Rust's [`async fn`s](https://doc.rust-lang.org/std/keyword.async.html). But to implement such an `async fn`, the API needs to know when the command completed, either by a completion callback or via a change of state. --- generator/API.ml | 32 ++++++++++++++++++++++++++++++++ generator/API.mli | 11 +++++++++++ 2 files changed, 43 insertions(+) diff --git a/generator/API.ml b/generator/API.ml index 5fcb0e1..f90a6fa 100644 --- a/generator/API.ml +++ b/generator/API.ml @@ -32,6 +32,7 @@ type call = { permitted_states : permitted_state list; is_locked : bool; may_set_error : bool; + async_kind : async_kind option; mutable first_version : int * int; } and arg @@ -102,6 +103,9 @@ and permitted_state | Negotiating | Connected | Closed | Dead +and async_kind +| WithCompletionCallback +| ChangesState of string * bool and link | Link of string | SectionLink of string @@ -249,6 +253,7 @@ let default_call = { args = []; optargs = []; ret = RErr; see_also = []; permitted_states = []; is_locked = true; may_set_error = true; + async_kind = None; first_version = (0, 0) } (* Calls. @@ -2798,6 +2803,7 @@ wait for an L<eventfd(2)>."; default_call with args = [ SockAddrAndLen ("addr", "addrlen") ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to the NBD server"; longdesc = "\ Begin connecting to the NBD server. The C<addr> and C<addrlen> @@ -2810,6 +2816,7 @@ parameters specify the address of the socket to connect to. default_call with args = [ String "uri" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to an NBD URI"; longdesc = "\ Begin connecting to the NBD URI C<uri>. Parameters behave as @@ -2823,6 +2830,7 @@ documented in L<nbd_connect_uri(3)>. default_call with args = [ Path "unixsocket" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to the NBD server over a Unix domain socket"; longdesc = "\ Begin connecting to the NBD server over Unix domain socket @@ -2837,6 +2845,7 @@ L<nbd_connect_unix(3)>. default_call with args = [ UInt32 "cid"; UInt32 "port" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to the NBD server over AF_VSOCK socket"; longdesc = "\ Begin connecting to the NBD server over the C<AF_VSOCK> @@ -2850,6 +2859,7 @@ L<nbd_connect_vsock(3)>. default_call with args = [ String "hostname"; String "port" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to the NBD server over a TCP port"; longdesc = "\ Begin connecting to the NBD server listening on C<hostname:port>. @@ -2862,6 +2872,7 @@ Parameters behave as documented in L<nbd_connect_tcp(3)>. default_call with args = [ Fd "sock" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect directly to a connected socket"; longdesc = "\ Begin connecting to the connected socket C<fd>. @@ -2874,6 +2885,7 @@ Parameters behave as documented in L<nbd_connect_socket(3)>. default_call with args = [ StringList "argv" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect to the NBD server"; longdesc = "\ Run the command as a subprocess and begin connecting to it over @@ -2887,6 +2899,7 @@ L<nbd_connect_command(3)>. default_call with args = [ StringList "argv" ]; ret = RErr; permitted_states = [ Created ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "connect using systemd socket activation"; longdesc = "\ Run the command as a subprocess and begin connecting to it using @@ -2903,6 +2916,7 @@ L<nbd_connect_systemd_socket_activation(3)>. optargs = [ OClosure completion_closure ]; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "end negotiation and move on to using an export"; longdesc = "\ Request that the server finish negotiation and move on to serving the @@ -2926,6 +2940,7 @@ when L<nbd_aio_is_negotiating(3)> returns true."; default_call with args = []; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some (ChangesState ("aio_is_connecting", false)); shortdesc = "end negotiation and close the connection"; longdesc = "\ Request that the server finish negotiation, gracefully if possible, then @@ -2943,6 +2958,7 @@ L<nbd_aio_is_connecting(3)> to return false."; optargs = [ OClosure completion_closure ]; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request the server to initiate TLS"; longdesc = "\ Request that the server initiate a secure TLS connection, by @@ -2967,6 +2983,7 @@ callback."; optargs = [ OClosure completion_closure ]; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request the server to enable structured replies"; longdesc = "\ Request that the server use structured replies, by sending @@ -2991,6 +3008,7 @@ callback."; optargs = [ OClosure completion_closure ]; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request the server to list all exports during negotiation"; longdesc = "\ Request that the server list all exports that it supports. This can @@ -3013,6 +3031,7 @@ callback."; optargs = [ OClosure completion_closure ]; ret = RErr; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request the server for information about an export"; longdesc = "\ Request that the server supply information about the export name @@ -3036,6 +3055,7 @@ callback."; args = [ Closure context_closure ]; ret = RInt; optargs = [ OClosure completion_closure ]; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request list of available meta contexts, using implicit query"; longdesc = "\ Request that the server list available meta contexts associated with @@ -3063,6 +3083,7 @@ callback."; args = [ StringList "queries"; Closure context_closure ]; ret = RInt; optargs = [ OClosure completion_closure ]; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "request list of available meta contexts, using explicit query"; longdesc = "\ Request that the server list available meta contexts associated with @@ -3090,6 +3111,7 @@ callback."; args = [ Closure context_closure ]; ret = RInt; optargs = [ OClosure completion_closure ]; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "select specific meta contexts, with implicit query list"; longdesc = "\ Request that the server supply all recognized meta contexts @@ -3123,6 +3145,7 @@ callback."; args = [ StringList "queries"; Closure context_closure ]; ret = RInt; optargs = [ OClosure completion_closure ]; permitted_states = [ Negotiating ]; + async_kind = Some WithCompletionCallback; shortdesc = "select specific meta contexts, with explicit query list"; longdesc = "\ Request that the server supply all recognized meta contexts @@ -3157,6 +3180,7 @@ callback."; OFlags ("flags", cmd_flags, Some []) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "read from the NBD server"; longdesc = "\ Issue a read command to the NBD server. @@ -3191,6 +3215,7 @@ Other parameters behave as documented in L<nbd_pread(3)>." OFlags ("flags", cmd_flags, Some ["DF"]) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "read from the NBD server"; longdesc = "\ Issue a read command to the NBD server. @@ -3223,6 +3248,7 @@ Other parameters behave as documented in L<nbd_pread_structured(3)>." OFlags ("flags", cmd_flags, Some ["FUA"]) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "write to the NBD server"; longdesc = "\ Issue a write command to the NBD server. @@ -3242,6 +3268,7 @@ completed. Other parameters behave as documented in L<nbd_pwrite(3)>." default_call with args = []; optargs = [ OFlags ("flags", cmd_flags, Some []) ]; ret = RErr; permitted_states = [ Connected ]; + async_kind = Some (ChangesState ("aio_is_closed", true)); shortdesc = "disconnect from the NBD server"; longdesc = "\ Issue the disconnect command to the NBD server. This is @@ -3269,6 +3296,7 @@ however, L<nbd_shutdown(3)> will call this function if appropriate."; OFlags ("flags", cmd_flags, Some []) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "send flush command to the NBD server"; longdesc = "\ Issue the flush command to the NBD server. @@ -3290,6 +3318,7 @@ Other parameters behave as documented in L<nbd_flush(3)>." OFlags ("flags", cmd_flags, Some ["FUA"]) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "send trim command to the NBD server"; longdesc = "\ Issue a trim command to the NBD server. @@ -3311,6 +3340,7 @@ Other parameters behave as documented in L<nbd_trim(3)>." OFlags ("flags", cmd_flags, Some []) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "send cache (prefetch) command to the NBD server"; longdesc = "\ Issue the cache (prefetch) command to the NBD server. @@ -3333,6 +3363,7 @@ Other parameters behave as documented in L<nbd_cache(3)>." Some ["FUA"; "NO_HOLE"; "FAST_ZERO"]) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "send write zeroes command to the NBD server"; longdesc = "\ Issue a write zeroes command to the NBD server. @@ -3355,6 +3386,7 @@ Other parameters behave as documented in L<nbd_zero(3)>." OFlags ("flags", cmd_flags, Some ["REQ_ONE"]) ]; ret = RCookie; permitted_states = [ Connected ]; + async_kind = Some WithCompletionCallback; shortdesc = "send block status command to the NBD server"; longdesc = "\ Send the block status command to the NBD server. diff --git a/generator/API.mli b/generator/API.mli index c5bba8c..361132d 100644 --- a/generator/API.mli +++ b/generator/API.mli @@ -36,6 +36,11 @@ type call = { {b guaranteed} never to do that we can save a bit of time by setting this to false. *) may_set_error : bool; + (** There are two types of asynchronous functions, those with a completion + callback and those which changes state when completed. This field tells + if the function is asynchronous and in that case how one can check if + it has completed. *) + async_kind : async_kind option; (** The first stable version that the symbol appeared in, for example (1, 2) if the symbol was added in development cycle 1.1.x and thus the first stable version was 1.2. This is @@ -117,6 +122,12 @@ and permitted_state not including CLOSED or DEAD *) | Closed | Dead (** can be called when the handle is CLOSED or DEAD *) +and async_kind +(** The asynchronous call has a completion callback. *) +| WithCompletionCallback +(** The asynchronous call is completed when the given handle call returns the + given boolean value. Might for instance be ("aio_is_connected", false). *) +| ChangesState of string * bool and link | Link of string (** link to L<nbd_PAGE(3)> *) | SectionLink of string (** link to L<libnbd(3)/SECTION> *) -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 03/10] generator: Add information about the lifetime of closures
Add two new fields, `cblifetime` and `cbcount`, to the `closure` type in generator/API.ml*. `cblifetime` tells if the closure may only be used for as long as the command is in flight or if the closure may be used until the handle is destructed. `cbcount` tells whether the closure may be called many times or just once. This information is needed in the Rust bindings for: a) Knowing if the closure trait should be `FnMut` or `FnOnce` (see <https://doc.rust-lang.org/std/ops/trait.FnOnce.html>). b) Knowing for what lifetime the closure should be valid. A closure that may be called after the function invokation has returned must live for the `'static` lietime. But static closures are inconveniant for the user since they can't effectively borrow any local data. So it is good if this restriction is relaxed when it is not needed. --- generator/API.ml | 20 ++++++++++++++++++++ generator/API.mli | 17 +++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/generator/API.ml b/generator/API.ml index f90a6fa..42b9eec 100644 --- a/generator/API.ml +++ b/generator/API.ml @@ -77,6 +77,8 @@ and ret and closure = { cbname : string; cbargs : cbarg list; + cblifetime : cblifetime; + cbcount : cbcount } and cbarg | CBArrayAndLen of arg * string @@ -87,6 +89,12 @@ and cbarg | CBString of string | CBUInt of string | CBUInt64 of string +and cblifetime +| CBCommand +| CBHandle +and cbcount +| CBOnce +| CBMany and enum = { enum_prefix : string; enums : (string * int) list @@ -141,20 +149,28 @@ the handle from the NBD protocol handshake." (* Closures. *) let chunk_closure = { cbname = "chunk"; + cblifetime = CBCommand; + cbcount = CBMany; cbargs = [ CBBytesIn ("subbuf", "count"); CBUInt64 "offset"; CBUInt "status"; CBMutable (Int "error") ] } let completion_closure = { cbname = "completion"; + cblifetime = CBCommand; + cbcount = CBOnce; cbargs = [ CBMutable (Int "error") ] } let debug_closure = { cbname = "debug"; + cblifetime = CBHandle; + cbcount = CBMany; cbargs = [ CBString "context"; CBString "msg" ] } let extent_closure = { cbname = "extent"; + cblifetime = CBCommand; + cbcount = CBMany; cbargs = [ CBString "metacontext"; CBUInt64 "offset"; CBArrayAndLen (UInt32 "entries", @@ -163,10 +179,14 @@ let extent_closure = { } let list_closure = { cbname = "list"; + cblifetime = CBCommand; + cbcount = CBMany; cbargs = [ CBString "name"; CBString "description" ] } let context_closure = { cbname = "context"; + cblifetime = CBCommand; + cbcount = CBMany; cbargs = [ CBString "name" ] } let all_closures = [ chunk_closure; completion_closure; diff --git a/generator/API.mli b/generator/API.mli index 361132d..ff85849 100644 --- a/generator/API.mli +++ b/generator/API.mli @@ -94,6 +94,12 @@ and ret and closure = { cbname : string; (** name of callback function *) cbargs : cbarg list; (** all closures return int for now *) + (** An upper bound of the lifetime of the closure. Either it will be used for + as long as the command is in flight or it may be used until the handle + is destructed. *) + cblifetime : cblifetime; + (** Whether the callback may only be called once or many times. *) + cbcount : cbcount; } and cbarg | CBArrayAndLen of arg * string (** array + number of entries *) @@ -104,6 +110,17 @@ and cbarg | CBString of string (** like String *) | CBUInt of string (** like UInt *) | CBUInt64 of string (** like UInt64 *) +and cblifetime +| CBCommand (** The closure may only be used until the command is retired. + (E.G., completion callback or list callback.) *) +| CBHandle (** The closure might be used until the handle is descructed. + (E.G., debug callback.) *) +and cbcount +| CBOnce (** The closure will be used 0 or 1 time if the aio_* call returned an + error and exactly once if the call succeeded. + (E.g., completion callback.) *) +| CBMany (** The closure may be used any number of times. + (E.g., list callback.) *) and enum = { enum_prefix : string; (** prefix of each enum variant *) enums : (string * int) list (** enum names and their values in C *) -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 04/10] rust: Use more specific closure traits
For closures with `cb,count = CBOnce`, `FnOnce` will be used instead of `FnMut`. Moreover, closures in synchronous commands with `cblifetime = CBCommand` will not need to live for the static lifetime. See [here](https://doc.rust-lang.org/std/ops/trait.FnOnce.html) for more information about the advantages of using `FnOnce` when possible. --- generator/Rust.ml | 44 ++++++++++++++++++++++++++++---------------- rust/src/error.rs | 13 ++++++++++--- rust/src/handle.rs | 2 ++ rust/src/types.rs | 2 ++ 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/generator/Rust.ml b/generator/Rust.ml index 8e2ca13..a4b5257 100644 --- a/generator/Rust.ml +++ b/generator/Rust.ml @@ -113,7 +113,7 @@ let rust_cbarg_name : cbarg -> string = function | CBArrayAndLen (arg, _) | CBMutable arg -> rust_arg_name arg (* Get the Rust type for an argument. *) -let rec rust_arg_type : arg -> string = function +let rec rust_arg_type ?(async_kind = None) : arg -> string = function | Bool _ -> "bool" | Int _ -> "c_int" | UInt _ -> "c_uint" @@ -133,15 +133,18 @@ let rec rust_arg_type : arg -> string = function | BytesOut _ -> "&mut [u8]" | BytesPersistIn _ -> "&'static [u8]" | BytesPersistOut _ -> "&'static mut [u8]" - | Closure { cbargs } -> "impl " ^ rust_closure_trait cbargs + | Closure { cbargs; cbcount } -> ( + match async_kind with + | Some _ -> "impl " ^ rust_closure_trait cbargs cbcount + | None -> "impl " ^ rust_closure_trait cbargs cbcount ~lifetime:None) (* Get the Rust closure trait for a callback, That is `Fn*(...) -> ...)`. *) -and rust_closure_trait ?(lifetime = Some "'static") cbargs : string +and rust_closure_trait ?(lifetime = Some "'static") cbargs cbcount : string let rust_cbargs = String.concat ", " (List.map rust_cbarg_type cbargs) - and lifetime_constraint - match lifetime with None -> "" | Some x -> " + " ^ x - in - "FnMut(" ^ rust_cbargs ^ ") -> c_int + Send + Sync" ^ lifetime_constraint + and closure_type + match cbcount with CBOnce -> "FnOnce" | CBMany -> "FnMut" + and lifetime = match lifetime with None -> "" | Some x -> " + " ^ x in + sprintf "%s(%s) -> c_int + Send + Sync%s" closure_type rust_cbargs lifetime (* Get the Rust type for a callback argument. *) and rust_cbarg_type : cbarg -> string = function @@ -155,8 +158,8 @@ and rust_cbarg_type : cbarg -> string = function | 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)) +let rust_optarg_type ?(async_kind = None) : optarg -> string = function + | OClosure x -> sprintf "Option<%s>" (rust_arg_type (Closure x) ~async_kind) | OFlags (name, flags, _) -> sprintf "Option<%s>" (rust_arg_type (Flags (name, flags))) @@ -448,8 +451,8 @@ let ffi_ret_to_rust (call : call) 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 ~lifetime:None in +let print_rust_closure_to_raw_fn ({ cbname; cbargs; cbcount } : closure) + let closure_trait = rust_closure_trait cbargs cbcount ~lifetime:None 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 @@ -464,16 +467,24 @@ let print_rust_closure_to_raw_fn ({ cbname; cbargs } : closure) (List.map2 (sprintf "%s: %s") 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"; + (match cbcount with + | CBMany -> + pr " let callback_ptr = data as *mut F;\n"; + pr " let callback = &mut *callback_ptr;\n" + | CBOnce -> + pr " let callback_ptr = data as *mut Option<F>;\n"; + pr " let callback_option: &mut Option<F> = &mut *callback_ptr;\n"; + pr " let callback: F = callback_option.take().unwrap();\n"); List.iter ffi_cbargs_to_rust cbargs; pr " callback(%s)\n" (String.concat ", " rust_cbargs_names); pr " }\n"; - pr " let callback_data = Box::into_raw(Box::new(f));\n"; + pr " let callback_data = Box::into_raw(Box::new(%s));\n" + (match cbcount with CBMany -> "f" | CBOnce -> "Some(f)"); 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 " free: Some(utils::drop_data::<%s>),\n" + (match cbcount with CBMany -> "F" | CBOnce -> "Option<F>"); pr " }\n"; pr "}\n"; pr "\n" @@ -530,7 +541,8 @@ 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 and rust_args_types - List.map rust_arg_type call.args @ List.map rust_optarg_type call.optargs + List.map (rust_arg_type ~async_kind:call.async_kind) call.args + @ List.map (rust_optarg_type ~async_kind:call.async_kind) call.optargs in let rust_args String.concat ", " diff --git a/rust/src/error.rs b/rust/src/error.rs index 615a178..337d499 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -23,12 +23,16 @@ use std::io; /// A general error type for libnbd. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Non fatal errors used when a command failed but the handle is not dead. #[error(transparent)] Recoverable(ErrorKind), + /// A fatal error. After such an error, the handle is dead and there is no + /// point in issuing further commands. #[error("Fatal: NBD handle is dead: {0}")] Fatal(FatalErrorKind), } +/// An error kind for a Libnbd related error. #[derive(Debug, thiserror::Error)] pub enum ErrorKind { #[error("Errno: {errno}: {description}")] @@ -39,10 +43,13 @@ pub enum ErrorKind { Errno(#[from] Errno), } +/// The kind of a fatal error. #[derive(Debug, thiserror::Error)] pub enum FatalErrorKind { + /// A Libnbd related error. #[error(transparent)] - Kind(#[from] ErrorKind), + Libnbd(#[from] ErrorKind), + /// Some other io error. #[error(transparent)] Io(#[from] io::Error), } @@ -87,7 +94,7 @@ impl Error { pub(crate) unsafe fn get_error(handle: *mut sys::nbd_handle) -> Self { let kind = ErrorKind::get_error(); if sys::nbd_aio_is_dead(handle) != 0 { - Self::Fatal(FatalErrorKind::Kind(kind)) + Self::Fatal(FatalErrorKind::Libnbd(kind)) } else { Self::Recoverable(kind) } @@ -96,7 +103,7 @@ impl Error { /// Get the errno value if any. pub fn errno(&self) -> Option<i32> { match self { - Self::Recoverable(e) | Self::Fatal(FatalErrorKind::Kind(e)) => { + Self::Recoverable(e) | Self::Fatal(FatalErrorKind::Libnbd(e)) => { e.errno() } Self::Fatal(FatalErrorKind::Io(e)) => e.raw_os_error(), diff --git a/rust/src/handle.rs b/rust/src/handle.rs index 477faa4..9e5e7d8 100644 --- a/rust/src/handle.rs +++ b/rust/src/handle.rs @@ -31,6 +31,8 @@ impl Handle { if handle.is_null() { Err(unsafe { Error::Fatal(ErrorKind::get_error().into()) }) } else { + // Set a debug callback communicating with any logging + // implementation as defined by the log crate. #[allow(unused_mut)] let mut nbd = Handle { handle }; #[cfg(feature = "log")] diff --git a/rust/src/types.rs b/rust/src/types.rs index eb2df06..af62140 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -15,4 +15,6 @@ // 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 cookie is a 64 bit integer returned by some aio_* methods on +/// [crate::Handle] used to identify a running command. pub struct Cookie(pub(crate) u64); -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 05/10] rust: Add a couple of integration tests
A couple of integration tests are added in rust/tests. They are mostly ported from the OCaml tests. --- rust/Cargo.toml | 4 + rust/Makefile.am | 1 + rust/run-tests.sh | 6 +- rust/tests/nbdkit_pattern/mod.rs | 28 ++++ 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_230_opt_info.rs | 124 +++++++++++++++ rust/tests/test_240_opt_list_meta.rs | 151 +++++++++++++++++++ rust/tests/test_245_opt_list_meta_queries.rs | 98 ++++++++++++ rust/tests/test_250_opt_set_meta.rs | 124 +++++++++++++++ rust/tests/test_255_opt_set_meta_queries.rs | 111 ++++++++++++++ rust/tests/test_300_get_size.rs | 36 +++++ rust/tests/test_400_pread.rs | 40 +++++ rust/tests/test_405_pread_structured.rs | 80 ++++++++++ rust/tests/test_410_pwrite.rs | 62 ++++++++ rust/tests/test_460_block_status.rs | 96 ++++++++++++ rust/tests/test_620_stats.rs | 76 ++++++++++ rust/tests/test_log/mod.rs | 86 +++++++++++ 24 files changed, 1451 insertions(+), 2 deletions(-) create mode 100644 rust/tests/nbdkit_pattern/mod.rs create mode 100644 rust/tests/test_100_handle.rs create mode 100644 rust/tests/test_110_defaults.rs create mode 100644 rust/tests/test_120_set_non_defaults.rs create mode 100644 rust/tests/test_130_private_data.rs create mode 100644 rust/tests/test_140_explicit_close.rs create mode 100644 rust/tests/test_200_connect_command.rs create mode 100644 rust/tests/test_210_opt_abort.rs create mode 100644 rust/tests/test_220_opt_list.rs create mode 100644 rust/tests/test_230_opt_info.rs create mode 100644 rust/tests/test_240_opt_list_meta.rs create mode 100644 rust/tests/test_245_opt_list_meta_queries.rs create mode 100644 rust/tests/test_250_opt_set_meta.rs create mode 100644 rust/tests/test_255_opt_set_meta_queries.rs create mode 100644 rust/tests/test_300_get_size.rs create mode 100644 rust/tests/test_400_pread.rs create mode 100644 rust/tests/test_405_pread_structured.rs create mode 100644 rust/tests/test_410_pwrite.rs create mode 100644 rust/tests/test_460_block_status.rs create mode 100644 rust/tests/test_620_stats.rs create mode 100644 rust/tests/test_log/mod.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e81360b..f74c3ac 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -48,3 +48,7 @@ byte-strings = "0.3.1" [features] default = ["log"] + +[dev-dependencies] +once_cell = "1.18.0" +tempfile = "3.6.0" diff --git a/rust/Makefile.am b/rust/Makefile.am index e8a916f..d353949 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -60,6 +60,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/nbdkit_pattern/mod.rs b/rust/tests/nbdkit_pattern/mod.rs new file mode 100644 index 0000000..5f4069e --- /dev/null +++ b/rust/tests/nbdkit_pattern/mod.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 + +use once_cell::sync::Lazy; + +/// The byte pattern as described in nbdkit-PATTERN-plugin(1). +pub static PATTERN: Lazy<Vec<u8>> = Lazy::new(|| { + let mut pattern = Vec::with_capacity(512); + for i in 0u64..64 { + pattern.extend_from_slice((i * 8).to_be_bytes().as_slice()); + } + assert_eq!(pattern.len(), 512); + pattern +}); diff --git a/rust/tests/test_100_handle.rs b/rust/tests/test_100_handle.rs new file mode 100644 index 0000000..85e18aa --- /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::Handle::new().unwrap(); +} diff --git a/rust/tests/test_110_defaults.rs b/rust/tests/test_110_defaults.rs new file mode 100644 index 0000000..88b072c --- /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 nbd = libnbd::Handle::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..08920d2 --- /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 nbd = libnbd::Handle::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..bb507fb --- /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 nbd = libnbd::Handle::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..59ab382 --- /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::Handle::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..dcaa1a2 --- /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 nbd = libnbd::Handle::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..1cba9a8 --- /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 nbd = libnbd::Handle::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..5abec5f --- /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::env; +use std::ffi::{CStr, CString}; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; + +/// 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 nbd = libnbd::Handle::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 = Arc::new(Mutex::new(Vec::new())); + let exports_clone = exports.clone(); + let count = nbd.opt_list(move |name, _| { + exports_clone.lock().unwrap().push(name.to_owned()); + 0 + })?; + let exports = Arc::into_inner(exports).unwrap().into_inner().unwrap(); + 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_230_opt_info.rs b/rust/tests/test_230_opt_info.rs new file mode 100644 index 0000000..f5103c1 --- /dev/null +++ b/rust/tests/test_230_opt_info.rs @@ -0,0 +1,124 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::env; +use std::ffi::CString; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; + +#[test] +fn test_opt_info() { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/opt-info.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + + let nbd = libnbd::Handle::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"), + &script_path, + ]) + .unwrap(); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + + // No size, flags, or meta-contexts yet + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // info with no prior name gets info on "" + assert!(nbd.opt_info().is_ok()); + assert_eq!(nbd.get_size().unwrap(), 0); + assert!(nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // changing export wipes out prior info + nbd.set_export_name(c_str!("b")).unwrap(); + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // info on something not present fails + nbd.set_export_name(c_str!("a")).unwrap(); + assert!(nbd.opt_info().is_err()); + + // info for a different export, with automatic meta_context disabled + nbd.set_export_name(c_str!("b")).unwrap(); + nbd.set_request_meta_context(false).unwrap(); + nbd.opt_info().unwrap(); + // idempotent name change is no-op + nbd.set_export_name(c_str!("b")).unwrap(); + assert_eq!(nbd.get_size().unwrap(), 1); + assert!(!nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + nbd.set_request_meta_context(true).unwrap(); + + // go on something not present + nbd.set_export_name(c_str!("a")).unwrap(); + assert!(nbd.opt_go().is_err()); + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // go on a valid export + nbd.set_export_name(c_str!("good")).unwrap(); + nbd.opt_go().unwrap(); + assert_eq!(nbd.get_size().unwrap(), 4); + assert!(nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // now info is no longer valid, but does not wipe data + assert!(nbd.set_export_name(c_str!("a")).is_err()); + assert_eq!(nbd.get_export_name().unwrap().as_c_str(), c_str!("good")); + assert!(nbd.opt_info().is_err()); + assert_eq!(nbd.get_size().unwrap(), 4); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.shutdown(None).unwrap(); + + // Another connection. This time, check that SET_META triggered by opt_info + // persists through nbd_opt_go with set_request_meta_context disabled. + let nbd = libnbd::Handle::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"), + &script_path, + ]) + .unwrap(); + nbd.add_meta_context(c_str!("x-unexpected:bogus")).unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + nbd.opt_info().unwrap(); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.set_request_meta_context(false).unwrap(); + // Adding to the request list now won't matter + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + nbd.opt_go().unwrap(); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_240_opt_list_meta.rs b/rust/tests/test_240_opt_list_meta.rs new file mode 100644 index 0000000..60c1841 --- /dev/null +++ b/rust/tests/test_240_opt_list_meta.rs @@ -0,0 +1,151 @@ +// 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::sync::Arc; +use std::sync::Mutex; + +/// A struct with information about listed meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:alloc" is listed. + has_alloc: bool, + /// The number of listed meta contexts. + count: u32, +} + +fn list_meta_ctxs(nbd: &libnbd::Handle) -> libnbd::Result<CtxInfo> { + let info = Arc::new(Mutex::new(CtxInfo { + has_alloc: false, + count: 0, + })); + let info_clone = info.clone(); + let replies = nbd.opt_list_meta_context(move |ctx| { + let mut info = info_clone.lock().unwrap(); + info.count += 1; + if ctx == libnbd::CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + })?; + let info = Arc::into_inner(info).unwrap().into_inner().unwrap(); + assert_eq!(info.count, replies); + Ok(info) +} + +#[test] +fn test_opt_list_meta() { + let nbd = libnbd::Handle::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!("memory"), + c_str!("size=1M"), + ]) + .unwrap(); + + // First pass: empty query should give at least "base:allocation". + let info = list_meta_ctxs(&nbd).unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); + let max = info.count; + + // Second pass: bogus query has no response. + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + assert_eq!( + list_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Third pass: specific query should have one match. + nbd.add_meta_context(c_str!("base:allocation")).unwrap(); + assert_eq!(nbd.get_nr_meta_contexts().unwrap(), 2); + assert_eq!( + nbd.get_meta_context(1).unwrap().as_c_str(), + c_str!("base:allocation") + ); + assert_eq!( + list_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + + // Fourth pass: opt_list_meta_context is stateless, so it should + // not wipe status learned during opt_info + assert!(nbd.can_meta_context(c_str!("base:allocation")).is_err()); + assert!(nbd.get_size().is_err()); + nbd.opt_info().unwrap(); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(c_str!("base:allocation")).unwrap()); + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + assert_eq!( + list_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(c_str!("base:allocation")).unwrap()); + + // Final pass: "base:" query should get at least "base:allocation" + nbd.add_meta_context(c_str!("base:")).unwrap(); + let info = list_meta_ctxs(&nbd).unwrap(); + assert!(info.count >= 1); + assert!(info.count <= max); + assert!(info.has_alloc); + + // Repeat but this time without structured replies. Deal gracefully + // with older servers that don't allow the attempt. + let nbd = libnbd::Handle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.set_request_structured_replies(false).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("memory"), + c_str!("size=1M"), + ]) + .unwrap(); + let bytes = nbd.stats_bytes_sent(); + if let Ok(info) = list_meta_ctxs(&nbd) { + assert!(info.count >= 1); + assert!(info.has_alloc) + } else { + assert!(nbd.stats_bytes_sent() > bytes); + // ignoring failure from old server + } + + // Now enable structured replies, and a retry should pass. + assert!(nbd.opt_structured_reply().unwrap()); + let info = list_meta_ctxs(&nbd).unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); +} diff --git a/rust/tests/test_245_opt_list_meta_queries.rs b/rust/tests/test_245_opt_list_meta_queries.rs new file mode 100644 index 0000000..0a5bb05 --- /dev/null +++ b/rust/tests/test_245_opt_list_meta_queries.rs @@ -0,0 +1,98 @@ +// 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::ffi::CStr; +use std::sync::Arc; +use std::sync::Mutex; + +/// A struct with information about listed meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is listed. + has_alloc: bool, + /// The number of listed meta contexts. + count: u32, +} + +fn list_meta_ctxs( + nbd: &libnbd::Handle, + queries: &[&CStr], +) -> libnbd::Result<CtxInfo> { + let info = Arc::new(Mutex::new(CtxInfo { + has_alloc: false, + count: 0, + })); + let info_clone = info.clone(); + let replies = nbd.opt_list_meta_context_queries(queries, move |ctx| { + let mut info = info_clone.lock().unwrap(); + info.count += 1; + if ctx == libnbd::CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + })?; + let info = Arc::into_inner(info).unwrap().into_inner().unwrap(); + assert_eq!(info.count, replies); + Ok(info) +} + +#[test] +fn test_opt_list_meta_queries() { + let nbd = libnbd::Handle::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!("memory"), + c_str!("size=1M"), + ]) + .unwrap(); + + // First pass: empty query should give at least "base:allocation". + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + let info = list_meta_ctxs(&nbd, &[]).unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); + + // Second pass: bogus query has no response. + nbd.clear_meta_contexts().unwrap(); + assert_eq!( + list_meta_ctxs(&nbd, &[c_str!("x-nosuch:")]).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Third pass: specific query should have one match. + assert_eq!( + list_meta_ctxs( + &nbd, + &[c_str!("x-nosuch:"), libnbd::CONTEXT_BASE_ALLOCATION] + ) + .unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); +} diff --git a/rust/tests/test_250_opt_set_meta.rs b/rust/tests/test_250_opt_set_meta.rs new file mode 100644 index 0000000..bd78950 --- /dev/null +++ b/rust/tests/test_250_opt_set_meta.rs @@ -0,0 +1,124 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::sync::Arc; +use std::sync::Mutex; + +/// A struct with information about set meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is set. + has_alloc: bool, + /// The number of set meta contexts. + count: u32, +} + +fn set_meta_ctxs(nbd: &libnbd::Handle) -> libnbd::Result<CtxInfo> { + let info = Arc::new(Mutex::new(CtxInfo { + has_alloc: false, + count: 0, + })); + let info_clone = info.clone(); + let replies = nbd.opt_set_meta_context(move |ctx| { + let mut info = info_clone.lock().unwrap(); + info.count += 1; + if ctx == CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + })?; + let info = Arc::into_inner(info).unwrap().into_inner().unwrap(); + assert_eq!(info.count, replies); + Ok(info) +} + +#[test] +fn test_opt_set_meta() { + let nbd = libnbd::Handle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.set_request_structured_replies(false).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("memory"), + c_str!("size=1M"), + ]) + .unwrap(); + + // No contexts negotiated yet; can_meta should be error if any requested + assert!(!nbd.get_structured_replies_negotiated().unwrap()); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // SET cannot succeed until SR is negotiated. + assert!(nbd.opt_structured_reply().unwrap()); + assert!(nbd.get_structured_replies_negotiated().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // nbdkit does not match wildcard for SET, even though it does for LIST + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("base:")).unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Negotiating with no contexts is not an error, but selects nothing + nbd.clear_meta_contexts().unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Request 2 with expectation of 1; with set_request_meta_context off + nbd.add_meta_context(c_str!("x-nosuch:context")).unwrap(); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + nbd.set_request_meta_context(false).unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Transition to transmission phase; our last set should remain active + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("x-nosuch:context")).unwrap(); + nbd.opt_go().unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Now too late to set; but should not lose earlier state + assert!(set_meta_ctxs(&nbd).is_err()); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_255_opt_set_meta_queries.rs b/rust/tests/test_255_opt_set_meta_queries.rs new file mode 100644 index 0000000..bbe53a9 --- /dev/null +++ b/rust/tests/test_255_opt_set_meta_queries.rs @@ -0,0 +1,111 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::ffi::CStr; +use std::sync::Arc; +use std::sync::Mutex; + +/// A struct with information about set meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is set. + has_alloc: bool, + /// The number of set meta contexts. + count: u32, +} + +fn set_meta_ctxs_queries( + nbd: &libnbd::Handle, + queries: &[&CStr], +) -> libnbd::Result<CtxInfo> { + let info = Arc::new(Mutex::new(CtxInfo { + has_alloc: false, + count: 0, + })); + let info_clone = info.clone(); + let replies = nbd.opt_set_meta_context_queries(queries, move |ctx| { + let mut info = info_clone.lock().unwrap(); + info.count += 1; + if ctx == CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + })?; + let info = Arc::into_inner(info).unwrap().into_inner().unwrap(); + assert_eq!(info.count, replies); + Ok(info) +} + +#[test] +fn test_opt_set_meta_queries() { + let nbd = libnbd::Handle::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!("memory"), + c_str!("size=1M"), + ]) + .unwrap(); + + // nbdkit does not match wildcard for SET, even though it does for LIST + assert_eq!( + set_meta_ctxs_queries(&nbd, &[c_str!("base:")]).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Negotiating with no contexts is not an error, but selects nothing + // An explicit empty list overrides a non-empty implicit list. + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + assert_eq!( + set_meta_ctxs_queries(&nbd, &[]).unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Request 2 with expectation of 1. + assert_eq!( + set_meta_ctxs_queries( + &nbd, + &[c_str!("x-nosuch:context"), CONTEXT_BASE_ALLOCATION] + ) + .unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Transition to transmission phase; our last set should remain active + nbd.set_request_meta_context(false).unwrap(); + nbd.opt_go().unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_300_get_size.rs b/rust/tests/test_300_get_size.rs new file mode 100644 index 0000000..fbf1c79 --- /dev/null +++ b/rust/tests/test_300_get_size.rs @@ -0,0 +1,36 @@ +// 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_get_size() { + let nbd = libnbd::Handle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + c_str!("size=1M"), + ]) + .unwrap(); + + assert_eq!(nbd.get_size().unwrap(), 1048576); +} diff --git a/rust/tests/test_400_pread.rs b/rust/tests/test_400_pread.rs new file mode 100644 index 0000000..9414379 --- /dev/null +++ b/rust/tests/test_400_pread.rs @@ -0,0 +1,40 @@ +// 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 nbdkit_pattern; +use byte_strings::c_str; +use nbdkit_pattern::PATTERN; + +#[test] +fn test_pread() { + let nbd = libnbd::Handle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("pattern"), + c_str!("size=1M"), + ]) + .unwrap(); + + let mut buf = [0; 512]; + nbd.pread(&mut buf, 0, None).unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); +} diff --git a/rust/tests/test_405_pread_structured.rs b/rust/tests/test_405_pread_structured.rs new file mode 100644 index 0000000..432d4d0 --- /dev/null +++ b/rust/tests/test_405_pread_structured.rs @@ -0,0 +1,80 @@ +// 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 nbdkit_pattern; +use byte_strings::c_str; +use nbdkit_pattern::PATTERN; + +#[test] +fn test_pread_structured() { + let nbd = libnbd::Handle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("pattern"), + c_str!("size=1M"), + ]) + .unwrap(); + + fn f(buf: &[u8], offset: u64, s: u32, err: &mut i32) { + assert_eq!(*err, 0); + *err = 42; + assert_eq!(buf, PATTERN.as_slice()); + assert_eq!(offset, 0); + assert_eq!(s, libnbd::READ_DATA); + } + + let mut buf = [0; 512]; + nbd.pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + 0 + }, + None, + ) + .unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); + + nbd.pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + 0 + }, + Some(libnbd::CmdFlag::DF), + ) + .unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); + + let res = nbd.pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + -1 + }, + Some(libnbd::CmdFlag::DF), + ); + assert_eq!(res.unwrap_err().errno(), Some(42)); +} diff --git a/rust/tests/test_410_pwrite.rs b/rust/tests/test_410_pwrite.rs new file mode 100644 index 0000000..612f65d --- /dev/null +++ b/rust/tests/test_410_pwrite.rs @@ -0,0 +1,62 @@ +// 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::ffi::CString; +use std::fs::{self, File}; +use std::os::unix::prelude::*; + +#[test] +fn test_pwrite() { + let tmp_dir = tempfile::tempdir().unwrap(); + let data_file_path = tmp_dir.path().join("pwrite_test.data"); + let data_file = File::create(&data_file_path).unwrap(); + data_file.set_len(512).unwrap(); + drop(data_file); + let nbd = libnbd::Handle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("file"), + &CString::new(data_file_path.clone().into_os_string().into_vec()) + .unwrap(), + ]) + .unwrap(); + + let mut buf_1 = [0; 512]; + buf_1[10] = 0x01; + buf_1[510] = 0x55; + buf_1[511] = 0xAA; + + let flags = Some(libnbd::CmdFlag::FUA); + nbd.pwrite(&buf_1, 0, flags).unwrap(); + + let mut buf_2 = [0; 512]; + nbd.pread(&mut buf_2, 0, None).unwrap(); + + assert_eq!(buf_1, buf_2); + + // Drop nbd before tmp_dir is dropped. + drop(nbd); + + let data_file_content = fs::read(&data_file_path).unwrap(); + assert_eq!(buf_1.as_slice(), data_file_content.as_slice()); +} diff --git a/rust/tests/test_460_block_status.rs b/rust/tests/test_460_block_status.rs new file mode 100644 index 0000000..58a2e7e --- /dev/null +++ b/rust/tests/test_460_block_status.rs @@ -0,0 +1,96 @@ +// 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::env; +use std::ffi::CString; +use std::os::unix::prelude::*; +use std::path::Path; +use std::sync::Arc; +use std::sync::Mutex; + +fn block_status_get_entries( + nbd: &libnbd::Handle, + count: u64, + offset: u64, + flags: Option<libnbd::CmdFlag>, +) -> Vec<u32> { + let entries = Arc::new(Mutex::new(None)); + let entries_clone = entries.clone(); + nbd.block_status( + count, + offset, + move |metacontext, _, entries, err| { + assert_eq!(*err, 0); + if metacontext == libnbd::CONTEXT_BASE_ALLOCATION { + *entries_clone.lock().unwrap() = Some(entries.to_vec()); + } + 0 + }, + flags, + ) + .unwrap(); + Arc::into_inner(entries) + .unwrap() + .into_inner() + .unwrap() + .unwrap() +} + +#[test] +fn test_block_status() { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/meta-base-allocation.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + let nbd = libnbd::Handle::new().unwrap(); + nbd.add_meta_context(libnbd::CONTEXT_BASE_ALLOCATION) + .unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("sh"), + &script_path, + ]) + .unwrap(); + + assert_eq!( + block_status_get_entries(&nbd, 65536, 0, None).as_slice(), + &[8192, 0, 8192, 1, 16384, 3, 16384, 2, 16384, 0,] + ); + + assert_eq!( + block_status_get_entries(&nbd, 1024, 32256, None).as_slice(), + &[512, 3, 16384, 2] + ); + + assert_eq!( + block_status_get_entries( + &nbd, + 1024, + 32256, + Some(libnbd::CmdFlag::REQ_ONE) + ) + .as_slice(), + &[512, 3] + ); +} diff --git a/rust/tests/test_620_stats.rs b/rust/tests/test_620_stats.rs new file mode 100644 index 0000000..0e37729 --- /dev/null +++ b/rust/tests/test_620_stats.rs @@ -0,0 +1,76 @@ +// 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_stats() { + let nbd = libnbd::Handle::new().unwrap(); + + // Pre-connection, stats start out at 0 + assert_eq!(nbd.stats_bytes_sent(), 0); + assert_eq!(nbd.stats_chunks_sent(), 0); + assert_eq!(nbd.stats_bytes_received(), 0); + assert_eq!(nbd.stats_chunks_received(), 0); + + // Connection performs handshaking, which increments stats. + // The number of bytes/chunks here may grow over time as more features get + // automatically negotiated, so merely check that they are non-zero. + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + ]) + .unwrap(); + + let bs1 = nbd.stats_bytes_sent(); + let cs1 = nbd.stats_chunks_sent(); + let br1 = nbd.stats_bytes_received(); + let cr1 = nbd.stats_chunks_received(); + assert!(cs1 > 0); + assert!(bs1 > cs1); + assert!(cr1 > 0); + assert!(br1 > cr1); + + // A flush command should be one chunk out, one chunk back (even if + // structured replies are in use) + nbd.flush(None).unwrap(); + let bs2 = nbd.stats_bytes_sent(); + let cs2 = nbd.stats_chunks_sent(); + let br2 = nbd.stats_bytes_received(); + let cr2 = nbd.stats_chunks_received(); + assert_eq!(bs2, bs1 + 28); + assert_eq!(cs2, cs1 + 1); + assert_eq!(br2, br1 + 16); // assumes nbdkit uses simple reply + assert_eq!(cr2, cr1 + 1); + + // Stats are still readable after the connection closes; we don't know if + // the server sent reply bytes to our NBD_CMD_DISC, so don't insist on it. + nbd.shutdown(None).unwrap(); + let bs3 = nbd.stats_bytes_sent(); + let cs3 = nbd.stats_chunks_sent(); + let br3 = nbd.stats_bytes_received(); + let cr3 = nbd.stats_chunks_received(); + assert!(bs3 > bs2); + assert_eq!(cs3, cs2 + 1); + assert!(br3 >= br2); + assert!(cr3 == cr2 || cr3 == cr2 + 1); +} 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
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 06/10] rust: Make it possible to run tests with Valgrind
Make it possible to run Rust tests with Valgrind with `make check-valgrind` in the rust directory. --- rust/Makefile.am | 3 +++ rust/run-tests.sh | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rust/Makefile.am b/rust/Makefile.am index d353949..cc17bb9 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -65,6 +65,9 @@ TESTS_ENVIRONMENT = \ LOG_COMPILER = $(top_builddir)/run TESTS = run-tests.sh +check-valgrind: + LIBNBD_VALGRIND=1 $(MAKE) check + endif clean-local: diff --git a/rust/run-tests.sh b/rust/run-tests.sh index 005000e..da7852a 100755 --- a/rust/run-tests.sh +++ b/rust/run-tests.sh @@ -23,4 +23,8 @@ set -x requires nbdkit --version -$CARGO test -- --nocapture +if [ -z "$VG" ]; then + $CARGO test -- --nocapture +else + $CARGO test --config "target.'cfg(all())'.runner = \"$VG\"" -- --nocapture +fi -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 07/10] rust: async: Create an async friendly handle type
Create another handle type: `AsyncHandle`, which makes use of Rust's builtin asynchronous functions (see <https://doc.rust-lang.org/std/keyword.async.html>) and runs on top of the Tokio runtime (see <https://docs.rs/tokio>). For every asynchronous command, like `aio_connect()`, a corresponding `async` method is created on the handle. In this case it would be: async fn connect(...) -> Result<(), ...> When called, it will poll the file descriptor until the command is complete, and then return with a result. All the synchronous counterparts (like `nbd_connect()`) are excluded from this handle type as they are unnecessary and since they might interfear with the polling made by the Tokio runtime. For more details about how the asynchronous commands are executed, please see the comments in rust/src/async_handle.rs. --- generator/Rust.ml | 227 +++++++++++++++++++++++++++++++++++++++ generator/Rust.mli | 2 + generator/generator.ml | 1 + rust/Cargo.toml | 1 + rust/Makefile.am | 1 + rust/src/async_handle.rs | 222 ++++++++++++++++++++++++++++++++++++++ rust/src/lib.rs | 4 + 7 files changed, 458 insertions(+) create mode 100644 rust/src/async_handle.rs diff --git a/generator/Rust.ml b/generator/Rust.ml index a4b5257..adc7ffa 100644 --- a/generator/Rust.ml +++ b/generator/Rust.ml @@ -588,3 +588,230 @@ let generate_rust_bindings () pr "impl Handle {\n"; List.iter print_rust_handle_method handle_calls; pr "}\n\n" + +(*********************************************************) +(* The rest of the file conserns the asynchronous API. *) +(* *) +(* See the comments in rust/src/async_handle.rs for more *) +(* information about how it works. *) +(*********************************************************) + +let excluded_handle_calls : NameSet.t + NameSet.of_list + [ + "aio_get_fd"; + "aio_get_direction"; + "aio_notify_read"; + "aio_notify_write"; + "clear_debug_callback"; + "get_debug"; + "poll"; + "poll2"; + "set_debug"; + "set_debug_callback"; + ] + +(* A mapping with names as keys. *) +module NameMap = Map.Make (String) + +(* Strip "aio_" from the beginning of a string. *) +let strip_aio name : string + if String.starts_with ~prefix:"aio_" name then + String.sub name 4 (String.length name - 4) + else failwithf "Asynchronous call %s must begin with aio_" name + +(* A map with all asynchronous handle calls. The keys are names with "aio_" + stripped, the values are a tuple with the actual name (with "aio_"), the + [call] and the [async_kind]. *) +let async_handle_calls : ((string * call) * async_kind) NameMap.t + handle_calls + |> List.filter (fun (n, _) -> not (NameSet.mem n excluded_handle_calls)) + |> List.filter_map (fun (name, call) -> + call.async_kind + |> Option.map (fun async_kind -> + (strip_aio name, ((name, call), async_kind)))) + |> List.to_seq |> NameMap.of_seq + +(* A mapping with all synchronous (not asynchronous) handle calls. Excluded + are also all synchronous calls that has an asynchronous counterpart. So if + "foo" is the name of a handle call and an asynchronous call "aio_foo" + exists, then "foo" will not b in this map. *) +let sync_handle_calls : call NameMap.t + handle_calls + |> List.filter (fun (n, _) -> not (NameSet.mem n excluded_handle_calls)) + |> List.filter (fun (name, _) -> + (not (NameMap.mem name async_handle_calls)) + && not + (String.starts_with ~prefix:"aio_" name + && NameMap.mem (strip_aio name) async_handle_calls)) + |> List.to_seq |> NameMap.of_seq + +(* Get the Rust type for an argument in the asynchronous API. Like + [rust_arg_type] but no static lifetime on some closures and buffers. *) +let rust_async_arg_type : arg -> string = function + | Closure { cbargs; cbcount; cblifetime } -> + let lifetime + match cblifetime with CBCommand -> None | CBHandle -> Some "'static" + in + "impl " ^ rust_closure_trait ~lifetime cbargs cbcount + | BytesPersistIn _ -> "&[u8]" + | BytesPersistOut _ -> "&mut [u8]" + | x -> rust_arg_type x + +(* Get the Rust type for an optional argument in the asynchronous API. Like + [rust_optarg_type] but no static lifetime on some closures. *) +let rust_async_optarg_type : optarg -> string = function + | OClosure x -> sprintf "Option<%s>" (rust_async_arg_type (Closure x)) + | x -> rust_optarg_type x + +(* A string of the argument list for a method on the handle, with both + mandotory and optional arguments. *) +let rust_async_handle_call_args { args; optargs } : string + let rust_args_names + List.map rust_arg_name args @ List.map rust_optarg_name optargs + and rust_args_types + List.map rust_async_arg_type args + @ List.map rust_async_optarg_type optargs + in + String.concat ", " + (List.map2 (sprintf "%s: %s") rust_args_names rust_args_types) + +(* Print the Rust function for a not asynchronous handle call. *) +let print_rust_sync_handle_call (name : string) (call : call) + print_rust_handle_call_comment call; + pr "pub fn %s(&self, %s) -> %s\n" name + (rust_async_handle_call_args call) + (rust_ret_type call); + print_ffi_call name "self.data.handle.handle" call; + pr "\n" + +(* Print the Rust function for an asynchronous handle call with a completion + callback. (Note that "callback" might be abbreviated with "cb" in the + following code. *) +let print_rust_async_handle_call_with_completion_cb name (aio_name, call) + (* An array of all optional arguments. Useful because we need to deel with + the index of the completion callback. *) + let optargs = Array.of_list call.optargs in + (* The index of the completion callback in [optargs] *) + let completion_cb_index + Array.find_map + (fun (i, optarg) -> + match optarg with + | OClosure { cbname } -> + if cbname = "completion" then Some i else None + | _ -> None) + (Array.mapi (fun x y -> (x, y)) optargs) + in + let completion_cb_index + match completion_cb_index with + | Some x -> x + | None -> + failwithf + "The handle call %s is claimed to have a completion callback among \ + its optional arguments by the async_kind field, but so does not \ + seem to be the case." + aio_name + in + let optargs_before_completion_cb + Array.to_list (Array.sub optargs 0 completion_cb_index) + and optargs_after_completion_cb + Array.to_list + (Array.sub optargs (completion_cb_index + 1) + (Array.length optargs - (completion_cb_index + 1))) + in + (* All optional arguments excluding the completion callback. *) + let optargs_without_completion_cb + optargs_before_completion_cb @ optargs_after_completion_cb + in + print_rust_handle_call_comment call; + pr "pub async fn %s(&self, %s) -> Result<(), Arc<Error>> {\n" name + (rust_async_handle_call_args + { call with optargs = optargs_without_completion_cb }); + pr " // A oneshot channel to notify when the call is completed.\n"; + pr " let (tx, rx) = oneshot::channel::<Result<(), Arc<Error>>>();\n"; + (* Completion callback: *) + pr " let %s = Some(|err: &mut i32| {\n" + (rust_optarg_name (Array.get optargs completion_cb_index)); + pr " let errno = if *err == 0 {\n"; + pr " tx.send(Ok(())).ok();\n"; + pr " return 1;\n"; + pr " } else { *err };\n"; + pr " // Spawn a task, which waits for the result of the next\n"; + pr " // aio_notify_* call.\n"; + pr " let mut res_rx = self.data.result_channel.subscribe();\n"; + pr " tokio::spawn(async move {\n"; + pr " let err = if let Ok(Err(err)) = res_rx.recv().await {\n"; + pr " err\n"; + pr " } else {\n"; + pr " Arc::new(Error::Recoverable(ErrorKind::from_errno(errno)))\n"; + pr " };\n"; + pr " tx.send(Err(err)).ok();\n"; + pr " });\n"; + pr " 1\n"; + pr " });\n"; + (* End of completion callback. *) + print_ffi_call aio_name "self.data.handle.handle" call; + pr "?;\n"; + pr " self.data.poll_notifier.notify_one();\n"; + pr " rx.await.unwrap()\n"; + pr "}\n\n" + +(* Print a Rust function for an asynchronous handle call which signals + completion by changing state. The predicate is a call like + "aio_is_connecting" which should get the value (like false) for the call to + be complete. *) +let print_rust_async_handle_call_changing_state name (aio_name, call) + (predicate, value) + let value = if value then "true" else "false" in + print_rust_handle_call_comment call; + pr "pub async fn %s(&self, %s) -> Result<(), Arc<Error>>\n" name + (rust_async_handle_call_args call); + pr "{\n"; + pr " let mut res_rx = self.data.result_channel.subscribe();\n"; + print_ffi_call aio_name "self.data.handle.handle" call; + pr "?;\n"; + pr " self.data.poll_notifier.notify_one();\n"; + pr " while self.data.handle.%s() != %s {\n" predicate value; + pr " match res_rx.recv().await {\n"; + pr " Ok(Ok(())) | Err(RecvError::Lagged(_)) => (),\n"; + pr " Ok(err @ Err(_)) => return err,\n"; + pr " Err(RecvError::Closed) => unreachable!(),\n"; + pr " }\n"; + pr " }\n"; + pr " Ok(())\n"; + pr "}\n\n" + +(* Print an impl with all handle calls. *) +let print_rust_async_handle_impls () + pr "impl AsyncHandle {\n"; + NameMap.iter print_rust_sync_handle_call sync_handle_calls; + NameMap.iter + (fun name (call, async_kind) -> + match async_kind with + | WithCompletionCallback -> + print_rust_async_handle_call_with_completion_cb name call + | ChangesState (predicate, value) -> + print_rust_async_handle_call_changing_state name call + (predicate, value)) + async_handle_calls; + pr "}\n\n" + +let print_rust_async_imports () + pr "use crate::{*, types::*};\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};\n"; + pr "use std::path::PathBuf;\n"; + pr "use std::ptr;\n"; + pr "use std::sync::Arc;\n"; + pr "use tokio::sync::{oneshot, broadcast::error::RecvError};\n"; + pr "\n" + +let generate_rust_async_bindings () + generate_header CStyle ~copyright:"Tage Johansson"; + pr "\n"; + print_rust_async_imports (); + print_rust_async_handle_impls () diff --git a/generator/Rust.mli b/generator/Rust.mli index 450e4ca..0960170 100644 --- a/generator/Rust.mli +++ b/generator/Rust.mli @@ -18,3 +18,5 @@ (* Print all flag-structs, enums, constants and handle calls in Rust code. *) val generate_rust_bindings : unit -> unit + +val generate_rust_async_bindings : unit -> unit diff --git a/generator/generator.ml b/generator/generator.ml index 67b9502..dc9eb80 100644 --- a/generator/generator.ml +++ b/generator/generator.ml @@ -63,3 +63,4 @@ let () output_to "golang/wrappers.h" GoLang.generate_golang_wrappers_h; output_to ~formatter:(Some Rustfmt) "rust/src/bindings.rs" Rust.generate_rust_bindings; + output_to ~formatter:(Some Rustfmt) "rust/src/async_bindings.rs" Rust.generate_rust_async_bindings; diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f74c3ac..c3e27b3 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -45,6 +45,7 @@ thiserror = "1.0.40" log = { version = "0.4.19", optional = true } libc = "0.2.147" byte-strings = "0.3.1" +tokio = { version = "1.29.1", default-features = false, features = ["rt", "sync", "net", "macros"] } [features] default = ["log"] diff --git a/rust/Makefile.am b/rust/Makefile.am index cc17bb9..a6fd9b1 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -18,6 +18,7 @@ include $(top_srcdir)/subdir-rules.mk generator_built = \ + src/async_bindings.rs \ src/bindings.rs \ $(NULL) diff --git a/rust/src/async_handle.rs b/rust/src/async_handle.rs new file mode 100644 index 0000000..346c4ef --- /dev/null +++ b/rust/src/async_handle.rs @@ -0,0 +1,222 @@ +// 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 + +// This module implements an asynchronous handle working on top of the +// [Tokio](https://tokio.rs) runtime. When the handle is created, +// a "polling task" is spawned on the Tokio runtime. The purpose of that +// "polling task" is to call `aio_notify_*` when appropriate. It shares a +// reference to the handle as well as some channels with the handle in the +// [HandleData] struct. The "polling task" is sleeping when no command is in +// flight, but wakes up as soon as any command is issued. +// +// The commands are implemented as +// [`async fn`s](https://doc.rust-lang.org/std/keyword.async.html) +// in async_bindings.rs. There are two types of commands: Those with a +// completion callback and those that waits for the handle to reach some state +// before completing. +// +// The asynchronous function for a command with a completion callback will +// setup a [oneshot channel](tokio::sync::oneshot). When the completion +// callback is called, it will send the result of the command on the channel. +// The asynchronous function itself will wait for this result and return when +// it is received. +// +// The asynchronous function for a command which is finished when the handle +// enters a certain state will use a while loop to wait for this state to be +// reached. Each iteration in the loop will block until the next call to +// `aio_notify_*`, so there is no busy waiting. +// +// Both for sending the result to commands with a completion callback, and for +// waking up the while loop in state changing commands, an +// [SPMC (single-producer, multiple-comsumer) channel](tokio::sync::broadcast) +// is used. (See [HandleData.result_channel].) The "polling task" will send +// every result of calls to `aio_notify_*` on this channel. + +use crate::sys; +use crate::Handle; +use crate::{Error, Result}; +use crate::{AIO_DIRECTION_BOTH, AIO_DIRECTION_READ, AIO_DIRECTION_WRITE}; +use std::sync::Arc; +use tokio::io::{unix::AsyncFd, Interest, Ready as IoReady}; +use tokio::sync::{broadcast, Notify}; +use tokio::task; + +/// An upper bound of the number of items in the "result_channel". +/// +/// Shall usually never be more than one unless many commands are run concurrently. +const RESULT_CHANNEL_CAPACITY: usize = 4; + +/// An NBD handle using Rust's `async` functionality on top of the +/// [Tokio](https://docs.rs/tokio/) runtime. +#[derive(Debug)] +pub struct AsyncHandle { + /// Data shared both by this struct and the polling task. + pub(crate) data: Arc<HandleData>, + + /// A task which soely purpose is to poll the NBD handle. + polling_task: tokio::task::AbortHandle, +} + +#[derive(Debug)] +pub(crate) struct HandleData { + /// The underliing handle. + pub handle: Handle, + + /// For every call to an `aio_notify_*` method (`aio_notify_read()` or + /// `aio_notify_write()`), the result is sent on this channel. + pub result_channel: broadcast::Sender<Result<(), Arc<Error>>>, + + /// A notifier used by commands to notify the polling task when a new + /// asynchronous command is issued. + pub poll_notifier: Notify, +} + +impl AsyncHandle { + pub fn new() -> Result<Self> { + let handle_data = Arc::new(HandleData { + handle: Handle::new()?, + poll_notifier: Notify::new(), + result_channel: broadcast::channel(RESULT_CHANNEL_CAPACITY).0, + }); + + let handle_data_2 = handle_data.clone(); + let polling_task = task::spawn(async move { + // The polling task should never finish without an error. If the + // handle is dropped, the task is aborted so it'll not return in + // that case either. + let Err(err) = polling_task(&handle_data_2).await else { + unreachable!() + }; + // Send the error as the last thing on the result channel. + let err = Arc::new(err); + handle_data_2.result_channel.send(Err(err)).ok(); + }) + .abort_handle(); + Ok(Self { + data: handle_data, + polling_task, + }) + } + + /// Get the underliing C pointer to the handle. + pub(crate) fn raw_handle(&self) -> *mut sys::nbd_handle { + self.data.handle.raw_handle() + } +} + +/// Get the read/write direction that the handle wants on the file descriptor. +fn get_fd_interest(handle: &Handle) -> Option<Interest> { + match handle.aio_get_direction() { + 0 => None, + AIO_DIRECTION_READ => Some(Interest::READABLE), + AIO_DIRECTION_WRITE => Some(Interest::WRITABLE), + AIO_DIRECTION_BOTH => Some(Interest::READABLE | Interest::WRITABLE), + _ => unreachable!(), + } +} + +/// A task that will run as long as the handle is alive. It will poll the +/// file descriptor when new data is availlable. +async fn polling_task(handle_data: &HandleData) -> Result<()> { + let HandleData { + handle, + result_channel, + poll_notifier, + } = handle_data; + // XXX: Might the file descriptor ever be changed? + let fd = handle.aio_get_fd()?; + let fd = AsyncFd::new(fd)?; + + // The following loop does approximately the following things: + // + // 1. Determine what Libnbd wants to do next on the file descriptor, + // (read/write/both/none), and store that in [next_fd_interest]. + // 2. Wait for either: + // a) That interest to be available on the file descriptor in which case: + // I. Call the correct `aio_notify_*` method. + // II. Execute step 1. + // III. Send the result of the call to `aio_notify_*` on + // [result_channel] to notify pending commands that some progress + // has been made. + // IV. Resume execution from step 2. + // b) A notification was received on [poll_notifier] signaling that a new + // command was registered and that the intrest on the file descriptor + // might has changed. Resume execution from step 1. + let mut next_fd_interest = get_fd_interest(handle); + loop { + let Some(fd_interest) = next_fd_interest else { + // The handle does not wait for any data of the file descriptor, + // so we wait until some command is issued. + poll_notifier.notified().await; + next_fd_interest = get_fd_interest(handle); + continue; + }; + + let res = tokio::select! { + ready_guard = fd.ready(fd_interest) => { + // Some new data is availlable. + let mut ready_guard = ready_guard?; + let readyness = ready_guard.ready(); + if readyness.is_readable() && fd_interest.is_readable() { + let res = match handle.aio_notify_read() { + res @ Ok(_) + | res @ Err(Error::Recoverable(_)) => res, + err @ Err(Error::Fatal(_)) => return err, + }; + next_fd_interest = get_fd_interest(handle); + // We do only know that the read blocked if the next + // interest contains a read as well. + if next_fd_interest.map_or(false, Interest::is_readable) { + ready_guard.clear_ready_matching(IoReady::READABLE); + } else { + ready_guard.retain_ready(); + } + res + } + else if readyness.is_writable() && fd_interest.is_writable() { + let res = match handle.aio_notify_write() { + res @ Ok(_) + | res @ Err(Error::Recoverable(_)) => res, + err @ Err(Error::Fatal(_)) => return err, + }; + next_fd_interest = get_fd_interest(handle); + if next_fd_interest.map_or(false, Interest::is_writable) { + ready_guard.clear_ready_matching(IoReady::WRITABLE); + } else { + ready_guard.retain_ready(); + } + res + } else { + continue; + } + } + _ = poll_notifier.notified() => { + // Someone issued a command so the interest might have changed. + next_fd_interest = get_fd_interest(handle); + continue; + } + }; + + result_channel.send(res.map_err(Arc::new)).ok(); + } +} + +impl Drop for AsyncHandle { + fn drop(&mut self) { + self.polling_task.abort(); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a6f3131..eb3f6cb 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -17,11 +17,15 @@ #![deny(warnings)] +mod async_bindings; +mod async_handle; mod bindings; mod error; mod handle; pub mod types; mod utils; +pub use async_bindings::*; +pub use async_handle::AsyncHandle; pub use bindings::*; pub use error::{Error, ErrorKind, FatalErrorKind, Result}; pub use handle::Handle; -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 08/10] rust: async: Add a couple of integration tests
Add a couple of integration tests as rust/tests/test_async_*.rs. They are very similar to the tests for the synchronous API. --- rust/tests/test_async_100_handle.rs | 25 +++ rust/tests/test_async_200_connect_command.rs | 34 ++++ rust/tests/test_async_210_opt_abort.rs | 40 +++++ rust/tests/test_async_220_opt_list.rs | 86 ++++++++++ rust/tests/test_async_230_opt_info.rs | 126 +++++++++++++++ rust/tests/test_async_235memleak.rs | 57 +++++++ rust/tests/test_async_240_opt_list_meta.rs | 151 ++++++++++++++++++ .../test_async_245_opt_list_meta_queries.rs | 96 +++++++++++ rust/tests/test_async_250_opt_set_meta.rs | 123 ++++++++++++++ .../test_async_255_opt_set_meta_queries.rs | 111 +++++++++++++ rust/tests/test_async_400_pread.rs | 41 +++++ rust/tests/test_async_405_pread_structured.rs | 85 ++++++++++ rust/tests/test_async_410_pwrite.rs | 63 ++++++++ rust/tests/test_async_460_block_status.rs | 96 +++++++++++ rust/tests/test_async_620_stats.rs | 77 +++++++++ 15 files changed, 1211 insertions(+) create mode 100644 rust/tests/test_async_100_handle.rs create mode 100644 rust/tests/test_async_200_connect_command.rs create mode 100644 rust/tests/test_async_210_opt_abort.rs create mode 100644 rust/tests/test_async_220_opt_list.rs create mode 100644 rust/tests/test_async_230_opt_info.rs create mode 100644 rust/tests/test_async_235memleak.rs create mode 100644 rust/tests/test_async_240_opt_list_meta.rs create mode 100644 rust/tests/test_async_245_opt_list_meta_queries.rs create mode 100644 rust/tests/test_async_250_opt_set_meta.rs create mode 100644 rust/tests/test_async_255_opt_set_meta_queries.rs create mode 100644 rust/tests/test_async_400_pread.rs create mode 100644 rust/tests/test_async_405_pread_structured.rs create mode 100644 rust/tests/test_async_410_pwrite.rs create mode 100644 rust/tests/test_async_460_block_status.rs create mode 100644 rust/tests/test_async_620_stats.rs diff --git a/rust/tests/test_async_100_handle.rs b/rust/tests/test_async_100_handle.rs new file mode 100644 index 0000000..e50bad9 --- /dev/null +++ b/rust/tests/test_async_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)] + +#[tokio::test] +async fn test_async_nbd_handle_new() { + let _ = libnbd::AsyncHandle::new().unwrap(); +} diff --git a/rust/tests/test_async_200_connect_command.rs b/rust/tests/test_async_200_connect_command.rs new file mode 100644 index 0000000..dae08a6 --- /dev/null +++ b/rust/tests/test_async_200_connect_command.rs @@ -0,0 +1,34 @@ +// 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; + +#[tokio::test] +async fn test_async_connect_command() { + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + ]) + .await + .unwrap(); +} diff --git a/rust/tests/test_async_210_opt_abort.rs b/rust/tests/test_async_210_opt_abort.rs new file mode 100644 index 0000000..a84783e --- /dev/null +++ b/rust/tests/test_async_210_opt_abort.rs @@ -0,0 +1,40 @@ +// 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; + +#[tokio::test] +async fn test_opt_abort() { + let nbd = libnbd::AsyncHandle::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"), + ]) + .await + .unwrap(); + assert_eq!(nbd.get_protocol().unwrap(), c_str!("newstyle-fixed")); + assert!(nbd.get_structured_replies_negotiated().unwrap()); + + nbd.opt_abort().await.unwrap(); + assert!(nbd.aio_is_closed()); +} diff --git a/rust/tests/test_async_220_opt_list.rs b/rust/tests/test_async_220_opt_list.rs new file mode 100644 index 0000000..bfe29f2 --- /dev/null +++ b/rust/tests/test_async_220_opt_list.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 + +#![deny(warnings)] + +use byte_strings::c_str; +use std::env; +use std::ffi::{CStr, CString}; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; +use std::sync::Arc; + +/// 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 } + } + + async fn connect( + &self, + mode: u8, + expected_exports: &[&CStr], + ) -> Result<(), Arc<libnbd::Error>> { + let nbd = libnbd::AsyncHandle::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(), + ]) + .await + .unwrap(); + + // Collect all exports in this list. + let mut exports = Vec::new(); + nbd.opt_list(|name, _| { + exports.push(name.to_owned()); + 0 + }) + .await?; + assert_eq!(exports.len(), expected_exports.len()); + for (export, &expected) in exports.iter().zip(expected_exports) { + assert_eq!(export.as_c_str(), expected); + } + Ok(()) + } +} + +#[tokio::test] +async fn test_opt_list() { + let conn_tester = ConnTester::new(); + assert!(conn_tester.connect(0, &[]).await.is_err()); + assert!(conn_tester + .connect(1, &[c_str!("a"), c_str!("b")]) + .await + .is_ok()); + assert!(conn_tester.connect(2, &[]).await.is_ok()); + assert!(conn_tester.connect(3, &[c_str!("a")]).await.is_ok()); +} diff --git a/rust/tests/test_async_230_opt_info.rs b/rust/tests/test_async_230_opt_info.rs new file mode 100644 index 0000000..c40f016 --- /dev/null +++ b/rust/tests/test_async_230_opt_info.rs @@ -0,0 +1,126 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::env; +use std::ffi::CString; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; + +#[tokio::test] +async fn test_opt_info() { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/opt-info.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + + let nbd = libnbd::AsyncHandle::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"), + &script_path, + ]) + .await + .unwrap(); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + + // No size, flags, or meta-contexts yet + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // info with no prior name gets info on "" + assert!(nbd.opt_info().await.is_ok()); + assert_eq!(nbd.get_size().unwrap(), 0); + assert!(nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // changing export wipes out prior info + nbd.set_export_name(c_str!("b")).unwrap(); + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // info on something not present fails + nbd.set_export_name(c_str!("a")).unwrap(); + assert!(nbd.opt_info().await.is_err()); + + // info for a different export, with automatic meta_context disabled + nbd.set_export_name(c_str!("b")).unwrap(); + nbd.set_request_meta_context(false).unwrap(); + nbd.opt_info().await.unwrap(); + // idempotent name change is no-op + nbd.set_export_name(c_str!("b")).unwrap(); + assert_eq!(nbd.get_size().unwrap(), 1); + assert!(!nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + nbd.set_request_meta_context(true).unwrap(); + + // go on something not present + nbd.set_export_name(c_str!("a")).unwrap(); + assert!(nbd.opt_go().await.is_err()); + assert!(nbd.get_size().is_err()); + assert!(nbd.is_read_only().is_err()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // go on a valid export + nbd.set_export_name(c_str!("good")).unwrap(); + nbd.opt_go().await.unwrap(); + assert_eq!(nbd.get_size().unwrap(), 4); + assert!(nbd.is_read_only().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // now info is no longer valid, but does not wipe data + assert!(nbd.set_export_name(c_str!("a")).is_err()); + assert_eq!(nbd.get_export_name().unwrap().as_c_str(), c_str!("good")); + assert!(nbd.opt_info().await.is_err()); + assert_eq!(nbd.get_size().unwrap(), 4); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.disconnect(None).await.unwrap(); + + // Another connection. This time, check that SET_META triggered by opt_info + // persists through nbd_opt_go with set_request_meta_context disabled. + let nbd = libnbd::AsyncHandle::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"), + &script_path, + ]) + .await + .unwrap(); + nbd.add_meta_context(c_str!("x-unexpected:bogus")).unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + nbd.opt_info().await.unwrap(); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.set_request_meta_context(false).unwrap(); + // Adding to the request list now won't matter + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + nbd.opt_go().await.unwrap(); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_async_235memleak.rs b/rust/tests/test_async_235memleak.rs new file mode 100644 index 0000000..68ab909 --- /dev/null +++ b/rust/tests/test_async_235memleak.rs @@ -0,0 +1,57 @@ +// 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::env; +use std::ffi::CString; +use std::os::unix::ffi::OsStringExt as _; +use std::path::Path; + +#[tokio::test] +async fn test_opt_info() { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/opt-info.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + + let nbd = libnbd::Handle::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"), + &script_path, + ]) + .unwrap(); + + // go on a valid export + nbd.set_export_name(c_str!("good")).unwrap(); + nbd.opt_go().unwrap(); + let v = vec![1, 2, 3]; + assert!(nbd + .aio_opt_info(Some(move |i: &mut i32| { + println!("I is: {i}"); + dbg!(&v); + std::process::abort() + })) + .is_err()); +} diff --git a/rust/tests/test_async_240_opt_list_meta.rs b/rust/tests/test_async_240_opt_list_meta.rs new file mode 100644 index 0000000..ec353c5 --- /dev/null +++ b/rust/tests/test_async_240_opt_list_meta.rs @@ -0,0 +1,151 @@ +// 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::sync::Arc; + +/// A struct with information about listed meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:alloc" is listed. + has_alloc: bool, + /// The number of listed meta contexts. + count: u32, +} + +async fn list_meta_ctxs( + nbd: &libnbd::AsyncHandle, +) -> Result<CtxInfo, Arc<libnbd::Error>> { + let mut info = CtxInfo { + has_alloc: false, + count: 0, + }; + nbd.opt_list_meta_context(|ctx| { + info.count += 1; + if ctx == libnbd::CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + }) + .await?; + Ok(info) +} + +#[tokio::test] +async fn test_async_opt_list_meta() { + let nbd = libnbd::AsyncHandle::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!("memory"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + // First pass: empty query should give at least "base:allocation". + let info = list_meta_ctxs(&nbd).await.unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); + let max = info.count; + + // Second pass: bogus query has no response. + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + assert_eq!( + list_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Third pass: specific query should have one match. + nbd.add_meta_context(c_str!("base:allocation")).unwrap(); + assert_eq!(nbd.get_nr_meta_contexts().unwrap(), 2); + assert_eq!( + nbd.get_meta_context(1).unwrap().as_c_str(), + c_str!("base:allocation") + ); + assert_eq!( + list_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + + // Fourth pass: opt_list_meta_context is stateless, so it should + // not wipe status learned during opt_info + assert!(nbd.can_meta_context(c_str!("base:allocation")).is_err()); + assert!(nbd.get_size().is_err()); + nbd.opt_info().await.unwrap(); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(c_str!("base:allocation")).unwrap()); + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + assert_eq!( + list_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(c_str!("base:allocation")).unwrap()); + + // Final pass: "base:" query should get at least "base:allocation" + nbd.add_meta_context(c_str!("base:")).unwrap(); + let info = list_meta_ctxs(&nbd).await.unwrap(); + assert!(info.count >= 1); + assert!(info.count <= max); + assert!(info.has_alloc); + + // Repeat but this time without structured replies. Deal gracefully + // with older servers that don't allow the attempt. + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.set_request_structured_replies(false).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("memory"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + let bytes = nbd.stats_bytes_sent(); + if let Ok(info) = list_meta_ctxs(&nbd).await { + assert!(info.count >= 1); + assert!(info.has_alloc) + } else { + assert!(nbd.stats_bytes_sent() > bytes); + // ignoring failure from old server + } + + // Now enable structured replies, and a retry should pass. + nbd.opt_structured_reply().await.unwrap(); + let info = list_meta_ctxs(&nbd).await.unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); +} diff --git a/rust/tests/test_async_245_opt_list_meta_queries.rs b/rust/tests/test_async_245_opt_list_meta_queries.rs new file mode 100644 index 0000000..47046e7 --- /dev/null +++ b/rust/tests/test_async_245_opt_list_meta_queries.rs @@ -0,0 +1,96 @@ +// 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::ffi::CStr; +use std::sync::Arc; + +/// A struct with information about listed meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is listed. + has_alloc: bool, + /// The number of listed meta contexts. + count: u32, +} + +async fn list_meta_ctxs( + nbd: &libnbd::AsyncHandle, + queries: &[&CStr], +) -> Result<CtxInfo, Arc<libnbd::Error>> { + let mut info = CtxInfo { + has_alloc: false, + count: 0, + }; + nbd.opt_list_meta_context_queries(queries, |ctx| { + info.count += 1; + if ctx == libnbd::CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + }) + .await?; + Ok(info) +} + +#[tokio::test] +async fn test_async_opt_list_meta_queries() { + let nbd = libnbd::AsyncHandle::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!("memory"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + // First pass: empty query should give at least "base:allocation". + nbd.add_meta_context(c_str!("x-nosuch:")).unwrap(); + let info = list_meta_ctxs(&nbd, &[]).await.unwrap(); + assert!(info.count >= 1); + assert!(info.has_alloc); + + // Second pass: bogus query has no response. + nbd.clear_meta_contexts().unwrap(); + assert_eq!( + list_meta_ctxs(&nbd, &[c_str!("x-nosuch:")]).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Third pass: specific query should have one match. + assert_eq!( + list_meta_ctxs( + &nbd, + &[c_str!("x-nosuch:"), libnbd::CONTEXT_BASE_ALLOCATION] + ) + .await + .unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); +} diff --git a/rust/tests/test_async_250_opt_set_meta.rs b/rust/tests/test_async_250_opt_set_meta.rs new file mode 100644 index 0000000..7fc6953 --- /dev/null +++ b/rust/tests/test_async_250_opt_set_meta.rs @@ -0,0 +1,123 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::sync::Arc; + +/// A struct with information about set meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is set. + has_alloc: bool, + /// The number of set meta contexts. + count: u32, +} + +async fn set_meta_ctxs( + nbd: &libnbd::AsyncHandle, +) -> Result<CtxInfo, Arc<libnbd::Error>> { + let mut info = CtxInfo { + has_alloc: false, + count: 0, + }; + nbd.opt_set_meta_context(|ctx| { + info.count += 1; + if ctx == CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + }) + .await?; + Ok(info) +} + +#[tokio::test] +async fn test_async_opt_set_meta() { + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.set_opt_mode(true).unwrap(); + nbd.set_request_structured_replies(false).unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("memory"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + // No contexts negotiated yet; can_meta should be error if any requested + assert!(!nbd.get_structured_replies_negotiated().unwrap()); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // SET cannot succeed until SR is negotiated. + nbd.opt_structured_reply().await.unwrap(); + assert!(nbd.get_structured_replies_negotiated().unwrap()); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).is_err()); + + // nbdkit does not match wildcard for SET, even though it does for LIST + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("base:")).unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Negotiating with no contexts is not an error, but selects nothing + nbd.clear_meta_contexts().unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + + // Request 2 with expectation of 1; with set_request_meta_context off + nbd.add_meta_context(c_str!("x-nosuch:context")).unwrap(); + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + nbd.set_request_meta_context(false).unwrap(); + assert_eq!( + set_meta_ctxs(&nbd).await.unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Transition to transmission phase; our last set should remain active + nbd.clear_meta_contexts().unwrap(); + nbd.add_meta_context(c_str!("x-nosuch:context")).unwrap(); + nbd.opt_go().await.unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Now too late to set; but should not lose earlier state + assert!(set_meta_ctxs(&nbd).await.is_err()); + assert_eq!(nbd.get_size().unwrap(), 1048576); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_async_255_opt_set_meta_queries.rs b/rust/tests/test_async_255_opt_set_meta_queries.rs new file mode 100644 index 0000000..6cec20b --- /dev/null +++ b/rust/tests/test_async_255_opt_set_meta_queries.rs @@ -0,0 +1,111 @@ +// 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 libnbd::CONTEXT_BASE_ALLOCATION; +use std::ffi::CStr; +use std::sync::Arc; + +/// A struct with information about set meta contexts. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CtxInfo { + /// Whether the meta context "base:allocation" is set. + has_alloc: bool, + /// The number of set meta contexts. + count: u32, +} + +async fn set_meta_ctxs_queries( + nbd: &libnbd::AsyncHandle, + queries: &[&CStr], +) -> Result<CtxInfo, Arc<libnbd::Error>> { + let mut info = CtxInfo { + has_alloc: false, + count: 0, + }; + nbd.opt_set_meta_context_queries(queries, |ctx| { + info.count += 1; + if ctx == CONTEXT_BASE_ALLOCATION { + info.has_alloc = true; + } + 0 + }) + .await?; + Ok(info) +} + +#[tokio::test] +async fn test_async_opt_set_meta_queries() { + let nbd = libnbd::AsyncHandle::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!("memory"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + // nbdkit does not match wildcard for SET, even though it does for LIST + assert_eq!( + set_meta_ctxs_queries(&nbd, &[c_str!("base:")]) + .await + .unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Negotiating with no contexts is not an error, but selects nothing + // An explicit empty list overrides a non-empty implicit list. + nbd.add_meta_context(CONTEXT_BASE_ALLOCATION).unwrap(); + assert_eq!( + set_meta_ctxs_queries(&nbd, &[]).await.unwrap(), + CtxInfo { + count: 0, + has_alloc: false + } + ); + assert!(!nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Request 2 with expectation of 1. + assert_eq!( + set_meta_ctxs_queries( + &nbd, + &[c_str!("x-nosuch:context"), CONTEXT_BASE_ALLOCATION] + ) + .await + .unwrap(), + CtxInfo { + count: 1, + has_alloc: true + } + ); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); + + // Transition to transmission phase; our last set should remain active + nbd.set_request_meta_context(false).unwrap(); + nbd.opt_go().await.unwrap(); + assert!(nbd.can_meta_context(CONTEXT_BASE_ALLOCATION).unwrap()); +} diff --git a/rust/tests/test_async_400_pread.rs b/rust/tests/test_async_400_pread.rs new file mode 100644 index 0000000..96c4ce1 --- /dev/null +++ b/rust/tests/test_async_400_pread.rs @@ -0,0 +1,41 @@ +// 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 nbdkit_pattern; +use byte_strings::c_str; +use nbdkit_pattern::PATTERN; + +#[tokio::test] +async fn test_async_pread() { + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("pattern"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + let mut buf = [0; 512]; + nbd.pread(&mut buf, 0, None).await.unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); +} diff --git a/rust/tests/test_async_405_pread_structured.rs b/rust/tests/test_async_405_pread_structured.rs new file mode 100644 index 0000000..40b342f --- /dev/null +++ b/rust/tests/test_async_405_pread_structured.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)] + +mod nbdkit_pattern; +use byte_strings::c_str; +use nbdkit_pattern::PATTERN; + +#[tokio::test] +async fn test_async_pread_structured() { + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + //c_str!("-v"), XXX: uncomment this + c_str!("pattern"), + c_str!("size=1M"), + ]) + .await + .unwrap(); + + fn f(buf: &[u8], offset: u64, s: u32, err: &mut i32) { + assert_eq!(*err, 0); + *err = 42; + assert_eq!(buf, PATTERN.as_slice()); + assert_eq!(offset, 0); + assert_eq!(s, libnbd::READ_DATA); + } + + let mut buf = [0; 512]; + nbd.pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + 0 + }, + None, + ) + .await + .unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); + + nbd.pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + 0 + }, + Some(libnbd::CmdFlag::DF), + ) + .await + .unwrap(); + assert_eq!(buf.as_slice(), PATTERN.as_slice()); + + let res = nbd + .pread_structured( + &mut buf, + 0, + |b, o, s, e| { + f(b, o, s, e); + -1 + }, + Some(libnbd::CmdFlag::DF), + ) + .await; + assert_eq!(res.unwrap_err().errno(), Some(42)); +} diff --git a/rust/tests/test_async_410_pwrite.rs b/rust/tests/test_async_410_pwrite.rs new file mode 100644 index 0000000..fdaae42 --- /dev/null +++ b/rust/tests/test_async_410_pwrite.rs @@ -0,0 +1,63 @@ +// 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::ffi::CString; +use std::fs::{self, File}; +use std::os::unix::prelude::*; + +#[tokio::test] +async fn test_async_pwrite() { + let tmp_dir = tempfile::tempdir().unwrap(); + let data_file_path = tmp_dir.path().join("pwrite_test.data"); + let data_file = File::create(&data_file_path).unwrap(); + data_file.set_len(512).unwrap(); + drop(data_file); + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("file"), + &CString::new(data_file_path.clone().into_os_string().into_vec()) + .unwrap(), + ]) + .await + .unwrap(); + + let mut buf_1 = [0; 512]; + buf_1[10] = 0x01; + buf_1[510] = 0x55; + buf_1[511] = 0xAA; + + let flags = Some(libnbd::CmdFlag::FUA); + nbd.pwrite(&buf_1, 0, flags).await.unwrap(); + + let mut buf_2 = [0; 512]; + nbd.pread(&mut buf_2, 0, None).await.unwrap(); + + assert_eq!(buf_1, buf_2); + + // Drop nbd before tmp_dir is dropped. + drop(nbd); + + let data_file_content = fs::read(&data_file_path).unwrap(); + assert_eq!(buf_1.as_slice(), data_file_content.as_slice()); +} diff --git a/rust/tests/test_async_460_block_status.rs b/rust/tests/test_async_460_block_status.rs new file mode 100644 index 0000000..7f1b041 --- /dev/null +++ b/rust/tests/test_async_460_block_status.rs @@ -0,0 +1,96 @@ +// 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::env; +use std::ffi::CString; +use std::os::unix::prelude::*; +use std::path::Path; + +async fn block_status_get_entries( + nbd: &libnbd::AsyncHandle, + count: u64, + offset: u64, + flags: Option<libnbd::CmdFlag>, +) -> Vec<u32> { + let mut found_entries = None; + nbd.block_status( + count, + offset, + |metacontext, _, entries, err| { + assert_eq!(*err, 0); + if metacontext == libnbd::CONTEXT_BASE_ALLOCATION { + found_entries = Some(entries.to_vec()); + } + 0 + }, + flags, + ) + .await + .unwrap(); + found_entries.unwrap() +} + +#[tokio::test] +async fn test_async_block_status() { + let srcdir = env::var("srcdir").unwrap(); + let srcdir = Path::new(&srcdir); + let script_path = srcdir.join("../tests/meta-base-allocation.sh"); + let script_path + CString::new(script_path.into_os_string().into_vec()).unwrap(); + let nbd = libnbd::AsyncHandle::new().unwrap(); + nbd.add_meta_context(libnbd::CONTEXT_BASE_ALLOCATION) + .unwrap(); + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("sh"), + &script_path, + ]) + .await + .unwrap(); + + assert_eq!( + block_status_get_entries(&nbd, 65536, 0, None) + .await + .as_slice(), + &[8192, 0, 8192, 1, 16384, 3, 16384, 2, 16384, 0,] + ); + + assert_eq!( + block_status_get_entries(&nbd, 1024, 32256, None) + .await + .as_slice(), + &[512, 3, 16384, 2] + ); + + assert_eq!( + block_status_get_entries( + &nbd, + 1024, + 32256, + Some(libnbd::CmdFlag::REQ_ONE) + ) + .await + .as_slice(), + &[512, 3] + ); +} diff --git a/rust/tests/test_async_620_stats.rs b/rust/tests/test_async_620_stats.rs new file mode 100644 index 0000000..156b870 --- /dev/null +++ b/rust/tests/test_async_620_stats.rs @@ -0,0 +1,77 @@ +// 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; + +#[tokio::test] +async fn test_async_stats() { + let nbd = libnbd::AsyncHandle::new().unwrap(); + + // Pre-connection, stats start out at 0 + assert_eq!(nbd.stats_bytes_sent(), 0); + assert_eq!(nbd.stats_chunks_sent(), 0); + assert_eq!(nbd.stats_bytes_received(), 0); + assert_eq!(nbd.stats_chunks_received(), 0); + + // Connection performs handshaking, which increments stats. + // The number of bytes/chunks here may grow over time as more features get + // automatically negotiated, so merely check that they are non-zero. + nbd.connect_command(&[ + c_str!("nbdkit"), + c_str!("-s"), + c_str!("--exit-with-parent"), + c_str!("-v"), + c_str!("null"), + ]) + .await + .unwrap(); + + let bs1 = nbd.stats_bytes_sent(); + let cs1 = nbd.stats_chunks_sent(); + let br1 = nbd.stats_bytes_received(); + let cr1 = nbd.stats_chunks_received(); + assert!(cs1 > 0); + assert!(bs1 > cs1); + assert!(cr1 > 0); + assert!(br1 > cr1); + + // A flush command should be one chunk out, one chunk back (even if + // structured replies are in use) + nbd.flush(None).await.unwrap(); + let bs2 = nbd.stats_bytes_sent(); + let cs2 = nbd.stats_chunks_sent(); + let br2 = nbd.stats_bytes_received(); + let cr2 = nbd.stats_chunks_received(); + assert_eq!(bs2, bs1 + 28); + assert_eq!(cs2, cs1 + 1); + assert_eq!(br2, br1 + 16); // assumes nbdkit uses simple reply + assert_eq!(cr2, cr1 + 1); + + // Stats are still readable after the connection closes; we don't know if + // the server sent reply bytes to our NBD_CMD_DISC, so don't insist on it. + nbd.disconnect(None).await.unwrap(); + let bs3 = nbd.stats_bytes_sent(); + let cs3 = nbd.stats_chunks_sent(); + let br3 = nbd.stats_bytes_received(); + let cr3 = nbd.stats_chunks_received(); + assert!(bs3 > bs2); + assert_eq!(cs3, cs2 + 1); + assert!(br3 >= br2); + assert!(cr3 == cr2 || cr3 == cr2 + 1); +} -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 09/10] generator: Add `modifies_fd` flag to the [call] structure
Add a flag (`modifies_fd`) to the [call] structure in generator/API.ml which is set to [true] if the handle call may do something with the file descriptor. That is, it is [true] for all calls which are or may call [aio_notify_*], including all synchronous commands like [nbd_connect] or [nbd_opt_go]. The motivation for this is that the asynchronous handle in the Rust bindings uses its own loop for polling, modifying the file descriptor outside of this loop may cause unexpected behaviour. Including that the handle may hang. All commands which set this flag to [true] will be excluded from that handle. The asynchronous (`aio_*`) functions will be used instead. --- generator/API.ml | 32 ++++++++++++++++++++++++++++++++ generator/API.mli | 7 +++++++ 2 files changed, 39 insertions(+) diff --git a/generator/API.ml b/generator/API.ml index 42b9eec..6e1670d 100644 --- a/generator/API.ml +++ b/generator/API.ml @@ -33,6 +33,7 @@ type call = { is_locked : bool; may_set_error : bool; async_kind : async_kind option; + modifies_fd: bool; mutable first_version : int * int; } and arg @@ -274,6 +275,7 @@ let default_call = { args = []; optargs = []; ret = RErr; permitted_states = []; is_locked = true; may_set_error = true; async_kind = None; + modifies_fd = false; first_version = (0, 0) } (* Calls. @@ -1181,6 +1183,7 @@ Return true if option negotiation mode was enabled on this handle."; default_call with args = []; ret = RErr; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "end negotiation and move on to using an export"; longdesc = "\ Request that the server finish negotiation and move on to serving the @@ -1208,6 +1211,7 @@ although older servers will instead have killed the connection."; default_call with args = []; ret = RErr; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "end negotiation and close the connection"; longdesc = "\ Request that the server finish negotiation, gracefully if possible, then @@ -1221,6 +1225,7 @@ enabled option mode."; default_call with args = []; ret = RBool; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "request the server to initiate TLS"; longdesc = "\ Request that the server initiate a secure TLS connection, by @@ -1259,6 +1264,7 @@ established, as reported by L<nbd_get_tls_negotiated(3)>."; default_call with args = []; ret = RBool; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "request the server to enable structured replies"; longdesc = "\ Request that the server use structured replies, by sending @@ -1285,6 +1291,7 @@ later calls to this function return false."; default_call with args = [ Closure list_closure ]; ret = RInt; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "request the server to list all exports during negotiation"; longdesc = "\ Request that the server list all exports that it supports. This can @@ -1326,6 +1333,7 @@ description is set with I<-D>."; default_call with args = []; ret = RErr; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "request the server for information about an export"; longdesc = "\ Request that the server supply information about the export name @@ -1357,6 +1365,7 @@ corresponding L<nbd_opt_go(3)> would succeed."; default_call with args = [ Closure context_closure ]; ret = RInt; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "list available meta contexts, using implicit query list"; longdesc = "\ Request that the server list available meta contexts associated with @@ -1412,6 +1421,7 @@ a server may send a lengthy list."; default_call with args = [ StringList "queries"; Closure context_closure ]; ret = RInt; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "list available meta contexts, using explicit query list"; longdesc = "\ Request that the server list available meta contexts associated with @@ -1462,6 +1472,7 @@ a server may send a lengthy list."; default_call with args = [ Closure context_closure ]; ret = RInt; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "select specific meta contexts, using implicit query list"; longdesc = "\ Request that the server supply all recognized meta contexts @@ -1521,6 +1532,7 @@ no contexts are reported, or may fail but have a non-empty list."; default_call with args = [ StringList "queries"; Closure context_closure ]; ret = RInt; permitted_states = [ Negotiating ]; + modifies_fd = true; shortdesc = "select specific meta contexts, using explicit query list"; longdesc = "\ Request that the server supply all recognized meta contexts @@ -1750,6 +1762,7 @@ parameter in NBD URIs is allowed."; default_call with args = [ String "uri" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect to NBD URI"; longdesc = "\ Connect (synchronously) to an NBD server and export by specifying @@ -1928,6 +1941,7 @@ See L<nbd_get_uri(3)>."; default_call with args = [ Path "unixsocket" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect to NBD server over a Unix domain socket"; longdesc = "\ Connect (synchronously) over the named Unix domain socket (C<unixsocket>) @@ -1941,6 +1955,7 @@ to an NBD server running on the same machine. default_call with args = [ UInt32 "cid"; UInt32 "port" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect to NBD server over AF_VSOCK protocol"; longdesc = "\ Connect (synchronously) over the C<AF_VSOCK> protocol from a @@ -1961,6 +1976,7 @@ built on a system with vsock support, see L<nbd_supports_vsock(3)>. default_call with args = [ String "hostname"; String "port" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect to NBD server over a TCP port"; longdesc = "\ Connect (synchronously) to the NBD server listening on @@ -1975,6 +1991,7 @@ such as C<\"10809\">. default_call with args = [ Fd "sock" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect directly to a connected socket"; longdesc = "\ Pass a connected socket C<sock> through which libnbd will talk @@ -1996,6 +2013,7 @@ handle is closed. The caller must not use the socket in any way. default_call with args = [ StringList "argv" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect to NBD server command"; longdesc = "\ Run the command as a subprocess and connect to it over @@ -2031,6 +2049,7 @@ is killed. default_call with args = [ StringList "argv" ]; ret = RErr; permitted_states = [ Created ]; + modifies_fd = true; shortdesc = "connect using systemd socket activation"; longdesc = "\ Run the command as a subprocess and connect to it using @@ -2404,6 +2423,7 @@ requests sizes. optargs = [ OFlags ("flags", cmd_flags, Some []) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "read from the NBD server"; longdesc = "\ Issue a read command to the NBD server for the range starting @@ -2443,6 +2463,7 @@ on failure." optargs = [ OFlags ("flags", cmd_flags, Some ["DF"]) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "read from the NBD server"; longdesc = "\ Issue a read command to the NBD server for the range starting @@ -2535,6 +2556,7 @@ on failure." optargs = [ OFlags ("flags", cmd_flags, Some ["FUA"]) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "write to the NBD server"; longdesc = "\ Issue a write command to the NBD server, writing the data in @@ -2568,6 +2590,7 @@ L<nbd_can_fua(3)>)." args = []; optargs = [ OFlags ("flags", shutdown_flags, None) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "disconnect from the NBD server"; longdesc = "\ Issue the disconnect command to the NBD server. This is @@ -2605,6 +2628,7 @@ A future version of the library may add new flags."; default_call with args = []; optargs = [ OFlags ("flags", cmd_flags, Some []) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "send flush command to the NBD server"; longdesc = "\ Issue the flush command to the NBD server. The function should @@ -2625,6 +2649,7 @@ protocol extensions)." optargs = [ OFlags ("flags", cmd_flags, Some ["FUA"]) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "send trim command to the NBD server"; longdesc = "\ Issue a trim command to the NBD server, which if supported @@ -2656,6 +2681,7 @@ L<nbd_can_fua(3)>)." optargs = [ OFlags ("flags", cmd_flags, Some []) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "send cache (prefetch) command to the NBD server"; longdesc = "\ Issue the cache (prefetch) command to the NBD server, which @@ -2685,6 +2711,7 @@ protocol extensions)." Some ["FUA"; "NO_HOLE"; "FAST_ZERO"]) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "send write zeroes command to the NBD server"; longdesc = "\ Issue a write zeroes command to the NBD server, which if supported @@ -2723,6 +2750,7 @@ cannot do this, see L<nbd_can_fast_zero(3)>)." optargs = [ OFlags ("flags", cmd_flags, Some ["REQ_ONE"]) ]; ret = RErr; permitted_states = [ Connected ]; + modifies_fd = true; shortdesc = "send block status command to the NBD server"; longdesc = "\ Issue the block status command to the NBD server. If @@ -2789,6 +2817,7 @@ validate that the server obeyed the flag." "poll", { default_call with args = [ Int "timeout" ]; ret = RInt; + modifies_fd = true; shortdesc = "poll the handle once"; longdesc = "\ This is a simple implementation of L<poll(2)> which is used @@ -2810,6 +2839,7 @@ intended as something you would use."; "poll2", { default_call with args = [Fd "fd"; Int "timeout" ]; ret = RInt; + modifies_fd = true; shortdesc = "poll the handle once, with fd"; longdesc = "\ This is the same as L<nbd_poll(3)>, but an additional @@ -3493,6 +3523,7 @@ and invalidate the need to write more commands. "aio_notify_read", { default_call with args = []; ret = RErr; + modifies_fd = true; shortdesc = "notify that the connection is readable"; longdesc = "\ Send notification to the state machine that the connection @@ -3504,6 +3535,7 @@ connection is readable."; "aio_notify_write", { default_call with args = []; ret = RErr; + modifies_fd = true; shortdesc = "notify that the connection is writable"; longdesc = "\ Send notification to the state machine that the connection diff --git a/generator/API.mli b/generator/API.mli index ff85849..4bf4468 100644 --- a/generator/API.mli +++ b/generator/API.mli @@ -41,6 +41,13 @@ type call = { if the function is asynchronous and in that case how one can check if it has completed. *) async_kind : async_kind option; + (** A flag telling if the call may do something with the file descriptor. + Some bindings needs exclusive access to the file descriptor and can not + allow the user to call [aio_notify_read] or [aio_notify_write], neither + directly nor indirectly from another call. So all calls that might trigger + any of these functions to be called, including all synchronous commands + like [pread] or [connect], should set this to [true]. *) + modifies_fd : bool; (** The first stable version that the symbol appeared in, for example (1, 2) if the symbol was added in development cycle 1.1.x and thus the first stable version was 1.2. This is -- 2.41.0
Tage Johansson
2023-Jul-24 10:38 UTC
[Libguestfs] [libnbd PATCH v3 10/10] rust: async: Use the `modifies_fd` flag to exclude calls
All handle calls which has the `modifies_fd` flag set to [true] will be excluded from `AsyncHandle` (the asynchronous handle in the rust bindings). This is a better approach then listing all calls that should be excluded in Rust.ml explicetly. --- generator/Rust.ml | 63 ++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/generator/Rust.ml b/generator/Rust.ml index adc7ffa..4d81e3f 100644 --- a/generator/Rust.ml +++ b/generator/Rust.ml @@ -597,19 +597,21 @@ let generate_rust_bindings () (*********************************************************) let excluded_handle_calls : NameSet.t + let modifies_fd + handle_calls + |> List.filter (fun (_, { modifies_fd }) -> modifies_fd) + |> List.map (fun (name, _) -> name) + in NameSet.of_list - [ - "aio_get_fd"; - "aio_get_direction"; - "aio_notify_read"; - "aio_notify_write"; - "clear_debug_callback"; - "get_debug"; - "poll"; - "poll2"; - "set_debug"; - "set_debug_callback"; - ] + (modifies_fd + @ [ + "aio_get_fd"; + "aio_get_direction"; + "clear_debug_callback"; + "get_debug"; + "set_debug"; + "set_debug_callback"; + ]) (* A mapping with names as keys. *) module NameMap = Map.Make (String) @@ -620,16 +622,16 @@ let strip_aio name : string String.sub name 4 (String.length name - 4) else failwithf "Asynchronous call %s must begin with aio_" name -(* A map with all asynchronous handle calls. The keys are names with "aio_" - stripped, the values are a tuple with the actual name (with "aio_"), the - [call] and the [async_kind]. *) +(* A map with all asynchronous handle calls. The keys are names, the values + are tuples of: the name with "aio_" stripped, the [call] and the + [async_kind]. *) let async_handle_calls : ((string * call) * async_kind) NameMap.t handle_calls |> List.filter (fun (n, _) -> not (NameSet.mem n excluded_handle_calls)) |> List.filter_map (fun (name, call) -> call.async_kind |> Option.map (fun async_kind -> - (strip_aio name, ((name, call), async_kind)))) + (name, ((strip_aio name, call), async_kind)))) |> List.to_seq |> NameMap.of_seq (* A mapping with all synchronous (not asynchronous) handle calls. Excluded @@ -639,11 +641,7 @@ let async_handle_calls : ((string * call) * async_kind) NameMap.t let sync_handle_calls : call NameMap.t handle_calls |> List.filter (fun (n, _) -> not (NameSet.mem n excluded_handle_calls)) - |> List.filter (fun (name, _) -> - (not (NameMap.mem name async_handle_calls)) - && not - (String.starts_with ~prefix:"aio_" name - && NameMap.mem (strip_aio name) async_handle_calls)) + |> List.filter (fun (n, _) -> not (NameMap.mem n async_handle_calls)) |> List.to_seq |> NameMap.of_seq (* Get the Rust type for an argument in the asynchronous API. Like @@ -688,7 +686,7 @@ let print_rust_sync_handle_call (name : string) (call : call) (* Print the Rust function for an asynchronous handle call with a completion callback. (Note that "callback" might be abbreviated with "cb" in the following code. *) -let print_rust_async_handle_call_with_completion_cb name (aio_name, call) +let print_rust_async_handle_call_with_completion_cb aio_name (name, call) (* An array of all optional arguments. Useful because we need to deel with the index of the completion callback. *) let optargs = Array.of_list call.optargs in @@ -760,7 +758,7 @@ let print_rust_async_handle_call_with_completion_cb name (aio_name, call) completion by changing state. The predicate is a call like "aio_is_connecting" which should get the value (like false) for the call to be complete. *) -let print_rust_async_handle_call_changing_state name (aio_name, call) +let print_rust_async_handle_call_changing_state aio_name (name, call) (predicate, value) let value = if value then "true" else "false" in print_rust_handle_call_comment call; @@ -784,16 +782,15 @@ let print_rust_async_handle_call_changing_state name (aio_name, call) (* Print an impl with all handle calls. *) let print_rust_async_handle_impls () pr "impl AsyncHandle {\n"; - NameMap.iter print_rust_sync_handle_call sync_handle_calls; - NameMap.iter - (fun name (call, async_kind) -> - match async_kind with - | WithCompletionCallback -> - print_rust_async_handle_call_with_completion_cb name call - | ChangesState (predicate, value) -> - print_rust_async_handle_call_changing_state name call - (predicate, value)) - async_handle_calls; + sync_handle_calls |> NameMap.iter print_rust_sync_handle_call; + async_handle_calls + |> NameMap.iter (fun name (call, async_kind) -> + match async_kind with + | WithCompletionCallback -> + print_rust_async_handle_call_with_completion_cb name call + | ChangesState (predicate, value) -> + print_rust_async_handle_call_changing_state name call + (predicate, value)); pr "}\n\n" let print_rust_async_imports () -- 2.41.0