The existing Rust bindings for nbdkit aren't very idiomatic Rust, and they
are missing a lot of features. So I've rewritten them. The new bindings
aren't backwards compatible, but I doubt that's a problem. Most likely,
nobody has tried to use them yet, since the crate hasn't even published to
crates.io. Please review the attached patch.
-Alan
commit 01a7f6644b4c94ebb764a81ee27b971f3a8cc218
Author: Alan Somers <[email protected]>
Date: Thu Jun 4 16:22:47 2020 -0600
plugins/rust: rewrite the Rust bindings
The new bindings:
* Are idiomatic Rust. They don't require the users to manipulate raw
pointers, use unsafe code, or define C ABI functions, and they use
standard Rust mechanisms for reporting errors.
* Log error messages.
* Support the previously-unsupport .extents, .preconnect, and .get_ready
callbacks.
* Use Semver versioning. Rust users will expect the crate to use
semver, not to track the version number of the nbdkit binary.
* Include tests.
* Include complete Rustdoc documentation.
* Support nbdkit_peer_name, nbdkit_shutdown, and nbdkit_stdio_safe.
* Destroy resources using the standard Drop trait, rather than a close
callback.
* Is publishable to crates.io.
diff --git a/configure.ac b/configure.ac
index 220b9b0a..c6ae27bd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1077,7 +1077,6 @@ AC_CONFIG_FILES([Makefile
plugins/python/Makefile
plugins/random/Makefile
plugins/ruby/Makefile
- plugins/rust/Cargo.toml
plugins/rust/Makefile
plugins/sh/Makefile
plugins/ssh/Makefile
diff --git a/plugins/rust/Cargo.toml b/plugins/rust/Cargo.toml
new file mode 100644
index 00000000..4c9cac35
--- /dev/null
+++ b/plugins/rust/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "nbdkit"
+version = "0.1.0"
+authors = ["Alan Somers <[email protected]>"]
+license = "BSD-2-Clause"
+edition = "2018"
+readme = "README.md"
+repository = "https://github.com/libguestfs/nbdkit"
+categories = ["api-bindings", "filesystem"]
+keywords = ["network-block-device", "nbd"]
+homepage = "http://libguestfs.org/"
+exclude = ["nbdkit-rust-plugin.*", "Cargo.toml.in", "Makefile*"]
+description = """
+Rust bindings to the NBDKit framework for creating Network Block Device servers
+"""
+
+[package.metadata.docs.rs]
+features = ["nix", "nightly-docs"]
+
+[features]
+# For building documentation only; no functional change to the library.
+nightly-docs = []
+
+[dependencies]
+bitflags = "1.2.1"
+libc = "0.2.71"
+nix = { version = "0.17.0", optional = true }
+
+[dev-dependencies]
+errno = "0.2.5"
+lazy_static = "1.2.0"
+# Need Mockall's PR #141, which hasn't yet been released
+mockall = { git = "https://github.com/asomers/mockall.git", rev = "7c23b5cab5eca8a40ee3ab3270155f52bea4d658" }
+
+[[example]]
+name = "ramdisk"
+crate-type = ["cdylib"]
diff --git a/plugins/rust/Cargo.toml.in b/plugins/rust/Cargo.toml.in
deleted file mode 100644
index bb20adab..00000000
--- a/plugins/rust/Cargo.toml.in
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "nbdkit"
-version = "@VERSION@"
-authors = ["Richard W.M. Jones <[email protected]>"]
-edition = "2018"
-
-[dependencies]
-# lazy_static is used by the example.
-lazy_static = "1.2.0"
-
-[[example]]
-name = "ramdisk"
-crate-type = ["cdylib"]
diff --git a/plugins/rust/LICENSE b/plugins/rust/LICENSE
new file mode 100644
index 00000000..d900ae39
--- /dev/null
+++ b/plugins/rust/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 Axcient
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/plugins/rust/Makefile.am b/plugins/rust/Makefile.am
index e3063231..2aa16235 100644
--- a/plugins/rust/Makefile.am
+++ b/plugins/rust/Makefile.am
@@ -32,7 +32,7 @@
include $(top_srcdir)/common-rules.mk
EXTRA_DIST = \
- Cargo.toml.in \
+ Cargo.toml \
examples/ramdisk.rs \
nbdkit-rust-plugin.pod \
src/lib.rs \
diff --git a/plugins/rust/README.md b/plugins/rust/README.md
new file mode 100644
index 00000000..aae523ef
--- /dev/null
+++ b/plugins/rust/README.md
@@ -0,0 +1,47 @@
+# NBDKit
+
+Rust bindings to the Network Block Device Kit library.
+
+[](https://crates.io/crates/nbdkit)
+[](https://docs.rs/nbdkit)
+
+## Overview
+
+NBDKit is a framework for developing Network Block Device servers. Most of
+the logic lives in the C code, but there are plugins to implement a server
+in another language, such as Rust.
+
+## Usage
+
+To create an NBD server in Rust, you must implement the `nbdkit::Server`
+trait, and register it with `nbdkit::plugin!`, like this:
+
+```toml
+[dependencies]
+nbdkit = "0.1.0"
+```
+
+```rust
+use nbdkit::*;
+
+#[derive(Default)]
+struct MyPlugin {
+ // ...
+}
+
+impl Server for MyPlugin {
+ // ...
+}
+
+plugin!(MyPlugin {write_at, trim, ...});
+```
+
+# Minimum Supported Rust Version (MSRV)
+
+`nbdkit` is supported on Rust 1.40.0 and higher. The MSRV will not be
+changed in the future without raising the major or minor version.
+
+# License
+
+`nbdkit` is primarily distributed under the 2-clause BSD liense. See
+LICENSE for details.
diff --git a/plugins/rust/examples/ramdisk.rs b/plugins/rust/examples/ramdisk.rs
index e9462fca..f92271ee 100644
--- a/plugins/rust/examples/ramdisk.rs
+++ b/plugins/rust/examples/ramdisk.rs
@@ -1,121 +1,52 @@
-// nbdkit
-// Copyright (C) 2019 Red Hat Inc.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-// * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//
-// * Redistributions in binary form must reproduce the above copyright
-// notice, this list of conditions and the following disclaimer in the
-// documentation and/or other materials provided with the distribution.
-//
-// * Neither the name of Red Hat nor the names of its contributors may be
-// used to endorse or promote products derived from this software without
-// specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
-// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
-// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
-// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
-// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-// SUCH DAMAGE.
-
-extern crate nbdkit;
-
-#[macro_use]
-extern crate lazy_static;
-
-use std::ptr;
-use std::os::raw::{c_char, c_int, c_void};
+use lazy_static::lazy_static;
use std::sync::Mutex;
-
use nbdkit::*;
-use nbdkit::ThreadModel::*;
// The RAM disk.
lazy_static! {
static ref DISK: Mutex<Vec<u8>> = Mutex::new (vec![0; 100 * 1024 * 1024]);
}
-struct Handle {
+#[derive(Default)]
+struct RamDisk {
// Box::new doesn't allocate anything unless we put some dummy
// fields here. In a real implementation you would put per-handle
// data here as required.
_not_used: i32,
}
-extern fn ramdisk_thread_model () -> ThreadModel {
- Parallel
-}
+impl Server for RamDisk {
+ fn get_size(&self) -> Result<i64> {
+ Ok(DISK.lock().unwrap().len() as i64)
+ }
-extern fn ramdisk_open (_readonly: c_int) -> *mut c_void {
- let h = Handle {_not_used: 0};
- let h = Box::new(h);
- return Box::into_raw(h) as *mut c_void;
-}
+ fn name() -> &'static str {
+ "ramdisk"
+ }
-extern fn ramdisk_close (h: *mut c_void) {
- let h = unsafe { Box::from_raw(h as *mut Handle) };
- drop (h);
-}
+ fn open(_readonly: bool) -> Box<dyn Server> {
+ Box::new(RamDisk::default())
+ }
-extern fn ramdisk_get_size (_h: *mut c_void) -> i64 {
- return DISK.lock().unwrap().capacity() as i64;
-}
+ fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<()> {
+ let disk = DISK.lock().unwrap();
+ let ofs = offset as usize;
+ let end = ofs + buf.len();
+ buf.copy_from_slice(&disk[ofs..end]);
+ Ok(())
+ }
-extern fn ramdisk_pread (_h: *mut c_void, buf: *mut c_char, count: u32,
- offset: u64, _flags: u32) -> c_int {
- let offset = offset as usize;
- let count = count as usize;
- let disk = DISK.lock().unwrap();
- unsafe {
- ptr::copy_nonoverlapping (&disk[offset], buf as *mut u8, count);
+ fn thread_model() -> Result<ThreadModel> where Self: Sized {
+ Ok(ThreadModel::Parallel)
}
- return 0;
-}
-extern fn ramdisk_pwrite (_h: *mut c_void, buf: *const c_char, count: u32,
- offset: u64, _flags: u32) -> c_int {
- let offset = offset as usize;
- let count = count as usize;
- let mut disk = DISK.lock().unwrap();
- unsafe {
- ptr::copy_nonoverlapping (buf as *const u8, &mut disk[offset], count);
+ fn write_at(&self, buf: &[u8], offset: u64, _flags: Flags) -> Result<()> {
+ let mut disk = DISK.lock().unwrap();
+ let ofs = offset as usize;
+ let end = ofs + buf.len();
+ disk[ofs..end].copy_from_slice(buf);
+ Ok(())
}
- return 0;
}
-// Every plugin must define a public, C-compatible plugin_init
-// function which returns a pointer to a Plugin struct.
-#[no_mangle]
-pub extern fn plugin_init () -> *const Plugin {
- // Plugin name.
- // https://github.com/rust-lang/rfcs/issues/400
- let name = "ramdisk\0" as *const str as *const [c_char] as *const c_char;
-
- // Create a mutable plugin, setting the 4 required fields.
- let mut plugin = Plugin::new (
- name,
- ramdisk_open,
- ramdisk_get_size,
- ramdisk_pread
- );
- // Update any other fields as required.
- plugin.close = Some (ramdisk_close);
- plugin.pwrite = Some (ramdisk_pwrite);
- plugin.thread_model = Some (ramdisk_thread_model);
-
- // Return the pointer.
- let plugin = Box::new(plugin);
- // XXX Memory leak.
- return Box::into_raw(plugin);
-}
+plugin!(RamDisk {thread_model, write_at});
diff --git a/plugins/rust/nbdkit-rust-plugin.pod b/plugins/rust/nbdkit-rust-plugin.pod
index 77eb0c41..662f4240 100644
--- a/plugins/rust/nbdkit-rust-plugin.pod
+++ b/plugins/rust/nbdkit-rust-plugin.pod
@@ -24,46 +24,21 @@ L<https://github.com/libguestfs/nbdkit/blob/master/plugins/rust/examples/ramdisk
in the nbdkit source tree. The first describes the plugin interface
for Rust plugins and the second provides a simple example.
-We may change how Rust plugins are written in future to make them more
-idiomatic. At the moment each callback corresponds directly to a C
-callback - in fact each is called directly from the server.
+Your Rust code should define a public implementation of the C<Server> trait,
+and register it using the C<plugin!> macro.
-Your Rust code should define a public C<plugin_init> function which
-returns a pointer to a C<Plugin> struct. This struct is exactly
-compatible with the C struct used by C plugins.
-
- extern crate nbdkit;
use nbdkit::*;
- use nbdkit::ThreadModel::*;
- #[no_mangle]
- extern fn myplugin_thread_model () -> ThreadModel {
- SerializeAllRequests
+ #[derive(Default)]
+ struct MyPlugin {
+ // ...
}
- //... more functions
-
- pub extern fn plugin_init () -> *const Plugin {
- // Plugin name.
- let name = "myplugin\0"
- as *const str as *const [c_char] as *const c_char;
-
- // Create a mutable plugin, setting the 4 required fields.
- let mut plugin = Plugin::new (
- name,
- myplugin_open,
- myplugin_get_size,
- myplugin_pread
- );
- // Update any other fields as required.
- plugin.close = Some (myplugin_close);
- plugin.pwrite = Some (myplugin_pwrite);
- plugin.thread_model = Some (myplugin_thread_model);
-
- // Return the pointer.
- let plugin = Box::new(plugin);
- return Box::into_raw(plugin);
-}
+ impl Server for MyPlugin {
+ // ...
+ }
+
+ plugin!(MyPlugin {write_at, trim, ...});
=head2 Compiling a Rust nbdkit plugin
@@ -80,7 +55,7 @@ L<nbdkit-plugin(3)>):
=head2 Threads
-One of the members in C<Plugin> returned by C<plugin_init> is
+One of the methods of C<Server> is
C<thread_model>, which must return one of the values in the table
below. For more information on thread models, see
L<nbdkit-plugin(3)/THREADS>. If this optional function is not
@@ -101,17 +76,14 @@ C<nbdkit::ThreadModel::Parallel>.
=head2 Missing callbacks
-=over 4
-
-=item Missing: C<can_extents> and C<extents>
-
-These are not yet supported.
-
-=back
+All NBDKit callbacks are supported. However, the C<Server> trait has no
+C<close> method. Instead, you should implement C<Drop> if you need to clean
+up resources during destruction.
=head1 VERSION
-Rust plugins first appeared in nbdkit 1.12.
+Rust plugins first appeared in nbdkit 1.12. The crate was completely
+rewritten for nbdkit 1.21.9.
=head1 SEE ALSO
@@ -121,8 +93,8 @@ L<cargo(1)>.
=head1 AUTHORS
-Richard W.M. Jones
+Alan Somers
=head1 COPYRIGHT
-Copyright (C) 2019 Red Hat Inc.
+Copyright (C) 2020 Axcient.
diff --git a/plugins/rust/src/lib.rs b/plugins/rust/src/lib.rs
index 2d691e78..10eeded3 100644
--- a/plugins/rust/src/lib.rs
+++ b/plugins/rust/src/lib.rs
@@ -1,5 +1,5 @@
-// nbdkit
-// Copyright (C) 2019 Red Hat Inc.
+// vim: tw=80
+// Copyright (C) 2020 Axcient
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
@@ -12,10 +12,6 @@
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
-// * Neither the name of Red Hat nor the names of its contributors may be
-// used to endorse or promote products derived from this software without
-// specific prior written permission.
-//
// THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
@@ -29,17 +25,1057 @@
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
-use std::mem;
-use std::os::raw::{c_char, c_int, c_void};
+//! Rust bindings to NBDKit.
+//!
+//! NBDKit is a toolkit for building Network Block Device servers.
+//!
+//! [https://github.com/libguestfs/nbdkit](https://github.com/libguestfs/nbdkit)
+#![cfg_attr(feature = "nightly-docs", feature(doc_cfg))]
+#![deny(missing_docs)]
+
+use bitflags::bitflags;
+#[cfg(feature = "nix")]
+#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "nix")))]
+pub use nix::sys::socket::{SockAddr, sockaddr_storage_to_addr};
+use std::{
+ ffi::{CStr, CString},
+ error,
+ fmt,
+ io,
+ mem,
+ os::raw::{c_char, c_int, c_void},
+ ptr,
+ slice,
+ sync::Once,
+};
+
+/// The error type used by [`Result`].
+#[derive(Debug)]
+pub struct Error {
+ source: Box<dyn error::Error + 'static>,
+ errno: i32
+}
+impl Error {
+ fn errno(&self) -> i32 {
+ self.errno
+ }
+
+ /// Create a new Error with a supplied errno.
+ ///
+ /// # Examples
+ /// ```
+ /// # use nbdkit::Error;
+ /// let e = Error::new(libc::EINVAL, "Invalid value for option foo");
+ /// ```
+ pub fn new<E>(errno: i32, error: E) -> Error
+ where E: Into<Box<dyn error::Error + 'static>>
+ {
+ Error {
+ source: error.into(),
+ errno
+ }
+ }
+}
+impl error::Error for Error {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ Some(&*self.source)
+ }
+}
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.source.fmt(f)
+ }
+}
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Error {
+ Error {
+ errno: e.raw_os_error().unwrap_or(0),
+ source: Box::new(e),
+ }
+ }
+}
+
+/// The Result type returned by all [`Server`] callbacks.
+pub type Result<T> = std::result::Result<T, Error>;
+
+static mut INITIALIZED: bool = false;
+
+mod unreachable {
+ use super::*;
+ pub(super) fn config(_k: &str, _v: &str) -> Result<()> { unreachable!() }
+ pub(super) fn config_complete() -> Result<()> { unreachable!() }
+ pub(super) fn dump_plugin() { unreachable!() }
+ pub(super) fn open(_: bool) -> Box<dyn Server> { unreachable!() }
+ pub(super) fn preconnect(_: bool) -> Result<()> { unreachable!() }
+ pub(super) fn thread_model() -> Result<ThreadModel> { unreachable!() }
+}
+
+static mut CONFIG: fn(k: &str, v: &str) -> Result<()> = unreachable::config;
+static mut CONFIG_COMPLETE: fn() -> Result<()> = unreachable::config_complete;
+static mut CONFIG_HELP: Vec<u8> = Vec::new();
+static mut DESCRIPTION: Vec<u8> = Vec::new();
+static mut DUMP_PLUGIN: fn() = unreachable::dump_plugin;
+static mut GET_READY: fn() -> Result<()> = unreachable::config_complete;
+static mut LOAD: fn() = unreachable::dump_plugin;
+static mut LONGNAME: Vec<u8> = Vec::new();
+static mut MAGIC_CONFIG_KEY: Vec<u8> = Vec::new();
+static mut NAME: Vec<u8> = Vec::new();
+static mut OPEN: fn(readonly: bool) -> Box<dyn Server> = unreachable::open;
+static mut PRECONNECT: fn(readonly: bool) -> Result<()> = unreachable::preconnect;
+static mut THREAD_MODEL: fn() -> Result<ThreadModel> = unreachable::thread_model;
+static mut UNLOAD: fn() = unreachable::dump_plugin;
+static mut VERSION: Vec<u8> = Vec::new();
+static INIT: Once = Once::new();
+
+bitflags! {
+ /// Flags used by multiple [`Server`] methods
+ pub struct Flags: u32 {
+ /// This flag is used by the [`Server::zero`] callback.
+ ///
+ /// Indicates that the plugin may punch a hole instead of writing actual
+ /// zeros, but only if subsequent reads from that region will return
+ /// zeros. There is no way to disable this flag, although a plugin that
+ /// does not support trim as a way to write zeroes may ignore the flag
+ /// without violating expected semantics.
+ const MAY_TRIM = 0b00000001;
+
+ /// This flag represents Forced Unit Access semantics.
+ ///
+ /// It is used by the [`Server::write_at`], [`Server::zero`], and
+ /// [`Server::trim`] callbacks to indicate that the plugin must not
+ /// return a result until the action has landed in persistent storage.
+ /// This flag will not be sent to the plugin unless `can_fua` is
+ /// provided to [`plugin!`] and [`Server::can_fua`] returns
+ /// [`FuaFlags::Native`].
+ const FUA = 0b00000010;
+
+ /// Used with [`Server::extents`] to indicate that the client is only
+ /// requesting information about a single extent. The plugin may ignore
+ /// this flag, or as an optimization it may return
+ /// just a single extent.
+ const REQ_ONE = 0b00000100;
+
+ /// This flag is used by [`Server::zero`].
+ ///
+ /// If supplied, the plugin must decide up front if the implementation
+ /// is likely to be faster than a corresponding [`Server::write_at`]; if
+ /// not, then it must immediately fail with `ENOTSUP` or
+ /// `EOPNOTSUPP` and preferably without modifying the exported
+ /// image. It is acceptable to always fail a fast zero request (as a
+ /// fast failure is better than attempting the write only to find out
+ /// after the fact that it was not fast after all). Note that on Linux,
+ /// support for `ioctl(BLKZEROOUT)` is insufficient for determining
+ /// whether a zero request to a block device will be fast (because the
+ /// kernel will perform a slow fallback when needed).
+ const FAST_ZERO = 0b00001000;
+ }
+}
+
+/// Return values for [`Server::can_cache`]
+#[repr(i32)]
+pub enum CacheFlags {
+ /// Cache support is not advertised to the client.
+ None = 0,
+ /// Caching is emulated by the server calling [`Server::read_at`] and
+ /// ignoring the results.
+ Emulate = 1,
+ /// The [`Server::cache`] callback will be used.
+ Native = 2,
+}
+
+impl Into<i32> for CacheFlags {
+ fn into(self) -> i32 {
+ self as i32
+ }
+}
+
+/// Return values for [`Server::can_fua`]
+#[repr(i32)]
+pub enum FuaFlags {
+ /// FUA support is not advertised to the client
+ None = 0,
+ /// The [`Server::flush`] callback must work (even if [`Server::can_flush`]
+ /// returns false), and FUA support is emulated by calling `Server::flush`
+ /// after any write operation;
+ Emulate = 1,
+ /// The [`Server::write_at`], [`Server::zero`], and [`Server::trim`]
+ /// callbacks (if implemented) must handle the flag [`Flags::FUA`], by not
+ /// returning until that action has landed in persistent storage.
+ Native = 2,
+}
+
+impl Into<i32> for FuaFlags {
+ fn into(self) -> i32 {
+ self as i32
+ }
+}
+
+/// A plugin's maximum thread safety model
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(i32)]
+pub enum ThreadModel {
+ /// Only a single handle can be open at any time, and all requests happen
+ /// from one thread.
+ SerializeConnections = 0,
+
+ /// Multiple handles can be open at the same time, but requests are
+ /// serialized so that for the plugin as a whole only one
+ /// open/read/write/close (etc) request will be in progress at any time.
+ ///
+ /// This is a useful setting if the library you are using is not
+ /// thread-safe. However performance may not be good.
+ SerializeAllRequests = 1,
+
+ /// Multiple handles can be open and multiple data requests can happen in
+ /// parallel. However only one request will happen per handle at a time (but
+ /// requests on different handles might happen concurrently).
+ SerializeRequests = 2,
+
+ /// Multiple handles can be open and multiple data requests can happen in
+ /// parallel (even on the same handle). The server may reorder replies,
+ /// answering a later request before an earlier one.
+ ///
+ /// All the libraries you use must be thread-safe and reentrant, and any
+ /// code that creates a file descriptor should atomically set `FD_CLOEXEC`
+ /// if you do not want it accidentally leaked to another thread's child
+ /// process. You may also need to provide mutexes for fields in your
+ /// connection handle.
+ Parallel = 3,
+}
+
+/// Used by [`Server::extents`] to report extents to the client
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(u32)]
+pub enum ExtentType {
+ /// A normal, allocated data extent
+ Allocated = 0,
+ /// An unallocated extent (hole) which does not read back as zeroes. Note
+ /// this should only be used in specialized circumstances such as when
+ /// writing a plugin for (or to emulate) certain SCSI drives which do not
+ /// guarantee that trimmed blocks read back as zeroes.
+ Hole = 1,
+ /// An allocated extent which is known to contain only zeroes.
+ Zero = 2,
+ /// An unallocated extent, a.k.a. a “hole”, which reads back as zeroes. This
+ /// is the normal type of hole applicable to most disks.
+ HoleZero = 3
+}
+
+/// Used by [`Server::extents`] to report extents to the client
+#[derive(Debug)]
+pub struct ExtentHandle(*mut c_void);
+
+impl ExtentHandle {
+ /// Report a single extent spanning `[offset .. offset + len)` back to the
+ /// client.
+ pub fn add(&mut self, offset: u64, len: u64, et: ExtentType) -> Result<()> {
+ let r = unsafe { nbdkit_add_extent(self.0, offset, len, et as u32) };
+ match r {
+ 0 => Ok(()),
+ -1 => Err(io::Error::last_os_error().into()),
+ x => panic!("Undocumented return value {} from nbdkit_add_extent", x)
+ }
+ }
+}
+
+/// All the FFI functions called by C code
+mod ffi {
+ use super::*;
+
+ macro_rules! can_method {
+ ( $meth:ident ) => {
+ pub(super) extern fn $meth(h: *mut c_void) -> c_int {
+ let server = unsafe { downcast(h) };
+ match server.$meth() {
+ Err(e) => {
+ set_error(e);
+ -1
+ },
+ Ok(x) => x.into(),
+ }
+ }
+ }
+ }
+
+ macro_rules! trim_like {
+ ( $meth:ident) => {
+ pub(super) extern fn $meth(h: *mut c_void,
+ count: u32,
+ offset: u64,
+ rawflags: u32) -> c_int
+ {
+ let server = unsafe { downcast(h) };
+ let flags = Flags::from_bits(rawflags)
+ .expect(&format!("Unknown flags value {:#x?}", rawflags));
+ match server.$meth(count, offset, flags) {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+ }
+ }
+
+ pub(super) extern fn cache(h: *mut c_void,
+ count: u32,
+ offset: u64,
+ _rawflags: u32) -> c_int
+ {
+ let server = unsafe { downcast(h) };
+ match server.cache(count, offset) {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn close(selfp: *mut c_void) {
+ unsafe {
+ Box::from_raw(selfp as *mut Box<dyn Server>);
+ }
+ }
+
+ can_method!(can_cache);
+ can_method!(can_extents);
+ can_method!(can_flush);
+ can_method!(can_fua);
+ can_method!(can_fast_zero);
+ can_method!(can_multi_conn);
+ can_method!(can_write);
+ can_method!(can_trim);
+ can_method!(can_zero);
+
+ pub(super) extern fn config(k: *const c_char, v: *const c_char) -> c_int {
+ let key = match unsafe { CStr::from_ptr(k) }.to_str() {
+ Ok(s) => s,
+ Err(e) => {
+ let error = Error::new(libc::EINVAL, e.to_string());
+ set_error(error);
+ return -1;
+ }
+ };
+ let value = match unsafe { CStr::from_ptr(v) }.to_str() {
+ Ok(s) => s,
+ Err(e) => {
+ let error = Error::new(libc::EINVAL, e.to_string());
+ set_error(error);
+ return -1;
+ }
+ };
+ match unsafe { CONFIG(key, value) } {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn config_complete() -> c_int {
+ match unsafe { CONFIG_COMPLETE() } {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ /// Deference the pointer into a trait object reference
+ ///
+ /// # Safety
+ ///
+ /// The pointer must be a valid pointer to `Box<Box<dyn Server>>`. The
+ /// pointer's lifetime must be valid for the duration of the returned
+ /// reference.
+ unsafe fn downcast<'a>(p: *const c_void) -> &'a dyn Server {
+ &**(p as *const Box<dyn Server>)
+ }
+
+ pub(super) extern fn dump_plugin() {
+ unsafe { DUMP_PLUGIN() }
+ }
+
+ // TODO: document REQ_ONE
+ pub(super) extern fn extents(h: *mut c_void,
+ count: u32,
+ offset: u64,
+ rawflags: u32,
+ extents: *mut c_void ) -> c_int
+ {
+ let server = unsafe { downcast(h) };
+ let mut exh = ExtentHandle(extents);
+ let flags = Flags::from_bits(rawflags).expect("Unknown extents flags");
+ match server.extents(count, offset, flags, &mut exh) {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn flush(h: *mut c_void, _rawflags: u32) -> c_int {
+ let server = unsafe { downcast(h) };
+ match server.flush() {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
-// This struct describes the plugin ABI which your plugin_init()
-// function must return.
+ pub(super) extern fn get_ready() -> c_int {
+ match unsafe { GET_READY() } {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn get_size(h: *mut c_void) -> i64 {
+ let server = unsafe { downcast(h) };
+ match server.get_size() {
+ Ok(size) => size,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ can_method!(is_rotational);
+
+ pub(super) extern fn load() {
+ unsafe { LOAD() }
+ }
+
+ pub(super) extern fn open(readonly: c_int) -> *mut c_void {
+ // We need to double-box to turn the trait object (fat pointer) into a
+ // thin pointer
+ let server = Box::new(unsafe{OPEN(readonly != 0)});
+ // Leak the memory to C. We'll get it back in close
+ Box::into_raw(server) as *mut c_void
+ }
+
+ pub(super) extern fn pread(h: *mut c_void,
+ bufp: *mut c_char,
+ count: u32,
+ offset: u64,
+ _flags: u32) -> c_int
+ {
+ let server = unsafe { downcast(h) };
+ let buf = unsafe {
+ slice::from_raw_parts_mut(bufp as *mut u8, count as usize)
+ };
+ match server.read_at(buf, offset) {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn preconnect(readonly: c_int) -> c_int {
+ match unsafe { PRECONNECT(readonly != 0) } {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ pub(super) extern fn pwrite(h: *mut c_void,
+ bufp: *const c_char,
+ count: u32,
+ offset: u64,
+ rawflags: u32) -> c_int
+ {
+ let server = unsafe { downcast(h) };
+ let buf = unsafe {
+ slice::from_raw_parts(bufp as *mut u8, count as usize)
+ };
+ let flags = Flags::from_bits(rawflags).expect("Unknown pwrite flags");
+ match server.write_at(buf, offset, flags) {
+ Ok(()) => 0,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ fn set_error(e: Error) {
+ let fmt = CString::new("%s").unwrap();
+ let msg = CString::new(e.to_string()).expect("CString::new");
+ unsafe {
+ nbdkit_error(fmt.as_ptr(), msg.as_ptr());
+ nbdkit_set_error(e.errno());
+ }
+ }
+
+ pub(super) extern fn thread_model() -> c_int {
+ match unsafe { THREAD_MODEL() } {
+ Ok(x) => x as c_int,
+ Err(e) => {
+ set_error(e);
+ -1
+ }
+ }
+ }
+
+ trim_like!(trim);
+
+ pub(super) extern fn unload() {
+ unsafe { UNLOAD() }
+ }
+
+ trim_like!(zero);
+}
+
+
+/// Define the entry point for your plugin.
+///
+/// Any of the optional methods may be implemented. If so, each must be
+/// registered with [`plugin!`]. It is an error to register any method that you
+/// don't implement.
+// We want the argument names to show up without underscores in the API docs
+#[allow(unused_variables)]
+pub trait Server {
+ /// Indicates that the client intends to make further accesses to the given
+ /// data region.
+ ///
+ /// The nature of caching is not specified further by the NBD specification
+ /// (for example, a server may place limits on how much may be cached at
+ /// once, and there is no way to control if writes to a cached area have
+ /// write-through or write-back semantics). In fact, the cache command can
+ /// always fail and still be compliant, and success might not guarantee a
+ /// performance gain. If this callback is omitted, then the results of
+ /// [`Server::can_cache`] determine whether nbdkit will reject cache
+ /// requests, treat them as instant success, or emulate caching by calling
+ /// [`Server::pread` over the same region and ignoring the results.
+ fn cache(&self, count: u32, offset: u64) -> Result<()> {
+ unimplemented!()
+ }
+
+ /// Indicate level of cacheing support to the client.
+ ///
+ /// This is called during the option negotiation phase to find out if the
+ /// plugin supports a cache operation. The nature of the caching is
+ /// unspecified (including whether there are limits on how much can be
+ /// cached at once, and whether writes to a cached region have write-through
+ /// or write-back semantics), but the command exists to let clients issue a
+ /// hint to the server that they will be accessing that region of the
+ /// export.
+ fn can_cache(&self) -> Result<CacheFlags> { unimplemented!() }
+
+ /// Indicate to the client whether the plugin supports detecting allocated
+ /// (non-sparse) regions of the disk with the [`Server::extents`]
+ fn can_extents(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicate to the client wheter the plugin supports the flush-to-disk
+ /// operation.
+ fn can_flush(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicate to the client whether the plugin supports fast-zero requests.
+ fn can_fast_zero(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicate to the client whether the plugin supports Forced Unit Access
+ /// (FUA) flag on write, trim, and zero requests.
+ ///
+ /// If this callback is not implemented, then nbdkit checks whether
+ /// [`Server::flush`] is implemented,
+ /// exists, and behaves as if this function returns [`FuaFlags::None`] or
+ /// [`FuaFlags::Emulate`] as appropriate.
+ fn can_fua(&self) -> Result<FuaFlags> { unimplemented!() }
+
+ /// Indicate to the client whether the plugin is prepared to handle multiple
+ /// connections from a single client.
+ ///
+ /// If thie method returns `true` then a
+ /// client may try to open multiple connections to the nbdkit server and
+ /// spread requests across all connections to maximize parallelism. If it
+ /// returns `false` false (which is the default) then well-behaved clients
+ /// should only open a single connection, although we cannot control what
+ /// clients do in practice.
+ ///
+ /// Specifically it means that either the plugin does not cache requests at
+ /// all, or if it does cache them then the effects of a [`Server::flush`]
+ /// request or setting [`Flags::FUA`] on a write/trim/zero must be visible
+ /// across all connections to the plugin before the plugin replies to that
+ /// request.
+ fn can_multi_conn(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicate to the client whether the plugin supports the trim/discard
+ /// operation for punching holes in the backing store.
+ fn can_trim(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicates to the client whether the plugin supports writes
+ fn can_write(&self) -> Result<bool> { unimplemented!() }
+
+ /// Indicates to the client whether the [`Server::zero`] callback should be
+ /// used.
+ ///
+ /// Support for writing zeroes is still advertised to the client, so
+ /// returning false merely serves as a way to avoid complicating the
+ /// [`Server::zero`] callback to have to fail with `ENOTSUP` or
+ /// `EOPNOTSUPP` on the connections where it will never be more efficient
+ /// than using [`Server::write_at`] up front.
+ fn can_zero(&self) -> Result<bool> { unimplemented!() }
+
+ /// Supplies command-line parameters, one at a time, to the plugin.
+ ///
+ /// On the nbdkit command line, after the plugin filename, come an optional
+ /// list of key=value arguments. These are passed to the plugin through this
+ /// callback when the plugin is first loaded and before any connections are
+ /// accepted.
+ ///
+ /// This callback may be called zero or more times.
+ ///
+ /// The key will be a non-empty string beginning with an ASCII alphabetic
+ /// character (A-Z a-z). The rest of the key must contain only ASCII
+ /// alphanumeric plus period, underscore or dash characters (A-Z a-z 0-9 . _
+ /// -). The value may be an arbitrary string, including an empty string.
+ ///
+ /// The names of keys accepted by plugins is up to the plugin, but you
+ /// should probably look at other plugins and follow the same conventions.
+ fn config(key: &str, value: &str) -> Result<()> where Self: Sized {
+ unimplemented!()
+ }
+
+ /// This optional callback is called after all the configuration has been
+ /// passed to the plugin.
+ ///
+ /// It is a good place to do checks, for example that the user has passed
+ /// the required parameters to the plugin.
+ fn config_complete() -> Result<()> where Self: Sized { unimplemented!() }
+
+ /// This optional callback is called when the `nbdkit plugin --dump-plugin`
+ /// command is used. It should print any additional informative key=value
+ /// fields to stdout as needed. Prefixing the keys with the name of the
+ /// plugin will avoid conflicts.
+ fn dump_plugin() where Self: Sized { unimplemented!() }
+
+ /// This optional multi-line help message should summarize any key=value
+ /// parameters that it takes. It does not need to repeat what already
+ /// appears in [`Server::description`].
+ fn config_help() -> Option<&'static str> where Self: Sized { None }
+
+ /// An optional multi-line description of the plugin.
+ fn description() -> Option<&'static str> where Self: Sized { None }
+
+ /// During the data serving phase, this callback is used to detect
+ /// allocated, sparse and zeroed regions of the disk.
+ ///
+ /// This function will not be called if [`Server::can_extents`] returned `false`.
+ /// nbdkit's default behaviour in this case is to treat the whole virtual
+ /// disk as if it were allocated. Also, this function will not be called by
+ /// a client that does not request structured replies (the `--no-sr` option
+ /// of nbdkit can be used to test behavior when `extents` is unavailable to
+ /// the client).
+ ///
+ /// The callback should detect and return the list of extents overlapping
+ /// the range `[offset...offset+count)`. Each extent should be reported
+ /// by calling [`ExtentHandle::add`].
+ ///
+ /// The flags parameter of the `extents` callback may contain
+ /// [`Flags::REQ_ONE`]. This means that the client is only requesting
+ /// information about the extent overlapping `offset`. The plugin may ignore
+ /// this flag, or as an optimization it may return just a single extent for
+ /// offset.
+ // Alternatively, the method could be defined as returning a Vec of extent
+ // objects, rather than using the ExtentHandle. I'm not sure which would be
+ // more ergonomic, but this way requires fewer memory allocations.
+ fn extents(&self,
+ count: u32,
+ offset: u64,
+ flags: Flags,
+ extent_handle: &mut ExtentHandle)
+ -> Result<()>
+ {
+ unimplemented!()
+ }
+
+ /// During the data serving phase, this callback is used to sync the
+ /// backing store, ie. to ensure it has been completely written to a
+ /// permanent medium. If that is not possible then you can omit this
+ /// callback.
+ ///
+ /// This function will not be called directly by the client if
+ /// [`Server::can_flush`] returned `false`; however, it may still be called
+ /// by nbdkit if [`Server::can_fua`] returned [`FuaFlags::Emulate`].
+ fn flush(&self) -> Result<()> { unimplemented!() }
+
+ /// This optional callback is called before the server starts serving.
+ ///
+ /// It is called before the server forks or changes directory. It is the
+ /// last chance to do any global preparation that is needed to serve
+ /// connections.
+ fn get_ready() -> Result<()> where Self: Sized { unimplemented!() }
+
+ /// Return the size in bytes of the exported block device
+ fn get_size(&self) -> Result<i64>;
+
+ /// Return `true` if the backing store is a rotational medium (like a
+ /// traditional hard disk) as opposed to a non-rotating one like an SSD.
+ ///
+ /// This may cause the client to reorder requests to make them more
+ /// efficient for a slow rotating disk.
+ fn is_rotational(&self) -> Result<bool> { unimplemented!() }
+
+ /// This is called once just after the plugin is loaded into memory. You can
+ /// use this to perform any global initialization needed by the plugin.
+ fn load() where Self: Sized { unimplemented!() }
+
+ /// An optional free text name of the plugin. This field is used in error
+ /// messages.
+ fn longname() -> Option<&'static str> where Self: Sized { None }
+
+ /// This optional string can be used to set a "magic" key used when parsing
+ /// plugin parameters. It affects how "bare parameters" (those which do not
+ /// contain an = character) are parsed on the command line.
+ ///
+ /// If `magic_config_key().is_some()` then any bare parameters are passed to
+ /// the [`Server::config`] method as: `config (magic_config_key, argv[i]);`.
+ ///
+ /// If `magic_config_key().is_none()` then we behave as in nbdkit < 1.7: If
+ /// the first parameter on the command line is bare then it is passed to the
+ /// `Server::config` method as: `config("script", value);`. Any other bare
+ /// parameters give errors.
+ fn magic_config_key() -> Option<&'static str> where Self: Sized { None }
+
+ /// The name of the plugin.
+ ///
+ /// It must contain only ASCII alphanumeric characters and be unique amongst
+ /// all plugins.
+ fn name() -> &'static str where Self: Sized;
+
+ /// Allocate and return a new `Server` handle to the client.
+ ///
+ /// Called whenever a new client connects to the server. The `readonly`
+ /// flag informs the plugin that the server was started with the -r flag on
+ /// the command line which forces connections to be read-only. Note that the
+ /// plugin may *additionally* force the connection to be readonly (even if
+ /// this flag is false) by returning false from the [`Server::can_write`]
+ /// callback. So if your plugin can only serve read-only, you can ignore
+ /// this parameter.
+ fn open(readonly: bool) -> Box<dyn Server> where Self: Sized;
+
+ /// This optional callback is called when a TCP connection has been made to
+ /// the server. This happens early, before NBD or TLS negotiation. If TLS
+ /// authentication is required to access the server, then it has not been
+ /// negotiated at this point.
+ ///
+ /// For security reasons (to avoid denial of service attacks) this callback
+ /// should be written to be as fast and take as few resources as possible.
+ /// If you use this callback, only use it to do basic access control, such
+ /// as checking [`peername`] against a whitelist. It may be better to do
+ /// access control outside the server, for example using TCP wrappers or a
+ /// firewall.
+ ///
+ /// The `readonly` flag informs the plugin that the server was started with
+ /// the `-r` flag on the command line.
+ ///
+ /// Returning `Ok(())` will allow the connection to continue. If there is an
+ /// error or you want to deny the connection, return an error.
+ fn preconnect(readonly: bool) -> Result<()> where Self: Sized {
+ unimplemented!()
+ }
+
+ /// Read data from the backing store, starting at `offset`.
+ ///
+ /// The callback must read the entire range if it can. If it, it should
+ /// return an error.
+ fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<()>;
+
+ /// This optional callback is called after all the configuration has been
+ /// passed to the plugin.
+ ///
+ /// It can be used to force a stricter thread model than the default
+ /// ([`ThreadModel::Parallel`]).
+ fn thread_model() -> Result<ThreadModel> where Self: Sized { unimplemented!() }
+
+ /// Punch a hole in the backing store.
+ ///
+ /// This function will not be called if [`Server::can_trim`] returned
+ /// `false`. The parameter flags may include `Flags::FUA` on input based on
+ /// the result of [`Server::can_fua`].
+ fn trim(&self, count: u32, offset: u64, flags: Flags) -> Result<()> {
+ unimplemented!()
+ }
+
+ /// This may be called once just before the plugin is unloaded from memory.
+ fn unload() where Self: Sized { unimplemented!() }
+
+ /// An optional version string which is displayed in help and debugging
+ /// output.
+ fn version() -> Option<&'static str> where Self: Sized { None }
+
+ /// Write data to the backing store.
+ ///
+ /// The `flags` argument may include [`Flags::FUA`] based on the result of
+ /// [`Server::can_fua`].
+ ///
+ /// The callback must write the entire range if it can. If it, it should
+ /// return an error.
+ fn write_at(&self, buf: &[u8], offset: u64, flags: Flags) -> Result<()> {
+ unimplemented!()
+ }
+
+ /// Write consecutive zeros to the backing store.
+ ///
+ /// The callback must write the whole region if it can. The NBD protocol
+ /// doesn't allow partial writes (instead, these are errors).
+ ///
+ /// If this callback is omitted, or if it fails with `ENOTSUP` or
+ /// `EOPNOTSUPP` , then [`Server::write_at`] will be used as an
+ /// automatic fallback except when the client requested a fast zero.
+ ///
+ /// # Arguments
+ ///
+ /// * `count`: Length of the region to write in bytes
+ /// * `offset`: Offset of the region to write in the backing store.
+ /// * `flags`: May include [`Flags::MAY_TRIM`], [`Flags::FAST_ZERO`],
+ /// and/or [`Flags::FUA`].
+ fn zero(&self, count: u32, offset: u64, flags: Flags) -> Result<()> {
+ unimplemented!()
+ }
+}
+
+macro_rules! opt_method {
+ ( $self:ident, $method:ident ) => {
+ if $self.$method {Some(ffi::$method)} else {None}
+ };
+ ( $self:ident, $method:ident, $ffi_method:ident ) => {
+ if $self.$method {Some(ffi::$ffi_method)} else {None}
+ }
+}
+
+/// Used by (`plugin!`)[macro.plugin.html], but should never be accessed
+/// directly by the user.
+#[doc(hidden)]
+#[derive(Default)]
+pub struct Builder {
+ pub cache: bool,
+ pub can_cache: bool,
+ pub can_extents: bool,
+ pub can_flush: bool,
+ pub can_fast_zero: bool,
+ pub can_fua: bool,
+ pub can_multi_conn: bool,
+ pub can_trim: bool,
+ pub can_write: bool,
+ pub can_zero: bool,
+ pub config: bool,
+ pub config_complete: bool,
+ pub config_help: bool,
+ pub dump_plugin: bool,
+ pub extents: bool,
+ pub flush: bool,
+ pub get_ready: bool,
+ pub is_rotational: bool,
+ pub load: bool,
+ pub preconnect: bool,
+ pub thread_model: bool,
+ pub trim: bool,
+ pub unload: bool,
+ pub write_at: bool,
+ pub zero: bool,
+}
+
+impl Builder {
+ #[doc(hidden)]
+ pub fn into_ptr<S: Server>(self) -> *const Plugin {
+ INIT.call_once(|| {
+ unsafe {
+ assert!(!INITIALIZED);
+ INITIALIZED = true;
+ CONFIG = S::config;
+ CONFIG_COMPLETE = S::config_complete;
+ DUMP_PLUGIN = S::dump_plugin;
+ GET_READY = S::get_ready;
+ LOAD = S::load;
+ OPEN = S::open;
+ PRECONNECT = S::preconnect;
+ THREAD_MODEL = S::thread_model;
+ UNLOAD = S::unload;
+ NAME = CString::new(S::name()).unwrap().into_bytes_with_nul();
+ if let Some(s) = S::config_help() {
+ CONFIG_HELP = CString::new(s).unwrap().into_bytes_with_nul();
+ }
+ if let Some(s) = S::description() {
+ DESCRIPTION = CString::new(s).unwrap().into_bytes_with_nul();
+ }
+ if let Some(s) = S::longname() {
+ LONGNAME = CString::new(s).unwrap().into_bytes_with_nul();
+ }
+ if let Some(s) = S::magic_config_key() {
+ MAGIC_CONFIG_KEY = CString::new(s).unwrap()
+ .into_bytes_with_nul();
+ }
+ if let Some(s) = S::version() {
+ VERSION = CString::new(s).unwrap().into_bytes_with_nul();
+ }
+ };
+ });
+
+ let config_help = S::config_help()
+ .map(|_| unsafe {CONFIG_HELP.as_ptr()} as *const i8)
+ .unwrap_or(ptr::null());
+ let description = S::description()
+ .map(|_| unsafe {DESCRIPTION.as_ptr()} as *const i8)
+ .unwrap_or(ptr::null());
+ let longname = S::longname()
+ .map(|_| unsafe {LONGNAME.as_ptr()} as *const i8)
+ .unwrap_or(ptr::null());
+ let magic_config_key = S::magic_config_key()
+ .map(|_| unsafe {MAGIC_CONFIG_KEY.as_ptr()} as *const i8)
+ .unwrap_or(ptr::null());
+ let version = S::version()
+ .map(|_| unsafe {VERSION.as_ptr()} as *const i8)
+ .unwrap_or(ptr::null());
+ let plugin = Plugin {
+ _struct_size: mem::size_of::<Plugin>() as u64,
+ _api_version: 2,
+ _thread_model: ThreadModel::Parallel as c_int,
+ name: unsafe{ NAME.as_ptr() } as *const i8,
+ longname,
+ version,
+ description,
+ load: opt_method!(self, load),
+ unload: opt_method!(self, unload),
+ config: opt_method!(self, config),
+ config_complete: opt_method!(self, config_complete),
+ config_help,
+ open: ffi::open,
+ close: ffi::close,
+ get_size: ffi::get_size,
+ can_write: opt_method!(self, can_write),
+ can_flush: opt_method!(self, can_flush),
+ is_rotational: opt_method!(self, is_rotational),
+ can_trim: opt_method!(self, can_trim),
+ _pread_v1: None,
+ _pwrite_v1: None,
+ _flush_v1: None,
+ _trim_v1: None,
+ _zero_v1: None,
+ errno_is_preserved: 0,
+ dump_plugin: opt_method!(self, dump_plugin),
+ can_zero: opt_method!(self, can_zero),
+ can_fua: opt_method!(self, can_fua),
+ pread: ffi::pread,
+ pwrite: opt_method!(self, write_at, pwrite),
+ flush: opt_method!(self, flush),
+ trim: opt_method!(self, trim),
+ zero: opt_method!(self, zero),
+ magic_config_key,
+ can_multi_conn: opt_method!(self, can_multi_conn),
+ can_extents: opt_method!(self, can_extents),
+ extents: opt_method!(self, extents),
+ can_cache: opt_method!(self, can_cache),
+ cache: opt_method!(self, cache),
+ thread_model: opt_method!(self, thread_model),
+ can_fast_zero: opt_method!(self, can_fast_zero),
+ preconnect: opt_method!(self, preconnect),
+ get_ready: opt_method!(self, get_ready)
+ };
+ // Leak the memory to C. NBDKit will never give it back.
+ Box::into_raw(Box::new(plugin))
+ }
+
+ #[doc(hidden)]
+ pub fn new() -> Builder
+ {
+ Builder::default()
+ }
+
+}
+
+// C functions provided by the nbdkit binary
+// TODO: nbdkit_peer_name
+extern "C" {
+ fn nbdkit_add_extent(extents: *mut c_void, offset: u64, length: u64,
+ ty: u32) -> c_int;
+ fn nbdkit_error(fmt: *const c_char, ...);
+ fn nbdkit_export_name() -> *const c_char;
+ fn nbdkit_set_error(errno: c_int);
+ #[cfg(feature = "nix")]
+ fn nbdkit_peer_name( addr: *mut libc::sockaddr,
+ addrlen: *mut libc::socklen_t) -> c_int;
+ fn nbdkit_shutdown();
+ fn nbdkit_stdio_safe() -> c_int;
+}
+
+/// Return the optional NBD export name if one was negotiated with the current
+/// client
+///
+/// Note that this function must be called from one of nbdkit's own threads.
+/// That is, it can only be called in the same thread as one of the `Server`
+/// callbacks.
+pub fn export_name() -> std::result::Result<String, Box<dyn error::Error>> {
+ unsafe {
+ let p = nbdkit_export_name();
+ if p.is_null() {
+ return Err("No export name available".into());
+ }
+ CStr::from_ptr(p)
+ }.to_str()
+ .map(|s| s.to_owned())
+ .map_err(|e| Box::new(e) as Box<dyn error::Error>)
+}
+
+/// Is it safe to interact with stdin and stdout during the configuration phase?
+///
+/// This function is only relevant up through `config_complete`. After the
+/// configuration phase, the client should assume that stdin and stdout have
+/// been closed.
+pub fn is_stdio_safe() -> bool {
+ unsafe { nbdkit_stdio_safe() == 1 }
+}
+
+/// Return the peer (client) address, if available.
+///
+/// Note that this function must be called from one of nbdkit's own threads.
+/// That is, it can only be called in the same thread as one of the `Server`
+/// callbacks.
+#[cfg(any(feature = "nix", all(feature = "nightly-docs", rustdoc)))]
+#[cfg_attr(feature = "nightly-docs", doc(cfg(feature = "nix")))]
+pub fn peername() -> std::result::Result<SockAddr, Box<dyn error::Error>> {
+ let mut ss = mem::MaybeUninit::<libc::sockaddr_storage>::uninit();
+ let mut len = mem::size_of_val(&ss) as libc::socklen_t;
+ unsafe {
+ let sa = ss.as_mut_ptr() as *mut libc::sockaddr;
+ let r = nbdkit_peer_name(sa, &mut len as *mut _);
+ if r == -1 {
+ // Note that nbdkit_peer_name does _not_ set errno
+ return Err("No peer name available".into());
+ }
+ sockaddr_storage_to_addr(&ss.assume_init(), len as usize)
+ .map_err(|e| Box::new(e) as Box<dyn error::Error>)
+ }
+}
+
+/// Request nbdkit to asynchronously and safely shutdown the server.
+pub fn shutdown() {
+ unsafe { nbdkit_shutdown() };
+}
+
+#[doc(hidden)]
#[repr(C)]
pub struct Plugin {
// Do not modify these three fields directly.
- _struct_size: u64,
- _api_version: c_int,
- _thread_model: c_int,
+ #[doc(hidden)] pub _struct_size: u64,
+ #[doc(hidden)] pub _api_version: c_int,
+ #[doc(hidden)] pub _thread_model: c_int,
pub name: *const c_char,
pub longname: *const c_char,
@@ -49,12 +1085,12 @@ pub struct Plugin {
pub load: Option<extern fn ()>,
pub unload: Option<extern fn ()>,
- pub config: Option<extern fn (*const c_char, *const c_char)>,
+ pub config: Option<extern fn (*const c_char, *const c_char) -> c_int>,
pub config_complete: Option<extern fn () -> c_int>,
pub config_help: *const c_char,
pub open: extern fn (c_int) -> *mut c_void,
- pub close: Option<extern fn (*mut c_void)>,
+ pub close: extern fn (*mut c_void),
pub get_size: extern fn (*mut c_void) -> i64,
@@ -64,13 +1100,13 @@ pub struct Plugin {
pub can_trim: Option<extern fn (*mut c_void) -> c_int>,
// Slots for old v1 API functions.
- _pread_v1: Option<extern fn ()>,
- _pwrite_v1: Option<extern fn ()>,
- _flush_v1: Option<extern fn ()>,
- _trim_v1: Option<extern fn ()>,
- _zero_v1: Option<extern fn ()>,
+ #[doc(hidden)] pub _pread_v1: Option<extern fn ()>,
+ #[doc(hidden)] pub _pwrite_v1: Option<extern fn ()>,
+ #[doc(hidden)] pub _flush_v1: Option<extern fn ()>,
+ #[doc(hidden)] pub _trim_v1: Option<extern fn ()>,
+ #[doc(hidden)] pub _zero_v1: Option<extern fn ()>,
- errno_is_preserved: c_int,
+ #[doc(hidden)] pub errno_is_preserved: c_int,
pub dump_plugin: Option<extern fn ()>,
@@ -95,77 +1131,199 @@ pub struct Plugin {
pub can_multi_conn: Option<extern fn (h: *mut c_void) -> c_int>,
- // Slots for extents functions, which needs more integration.
- _can_extents: Option<extern fn ()>,
- _extents: Option<extern fn ()>,
+ pub can_extents: Option<extern fn (h: *mut c_void) -> c_int>,
+ pub extents: Option<extern fn (h: *mut c_void,
+ count: u32,
+ offset: u64,
+ rawflags: u32,
+ extent_handle: *mut c_void) -> c_int>,
pub can_cache: Option<extern fn (h: *mut c_void) -> c_int>,
pub cache: Option<extern fn (h: *mut c_void,
count: u32, offset: u64,
flags: u32) -> c_int>,
- pub thread_model: Option<extern fn () -> ThreadModel>,
+ pub thread_model: Option<extern fn () -> c_int>,
pub can_fast_zero: Option<extern fn (h: *mut c_void) -> c_int>,
+ pub preconnect: Option<extern fn(readonly: c_int) -> c_int>,
+ pub get_ready: Option<extern fn() -> c_int>,
}
-#[repr(C)]
-pub enum ThreadModel {
- SerializeConnections = 0,
- SerializeAllRequests = 1,
- SerializeRequests = 2,
- Parallel = 3,
+/// Register your plugin with NBDKit.
+///
+/// Declare which optional methods it supports by supplying each as an argument.
+///
+/// # Examples
+///
+/// ```
+/// # use nbdkit::*;
+/// struct MyPlugin{
+/// // ...
+/// }
+/// impl Server for MyPlugin {
+/// fn get_size(&self) -> Result<i64> {
+/// # unimplemented!();
+/// // ...
+/// }
+///
+/// fn name() -> &'static str {
+/// "my_plugin"
+/// }
+///
+/// fn open(_readonly: bool) -> Box<dyn Server> {
+/// # unimplemented!();
+/// // ...
+/// }
+///
+/// fn read_at(&self, buf: &mut [u8], offs: u64) -> Result<()> {
+/// # unimplemented!();
+/// // ...
+/// }
+/// fn write_at(&self, buf: &[u8], offs: u64, flags: Flags) -> Result<()> {
+/// # unimplemented!();
+/// // ...
+/// }
+/// }
+///
+/// plugin!(MyPlugin {write_at});
+/// ```
+#[macro_export]
+macro_rules! plugin {
+ ( $cls:path { $($feat:ident),* } ) => {
+ #[no_mangle]
+ pub extern fn plugin_init () -> *const Plugin {
+ let mut plugin = Builder::new();
+ $(plugin.$feat = true;)*
+ plugin.into_ptr::<$cls>()
+ }
+ }
}
-impl Plugin {
- pub fn new (name: *const c_char,
- open: extern fn (c_int) -> *mut c_void,
- get_size: extern fn (*mut c_void) -> i64,
- pread: extern fn (h: *mut c_void, buf: *mut c_char,
- count: u32, offset: u64,
- flags: u32) -> c_int) -> Plugin {
- Plugin {
- _struct_size: mem::size_of::<Plugin>() as u64,
- _api_version: 2,
- _thread_model: ThreadModel::Parallel as c_int,
- name: name,
- longname: std::ptr::null(),
- version: std::ptr::null(),
- description: std::ptr::null(),
- load: None,
- unload: None,
- config: None,
- config_complete: None,
- config_help: std::ptr::null(),
- open: open,
- close: None,
- get_size: get_size,
- can_write: None,
- can_flush: None,
- is_rotational: None,
- can_trim: None,
- _pread_v1: None,
- _pwrite_v1: None,
- _flush_v1: None,
- _trim_v1: None,
- _zero_v1: None,
- errno_is_preserved: 0,
- dump_plugin: None,
- can_zero: None,
- can_fua: None,
- pread: pread,
- pwrite: None,
- flush: None,
- trim: None,
- zero: None,
- magic_config_key: std::ptr::null(),
- can_multi_conn: None,
- _can_extents: None,
- _extents: None,
- can_cache: None,
- cache: None,
- thread_model: None,
- can_fast_zero: None,
+#[cfg(test)]
+mod t {
+ #[cfg(feature = "nix")]
+ mod peername {
+ use super::super::*;
+ use lazy_static::lazy_static;
+ use mockall::{mock, predicate::*};
+ use std::sync::Mutex;
+
+ lazy_static! {
+ /// Mediates access to MockNbdkit's global expectations
+ /// Any test that sets an expectation on a static method
+ /// grab this mutex
+ static ref MOCK_NBDKIT_MTX: Mutex<()> = Mutex::new(());
+ }
+
+ mock! {
+ pub Nbdkit {
+ fn peer_name(addr: *mut libc::sockaddr,
+ addrlen: *mut libc::socklen_t) -> c_int;
+ }
+ }
+
+ #[no_mangle]
+ extern fn nbdkit_peer_name( addr: *mut libc::sockaddr,
+ addrlen: *mut libc::socklen_t) -> c_int
+ {
+ MockNbdkit::peer_name(addr, addrlen)
+ }
+
+ #[test]
+ fn error() {
+ let _m = MOCK_NBDKIT_MTX.lock().unwrap();
+ let ctx = MockNbdkit::peer_name_context();
+ ctx.expect()
+ // Since nbdkit_peer_name does not set errno, all types of
+ // errors are indistinguishable to a plugin
+ .return_const(-1);
+ let e = peername().unwrap_err();
+ ctx.checkpoint();
+ assert_eq!("No peer name available", e.to_string());
+ }
+
+ #[test]
+ fn in4() {
+ let _m = MOCK_NBDKIT_MTX.lock().unwrap();
+ let ctx = MockNbdkit::peer_name_context();
+ ctx.expect()
+ .withf(|_, len| {
+ let l = unsafe {**len as usize};
+ l == mem::size_of::<libc::sockaddr_storage>()
+ }).returning(|sa, sl| {
+ let sin = sa as *mut libc::sockaddr_in;
+ unsafe {
+ *sin = libc::sockaddr_in {
+ sin_family: libc::AF_INET as libc::sa_family_t,
+ sin_port: u16::from_le_bytes([4, 0xd2]),
+ sin_addr: libc::in_addr {
+ s_addr: u32::from_le_bytes([127, 0, 0, 1])
+ },
+ .. mem::zeroed()
+ };
+ *sl = mem::size_of::<libc::sockaddr_in>()
+ as libc::socklen_t;
+ }
+ 0
+ });
+ assert_eq!("127.0.0.1:1234", peername().unwrap().to_str());
+ ctx.checkpoint();
+ }
+
+ #[test]
+ fn in6() {
+ let _m = MOCK_NBDKIT_MTX.lock().unwrap();
+ let ctx = MockNbdkit::peer_name_context();
+ ctx.expect()
+ .withf(|_, len| {
+ let l = unsafe {**len as usize};
+ l == mem::size_of::<libc::sockaddr_storage>()
+ }).returning(|sa, sl| {
+ let sin6 = sa as *mut libc::sockaddr_in6;
+ unsafe {
+ *sin6 = libc::sockaddr_in6 {
+ sin6_family: libc::AF_INET6 as libc::sa_family_t,
+ sin6_port: u16::from_le_bytes([4, 0xd2]),
+ sin6_addr: libc::in6_addr {
+ s6_addr: [0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1],
+ },
+ .. mem::zeroed()
+ };
+ *sl = mem::size_of::<libc::sockaddr_in6>()
+ as libc::socklen_t;
+ }
+ 0
+ });
+ assert_eq!("[::1]:1234", peername().unwrap().to_str());
+ ctx.checkpoint();
+ }
+
+ #[test]
+ fn un() {
+ let _m = MOCK_NBDKIT_MTX.lock().unwrap();
+ let ctx = MockNbdkit::peer_name_context();
+ ctx.expect()
+ .withf(|_, len| {
+ let l = unsafe {**len as usize};
+ l == mem::size_of::<libc::sockaddr_storage>()
+ }).returning(|sa, sl| {
+ let sun = sa as *mut libc::sockaddr_un;
+
+ unsafe {
+ *sun = mem::zeroed();
+ (*sun).sun_family = libc::AF_UNIX as libc::sa_family_t;
+ ptr::copy_nonoverlapping(
+ b"/tmp/foo.sock\0".as_ptr() as *const i8,
+ (*sun).sun_path.as_mut_ptr(), 14);
+ *sl = mem::size_of::<libc::sockaddr_un>()
+ as libc::socklen_t;
+ }
+ 0
+ });
+ assert_eq!("/tmp/foo.sock", peername().unwrap().to_str());
+ ctx.checkpoint();
}
}
}
diff --git a/plugins/rust/tests/bare_bones.rs b/plugins/rust/tests/bare_bones.rs
new file mode 100644
index 00000000..c77e1036
--- /dev/null
+++ b/plugins/rust/tests/bare_bones.rs
@@ -0,0 +1,121 @@
+//vim: tw=80
+//! A bare bones plugin that only defines the minimum callbacks
+// Test cases for different plugins must be run in separate processes due to the
+// one-time initialized global state.
+
+use nbdkit::*;
+use std::os::raw::c_char;
+use std::sync::Once;
+
+mod common;
+use common::*;
+
+static mut PLUGIN: Option<*const Plugin> = None;
+static INIT: Once = Once::new();
+
+plugin!(MockServer{});
+
+// One-time initialization of the plugin
+pub fn initialize() {
+ INIT.call_once(|| {
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+ let config_help_ctx = MockServer::config_help_context();
+ config_help_ctx.expect()
+ .return_const(None);
+ let desc_ctx = MockServer::description_context();
+ desc_ctx.expect()
+ .return_const(None);
+ let longname_ctx = MockServer::longname_context();
+ longname_ctx.expect()
+ .return_const(None);
+ let magic_config_key_ctx = MockServer::magic_config_key_context();
+ magic_config_key_ctx.expect()
+ .return_const(Some("magic_config_key"));
+ let name_ctx = MockServer::name_context();
+ name_ctx.expect()
+ .return_const("Mock NBD Server");
+ let version_ctx = MockServer::version_context();
+ version_ctx.expect()
+ .return_const(None);
+ unsafe {
+ PLUGIN = Some(plugin_init());
+ }
+ });
+}
+
+fn with_fixture<F: FnMut(&mut Fixture)>(mut f: F) {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let mut mock = Box::new(MockServer::default());
+ mock.expect_get_size()
+ .returning(|| Ok(0x4200));
+ let mockp = (&mut mock) as *mut Box<MockServer>;
+ let open_ctx = MockServer::open_context();
+ open_ctx.expect()
+ .return_once(|_| mock);
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+ let handle = ((*plugin).open)(0);
+ let mut fixture = Fixture {
+ mockp,
+ plugin,
+ handle
+ };
+
+ f(&mut fixture);
+
+ ((*plugin).close)(handle);
+}
+
+
+#[test]
+fn open_close() {
+ with_fixture(|fixture| {
+ let size = (fixture.plugin.get_size)(fixture.handle);
+ assert_eq!(0x4200, size);
+ });
+}
+
+mod pread {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_read_at()
+ .returning(|_, _| Err(Error::new(libc::EIO, "Mock error")));
+ let mut buf = vec![0u8; 512];
+ assert_eq!(-1, (fixture.plugin.pread)(fixture.handle,
+ buf.as_mut_ptr() as *mut c_char,
+ buf.len() as u32,
+ 1024,
+ 0));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("Mock error", *e.borrow()));
+
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_read_at()
+ .withf(|buf, offset|
+ buf.len() == 512 &&
+ *offset == 1024
+ ).returning(|buf, _offset| {
+ buf[0..10].copy_from_slice(b"0123456789");
+ Ok(())
+ });
+ let mut buf = vec![0u8; 512];
+ assert_eq!(0, (fixture.plugin.pread)(fixture.handle,
+ buf.as_mut_ptr() as *mut c_char,
+ buf.len() as u32,
+ 1024,
+ 0));
+ assert_eq!(&buf[0..10], b"0123456789");
+ });
+ }
+}
diff --git a/plugins/rust/tests/common/mod.rs b/plugins/rust/tests/common/mod.rs
new file mode 100644
index 00000000..46c9b0ac
--- /dev/null
+++ b/plugins/rust/tests/common/mod.rs
@@ -0,0 +1,104 @@
+//vim: tw=80
+use lazy_static::lazy_static;
+use mockall::mock;
+use std::{
+ cell::RefCell,
+ ffi::{CString, CStr},
+ os::raw::{c_char, c_int, c_void},
+ sync::Mutex
+};
+
+use nbdkit::*;
+
+thread_local!(pub static ERRMSG: RefCell<String> = RefCell::new(String::new()));
+thread_local!(pub static ERRNO: RefCell<i32> = RefCell::new(0));
+
+lazy_static! {
+ /// Mediates access to MockServer's global expectations
+ /// Any test that sets an expectation on a static method (like `open`) must
+ /// grab this mutex
+ pub static ref MOCK_SERVER_MTX: Mutex<()> = Mutex::new(());
+}
+
+mock!{
+ pub Server {}
+ trait Server {
+ fn cache(&self, count: u32, offset: u64) -> Result<()>;
+ fn can_cache(&self) -> Result<CacheFlags>;
+ fn can_extents(&self) -> Result<bool>;
+ fn can_fast_zero(&self) -> Result<bool>;
+ fn can_flush(&self) -> Result<bool>;
+ fn can_fua(&self) -> Result<FuaFlags>;
+ fn can_multi_conn(&self) -> Result<bool>;
+ fn can_trim(&self) -> Result<bool>;
+ fn can_write(&self) -> Result<bool>;
+ fn can_zero(&self) -> Result<bool>;
+ fn config(k: &str, v: &str) -> Result<()> where Self: Sized;
+ fn config_complete() -> Result<()> where Self: Sized;
+ fn config_help() -> Option<&'static str> where Self: Sized;
+ fn description() -> Option<&'static str> where Self: Sized;
+ fn dump_plugin() where Self: Sized;
+ fn extents(&self, c: u32, o: u64, f: Flags, e: &mut ExtentHandle)
+ -> Result<()>;
+ fn flush(&self) -> Result<()>;
+ fn get_ready() -> Result<()> where Self: Sized;
+ fn get_size(&self) -> Result<i64>;
+ fn is_rotational(&self) -> Result<bool>;
+ fn load() where Self: Sized;
+ fn longname() -> Option<&'static str> where Self: Sized;
+ fn magic_config_key() -> Option<&'static str> where Self: Sized;
+ fn name() -> &'static str where Self: Sized;
+ fn open(readonly: bool) -> Box<dyn Server> where Self: Sized;
+ fn preconnect(readonly: bool) -> Result<()> where Self: Sized;
+ fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<()>;
+ fn thread_model() -> Result<ThreadModel> where Self: Sized;
+ fn trim(&self, count: u32, offset: u64, flags: Flags) -> Result<()>;
+ fn unload() where Self: Sized;
+ fn version() -> Option<&'static str> where Self: Sized;
+ fn write_at(&self, _buf: &[u8], _offset: u64, _flags: Flags) -> Result<()>;
+ fn zero(&self, count: u32, offset: u64, flags: Flags) -> Result<()>;
+ }
+}
+
+mock! {
+ pub NbdkitAddExtent {
+ fn add(extents: *mut c_void, offset: u64, len: u64, ty: u32) -> c_int;
+
+ }
+}
+
+#[no_mangle]
+extern fn nbdkit_add_extent(extents: *mut c_void, offset: u64, len: u64, ty: u32)
+ -> c_int
+{
+ MockNbdkitAddExtent::add(extents, offset, len, ty)
+}
+
+// XXX The actual C function is variadic, but stable Rust can't create variadic
+// extern functions yet. So we pretend that it has two arguments.
+// https://github.com/rust-lang/rust/issues/44930
+// extern fn nbdkit_error(fmt: *const c_char, ...);
+#[no_mangle]
+extern fn nbdkit_error(fmt: *const c_char, msg: *const c_char) {
+ assert_eq!(
+ unsafe {CStr::from_ptr(fmt) },
+ CString::new("%s").unwrap().as_c_str()
+ );
+ ERRMSG.with(|m| *m.borrow_mut() = unsafe {
+ CStr::from_ptr(msg)
+ }.to_str()
+ .unwrap()
+ .to_owned()
+ );
+}
+#[no_mangle]
+extern fn nbdkit_set_error(errno: c_int) {
+ ERRNO.with(|e| *e.borrow_mut() = errno);
+}
+
+/// Holds common setup stuff needed by most test cases
+pub struct Fixture<'a> {
+ pub mockp: *mut Box<MockServer>,
+ pub plugin: &'a Plugin,
+ pub handle: *mut c_void
+}
diff --git a/plugins/rust/tests/full_featured.rs b/plugins/rust/tests/full_featured.rs
new file mode 100644
index 00000000..c8f5df0a
--- /dev/null
+++ b/plugins/rust/tests/full_featured.rs
@@ -0,0 +1,669 @@
+//vim: tw=80
+//! A full-featured plugin that implements all optional callbacks
+// Test cases for different plugins must be run in separate processes due to the
+// one-time initialized global state.
+
+use errno::{Errno, set_errno};
+use mockall::predicate::*;
+use nbdkit::*;
+use std::{
+ ffi::CStr,
+ sync::Once,
+ os::raw::c_char,
+ ptr
+};
+
+mod common;
+use common::*;
+
+static mut PLUGIN: Option<*const Plugin> = None;
+static INIT: Once = Once::new();
+
+plugin!(MockServer{
+ cache,
+ can_cache,
+ can_extents,
+ can_fast_zero,
+ can_flush,
+ can_fua,
+ can_multi_conn,
+ can_trim,
+ can_write,
+ can_zero,
+ config,
+ config_complete,
+ config_help,
+ dump_plugin,
+ extents,
+ flush,
+ get_ready,
+ is_rotational,
+ load,
+ preconnect,
+ thread_model,
+ trim,
+ unload,
+ write_at,
+ zero
+});
+
+// One-time initialization of the plugin
+pub fn initialize() {
+ INIT.call_once(|| {
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+ let config_help_ctx = MockServer::config_help_context();
+ config_help_ctx.expect()
+ .return_const(Some("Mock config help"));
+ let desc_ctx = MockServer::description_context();
+ desc_ctx.expect()
+ .return_const(Some("Mock description"));
+ let longname_ctx = MockServer::longname_context();
+ longname_ctx.expect()
+ .return_const(Some("Mock long name"));
+ let magic_config_key_ctx = MockServer::magic_config_key_context();
+ magic_config_key_ctx.expect()
+ .return_const(Some("magic_config_key"));
+ let name_ctx = MockServer::name_context();
+ name_ctx.expect()
+ .return_const("Mock NBD Server");
+ let version_ctx = MockServer::version_context();
+ version_ctx.expect()
+ .return_const("3.14.159");
+ unsafe {
+ PLUGIN = Some(plugin_init());
+ }
+ });
+}
+
+fn with_fixture<F: FnMut(&mut Fixture)>(mut f: F) {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let mut mock = Box::new(MockServer::default());
+ mock.expect_get_size()
+ .returning(|| Ok(0x4200));
+ let mockp = (&mut mock) as *mut Box<MockServer>;
+ let open_ctx = MockServer::open_context();
+ open_ctx.expect()
+ .return_once(|_| mock);
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+ let handle = ((*plugin).open)(0);
+ open_ctx.checkpoint(); // clear expectations for MockServer::open
+ let mut fixture = Fixture {
+ mockp,
+ plugin,
+ handle
+ };
+
+ f(&mut fixture);
+
+ ((*plugin).close)(handle);
+}
+
+/// Helper for testing methods that take a handle and return a boolean
+macro_rules! can_method {
+ ( $meth:ident, $expect_meth:ident ) => {
+ mod $meth {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .returning(|| Err(Error::new(libc::EIO, "I/O Schmerror")));
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_eq!(-1, ($meth)(fixture.handle));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .returning(|| Ok(true));
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_ne!(0, ($meth)(fixture.handle));
+ });
+ }
+ }
+ }
+}
+
+/// Helper for testing methods that take a handle and return a special Flags
+macro_rules! can_cache_like {
+ ( $meth:ident, $flags_ty:ident, $expect_meth:ident ) => {
+ mod $meth {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .returning(|| Err(Error::new(libc::EIO, "I/O Schmerror")));
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_eq!(-1, ($meth)(fixture.handle));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .returning(|| Ok($flags_ty::Emulate));
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_eq!(1, ($meth)(fixture.handle));
+ });
+ }
+ }
+ }
+}
+
+/// Helper for testing methods with signatures like trim
+macro_rules! trim_like {
+ ( $meth:ident, $expect_meth:ident ) => {
+ mod $meth {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .with(eq(42), eq(1024), eq(Flags::FUA))
+ .returning(|_, _, _| {
+ Err(Error::new(libc::EIO, "I/O Schmerror"))
+ });
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_eq!(-1, ($meth)(fixture.handle,42, 1024, 2));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.$expect_meth()
+ .with(eq(42), eq(1024), eq(Flags::FUA))
+ .returning(|_, _, _| Ok(()));
+ let $meth = fixture.plugin.$meth.as_ref().unwrap();
+ assert_eq!(0, ($meth)(fixture.handle, 42, 1024, 2));
+ });
+ }
+ }
+ }
+}
+
+/// Helper for testing methods with signatures like unload
+macro_rules! unload_like {
+ ( $meth:ident, $ctx_meth:ident ) => {
+ mod $meth {
+ use super::*;
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let ctx = MockServer::$ctx_meth();
+ ctx.expect()
+ .return_const(());
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let $meth = plugin.$meth.as_ref().unwrap();
+ $meth();
+ ctx.checkpoint();
+ }
+ }
+ }
+}
+
+can_cache_like!(can_cache, CacheFlags, expect_can_cache);
+can_method!(can_extents, expect_can_extents);
+can_method!(can_fast_zero, expect_can_fast_zero);
+can_method!(can_flush, expect_can_flush);
+can_cache_like!(can_fua, FuaFlags, expect_can_fua);
+can_method!(can_multi_conn, expect_can_multi_conn);
+can_method!(can_trim, expect_can_trim);
+can_method!(can_write, expect_can_write);
+can_method!(can_zero, expect_can_zero);
+
+mod cache {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_cache()
+ .with(eq(42), eq(1024))
+ .returning(|_, _| {
+ Err(Error::new(libc::EIO, "I/O Schmerror"))
+ });
+ let cache = fixture.plugin.cache.as_ref().unwrap();
+ assert_eq!(-1, (cache)(fixture.handle, 42, 1024, 0));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_cache()
+ .with(eq(42), eq(1024))
+ .returning(|_, _| Ok(()));
+ let cache = fixture.plugin.cache.as_ref().unwrap();
+ assert_eq!(0, (cache)(fixture.handle, 42, 1024, 0));
+ });
+ }
+}
+
+mod config {
+ use super::*;
+
+ #[test]
+ fn einval() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let config_ctx = MockServer::config_context();
+ config_ctx.expect()
+ .with(eq("foo"), eq("bar"))
+ .returning(|_, _| Err(Error::new(libc::EINVAL, "Invalid value for foo")));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let config = plugin.config.as_ref().unwrap();
+ assert_eq!(
+ -1,
+ config(b"foo\0".as_ptr() as *const c_char,
+ b"bar\0".as_ptr() as *const c_char)
+ );
+ ERRNO.with(|e| assert_eq!(libc::EINVAL, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("Invalid value for foo", *e.borrow()));
+ }
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let config_ctx = MockServer::config_context();
+ config_ctx.expect()
+ .with(eq("foo"), eq("bar"))
+ .returning(|_, _| Ok(()));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let config = plugin.config.as_ref().unwrap();
+ assert_eq!(
+ 0,
+ config(b"foo\0".as_ptr() as *const c_char,
+ b"bar\0".as_ptr() as *const c_char)
+ );
+ }
+}
+
+mod config_complete {
+ use super::*;
+
+ #[test]
+ fn einval() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let config_complete_ctx = MockServer::config_complete_context();
+ config_complete_ctx.expect()
+ .returning(|| Err(Error::new(libc::EINVAL, "foo is required")));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let config_complete = plugin.config_complete.as_ref().unwrap();
+ assert_eq!( -1, config_complete());
+ ERRNO.with(|e| assert_eq!(libc::EINVAL, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("foo is required", *e.borrow()));
+ }
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let config_complete_ctx = MockServer::config_complete_context();
+ config_complete_ctx.expect()
+ .returning(|| Ok(()));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let config_complete = plugin.config_complete.as_ref().unwrap();
+ assert_eq!( 0, config_complete());
+ }
+}
+
+#[test]
+fn config_help() {
+ initialize();
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let help = unsafe { CStr::from_ptr(plugin.config_help) };
+ assert_eq!(help.to_str().unwrap(), "Mock config help");
+}
+
+#[test]
+fn description() {
+ initialize();
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let description = unsafe { CStr::from_ptr(plugin.description) };
+ assert_eq!(description.to_str().unwrap(), "Mock description");
+}
+
+unload_like!(dump_plugin, dump_plugin_context);
+
+mod extents {
+ use super::*;
+
+ #[test]
+ fn eopnotsupp() {
+ with_fixture(|fixture| {
+ let msg = "Backing store does not support extents";
+ unsafe{ &mut *fixture.mockp }.expect_extents()
+ .with(eq(512), eq(10240), eq(Flags::empty()), always())
+ .returning(move |_, _, _, _| Err(Error::new(libc::EOPNOTSUPP, msg)));
+ let extents = fixture.plugin.extents.as_ref().unwrap();
+ assert_eq!(-1, (extents)(fixture.handle,
+ 512,
+ 10240,
+ 0,
+ ptr::null_mut()));
+ ERRNO.with(|e| assert_eq!(libc::EOPNOTSUPP, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!(msg, *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn extent_error() {
+ with_fixture(|fixture| {
+ let ctx = MockNbdkitAddExtent::add_context();
+ ctx.expect()
+ .with(always(), eq(8192), eq(4096), eq(2))
+ .returning(|_, _, _, _| {
+ set_errno(Errno(libc::ERANGE));
+ -1
+ });
+
+ unsafe{ &mut *fixture.mockp }.expect_extents()
+ .with(eq(512), eq(10240), eq(Flags::empty()), always())
+ .returning(|_, _, _, exh| {
+ exh.add(8192, 4096, ExtentType::Zero)
+ });
+ let extents = fixture.plugin.extents.as_ref().unwrap();
+ assert_eq!(-1, (extents)(fixture.handle,
+ 512,
+ 10240,
+ 0,
+ ptr::null_mut()));
+ ERRNO.with(|e| assert_eq!(libc::ERANGE, *e.borrow()));
+ ctx.checkpoint();
+ });
+ }
+
+ #[test]
+ fn req_one() {
+ with_fixture(|fixture| {
+ let ctx = MockNbdkitAddExtent::add_context();
+ ctx.expect()
+ .with(always(), eq(8192), eq(4096), eq(2))
+ .return_const(0);
+
+ unsafe{ &mut *fixture.mockp }.expect_extents()
+ .with(eq(512), eq(10240), eq(Flags::REQ_ONE), always())
+ .returning(|_, _, _, exh| {
+ exh.add(8192, 4096, ExtentType::Zero).unwrap();
+ Ok(())
+ });
+ let extents = fixture.plugin.extents.as_ref().unwrap();
+ assert_eq!(0, (extents)(fixture.handle,
+ 512,
+ 10240,
+ 4,
+ ptr::null_mut()));
+
+ ctx.checkpoint();
+ });
+ }
+}
+
+mod flush {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_flush()
+ .returning(|| Err(Error::new(libc::EIO, "I/O Schmerror")));
+ let flush = fixture.plugin.flush.as_ref().unwrap();
+ assert_eq!(-1, (flush)(fixture.handle, 0));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_flush()
+ .returning(|| Ok(()));
+ let flush = fixture.plugin.flush.as_ref().unwrap();
+ assert_eq!(0, (flush)(fixture.handle, 0));
+ });
+ }
+}
+
+mod get_ready {
+ use super::*;
+
+ #[test]
+ fn einval() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let get_ready_ctx = MockServer::get_ready_context();
+ get_ready_ctx.expect()
+ .returning(|| Err(Error::new(libc::EINVAL, "foo is required")));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let get_ready = plugin.get_ready.as_ref().unwrap();
+ assert_eq!( -1, get_ready());
+ ERRNO.with(|e| assert_eq!(libc::EINVAL, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("foo is required", *e.borrow()));
+ }
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let get_ready_ctx = MockServer::get_ready_context();
+ get_ready_ctx.expect()
+ .returning(|| Ok(()));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let get_ready = plugin.get_ready.as_ref().unwrap();
+ assert_eq!( 0, get_ready());
+ }
+}
+
+can_method!(is_rotational, expect_is_rotational);
+
+unload_like!(load, load_context);
+
+#[test]
+fn longname() {
+ initialize();
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let longname = unsafe { CStr::from_ptr(plugin.longname) };
+ assert_eq!(longname.to_str().unwrap(), "Mock long name");
+}
+
+#[test]
+fn magic_config_key() {
+ initialize();
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let magic_config_key = unsafe { CStr::from_ptr(plugin.magic_config_key) };
+ assert_eq!(magic_config_key.to_str().unwrap(), "magic_config_key");
+}
+
+#[test]
+fn name() {
+ initialize();
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let name = unsafe { CStr::from_ptr(plugin.name) };
+ assert_eq!(name.to_str().unwrap(), "Mock NBD Server");
+}
+
+mod preconnect {
+ use super::*;
+
+ #[test]
+ fn einval() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let preconnect_ctx = MockServer::preconnect_context();
+ preconnect_ctx.expect()
+ .returning(|_| Err(Error::new(libc::EINVAL, "foo is required")));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let preconnect = plugin.preconnect.as_ref().unwrap();
+ assert_eq!( -1, preconnect(0));
+ ERRNO.with(|e| assert_eq!(libc::EINVAL, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("foo is required", *e.borrow()));
+ }
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let preconnect_ctx = MockServer::preconnect_context();
+ preconnect_ctx.expect()
+ .with(eq(true))
+ .returning(|_| Ok(()));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let preconnect = plugin.preconnect.as_ref().unwrap();
+ assert_eq!( 0, preconnect(1));
+ }
+}
+
+mod pwrite {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_write_at()
+ .with(eq(&b"0123456789"[..]), eq(1024), eq(Flags::empty()))
+ .returning(|_, _, _| Err(Error::new(libc::EIO, "I/O Schmerror")));
+ let buf = b"0123456789";
+ let pwrite = fixture.plugin.pwrite.as_ref().unwrap();
+ assert_eq!(-1, (pwrite)(fixture.handle,
+ buf.as_ptr() as *mut c_char,
+ buf.len() as u32,
+ 1024,
+ 0));
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("I/O Schmerror", *e.borrow()));
+ });
+ }
+
+ #[test]
+ fn ok() {
+ with_fixture(|fixture| {
+ unsafe{ &mut *fixture.mockp }.expect_write_at()
+ .with(eq(&b"0123456789"[..]), eq(1024), eq(Flags::FUA))
+ .returning(|_, _, _| Ok(()));
+ let buf = b"0123456789";
+ let pwrite = fixture.plugin.pwrite.as_ref().unwrap();
+ assert_eq!(0, (pwrite)(fixture.handle,
+ buf.as_ptr() as *mut c_char,
+ buf.len() as u32,
+ 1024,
+ 2));
+ });
+ }
+}
+
+mod thread_model {
+ use super::*;
+
+ #[test]
+ fn eio() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let thread_model_ctx = MockServer::thread_model_context();
+ thread_model_ctx.expect()
+ .returning(|| Err(Error::new(libc::EIO, "Mock error")));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ assert_eq!(-1, (plugin.thread_model.as_ref().unwrap())());
+ ERRNO.with(|e| assert_eq!(libc::EIO, *e.borrow()));
+ ERRMSG.with(|e| assert_eq!("Mock error", *e.borrow()));
+ }
+
+ #[test]
+ fn ok() {
+ initialize();
+ let _m = MOCK_SERVER_MTX.lock().unwrap();
+
+ let thread_model_ctx = MockServer::thread_model_context();
+ thread_model_ctx.expect()
+ .returning(|| Ok(ThreadModel::SerializeAllRequests ));
+
+ let pluginp = unsafe { PLUGIN.unwrap()};
+ let plugin = unsafe {&*pluginp};
+
+ let thread_model = (plugin.thread_model.as_ref().unwrap())();
+ assert_eq!(thread_model, 1);
+ }
+}
+
+trim_like!(trim, expect_trim);
+unload_like!(unload, unload_context);
+trim_like!(zero, expect_zero);
_______________________________________________
Libguestfs mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/libguestfs