From 355f7db5cc0ef257c4c1d3a227778a51d99d9c01 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sun, 27 Oct 2024 21:41:38 +0100 Subject: [PATCH 1/6] Add convenience function to return a result --- examples/gio_dbus_register_object/main.rs | 17 ++++++++++------- gio/src/dbus_method_invocation.rs | 21 ++++++++++++++++++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/examples/gio_dbus_register_object/main.rs b/examples/gio_dbus_register_object/main.rs index 8915dd9561a3..de36ca577848 100644 --- a/examples/gio_dbus_register_object/main.rs +++ b/examples/gio_dbus_register_object/main.rs @@ -28,13 +28,16 @@ fn on_startup(app: &gio::Application, tx: &Sender) { 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"); - } + let result = <(String,)>::from_variant(¶ms) + .ok_or_else(|| { + glib::Error::new(gio::IOErrorEnum::Failed, "Invalid parameters") + }) + .map(|(name,)| { + let greet = format!("Hello {name}!"); + println!("{greet}"); + Some(greet.to_variant()) + }); + invocation.return_result(result); } _ => unreachable!(), } diff --git a/gio/src/dbus_method_invocation.rs b/gio/src/dbus_method_invocation.rs index 01d8cf3869c3..f4fca0da9dc0 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,23 @@ 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), + } + } } From 9615413b7d570ad7adcae6a6542c188718fe72ba Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sun, 27 Oct 2024 21:51:43 +0100 Subject: [PATCH 2/6] Streamline call dispatching in example --- examples/gio_dbus_register_object/main.rs | 43 +++++++++++++++-------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/examples/gio_dbus_register_object/main.rs b/examples/gio_dbus_register_object/main.rs index de36ca577848..8cb403ca95f4 100644 --- a/examples/gio_dbus_register_object/main.rs +++ b/examples/gio_dbus_register_object/main.rs @@ -1,4 +1,4 @@ -use gio::prelude::*; +use gio::{prelude::*, IOErrorEnum}; use std::sync::mpsc::{channel, Receiver, Sender}; const EXAMPLE_XML: &str = r#" @@ -12,6 +12,26 @@ const EXAMPLE_XML: &str = r#" "#; +#[derive(Debug, glib::Variant)] +struct Hello { + name: String, +} + +#[derive(Debug)] +enum Call { + Hello(Hello), +} + +impl Call { + pub fn parse(method: &str, parameters: glib::Variant) -> Result { + match method { + "Hello" => Ok(parameters.get::().map(Call::Hello)), + _ => 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"); @@ -26,21 +46,14 @@ fn on_startup(app: &gio::Application, tx: &Sender) { #[strong] app, move |_, _, _, _, method, params, invocation| { - match method { - "Hello" => { - let result = <(String,)>::from_variant(¶ms) - .ok_or_else(|| { - glib::Error::new(gio::IOErrorEnum::Failed, "Invalid parameters") - }) - .map(|(name,)| { - let greet = format!("Hello {name}!"); - println!("{greet}"); - Some(greet.to_variant()) - }); - invocation.return_result(result); + let result = Call::parse(method, params).map(|call| match call { + Call::Hello(Hello { name }) => { + let greet = format!("Hello {name}!"); + println!("{greet}"); + Some(greet.to_variant()) } - _ => unreachable!(), - } + }); + invocation.return_result(result); app.quit(); } )) From cbd84ae986dcb4761c1ef0dccd74d4c638de45ed Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 28 Oct 2024 17:38:50 +0100 Subject: [PATCH 3/6] Add helper to return result from a future --- gio/src/dbus_method_invocation.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/gio/src/dbus_method_invocation.rs b/gio/src/dbus_method_invocation.rs index f4fca0da9dc0..65150a8a4afd 100644 --- a/gio/src/dbus_method_invocation.rs +++ b/gio/src/dbus_method_invocation.rs @@ -45,4 +45,25 @@ impl DBusMethodInvocation { 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); + }) + } } From 39b18f853daa1664d79afb2926a00ac8c80511d2 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 28 Oct 2024 17:39:17 +0100 Subject: [PATCH 4/6] Demo async call handling for gdbus --- examples/gio_dbus_register_object/main.rs | 43 ++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/examples/gio_dbus_register_object/main.rs b/examples/gio_dbus_register_object/main.rs index 8cb403ca95f4..37a4143e8453 100644 --- a/examples/gio_dbus_register_object/main.rs +++ b/examples/gio_dbus_register_object/main.rs @@ -1,5 +1,8 @@ use gio::{prelude::*, IOErrorEnum}; -use std::sync::mpsc::{channel, Receiver, Sender}; +use std::{ + sync::mpsc::{channel, Receiver, Sender}, + time::Duration, +}; const EXAMPLE_XML: &str = r#" @@ -8,6 +11,11 @@ const EXAMPLE_XML: &str = r#" + + + + + "#; @@ -17,15 +25,23 @@ struct Hello { name: String, } +#[derive(Debug, glib::Variant)] +struct SlowHello { + name: String, + delay: u32, +} + #[derive(Debug)] enum Call { Hello(Hello), + SlowHello(SlowHello), } impl Call { pub fn parse(method: &str, parameters: glib::Variant) -> Result { match method { "Hello" => Ok(parameters.get::().map(Call::Hello)), + "SlowHello" => Ok(parameters.get::().map(Call::SlowHello)), _ => Err(glib::Error::new(IOErrorEnum::Failed, "No such method")), } .and_then(|p| p.ok_or_else(|| glib::Error::new(IOErrorEnum::Failed, "Invalid parameters"))) @@ -42,21 +58,24 @@ 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| { - let result = Call::parse(method, params).map(|call| match call { + .method_call(move |_, _, _, _, method, params, invocation| { + let call = Call::parse(method, params); + invocation.return_future_local(async move { + match call? { Call::Hello(Hello { name }) => { let greet = format!("Hello {name}!"); println!("{greet}"); - Some(greet.to_variant()) + Ok(Some(greet.to_variant())) + } + Call::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())) } - }); - invocation.return_result(result); - app.quit(); - } - )) + } + }); + }) .build() { println!("Registered object"); From 22688c1a72cbd47fa6ade924c181b1ca3bfe42d5 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Thu, 31 Oct 2024 07:54:34 +0100 Subject: [PATCH 5/6] Add DBusMethodCall convenience trait This trait represents a parsed method call with deserialized arguments, to abstract over call parsing. Then add new registration builder helpers to register method calls with a simplified callback which receives parsed arguments, and can optionally return an async result. --- examples/gio_dbus_register_object/main.rs | 30 +++--- gio/src/dbus_connection.rs | 120 +++++++++++++++++++++- gio/src/prelude.rs | 9 +- 3 files changed, 139 insertions(+), 20 deletions(-) diff --git a/examples/gio_dbus_register_object/main.rs b/examples/gio_dbus_register_object/main.rs index 37a4143e8453..f3422e232aeb 100644 --- a/examples/gio_dbus_register_object/main.rs +++ b/examples/gio_dbus_register_object/main.rs @@ -32,16 +32,21 @@ struct SlowHello { } #[derive(Debug)] -enum Call { +enum HelloMethod { Hello(Hello), SlowHello(SlowHello), } -impl Call { - pub fn parse(method: &str, parameters: glib::Variant) -> Result { +impl DBusMethodCall for HelloMethod { + fn parse_call( + _obj_path: &str, + _interface: &str, + method: &str, + params: glib::Variant, + ) -> Result { match method { - "Hello" => Ok(parameters.get::().map(Call::Hello)), - "SlowHello" => Ok(parameters.get::().map(Call::SlowHello)), + "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"))) @@ -58,23 +63,24 @@ fn on_startup(app: &gio::Application, tx: &Sender) { if let Ok(id) = connection .register_object("/com/github/gtk_rs/examples/HelloWorld", &example) - .method_call(move |_, _, _, _, method, params, invocation| { - let call = Call::parse(method, params); - invocation.return_future_local(async move { - match call? { - Call::Hello(Hello { name }) => { + .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())) } - Call::SlowHello(SlowHello { name, delay }) => { + 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())) } } - }); + } }) .build() { 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/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, From 7bb83b2daeaad01cba5d7f7e7fc6d9abee841aa0 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Fri, 1 Nov 2024 18:09:30 +0100 Subject: [PATCH 6/6] Fix typo to get Github actions to pass --- glib/src/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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);