rpm-ostree/rust/src/cxxrsutil.rs
Colin Walters ac92456bdd postprocess: Move script execution to Rust
Continuing oxidation.
2021-03-15 13:18:57 +00:00

221 lines
7.4 KiB
Rust

//! Wrappers that bridge the world of cxx.rs and GObject-introspect based crate bindings.
//! See https://github.com/dtolnay/cxx/issues/544
//! While cxx.rs supports including externally-bound types (like ostree::Repo),
//! two things make this more complicated. First, cxx.rs requires implementing
//! a trait, and due to the orphan rule we can't implement foreign traits on
//! foreign types. Second, what we *actually* want on the Rust side isn't
//! the *_sys type (e.g. ostree_sys::OstreeRepo) but the `ostree::Repo` type.
//! So for now, we define a `FFIGObjectWrapper` trait that helps with this.
//! In the future though hopefully cxx.rs improves this situation.
// SPDX-License-Identifier: Apache-2.0 OR MIT
use cxx::{type_id, ExternType};
use glib::translate::ToGlibPtr;
use paste::paste;
use std::pin::Pin;
/// Map an empty string to a `None`.
pub(crate) fn opt_string(input: &str) -> Option<&str> {
// TODO(lucab): drop this polyfill once cxx-rs starts supporting Option<_>.
Some(input).filter(|s| !s.is_empty())
}
/// A custom trait used to translate a *_sys C type wrapper
/// to its GObject version.
pub trait FFIGObjectWrapper {
type Wrapper;
/// Use this function in Rust code that accepts glib-rs
/// objects passed via cxx-rs to synthesize the expected glib-rs
/// wrapper type.
fn gobj_wrap(&mut self) -> Self::Wrapper;
}
pub trait FFIGObjectReWrap<'a> {
type ReWrapped;
/// Convert a glib-rs wrapper object into a Pin pointer
/// to our FFI newtype. This is necessary to call
/// cxx-rs wrapped functions from Rust.
fn gobj_rewrap(&'a self) -> Pin<&'a mut Self::ReWrapped>;
}
/// Implement FFIGObjectWrapper given a pair of wrapper type
/// and sys type.
macro_rules! impl_wrap {
($w:ident, $bound:path, $sys:path) => {
impl FFIGObjectWrapper for $w {
type Wrapper = $bound;
fn gobj_wrap(&mut self) -> Self::Wrapper {
unsafe { glib::translate::from_glib_none(&mut self.0 as *mut _) }
}
}
impl<'a> FFIGObjectReWrap<'a> for $bound {
type ReWrapped = $w;
fn gobj_rewrap(&'a self) -> Pin<&'a mut Self::ReWrapped> {
// Access the underlying raw pointer behind the glib-rs
// newtype wrapper, e.g. `ostree_sys::OstreeRepo`.
let p: *mut $sys = self.to_glib_none().0;
// Safety: Pin<T> is a #[repr(transparent)] newtype wrapper
// around our #[repr(transparent)] FFI newtype wrapper which
// for the glib-rs newtype wrapper, which finally holds the real
// raw pointer. Phew!
// In other words: Pin(FFINewType(GlibRs(RawPointer)))
// Here we're just powering through those layers of wrappers to
// convert the raw pointer. See also https://internals.rust-lang.org/t/pre-rfc-v2-safe-transmute/11431
//
// However, since what we're handing out is a raw pointer,
// we ensure that the lifetime of our return value is tied to
// that of the glib-rs wrapper (which holds a GObject strong reference),
// which ensures the value isn't freed.
unsafe { std::mem::transmute(p) }
}
}
};
}
/// Custom macro to bind gtk-rs bridged types.
macro_rules! cxxrs_bind {
($ns:ident, $lowerns:ident, $sys:ident, [ $( $i:ident ),* ]) => {
paste! {
$(
#[repr(transparent)]
pub struct [<FFI $ns $i>](pub(crate) $sys::[<$ns $i>]);
unsafe impl ExternType for [<FFI $ns $i>] {
type Id = type_id!(rpmostreecxx::[<$ns $i>]);
type Kind = cxx::kind::Trivial;
}
impl_wrap!([<FFI $ns $i>], $lowerns::$i, $sys::[<$ns $i>]);
)*
}
};
}
// When extending this list, also update rpmostree-cxxrs-prelude.h and lib.rs
// This macro is special to ostree types currently.
cxxrs_bind!(Ostree, ostree, ostree_sys, [Sysroot, Repo, Deployment]);
cxxrs_bind!(G, glib, gobject_sys, [Object]);
cxxrs_bind!(G, gio, gio_sys, [Cancellable, DBusConnection]);
cxxrs_bind!(G, glib, glib_sys, [Variant, VariantDict]);
// An error type helper; separate from the GObject bridging
mod err {
use std::error::Error as StdError;
use std::fmt::Display;
use std::io::Error as IoError;
// See the documentation for CxxResult
#[derive(Debug)]
pub(crate) struct CxxError(String);
impl Display for CxxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
impl StdError for CxxError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
None
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn StdError> {
None
}
}
impl From<anyhow::Error> for CxxError {
fn from(v: anyhow::Error) -> Self {
Self(format!("{:#}", v))
}
}
impl From<cxx::Exception> for CxxError {
fn from(v: cxx::Exception) -> Self {
Self(format!("{:#}", v))
}
}
impl From<IoError> for CxxError {
fn from(v: IoError) -> Self {
Self(format!("{}", v))
}
}
impl From<nix::Error> for CxxError {
fn from(v: nix::Error) -> Self {
Self(format!("{}", v))
}
}
impl From<glib::error::Error> for CxxError {
fn from(v: glib::error::Error) -> Self {
Self(format!("{}", v))
}
}
impl CxxError {
/// Prefix an error message with `<context>: `. See
/// https://docs.rs/anyhow/1.0.38/anyhow/struct.Error.html#method.context
/// This is necessary for use with the `fn-error-context` crate.
pub(crate) fn context<C>(self, context: C) -> Self
where
C: Display + Send + Sync + 'static,
{
Self(format!("{}: {}", context.to_string(), self))
}
}
// Use this on exit from Rust functions that return to C++ (bridged via cxx-rs).
// This is a workaround for https://github.com/dtolnay/cxx/issues/290#issuecomment-756432907
// which is that cxx-rs only shows the first entry in the cause chain.
pub(crate) type CxxResult<T> = std::result::Result<T, CxxError>;
#[cfg(test)]
mod tests {
use super::*;
use fn_error_context::context;
#[test]
fn throwchain() {
#[context("outer")]
fn outer() -> CxxResult<()> {
#[context("inner")]
fn inner() -> anyhow::Result<()> {
anyhow::bail!("oops")
}
Ok(inner()?)
}
assert_eq!(format!("{}", outer().err().unwrap()), "outer: inner: oops")
}
}
}
pub(crate) use err::*;
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
#[test]
fn passthrough() -> Result<()> {
let cancellable = gio::NONE_CANCELLABLE;
let td = tempfile::tempdir()?;
let p = td.path().join("repo");
let r = ostree::Repo::new_for_path(&p);
r.create(ostree::RepoMode::Archive, cancellable)?;
let fd = r.get_dfd();
assert_eq!(
fd,
crate::ffi::testutil_validate_cxxrs_passthrough(r.gobj_rewrap())
);
Ok(())
}
}