qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: "Marc-André Lureau" <marcandre.lureau@redhat.com>
To: Alistair Francis <alistair23@gmail.com>
Cc: Paolo Bonzini <pbonzini@redhat.com>,
	"Daniel P. Berrange" <berrange@redhat.com>,
	"qemu-devel@nongnu.org Developers" <qemu-devel@nongnu.org>,
	Stefan Hajnoczi <stefanha@redhat.com>,
	Markus Armbruster <armbru@redhat.com>
Subject: Re: [RFC v3 12/32] rust: provide a common crate for QEMU
Date: Fri, 10 Sep 2021 11:43:04 +0400	[thread overview]
Message-ID: <CAMxuvaytkGSG+2gjzQJj_C95SxxD9RZ8YD6OT7G4D2cWsrnQyA@mail.gmail.com> (raw)
In-Reply-To: <CAKmqyKP3+rh+vxq=Ci1xiZx7N0Pzyx2Dy8zgp4EsM0UUHizrkw@mail.gmail.com>

[-- Attachment #1: Type: text/plain, Size: 35658 bytes --]

Hi

On Fri, Sep 10, 2021 at 5:19 AM Alistair Francis <alistair23@gmail.com>
wrote:

> On Tue, Sep 7, 2021 at 10:41 PM <marcandre.lureau@redhat.com> wrote:
> >
> > From: Marc-André Lureau <marcandre.lureau@redhat.com>
> >
> > This crates provides common bindings and facilities for QEMU C API
> > shared by various projects.
> >
> > Most importantly, it defines the conversion traits used to convert from
> > C to Rust types. Those traits are largely adapted from glib-rs, since
> > those have proved to be very flexible, and should guide us to bind
> > further QEMU types such as QOM. If glib-rs becomes a dependency, we
> > should consider adopting glib translate traits. For QAPI, we need a
> > smaller subset.
> >
> > Cargo.lock is checked-in, as QEMU produces end-of-chain binaries, and it
> > is desirable to track the exact set of packages that are involved in
> > managed builds.
> >
> > Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> > ---
> >  Cargo.lock                   |  63 +++++
> >  Cargo.toml                   |   4 +-
> >  rust/common/Cargo.toml       |  11 +
> >  rust/common/src/error.rs     | 113 ++++++++
> >  rust/common/src/ffi.rs       |  93 +++++++
> >  rust/common/src/lib.rs       |  21 ++
> >  rust/common/src/qemu.rs      | 101 ++++++++
> >  rust/common/src/qmp.rs       |   0
> >  rust/common/src/translate.rs | 482 +++++++++++++++++++++++++++++++++++
> >  9 files changed, 887 insertions(+), 1 deletion(-)
> >  create mode 100644 Cargo.lock
> >  create mode 100644 rust/common/Cargo.toml
> >  create mode 100644 rust/common/src/error.rs
> >  create mode 100644 rust/common/src/ffi.rs
> >  create mode 100644 rust/common/src/lib.rs
> >  create mode 100644 rust/common/src/qemu.rs
> >  create mode 100644 rust/common/src/qmp.rs
> >  create mode 100644 rust/common/src/translate.rs
> >
> > diff --git a/Cargo.lock b/Cargo.lock
> > new file mode 100644
> > index 0000000000..8dc2dd9da7
> > --- /dev/null
> > +++ b/Cargo.lock
> > @@ -0,0 +1,63 @@
> > +# This file is automatically @generated by Cargo.
> > +# It is not intended for manual editing.
> > +version = 3
> > +
> > +[[package]]
> > +name = "autocfg"
> > +version = "1.0.1"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
> > +
> > +[[package]]
> > +name = "bitflags"
> > +version = "1.2.1"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
> > +
> > +[[package]]
> > +name = "cc"
> > +version = "1.0.70"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
> > +
> > +[[package]]
> > +name = "cfg-if"
> > +version = "1.0.0"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
> > +
> > +[[package]]
> > +name = "common"
> > +version = "0.1.0"
> > +dependencies = [
> > + "libc",
> > + "nix",
> > +]
> > +
> > +[[package]]
> > +name = "libc"
> > +version = "0.2.101"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
> > +
> > +[[package]]
> > +name = "memoffset"
> > +version = "0.6.4"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
> > +dependencies = [
> > + "autocfg",
> > +]
> > +
> > +[[package]]
> > +name = "nix"
> > +version = "0.20.1"
> > +source = "registry+https://github.com/rust-lang/crates.io-index"
> > +checksum =
> "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56"
> > +dependencies = [
> > + "bitflags",
> > + "cc",
> > + "cfg-if",
> > + "libc",
> > + "memoffset",
> > +]
> > diff --git a/Cargo.toml b/Cargo.toml
> > index c4b464ff15..14131eed3c 100644
> > --- a/Cargo.toml
> > +++ b/Cargo.toml
> > @@ -1,2 +1,4 @@
> >  [workspace]
> > -members = []
> > +members = [
> > +  "rust/common",
> > +]
> > diff --git a/rust/common/Cargo.toml b/rust/common/Cargo.toml
> > new file mode 100644
> > index 0000000000..6c240447f3
> > --- /dev/null
> > +++ b/rust/common/Cargo.toml
> > @@ -0,0 +1,11 @@
> > +[package]
> > +name = "common"
> > +version = "0.1.0"
> > +edition = "2018"
> > +publish = false
> > +
> > +[dependencies]
> > +libc = "0.2.92"
> > +
> > +[target."cfg(unix)".dependencies]
> > +nix = "0.20.0"
> > diff --git a/rust/common/src/error.rs b/rust/common/src/error.rs
> > new file mode 100644
> > index 0000000000..f166ac42ea
> > --- /dev/null
> > +++ b/rust/common/src/error.rs
> > @@ -0,0 +1,113 @@
> > +use std::{self, ffi::CString, fmt, io, ptr};
> > +
> > +use crate::translate::*;
>
> It's not uncommon to ban wildcard imports that aren't preludes as it
> can make it confusing to read. Does QEMU have a stance on that type of
> thing?
>

There is no such common rule in Rust afaik. It's based on judgement and
style. If the imported symbols pollute your namespace or not. But yes, in
general, it's better to selectively import what you need, mostly for
readability.


> > +use crate::{ffi, qemu};
> > +
> > +/// Common error type for QEMU and related projects.
> > +#[derive(Debug)]
> > +pub enum Error {
> > +    /// A generic error with file and line location.
> > +    FailedAt(String, &'static str, u32),
> > +    /// An IO error.
> > +    Io(io::Error),
> > +    #[cfg(unix)]
> > +    /// A nix error.
> > +    Nix(nix::Error),
> > +}
> > +
> > +/// Alias for a `Result` with the error type for QEMU.
> > +pub type Result<T> = std::result::Result<T, Error>;
>
> I think this is very confusing. Rust developers expect `Result` to be
> the one from `std::result`, it would be better to call this
> `QEMUResult`
>

It's very common in Rust to redefine Result for your crate error. Users
don't have to import it if they prefer the std::result::Result<T,E>. This
redefinition was probably popularized with the `std::io::Result<T>` type. (
https://doc.rust-lang.org/std/io/type.Result.html)


>
> > +
> > +impl Error {
> > +    fn message(&self) -> String {
> > +        use Error::*;
>
> Do we need this here? Why not put it at the top of the file?
>

It's limited here to avoid enum prefix repetition:
match self {
 Error::FailedAt ..
 Error::Io ..
 Error::Foo ..
}

(It wouldn't be a good idea to import it in the top namespace)


> > +        match self {
> > +            FailedAt(msg, _, _) => msg.into(),
> > +            Io(io) => format!("IO error: {}", io),
> > +            #[cfg(unix)]
> > +            Nix(nix) => format!("Nix error: {}", nix),
> > +        }
> > +    }
> > +
> > +    fn location(&self) -> Option<(&'static str, u32)> {
> > +        use Error::*;
> > +        match self {
> > +            FailedAt(_, file, line) => Some((file, *line)),
> > +            Io(_) => None,
> > +            #[cfg(unix)]
> > +            Nix(_) => None,
> > +        }
> > +    }
> > +}
> > +
> > +impl fmt::Display for Error {
> > +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
> > +        use Error::*;
> > +        match self {
> > +            FailedAt(msg, file, line) => write!(f, "{} ({}:{})", msg,
> file, line),
> > +            _ => write!(f, "{}", self.message()),
> > +        }
> > +    }
> > +}
> > +
> > +impl From<io::Error> for Error {
> > +    fn from(val: io::Error) -> Self {
> > +        Error::Io(val)
> > +    }
> > +}
> > +
> > +#[cfg(unix)]
> > +impl From<nix::Error> for Error {
> > +    fn from(val: nix::Error) -> Self {
> > +        Error::Nix(val)
> > +    }
> > +}
> > +
> > +impl QemuPtrDefault for Error {
> > +    type QemuType = *mut ffi::Error;
> > +}
> > +
> > +impl<'a> ToQemuPtr<'a, *mut ffi::Error> for Error {
> > +    type Storage = qemu::CError;
> > +
> > +    fn to_qemu_none(&'a self) -> Stash<'a, *mut ffi::Error, Self> {
> > +        let err = self.to_qemu_full();
> > +
> > +        Stash(err, unsafe { from_qemu_full(err) })
> > +    }
> > +
> > +    fn to_qemu_full(&self) -> *mut ffi::Error {
> > +        let cmsg =
> > +            CString::new(self.message()).expect("ToQemuPtr<Error>:
> unexpected '\0' character");
> > +        let mut csrc = CString::new("").unwrap();
> > +        let (src, line) = self.location().map_or((ptr::null(), 0_i32),
> |loc| {
> > +            csrc = CString::new(loc.0).expect("ToQemuPtr<Error>::
> unexpected '\0' character");
> > +            (csrc.as_ptr() as *const libc::c_char, loc.1 as i32)
> > +        });
> > +        let func = ptr::null();
> > +
> > +        let mut err: *mut ffi::Error = ptr::null_mut();
> > +        unsafe {
> > +            ffi::error_setg_internal(
> > +                &mut err as *mut *mut _,
> > +                src,
> > +                line,
> > +                func,
> > +                cmsg.as_ptr() as *const libc::c_char,
> > +            );
> > +            err
> > +        }
> > +    }
> > +}
> > +
> > +/// Convenience macro to build a [`Error::FailedAt`] error.
> > +///
> > +/// Returns a `Result::Err` with the file:line location.
> > +/// (the error can then be converted to a QEMU `ffi::Error`)
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! err {
> > +    ($msg:expr) => {
> > +        Err(Error::FailedAt($msg.into(), file!(), line!()))
> > +    };
> > +}
> > diff --git a/rust/common/src/ffi.rs b/rust/common/src/ffi.rs
> > new file mode 100644
> > index 0000000000..82818d503a
> > --- /dev/null
> > +++ b/rust/common/src/ffi.rs
> > @@ -0,0 +1,93 @@
> > +//! Bindings to the raw low-level C API commonly provided by QEMU
> projects.
> > +//!
> > +//! Manual bindings to C API availabe when linking QEMU projects.
>
> s/availabe/available/g
>
>
yup thanks

> +//! It includes minimal glib allocation functions too, since it's the
> default
> > +//! allocator used by QEMU, and we don't depend on glib-rs crate yet).
> > +//!
> > +//! Higher-level Rust-friendly bindings are provided by different
> modules.
> > +
> > +use libc::{c_char, c_void, size_t};
> > +
> > +extern "C" {
> > +    pub fn g_malloc0(n_bytes: size_t) -> *mut c_void;
> > +    pub fn g_free(ptr: *mut c_void);
> > +    pub fn g_strndup(str: *const c_char, n: size_t) -> *mut c_char;
> > +}
>
> We can get there from the glib/glib_sys crate:
>
> https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib_sys/fn.g_malloc0.html
>
> If we only plan on using these 3 I think this approach is fine, but
> something to keep in mind if we use more glib functions.
>
>
Yes, I think I mentioned this somewhere. We might need to import glib-sys
or glib-rs depending on what we need to write in Rust. We may actually not
need more than a few FFI functions though, so importing external crates for
that isn't worth it.


> > +
> > +#[repr(C)]
> > +pub struct QObject(c_void);
> > +
> > +impl ::std::fmt::Debug for QObject {
> > +    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
> > +        f.debug_struct(&format!("QObject @ {:?}", self as *const _))
> > +            .finish()
> > +    }
> > +}
> > +
> > +#[repr(C)]
> > +pub struct QNull(c_void);
> > +
> > +impl ::std::fmt::Debug for QNull {
> > +    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
> > +        f.debug_struct(&format!("QNull @ {:?}", self as *const _))
> > +            .finish()
> > +    }
> > +}
> > +
> > +#[repr(C)]
> > +pub struct Error(c_void);
> > +
> > +impl ::std::fmt::Debug for Error {
> > +    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
> > +        f.debug_struct(&format!("Error @ {:?}", self as *const _))
> > +            .finish()
> > +    }
> > +}
> > +
> > +extern "C" {
> > +    pub fn error_setg_internal(
> > +        errp: *mut *mut Error,
> > +        src: *const c_char,
> > +        line: i32,
> > +        func: *const c_char,
> > +        fmt: *const c_char,
> > +        ...
> > +    );
> > +    pub fn error_get_pretty(err: *const Error) -> *const c_char;
> > +    pub fn error_free(err: *mut Error);
> > +}
> > +
> > +/// Wrap a QMP hanlder.
>
> handler
>

thanks!


> > +#[macro_export]
> > +macro_rules! qmp {
> > +    // the basic return value variant
> > +    ($e:expr, $errp:ident, $errval:expr) => {{
> > +        assert!(!$errp.is_null());
> > +        unsafe {
> > +            *$errp = std::ptr::null_mut();
> > +        }
> > +
> > +        match $e {
> > +            Ok(val) => val,
> > +            Err(err) => unsafe {
> > +                *$errp = err.to_qemu_full();
> > +                $errval
> > +            },
> > +        }
> > +    }};
> > +    // the ptr return value variant
> > +    ($e:expr, $errp:ident) => {{
> > +        assert!(!$errp.is_null());
> > +        unsafe {
> > +            *$errp = std::ptr::null_mut();
> > +        }
> > +
> > +        match $e {
> > +            Ok(val) => val.to_qemu_full().into(),
> > +            Err(err) => unsafe {
> > +                *$errp = err.to_qemu_full();
> > +                std::ptr::null_mut()
> > +            },
> > +        }
> > +    }};
> > +}
>
> It would be a good idea to document why this code is safe
>

Hmm, I am not sure that's the question. I assume Rust code is safe :)
However, the FFI borders are full of unsafe {}. It's basically a trust
relationship with the other side. But it's always great to document tricky
unsafe {}, or where panic is unexpected (unwrap()/expect()).. based on
review, judgement, style.. (read also
https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html).

Thanks!

Alistair
>
> > diff --git a/rust/common/src/lib.rs b/rust/common/src/lib.rs
> > new file mode 100644
> > index 0000000000..4de826bc2e
> > --- /dev/null
> > +++ b/rust/common/src/lib.rs
> > @@ -0,0 +1,21 @@
> > +//! Common code for QEMU
> > +//!
> > +//! This crates provides common bindings and facilities for QEMU C API
> shared by
> > +//! various projects. Most importantly, it defines the conversion
> traits used to
> > +//! convert from C to Rust types. Those traits are largely adapted from
> glib-rs,
> > +//! since those have prooven to be very flexible, and should guide us
> to bind
> > +//! further QEMU types such as QOM. If glib-rs becomes a dependency, we
> should
> > +//! consider adopting glib translate traits. For QAPI, we need a
> smaller subset.
> > +
> > +pub use libc;
> > +
> > +mod error;
> > +pub use error::*;
> > +
> > +mod qemu;
> > +pub use qemu::*;
> > +
> > +mod translate;
> > +pub use translate::*;
> > +
> > +pub mod ffi;
> > diff --git a/rust/common/src/qemu.rs b/rust/common/src/qemu.rs
> > new file mode 100644
> > index 0000000000..dd01c6d92d
> > --- /dev/null
> > +++ b/rust/common/src/qemu.rs
> > @@ -0,0 +1,101 @@
> > +use std::{ffi::CStr, ptr, str};
> > +
> > +use crate::{ffi, translate};
> > +use translate::{FromQemuPtrFull, FromQemuPtrNone, QemuPtrDefault,
> Stash, ToQemuPtr};
> > +
> > +/// A type representing an owned C QEMU Error.
> > +pub struct CError(ptr::NonNull<ffi::Error>);
> > +
> > +impl translate::FromQemuPtrFull<*mut ffi::Error> for CError {
> > +    unsafe fn from_qemu_full(ptr: *mut ffi::Error) -> Self {
> > +        assert!(!ptr.is_null());
> > +        Self(ptr::NonNull::new_unchecked(ptr))
> > +    }
> > +}
> > +
> > +impl CError {
> > +    pub fn pretty(&self) -> &str {
> > +        unsafe {
> > +            let pretty = ffi::error_get_pretty(self.0.as_ptr());
> > +            let bytes = CStr::from_ptr(pretty).to_bytes();
> > +            str::from_utf8(bytes)
> > +                .unwrap_or_else(|err|
> str::from_utf8(&bytes[..err.valid_up_to()]).unwrap())
> > +        }
> > +    }
> > +}
> > +
> > +impl Drop for CError {
> > +    fn drop(&mut self) {
> > +        unsafe { ffi::error_free(self.0.as_ptr()) }
> > +    }
> > +}
> > +
> > +/// QObject (JSON object)
> > +#[derive(Clone, Debug)]
> > +pub struct QObject;
> > +
> > +impl QemuPtrDefault for QObject {
> > +    type QemuType = *mut ffi::QObject;
> > +}
> > +
> > +impl FromQemuPtrFull<*mut ffi::QObject> for QObject {
> > +    #[inline]
> > +    unsafe fn from_qemu_full(_ffi: *mut ffi::QObject) -> Self {
> > +        unimplemented!()
> > +    }
> > +}
> > +
> > +impl FromQemuPtrNone<*const ffi::QObject> for QObject {
> > +    #[inline]
> > +    unsafe fn from_qemu_none(_ffi: *const ffi::QObject) -> Self {
> > +        unimplemented!()
> > +    }
> > +}
> > +
> > +impl<'a> ToQemuPtr<'a, *mut ffi::QObject> for QObject {
> > +    type Storage = ();
> > +
> > +    #[inline]
> > +    fn to_qemu_none(&self) -> Stash<'a, *mut ffi::QObject, QObject> {
> > +        unimplemented!()
> > +    }
> > +    #[inline]
> > +    fn to_qemu_full(&self) -> *mut ffi::QObject {
> > +        unimplemented!()
> > +    }
> > +}
> > +
> > +/// QNull (JSON null)
> > +#[derive(Clone, Debug)]
> > +pub struct QNull;
> > +
> > +impl QemuPtrDefault for QNull {
> > +    type QemuType = *mut ffi::QNull;
> > +}
> > +
> > +impl FromQemuPtrFull<*mut ffi::QObject> for QNull {
> > +    #[inline]
> > +    unsafe fn from_qemu_full(_ffi: *mut ffi::QObject) -> Self {
> > +        unimplemented!()
> > +    }
> > +}
> > +
> > +impl FromQemuPtrNone<*const ffi::QObject> for QNull {
> > +    #[inline]
> > +    unsafe fn from_qemu_none(_ffi: *const ffi::QObject) -> Self {
> > +        unimplemented!()
> > +    }
> > +}
> > +
> > +impl<'a> ToQemuPtr<'a, *mut ffi::QNull> for QNull {
> > +    type Storage = ();
> > +
> > +    #[inline]
> > +    fn to_qemu_none(&self) -> Stash<'a, *mut ffi::QNull, QNull> {
> > +        unimplemented!()
> > +    }
> > +    #[inline]
> > +    fn to_qemu_full(&self) -> *mut ffi::QNull {
> > +        unimplemented!()
> > +    }
> > +}
> > diff --git a/rust/common/src/qmp.rs b/rust/common/src/qmp.rs
> > new file mode 100644
> > index 0000000000..e69de29bb2
> > diff --git a/rust/common/src/translate.rs b/rust/common/src/translate.rs
> > new file mode 100644
> > index 0000000000..315e14fa25
> > --- /dev/null
> > +++ b/rust/common/src/translate.rs
> > @@ -0,0 +1,482 @@
> > +// largely adapted from glib-rs
> > +// we don't depend on glib-rs as this brings a lot more code that we
> may not need
> > +// and also because there are issues with the conversion traits for our
> ffi::*mut.
> > +use libc::{c_char, size_t};
> > +use std::ffi::{CStr, CString};
> > +use std::ptr;
> > +
> > +use crate::ffi;
> > +
> > +/// A pointer.
> > +pub trait Ptr: Copy + 'static {
> > +    fn is_null(&self) -> bool;
> > +    fn from<X>(ptr: *mut X) -> Self;
> > +    fn to<X>(self) -> *mut X;
> > +}
> > +
> > +impl<T: 'static> Ptr for *const T {
> > +    #[inline]
> > +    fn is_null(&self) -> bool {
> > +        (*self).is_null()
> > +    }
> > +
> > +    #[inline]
> > +    fn from<X>(ptr: *mut X) -> *const T {
> > +        ptr as *const T
> > +    }
> > +
> > +    #[inline]
> > +    fn to<X>(self) -> *mut X {
> > +        self as *mut X
> > +    }
> > +}
> > +
> > +impl<T: 'static> Ptr for *mut T {
> > +    #[inline]
> > +    fn is_null(&self) -> bool {
> > +        (*self).is_null()
> > +    }
> > +
> > +    #[inline]
> > +    fn from<X>(ptr: *mut X) -> *mut T {
> > +        ptr as *mut T
> > +    }
> > +
> > +    #[inline]
> > +    fn to<X>(self) -> *mut X {
> > +        self as *mut X
> > +    }
> > +}
> > +
> > +/// Macro to declare a `NewPtr` struct.
> > +///
> > +/// A macro to declare a newtype for pointers, to workaround that *T
> are not
> > +/// defined in our binding crates, and allow foreign traits
> implementations.
> > +/// (this is used by qapi-gen bindings)
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +#[doc(hidden)]
> > +macro_rules! new_ptr {
> > +    () => {
> > +        #[derive(Copy, Clone)]
> > +        pub struct NewPtr<P: Ptr>(pub P);
> > +
> > +        impl<P: Ptr> Ptr for NewPtr<P> {
> > +            #[inline]
> > +            fn is_null(&self) -> bool {
> > +                self.0.is_null()
> > +            }
> > +
> > +            #[inline]
> > +            fn from<X>(ptr: *mut X) -> Self {
> > +                NewPtr(P::from(ptr))
> > +            }
> > +
> > +            #[inline]
> > +            fn to<X>(self) -> *mut X {
> > +                self.0.to()
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +/// Provides the default pointer type to be used in some container
> conversions.
> > +///
> > +/// It's `*mut c_char` for `String`, `*mut ffi::GuestInfo` for
> `GuestInfo`...
> > +pub trait QemuPtrDefault {
> > +    type QemuType: Ptr;
> > +}
> > +
> > +impl QemuPtrDefault for String {
> > +    type QemuType = *mut c_char;
> > +}
> > +
> > +/// A Stash contains the temporary storage and a pointer into it.
> > +///
> > +/// The pointer is valid for the lifetime of the `Stash`. As the
> lifetime of the
> > +/// `Stash` returned from `to_qemu_none` is at least the enclosing
> statement,
> > +/// you can avoid explicitly binding the stash in most cases and just
> take the
> > +/// pointer out of it:
> > +///
> > +/// ```ignore
> > +///     pub fn set_device_name(&self, name: &str) {
> > +///         unsafe {
> > +///             ffi::qemu_device_set_name(self.pointer,
> name.to_qemu_none().0)
> > +///         }
> > +///     }
> > +/// ```
> > +pub struct Stash<'a, P: Copy, T: ?Sized + ToQemuPtr<'a, P>>(
> > +    pub P,
> > +    pub <T as ToQemuPtr<'a, P>>::Storage,
> > +);
> > +
> > +/// Translate to a pointer.
> > +pub trait ToQemuPtr<'a, P: Copy> {
> > +    type Storage;
> > +
> > +    /// The pointer in the `Stash` is only valid for the lifetime of
> the `Stash`.
> > +    fn to_qemu_none(&'a self) -> Stash<'a, P, Self>;
> > +
> > +    /// Transfer the ownership to the ffi.
> > +    fn to_qemu_full(&self) -> P {
> > +        unimplemented!();
> > +    }
> > +}
> > +
> > +impl<'a, P: Ptr, T: ToQemuPtr<'a, P>> ToQemuPtr<'a, P> for Option<T> {
> > +    type Storage = Option<<T as ToQemuPtr<'a, P>>::Storage>;
> > +
> > +    #[inline]
> > +    fn to_qemu_none(&'a self) -> Stash<'a, P, Option<T>> {
> > +        self.as_ref()
> > +            .map_or(Stash(Ptr::from::<()>(ptr::null_mut()), None), |s| {
> > +                let s = s.to_qemu_none();
> > +                Stash(s.0, Some(s.1))
> > +            })
> > +    }
> > +
> > +    #[inline]
> > +    fn to_qemu_full(&self) -> P {
> > +        self.as_ref()
> > +            .map_or(Ptr::from::<()>(ptr::null_mut()),
> ToQemuPtr::to_qemu_full)
> > +    }
> > +}
> > +
> > +impl<'a, P: Ptr, T: ToQemuPtr<'a, P>> ToQemuPtr<'a, P> for Box<T> {
> > +    type Storage = <T as ToQemuPtr<'a, P>>::Storage;
> > +
> > +    #[inline]
> > +    fn to_qemu_none(&'a self) -> Stash<'a, P, Box<T>> {
> > +        let s = self.as_ref().to_qemu_none();
> > +        Stash(s.0, s.1)
> > +    }
> > +
> > +    #[inline]
> > +    fn to_qemu_full(&self) -> P {
> > +        ToQemuPtr::to_qemu_full(self.as_ref())
> > +    }
> > +}
> > +
> > +impl<'a> ToQemuPtr<'a, *mut c_char> for String {
> > +    type Storage = CString;
> > +
> > +    #[inline]
> > +    fn to_qemu_none(&self) -> Stash<'a, *mut c_char, String> {
> > +        let tmp = CString::new(&self[..])
> > +            .expect("String::ToQemuPtr<*mut c_char>: unexpected '\0'
> character");
> > +        Stash(tmp.as_ptr() as *mut c_char, tmp)
> > +    }
> > +
> > +    #[inline]
> > +    fn to_qemu_full(&self) -> *mut c_char {
> > +        unsafe { ffi::g_strndup(self.as_ptr() as *const c_char,
> self.len() as size_t) }
> > +    }
> > +}
> > +
> > +/// Translate from a pointer type, without taking ownership.
> > +pub trait FromQemuPtrNone<P: Ptr>: Sized {
> > +    /// # Safety
> > +    ///
> > +    /// `ptr` must be a valid pointer. It is not referenced after the
> call.
> > +    unsafe fn from_qemu_none(ptr: P) -> Self;
> > +}
> > +
> > +/// Translate from a pointer type, taking ownership.
> > +pub trait FromQemuPtrFull<P: Ptr>: Sized {
> > +    /// # Safety
> > +    ///
> > +    /// `ptr` must be a valid pointer. Ownership is transferred.
> > +    unsafe fn from_qemu_full(ptr: P) -> Self;
> > +}
> > +
> > +/// See [`FromQemuPtrNone`](trait.FromQemuPtrNone.html).
> > +#[inline]
> > +#[allow(clippy::missing_safety_doc)]
> > +pub unsafe fn from_qemu_none<P: Ptr, T: FromQemuPtrNone<P>>(ptr: P) ->
> T {
> > +    FromQemuPtrNone::from_qemu_none(ptr)
> > +}
> > +
> > +/// See [`FromQemuPtrFull`](trait.FromQemuPtrFull.html).
> > +#[inline]
> > +#[allow(clippy::missing_safety_doc)]
> > +pub unsafe fn from_qemu_full<P: Ptr, T: FromQemuPtrFull<P>>(ptr: P) ->
> T {
> > +    FromQemuPtrFull::from_qemu_full(ptr)
> > +}
> > +
> > +impl<P: Ptr, T: FromQemuPtrNone<P>> FromQemuPtrNone<P> for Option<T> {
> > +    #[inline]
> > +    unsafe fn from_qemu_none(ptr: P) -> Option<T> {
> > +        if ptr.is_null() {
> > +            None
> > +        } else {
> > +            Some(from_qemu_none(ptr))
> > +        }
> > +    }
> > +}
> > +
> > +impl<P: Ptr, T: FromQemuPtrFull<P>> FromQemuPtrFull<P> for Option<T> {
> > +    #[inline]
> > +    unsafe fn from_qemu_full(ptr: P) -> Option<T> {
> > +        if ptr.is_null() {
> > +            None
> > +        } else {
> > +            Some(from_qemu_full(ptr))
> > +        }
> > +    }
> > +}
> > +
> > +impl FromQemuPtrNone<*const c_char> for String {
> > +    #[inline]
> > +    unsafe fn from_qemu_none(ptr: *const c_char) -> Self {
> > +        assert!(!ptr.is_null());
> > +
> String::from_utf8_lossy(CStr::from_ptr(ptr).to_bytes()).into_owned()
> > +    }
> > +}
> > +
> > +impl FromQemuPtrFull<*mut c_char> for String {
> > +    #[inline]
> > +    unsafe fn from_qemu_full(ptr: *mut c_char) -> Self {
> > +        let res = from_qemu_none(ptr as *const _);
> > +        ffi::g_free(ptr as *mut _);
> > +        res
> > +    }
> > +}
> > +
> > +#[doc(hidden)]
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! vec_ffi_wrapper {
> > +    ($ffi:ident) => {
> > +        #[allow(non_camel_case_types)]
> > +        pub struct $ffi(*mut qapi_ffi::$ffi);
> > +
> > +        impl Drop for $ffi {
> > +            fn drop(&mut self) {
> > +                let mut list = self.0;
> > +                unsafe {
> > +                    while !list.is_null() {
> > +                        let next = (*list).next;
> > +                        Box::from_raw(list);
> > +                        list = next;
> > +                    }
> > +                }
> > +            }
> > +        }
> > +
> > +        impl From<NewPtr<*mut qapi_ffi::$ffi>> for *mut qapi_ffi::$ffi {
> > +            fn from(p: NewPtr<*mut qapi_ffi::$ffi>) -> Self {
> > +                p.0
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +#[doc(hidden)]
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_vec_scalars_to_qemu {
> > +    ($rs:ty, $ffi:ident) => {
> > +        impl<'a> ToQemuPtr<'a, NewPtr<*mut qapi_ffi::$ffi>> for
> Vec<$rs> {
> > +            type Storage = $ffi;
> > +
> > +            #[inline]
> > +            fn to_qemu_none(&self) -> Stash<NewPtr<*mut
> qapi_ffi::$ffi>, Self> {
> > +                let mut list: *mut qapi_ffi::$ffi =
> std::ptr::null_mut();
> > +                for value in self.iter().rev() {
> > +                    let b = Box::new(qapi_ffi::$ffi {
> > +                        next: list,
> > +                        value: *value,
> > +                    });
> > +                    list = Box::into_raw(b);
> > +                }
> > +                Stash(NewPtr(list), $ffi(list))
> > +            }
> > +
> > +            #[inline]
> > +            fn to_qemu_full(&self) -> NewPtr<*mut qapi_ffi::$ffi> {
> > +                let mut list: *mut qapi_ffi::$ffi =
> std::ptr::null_mut();
> > +                unsafe {
> > +                    for value in self.iter().rev() {
> > +                        let l =
> ffi::g_malloc0(std::mem::size_of::<qapi_ffi::$ffi>())
> > +                            as *mut qapi_ffi::$ffi;
> > +                        (*l).next = list;
> > +                        (*l).value = *value;
> > +                        list = l;
> > +                    }
> > +                }
> > +                NewPtr(list)
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +#[doc(hidden)]
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_vec_scalars_from_qemu {
> > +    ($rs:ty, $ffi:ident, $free_ffi:ident) => {
> > +        impl FromQemuPtrFull<NewPtr<*mut qapi_ffi::$ffi>> for Vec<$rs> {
> > +            #[inline]
> > +            unsafe fn from_qemu_full(ffi: NewPtr<*mut qapi_ffi::$ffi>)
> -> Self {
> > +                let ret = from_qemu_none(NewPtr(ffi.0 as *const _));
> > +                qapi_ffi::$free_ffi(ffi.0);
> > +                ret
> > +            }
> > +        }
> > +
> > +        impl FromQemuPtrNone<NewPtr<*const qapi_ffi::$ffi>> for
> Vec<$rs> {
> > +            #[inline]
> > +            unsafe fn from_qemu_none(ffi: NewPtr<*const
> qapi_ffi::$ffi>) -> Self {
> > +                let mut ret = vec![];
> > +                let mut it = ffi.0;
> > +                while !it.is_null() {
> > +                    let e = &*it;
> > +                    ret.push(e.value);
> > +                    it = e.next;
> > +                }
> > +                ret
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +#[doc(hidden)]
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_vec_to_qemu {
> > +    ($rs:ty, $ffi:ident) => {
> > +        // impl doesn't use only types from inside the current crate
> > +        // impl QemuPtrDefault for Vec<$rs> {
> > +        //     type QemuType = NewPtr<*mut qapi_ffi::$ffi>;
> > +        // }
> > +
> > +        impl<'a> ToQemuPtr<'a, NewPtr<*mut qapi_ffi::$ffi>> for
> Vec<$rs> {
> > +            type Storage = ($ffi, Vec<Stash<'a, <$rs as
> QemuPtrDefault>::QemuType, $rs>>);
> > +
> > +            #[inline]
> > +            fn to_qemu_none(&self) -> Stash<NewPtr<*mut
> qapi_ffi::$ffi>, Self> {
> > +                let stash_vec: Vec<_> =
> self.iter().rev().map(ToQemuPtr::to_qemu_none).collect();
> > +                let mut list: *mut qapi_ffi::$ffi =
> std::ptr::null_mut();
> > +                for stash in &stash_vec {
> > +                    let b = Box::new(qapi_ffi::$ffi {
> > +                        next: list,
> > +                        value: Ptr::to(stash.0),
> > +                    });
> > +                    list = Box::into_raw(b);
> > +                }
> > +                Stash(NewPtr(list), ($ffi(list), stash_vec))
> > +            }
> > +
> > +            #[inline]
> > +            fn to_qemu_full(&self) -> NewPtr<*mut qapi_ffi::$ffi> {
> > +                let v: Vec<_> =
> self.iter().rev().map(ToQemuPtr::to_qemu_full).collect();
> > +                let mut list: *mut qapi_ffi::$ffi =
> std::ptr::null_mut();
> > +                unsafe {
> > +                    for val in v {
> > +                        let l =
> ffi::g_malloc0(std::mem::size_of::<qapi_ffi::$ffi>())
> > +                            as *mut qapi_ffi::$ffi;
> > +                        (*l).next = list;
> > +                        (*l).value = val;
> > +                        list = l;
> > +                    }
> > +                }
> > +                NewPtr(list)
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +#[doc(hidden)]
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_vec_from_qemu {
> > +    ($rs:ty, $ffi:ident, $free_ffi:ident) => {
> > +        impl FromQemuPtrFull<NewPtr<*mut qapi_ffi::$ffi>> for Vec<$rs> {
> > +            #[inline]
> > +            unsafe fn from_qemu_full(ffi: NewPtr<*mut qapi_ffi::$ffi>)
> -> Self {
> > +                let ret = from_qemu_none(NewPtr(ffi.0 as *const _));
> > +                qapi_ffi::$free_ffi(ffi.0);
> > +                ret
> > +            }
> > +        }
> > +
> > +        impl FromQemuPtrNone<NewPtr<*const qapi_ffi::$ffi>> for
> Vec<$rs> {
> > +            #[inline]
> > +            unsafe fn from_qemu_none(ffi: NewPtr<*const
> qapi_ffi::$ffi>) -> Self {
> > +                let mut ret = vec![];
> > +                let mut it = ffi.0;
> > +                while !it.is_null() {
> > +                    let e = &*it;
> > +                    ret.push(from_qemu_none(e.value as *const _));
> > +                    it = e.next;
> > +                }
> > +                ret
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +/// A macro to help the implementation of `Vec<T>` translations.
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! vec_type {
> > +    (Vec<$rs:ty>, $ffi:ident, $free_ffi:ident, 0) => {
> > +        vec_ffi_wrapper!($ffi);
> > +        impl_vec_from_qemu!($rs, $ffi, $free_ffi);
> > +        impl_vec_to_qemu!($rs, $ffi);
> > +    };
> > +    (Vec<$rs:ty>, $ffi:ident, $free_ffi:ident, 1) => {
> > +        vec_ffi_wrapper!($ffi);
> > +        impl_vec_scalars_from_qemu!($rs, $ffi, $free_ffi);
> > +        impl_vec_scalars_to_qemu!($rs, $ffi);
> > +    };
> > +}
> > +
> > +/// A macro to implement [`ToQemuPtr`] as boxed scalars
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_to_qemu_scalar_boxed {
> > +    ($ty:ty) => {
> > +        impl<'a> ToQemuPtr<'a, *mut $ty> for $ty {
> > +            type Storage = Box<$ty>;
> > +
> > +            fn to_qemu_none(&'a self) -> Stash<'a, *mut $ty, Self> {
> > +                let mut box_ = Box::new(*self);
> > +                Stash(&mut *box_, box_)
> > +            }
> > +
> > +            fn to_qemu_full(&self) -> *mut $ty {
> > +                unsafe {
> > +                    let ptr =
> ffi::g_malloc0(std::mem::size_of::<$ty>()) as *mut _;
> > +                    *ptr = *self;
> > +                    ptr
> > +                }
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +/// A macro to implement [`FromQemuPtrNone`] for scalar pointers.
> > +#[allow(unused_macros)]
> > +#[macro_export]
> > +macro_rules! impl_from_qemu_none_scalar {
> > +    ($ty:ty) => {
> > +        impl FromQemuPtrNone<*const $ty> for $ty {
> > +            unsafe fn from_qemu_none(ptr: *const $ty) -> Self {
> > +                *ptr
> > +            }
> > +        }
> > +    };
> > +}
> > +
> > +macro_rules! impl_scalar_boxed {
> > +    ($($t:ident)*) => (
> > +        $(
> > +            impl_to_qemu_scalar_boxed!($t);
> > +            impl_from_qemu_none_scalar!($t);
> > +        )*
> > +    )
> > +}
> > +
> > +// the only built-in used so far, feel free to add more as needed
> > +impl_scalar_boxed!(bool i64 f64);
> > --
> > 2.33.0.113.g6c40894d24
> >
> >
>
>

[-- Attachment #2: Type: text/html, Size: 49488 bytes --]

  reply	other threads:[~2021-09-10  7:46 UTC|newest]

Thread overview: 74+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-09-07 12:19 [RFC v3 00/32] Rust binding for QAPI and qemu-ga QMP handler examples marcandre.lureau
2021-09-07 12:19 ` [RFC v3 01/32] RFC: docs: add supported host CPUs section marcandre.lureau
2021-09-07 12:33   ` Peter Maydell
2021-09-13 11:32     ` Marc-André Lureau
2021-09-13 11:46       ` Peter Maydell
2021-09-07 12:19 ` [RFC v3 02/32] build-sys: add HAVE_IPPROTO_MPTCP marcandre.lureau
2021-09-08 12:01   ` Markus Armbruster
2021-09-13 13:02   ` Paolo Bonzini
2021-09-07 12:19 ` [RFC v3 03/32] scripts/qapi: teach c_param_type() to return const argument type marcandre.lureau
2021-09-08 12:10   ` Markus Armbruster
2021-09-08 14:33     ` Marc-André Lureau
2021-09-07 12:19 ` [RFC v3 04/32] glib-compat: add G_SIZEOF_MEMBER marcandre.lureau
2021-09-08 12:16   ` Markus Armbruster
2021-09-08 13:49     ` Marc-André Lureau
2021-09-07 12:19 ` [RFC v3 05/32] scripts/qapi: add QAPISchemaVisitor.visit_module_end marcandre.lureau
2021-09-08 12:26   ` Markus Armbruster
2021-09-07 12:19 ` [RFC v3 06/32] scripts/qapi: add a CABI module marcandre.lureau
2021-09-07 12:19 ` [RFC v3 07/32] scripts/qapi: generate CABI dump for C types marcandre.lureau
2021-09-07 12:19 ` [RFC v3 08/32] tests: build qapi-cabi (C ABI dump) marcandre.lureau
2021-09-07 12:19 ` [RFC v3 09/32] build-sys: add i686 cpu target marcandre.lureau
2021-09-08 13:45   ` Peter Maydell
2021-09-07 12:19 ` [RFC v3 10/32] build-sys: add --with-rust{-target} & basic build infrastructure marcandre.lureau
2021-09-08 14:00   ` Peter Maydell
2021-09-08 14:21     ` Marc-André Lureau
2021-09-07 12:19 ` [RFC v3 11/32] build-sys: add a cargo-wrapper script marcandre.lureau
2021-09-07 12:19 ` [RFC v3 12/32] rust: provide a common crate for QEMU marcandre.lureau
2021-09-10  1:18   ` Alistair Francis
2021-09-10  7:43     ` Marc-André Lureau [this message]
2021-09-13 17:11   ` Paolo Bonzini
2021-09-14 11:34     ` Marc-André Lureau
2021-09-07 12:19 ` [RFC v3 13/32] rust: use vendored-sources marcandre.lureau
2021-09-08 15:38   ` Ian Jackson
2021-09-08 15:47     ` Marc-André Lureau
2021-09-08 15:55       ` Ian Jackson
2021-09-08 16:15         ` Marc-André Lureau
2021-09-08 16:22           ` Peter Maydell
2021-09-08 16:22           ` Ian Jackson
2021-09-08 16:20     ` Marc-André Lureau
2021-09-08 16:29       ` Ian Jackson
2021-09-08 16:34         ` Marc-André Lureau
2021-09-08 16:50           ` Ian Jackson
2021-09-08 19:33             ` Marc-André Lureau
2021-09-09 16:02   ` Peter Maydell
2021-09-09 16:29     ` Marc-André Lureau
2021-09-09 16:53       ` Daniel P. Berrangé
2021-09-09 17:04         ` Ian Jackson
2021-09-13 14:21       ` Paolo Bonzini
2021-09-09 16:49     ` Daniel P. Berrangé
2021-09-09 17:02       ` Ian Jackson
2021-09-07 12:19 ` [RFC v3 14/32] scripts/qapi: add QAPISchemaIfCond.rsgen() marcandre.lureau
2021-09-08 12:33   ` Markus Armbruster
2021-09-08 14:06     ` Marc-André Lureau
2021-09-07 12:19 ` [RFC v3 15/32] scripts/qapi: strip trailing whitespaces marcandre.lureau
2021-09-07 12:19 ` [RFC v3 16/32] scripts/qapi: add Rust FFI bindings generation marcandre.lureau
2021-09-07 12:19 ` [RFC v3 17/32] scripts/qapi: learn to generate ABI dump for Rust FFI marcandre.lureau
2021-09-07 12:19 ` [RFC v3 18/32] tests: generate Rust bindings marcandre.lureau
2021-09-07 12:19 ` [RFC v3 19/32] tests: check Rust and C CABI diffs marcandre.lureau
2021-09-07 12:19 ` [RFC v3 20/32] scripts/qapi: generate high-level Rust bindings marcandre.lureau
2021-09-07 12:19 ` [RFC v3 21/32] tests/rust: build a common library, checking bindings compile marcandre.lureau
2021-09-07 12:19 ` [RFC v3 22/32] qga: build qapi-cabi binary (ABI from C) marcandre.lureau
2021-09-07 12:19 ` [RFC v3 23/32] qga/rust: build and link an empty static library marcandre.lureau
2021-09-07 12:19 ` [RFC v3 24/32] qga/rust: generate QGA QAPI types FFI bindings marcandre.lureau
2021-09-07 12:19 ` [RFC v3 25/32] qga/rust: build a qga-cabi-rs executable (ABI from Rust) marcandre.lureau
2021-09-07 12:19 ` [RFC v3 26/32] qga/rust: check the Rust C binding marcandre.lureau
2021-09-07 12:19 ` [RFC v3 27/32] qga/rust: build high-level Rust QAPI types marcandre.lureau
2021-09-07 12:19 ` [RFC v3 28/32] qga/rust: implement get-host-name in Rust (example) marcandre.lureau
2021-09-07 12:19 ` [RFC v3 29/32] qga/rust: implement {get,set}-vcpus " marcandre.lureau
2021-09-07 12:19 ` [RFC v3 30/32] tests/vm: add Rust to FreeBSD VM marcandre.lureau
2021-09-07 12:19 ` [RFC v3 31/32] tests/vm: bump fedora VM to f32 marcandre.lureau
2021-09-07 12:19 ` [RFC v3 32/32] tests/vm: add Rust to Fedora marcandre.lureau
2021-09-08 13:22 ` [RFC v3 00/32] Rust binding for QAPI and qemu-ga QMP handler examples Markus Armbruster
2021-09-08 13:55   ` Marc-André Lureau
2021-09-09 10:31     ` Markus Armbruster
2021-09-09 15:22       ` Marc-André Lureau

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAMxuvaytkGSG+2gjzQJj_C95SxxD9RZ8YD6OT7G4D2cWsrnQyA@mail.gmail.com \
    --to=marcandre.lureau@redhat.com \
    --cc=alistair23@gmail.com \
    --cc=armbru@redhat.com \
    --cc=berrange@redhat.com \
    --cc=pbonzini@redhat.com \
    --cc=qemu-devel@nongnu.org \
    --cc=stefanha@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).