diff --git a/examples/gio_dbus_register_object/main.rs b/examples/gio_dbus_register_object/main.rs index 8915dd9561a3..f3422e232aeb 100644 --- a/examples/gio_dbus_register_object/main.rs +++ b/examples/gio_dbus_register_object/main.rs @@ -1,5 +1,8 @@ -use gio::prelude::*; -use std::sync::mpsc::{channel, Receiver, Sender}; +use gio::{prelude::*, IOErrorEnum}; +use std::{ + sync::mpsc::{channel, Receiver, Sender}, + time::Duration, +}; const EXAMPLE_XML: &str = r#" @@ -8,10 +11,48 @@ const EXAMPLE_XML: &str = r#" + + + + + "#; +#[derive(Debug, glib::Variant)] +struct Hello { + name: String, +} + +#[derive(Debug, glib::Variant)] +struct SlowHello { + name: String, + delay: u32, +} + +#[derive(Debug)] +enum HelloMethod { + Hello(Hello), + SlowHello(SlowHello), +} + +impl DBusMethodCall for HelloMethod { + fn parse_call( + _obj_path: &str, + _interface: &str, + method: &str, + params: glib::Variant, + ) -> Result { + match method { + "Hello" => Ok(params.get::().map(Self::Hello)), + "SlowHello" => Ok(params.get::().map(Self::SlowHello)), + _ => Err(glib::Error::new(IOErrorEnum::Failed, "No such method")), + } + .and_then(|p| p.ok_or_else(|| glib::Error::new(IOErrorEnum::Failed, "Invalid parameters"))) + } +} + fn on_startup(app: &gio::Application, tx: &Sender) { let connection = app.dbus_connection().expect("connection"); @@ -22,25 +63,25 @@ fn on_startup(app: &gio::Application, tx: &Sender) { if let Ok(id) = connection .register_object("/com/github/gtk_rs/examples/HelloWorld", &example) - .method_call(glib::clone!( - #[strong] - app, - move |_, _, _, _, method, params, invocation| { - match method { - "Hello" => { - if let Some((name,)) = <(String,)>::from_variant(¶ms) { - let greet = format!("Hello {name}!"); - println!("{greet}"); - invocation.return_value(Some(&(greet,).to_variant())); - } else { - invocation.return_error(gio::IOErrorEnum::Failed, "Invalid parameters"); - } + .typed_method_call::() + .invoke_and_return_future_local(|_, sender, call| { + println!("Method call from {sender}"); + async { + match call { + HelloMethod::Hello(Hello { name }) => { + let greet = format!("Hello {name}!"); + println!("{greet}"); + Ok(Some(greet.to_variant())) + } + HelloMethod::SlowHello(SlowHello { name, delay }) => { + glib::timeout_future(Duration::from_secs(delay as u64)).await; + let greet = format!("Hello {name} after {delay} seconds!"); + println!("{greet}"); + Ok(Some(greet.to_variant())) } - _ => unreachable!(), } - app.quit(); } - )) + }) .build() { println!("Registered object"); diff --git a/gio/src/dbus_connection.rs b/gio/src/dbus_connection.rs index 982c2f8a164c..d9b15e98cb1e 100644 --- a/gio/src/dbus_connection.rs +++ b/gio/src/dbus_connection.rs @@ -1,13 +1,110 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use std::{boxed::Box as Box_, num::NonZeroU32}; - -use glib::{prelude::*, translate::*}; +use std::{boxed::Box as Box_, future::Future, marker::PhantomData, num::NonZeroU32}; use crate::{ ffi, ActionGroup, DBusConnection, DBusInterfaceInfo, DBusMessage, DBusMethodInvocation, DBusSignalFlags, MenuModel, }; +use glib::{prelude::*, translate::*}; + +pub trait DBusMethodCall: Sized { + fn parse_call( + obj_path: &str, + interface: &str, + method: &str, + params: glib::Variant, + ) -> Result; +} + +// rustdoc-stripper-ignore-next +/// Handle method invocations. +pub struct MethodCallBuilder<'a, T> { + registration: RegistrationBuilder<'a>, + capture_type: PhantomData, +} + +impl<'a, T: DBusMethodCall> MethodCallBuilder<'a, T> { + // rustdoc-stripper-ignore-next + /// Handle invocation of a parsed method call. + /// + /// For each DBus method call parse the call, and then invoke the given closure + /// with + /// + /// 1. the DBus connection object, + /// 2. the name of the sender of the method call, + /// 3. the parsed call, and + /// 4. the method invocation object. + /// + /// The closure **must** return a value through the invocation object in all + /// code paths, using any of its `return_` functions, such as + /// [`DBusMethodInvocation::return_result`] or + /// [`DBusMethodInvocation::return_future_local`], to finish the call. + /// + /// If direct access to the invocation object is not needed, + /// [`invoke_and_return`] and [`invoke_and_return_future_local`] provide a + /// safer interface where the callback returns a result directly. + pub fn invoke(self, f: F) -> RegistrationBuilder<'a> + where + F: Fn(DBusConnection, &str, T, DBusMethodInvocation) + 'static, + { + self.registration.method_call( + move |connection, sender, obj_path, interface, method, params, invocation| { + match T::parse_call(obj_path, interface, method, params) { + Ok(call) => f(connection, sender, call, invocation), + Err(error) => invocation.return_gerror(error), + } + }, + ) + } + + // rustdoc-stripper-ignore-next + /// Handle invocation of a parsed method call. + /// + /// For each DBus method call parse the call, and then invoke the given closure + /// with + /// + /// 1. the DBus connection object, + /// 2. the name of the sender of the method call, and + /// 3. the parsed call. + /// + /// The return value of the closure is then returned on the method call. + /// If the returned variant value is not a tuple, it is automatically wrapped + /// in a single element tuple, as DBus methods must always return tuples. + /// See [`DBusMethodInvocation::return_result`] for details. + pub fn invoke_and_return(self, f: F) -> RegistrationBuilder<'a> + where + F: Fn(DBusConnection, &str, T) -> Result, glib::Error> + 'static, + { + self.invoke(move |connection, sender, call, invocation| { + invocation.return_result(f(connection, sender, call)) + }) + } + + // rustdoc-stripper-ignore-next + /// Handle an async invocation of a parsed method call. + /// + /// For each DBus method call parse the call, and then invoke the given closure + /// with + /// + /// 1. the DBus connection object, + /// 2. the name of the sender of the method call, and + /// 3. the parsed call. + /// + /// The output of the future is then returned on the method call. + /// If the returned variant value is not a tuple, it is automatically wrapped + /// in a single element tuple, as DBus methods must always return tuples. + /// See [`DBusMethodInvocation::return_future_local`] for details. + pub fn invoke_and_return_future_local(self, f: F) -> RegistrationBuilder<'a> + where + F: Fn(DBusConnection, &str, T) -> Fut + 'static, + Fut: Future, glib::Error>> + 'static, + { + self.invoke(move |connection, sender, call, invocation| { + invocation.return_future_local(f(connection, sender, call)); + }) + } +} #[derive(Debug, Eq, PartialEq)] pub struct RegistrationId(NonZeroU32); @@ -22,6 +119,8 @@ pub struct FilterId(NonZeroU32); #[derive(Debug, Eq, PartialEq)] pub struct SignalSubscriptionId(NonZeroU32); +// rustdoc-stripper-ignore-next +/// Build a registered DBus object, by handling different parts of DBus. #[must_use = "The builder must be built to be used"] pub struct RegistrationBuilder<'a> { connection: &'a DBusConnection, @@ -38,7 +137,7 @@ pub struct RegistrationBuilder<'a> { Option bool>>, } -impl RegistrationBuilder<'_> { +impl<'a> RegistrationBuilder<'a> { pub fn method_call< F: Fn(DBusConnection, &str, &str, &str, &str, glib::Variant, DBusMethodInvocation) + 'static, >( @@ -49,6 +148,19 @@ impl RegistrationBuilder<'_> { self } + // rustdoc-stripper-ignore-next + /// Handle method calls on this object. + /// + /// Return a builder for method calls which parses method names and + /// parameters with the given [`DBusMethodCall`] and then allows to dispatch + /// the parsed call either synchronously or asynchronously. + pub fn typed_method_call(self) -> MethodCallBuilder<'a, T> { + MethodCallBuilder { + registration: self, + capture_type: Default::default(), + } + } + #[doc(alias = "get_property")] pub fn property glib::Variant + 'static>( mut self, diff --git a/gio/src/dbus_method_invocation.rs b/gio/src/dbus_method_invocation.rs index 01d8cf3869c3..65150a8a4afd 100644 --- a/gio/src/dbus_method_invocation.rs +++ b/gio/src/dbus_method_invocation.rs @@ -1,6 +1,6 @@ // Take a look at the license at the top of the repository in the LICENSE file. -use glib::{prelude::*, translate::*}; +use glib::{prelude::*, translate::*, VariantTy}; use crate::{ffi, DBusMethodInvocation}; @@ -26,4 +26,44 @@ impl DBusMethodInvocation { ); } } + + // rustdoc-stripper-ignore-next + /// Return a result for this invocation. + /// + /// If `Ok` return the contained value with [`return_value`]. If the return + /// value is not a tuple, automatically convert it to a one-element tuple, as + /// DBus return values must be tuples. + /// + /// If `Err` return the contained error with [`return_gerror`]. + pub fn return_result(self, result: Result, glib::Error>) { + match result { + Ok(Some(value)) if !value.is_type(VariantTy::TUPLE) => { + let tupled = glib::Variant::tuple_from_iter(std::iter::once(value)); + self.return_value(Some(&tupled)); + } + Ok(value) => self.return_value(value.as_ref()), + Err(error) => self.return_gerror(error), + } + } + + // rustdoc-stripper-ignore-next + /// Return an async result for this invocation. + /// + /// Spawn the given future on the thread-default main context, and return the + /// the result with [`return_result`]. Specifically, if a variant is returned + /// that is not a tuple it is automatically wrapped into a tuple. + /// + /// The given `Future` does not have to be `Send`. + /// + /// This can be called only from the thread where the main context is running, e.g. + /// from any other `Future` that is executed on this main context, or after calling + /// `with_thread_default` or `acquire` on the main context. + pub fn return_future_local(self, f: F) -> glib::JoinHandle<()> + where + F: std::future::Future, glib::Error>> + 'static, + { + glib::spawn_future_local(async move { + self.return_result(f.await); + }) + } } diff --git a/gio/src/prelude.rs b/gio/src/prelude.rs index 9c9e1998c57c..05bef53b4480 100644 --- a/gio/src/prelude.rs +++ b/gio/src/prelude.rs @@ -35,10 +35,11 @@ pub use crate::{ action_map::ActionMapExtManual, application::ApplicationExtManual, auto::traits::*, cancellable::CancellableExtManual, converter::ConverterExtManual, data_input_stream::DataInputStreamExtManual, datagram_based::DatagramBasedExtManual, - dbus_proxy::DBusProxyExtManual, file::FileExtManual, file_enumerator::FileEnumeratorExtManual, - inet_address::InetAddressExtManual, input_stream::InputStreamExtManual, - io_stream::IOStreamExtManual, list_model::ListModelExtManual, - output_stream::OutputStreamExtManual, pollable_input_stream::PollableInputStreamExtManual, + dbus_connection::DBusMethodCall, dbus_proxy::DBusProxyExtManual, file::FileExtManual, + file_enumerator::FileEnumeratorExtManual, inet_address::InetAddressExtManual, + input_stream::InputStreamExtManual, io_stream::IOStreamExtManual, + list_model::ListModelExtManual, output_stream::OutputStreamExtManual, + pollable_input_stream::PollableInputStreamExtManual, pollable_output_stream::PollableOutputStreamExtManual, settings::SettingsExtManual, simple_proxy_resolver::SimpleProxyResolverExtManual, socket::SocketExtManual, socket_control_message::SocketControlMessageExtManual, diff --git a/glib/src/source.rs b/glib/src/source.rs index ba0ba5f7e85d..a1cf045b754b 100644 --- a/glib/src/source.rs +++ b/glib/src/source.rs @@ -53,7 +53,7 @@ impl FromGlib for SourceId { } // rustdoc-stripper-ignore-next -/// Process identificator +/// Process identifier #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[doc(alias = "GPid")] pub struct Pid(pub ffi::GPid);