diff mbox series

[26/26] rust: callbacks: allow passing optional callbacks as ()

Message ID 20241209123717.99077-27-pbonzini@redhat.com (mailing list archive)
State New
Headers show
Series rust: bundle of prerequisites for HPET implementation | expand

Commit Message

Paolo Bonzini Dec. 9, 2024, 12:37 p.m. UTC
In some cases, callbacks are optional.  Using "Some(function)" and "None"
does not work well, because when someone writes "None" the compiler does
not know what to use for "F" in "Option<F>".

Therefore, adopt () to mean a "null" callback.  It is possible to enforce
that a callback is valid by adding a "let _: () = F::ASSERT_IS_SOME" before
the invocation of F::call.

Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
 rust/qemu-api/src/callbacks.rs | 97 ++++++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)

Comments

Zhao Liu Dec. 17, 2024, 4:13 p.m. UTC | #1
On Mon, Dec 09, 2024 at 01:37:17PM +0100, Paolo Bonzini wrote:
> Date: Mon,  9 Dec 2024 13:37:17 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH 26/26] rust: callbacks: allow passing optional callbacks as
>  ()
> X-Mailer: git-send-email 2.47.1
> 
> In some cases, callbacks are optional.  Using "Some(function)" and "None"
> does not work well, because when someone writes "None" the compiler does
> not know what to use for "F" in "Option<F>".

I understand the direct use case is MemoryRegionOps, which has optional
callbacks. However, I'm not quite sure how exactly it should be applied
to C bindings and how it will play with Option<callback>.

Could u pls provide a simple example?

> Therefore, adopt () to mean a "null" callback.  It is possible to enforce
> that a callback is valid by adding a "let _: () = F::ASSERT_IS_SOME" before
> the invocation of F::call.
> 
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
>  rust/qemu-api/src/callbacks.rs | 97 ++++++++++++++++++++++++++++++++++
>  1 file changed, 97 insertions(+)
> 
> diff --git a/rust/qemu-api/src/callbacks.rs b/rust/qemu-api/src/callbacks.rs
> index 6401d807198..83c681d6478 100644
> --- a/rust/qemu-api/src/callbacks.rs
> +++ b/rust/qemu-api/src/callbacks.rs
> @@ -76,6 +76,31 @@
>  /// call_it(&move |_| String::from(x), "hello workd");

typo: s/workd/word/ (in previous patch :-))
Paolo Bonzini Dec. 17, 2024, 4:40 p.m. UTC | #2
Il mar 17 dic 2024, 16:55 Zhao Liu <zhao1.liu@intel.com> ha scritto:

> On Mon, Dec 09, 2024 at 01:37:17PM +0100, Paolo Bonzini wrote:
> > Date: Mon,  9 Dec 2024 13:37:17 +0100
> > From: Paolo Bonzini <pbonzini@redhat.com>
> > Subject: [PATCH 26/26] rust: callbacks: allow passing optional callbacks
> as
> >  ()
> > X-Mailer: git-send-email 2.47.1
> >
> > In some cases, callbacks are optional.  Using "Some(function)" and "None"
> > does not work well, because when someone writes "None" the compiler does
> > not know what to use for "F" in "Option<F>".
>
> I understand the direct use case is MemoryRegionOps, which has optional
> callbacks. However, I'm not quite sure how exactly it should be applied
> to C bindings and how it will play with Option<callback>.
>

You wouldn't use Option<callback> at all, using () instead of None; the
difference is that () does not have a parameter while None does (and the
compiler cannot infer it). But I am okay with leaving this patch behind
until there's a need.

Paolo

Could u pls provide a simple example?
>
> > Therefore, adopt () to mean a "null" callback.  It is possible to enforce
> > that a callback is valid by adding a "let _: () = F::ASSERT_IS_SOME"
> before
> > the invocation of F::call.
> >
> > Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> > ---
> >  rust/qemu-api/src/callbacks.rs | 97 ++++++++++++++++++++++++++++++++++
> >  1 file changed, 97 insertions(+)
> >
> > diff --git a/rust/qemu-api/src/callbacks.rs b/rust/qemu-api/src/
> callbacks.rs
> > index 6401d807198..83c681d6478 100644
> > --- a/rust/qemu-api/src/callbacks.rs
> > +++ b/rust/qemu-api/src/callbacks.rs
> > @@ -76,6 +76,31 @@
> >  /// call_it(&move |_| String::from(x), "hello workd");
>
> typo: s/workd/word/ (in previous patch :-))
>
>
Zhao Liu Dec. 18, 2024, 7:09 a.m. UTC | #3
On Tue, Dec 17, 2024 at 05:40:14PM +0100, Paolo Bonzini wrote:
> Date: Tue, 17 Dec 2024 17:40:14 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: Re: [PATCH 26/26] rust: callbacks: allow passing optional
>  callbacks as ()
> 
> Il mar 17 dic 2024, 16:55 Zhao Liu <zhao1.liu@intel.com> ha scritto:
> 
> > On Mon, Dec 09, 2024 at 01:37:17PM +0100, Paolo Bonzini wrote:
> > > Date: Mon,  9 Dec 2024 13:37:17 +0100
> > > From: Paolo Bonzini <pbonzini@redhat.com>
> > > Subject: [PATCH 26/26] rust: callbacks: allow passing optional callbacks
> > as
> > >  ()
> > > X-Mailer: git-send-email 2.47.1
> > >
> > > In some cases, callbacks are optional.  Using "Some(function)" and "None"
> > > does not work well, because when someone writes "None" the compiler does
> > > not know what to use for "F" in "Option<F>".
> >
> > I understand the direct use case is MemoryRegionOps, which has optional
> > callbacks. However, I'm not quite sure how exactly it should be applied
> > to C bindings and how it will play with Option<callback>.
> >
> 
> You wouldn't use Option<callback> at all, using () instead of None; the
> difference is that () does not have a parameter while None does (and the
> compiler cannot infer it). But I am okay with leaving this patch behind
> until there's a need.

Am I using the wrong terminology? Function pointers in a structure should
be called a vtable, rather than callbacks (for example, methods in TypeInfo,
read/write methods in MemoryRegionOps). Callbacks are typically function
pointers used as function parameters (for example, timer/gpio). So, is the
callback implementation here only used for the latter case?

Thanks,
Zhao
Paolo Bonzini Dec. 18, 2024, 7:32 a.m. UTC | #4
Il mer 18 dic 2024, 07:50 Zhao Liu <zhao1.liu@intel.com> ha scritto:

> Am I using the wrong terminology? Function pointers in a structure should
> be called a vtable, rather than callbacks (for example, methods in
> TypeInfo,
> read/write methods in MemoryRegionOps). Callbacks are typically function
> pointers used as function parameters (for example, timer/gpio). So, is the
> callback implementation here only used for the latter case?
>

The callback implementation is not used for QOM indeed. In that case, using
FnCall would require something like

const UNPARENT: impl FnCall((&Self,));

which does not exist as far as I know?

MemoryRegionOps is a mix of callbacks and a vtable.  From the Rust point of
view, with the API that uses the builder pattern, MemoryRegionOps (and
VMStateDescription too) would be closer to callbacks. Instead when you use
traits and fill in the class object, that's clearly a vtable.

But in this sense MemoryRegionOps do not need optional callbacks. You just
don't call the ops.read() method if you don't need to set a read callback
for example. So I am not sure if anything that is planned right now needs
the optional callbacks. It's good to have the patch for the future but it's
not necessary right now.

Paolo
Zhao Liu Dec. 18, 2024, 3:09 p.m. UTC | #5
On Wed, Dec 18, 2024 at 08:32:59AM +0100, Paolo Bonzini wrote:
> Date: Wed, 18 Dec 2024 08:32:59 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: Re: [PATCH 26/26] rust: callbacks: allow passing optional
>  callbacks as ()
> 
> Il mer 18 dic 2024, 07:50 Zhao Liu <zhao1.liu@intel.com> ha scritto:
> 
> > Am I using the wrong terminology? Function pointers in a structure should
> > be called a vtable, rather than callbacks (for example, methods in
> > TypeInfo,
> > read/write methods in MemoryRegionOps). Callbacks are typically function
> > pointers used as function parameters (for example, timer/gpio). So, is the
> > callback implementation here only used for the latter case?
> >
> 
> The callback implementation is not used for QOM indeed. In that case, using
> FnCall would require something like
> 
> const UNPARENT: impl FnCall((&Self,));
> 
> which does not exist as far as I know?

Yes, it's incorrect. (I know you have some magic, so I have this question
to see your idea.)

> MemoryRegionOps is a mix of callbacks and a vtable.  From the Rust point of
> view, with the API that uses the builder pattern, MemoryRegionOps (and
> VMStateDescription too) would be closer to callbacks. Instead when you use
> traits and fill in the class object, that's clearly a vtable.
> 
> But in this sense MemoryRegionOps do not need optional callbacks. You just
> don't call the ops.read() method if you don't need to set a read callback
> for example. So I am not sure if anything that is planned right now needs
> the optional callbacks. It's good to have the patch for the future but it's
> not necessary right now.

Thanks for your explaination. I agree your plan. I also think it will be
useful in the future.

Regards,
Zhao
diff mbox series

Patch

diff --git a/rust/qemu-api/src/callbacks.rs b/rust/qemu-api/src/callbacks.rs
index 6401d807198..83c681d6478 100644
--- a/rust/qemu-api/src/callbacks.rs
+++ b/rust/qemu-api/src/callbacks.rs
@@ -76,6 +76,31 @@ 
 /// call_it(&move |_| String::from(x), "hello workd");
 /// ```
 ///
+/// `()` can be used to indicate "no function":
+///
+/// ```
+/// # use qemu_api::callbacks::FnCall;
+/// fn optional<F: for<'a> FnCall<(&'a str,), String>>(_f: &F, s: &str) -> Option<String> {
+///     if F::IS_SOME {
+///         Some(F::call((s,)))
+///     } else {
+///         None
+///     }
+/// }
+///
+/// assert!(optional(&(), "hello world").is_none());
+/// ```
+///
+/// Invoking `F::call` will then be a run-time error.
+///
+/// ```should_panic
+/// # use qemu_api::callbacks::FnCall;
+/// # fn call_it<F: for<'a> FnCall<(&'a str,), String>>(_f: &F, s: &str) -> String {
+/// #     F::call((s,))
+/// # }
+/// let s: String = call_it(&(), "hello world"); // panics
+/// ```
+///
 /// # Safety
 ///
 /// Because `Self` is a zero-sized type, all instances of the type are
@@ -90,10 +115,70 @@  pub unsafe trait FnCall<Args, R = ()>: 'static + Sync + Sized {
     /// Rust 1.79.0+.
     const ASSERT_ZERO_SIZED: () = { assert!(mem::size_of::<Self>() == 0) };
 
+    /// Referring to this constant asserts that the `Self` type is an actual
+    /// function type, which can be used to catch incorrect use of `()`
+    /// at  compile time.
+    ///
+    /// # Examples
+    ///
+    /// ```compile_fail
+    /// # use qemu_api::callbacks::FnCall;
+    /// fn call_it<F: for<'a> FnCall<(&'a str,), String>>(_f: &F, s: &str) -> String {
+    ///     let _: () = F::ASSERT_IS_SOME;
+    ///     F::call((s,))
+    /// }
+    ///
+    /// let s: String = call_it((), "hello world"); // does not compile
+    /// ```
+    ///
+    /// Note that this use more simply `const { assert!(F::IS_SOME) }` in
+    /// Rust 1.79.0 or newer.
+    const ASSERT_IS_SOME: () = { assert!(Self::IS_SOME) };
+
+    /// `true` if `Self` is an actual function type and not `()`.
+    ///
+    /// # Examples
+    ///
+    /// You can use `IS_SOME` to catch this at compile time:
+    ///
+    /// ```compile_fail
+    /// # use qemu_api::callbacks::FnCall;
+    /// fn call_it<F: for<'a> FnCall<(&'a str,), String>>(_f: &F, s: &str) -> String {
+    ///     const { assert!(F::IS_SOME) }
+    ///     F::call((s,))
+    /// }
+    ///
+    /// let s: String = call_it((), "hello world"); // does not compile
+    /// ```
+    const IS_SOME: bool;
+
+    /// `false` if `Self` is an actual function type, `true` if it is `()`.
+    fn is_none() -> bool {
+        !Self::IS_SOME
+    }
+
+    /// `true` if `Self` is an actual function type, `false` if it is `()`.
+    fn is_some() -> bool {
+        Self::IS_SOME
+    }
+
     /// Call the function with the arguments in args.
     fn call(a: Args) -> R;
 }
 
+/// `()` acts as a "null" callback.  Using `()` and `function` is nicer
+/// than `None` and `Some(function)`, because the compiler is unable to
+/// infer the type of just `None`.  Therefore, the trait itself acts as the
+/// option type, with functions [`FnCall::is_some`] and [`FnCall::is_none`].
+unsafe impl<Args, R> FnCall<Args, R> for () {
+    const IS_SOME: bool = false;
+
+    /// Call the function with the arguments in args.
+    fn call(_a: Args) -> R {
+        panic!("callback not specified")
+    }
+}
+
 macro_rules! impl_call {
     ($($args:ident,)* ) => (
         // SAFETY: because each function is treated as a separate type,
@@ -103,6 +188,8 @@  unsafe impl<F, $($args,)* R> FnCall<($($args,)*), R> for F
         where
             F: 'static + Sync + Sized + Fn($($args, )*) -> R,
         {
+            const IS_SOME: bool = true;
+
             #[inline(always)]
             fn call(a: ($($args,)*)) -> R {
                 let _: () = Self::ASSERT_ZERO_SIZED;
@@ -138,4 +225,14 @@  fn do_test_call<'a, F: FnCall<(&'a str,), String>>(_f: &F) -> String {
     fn test_call() {
         assert_eq!(do_test_call(&str::to_owned), "hello world")
     }
+
+    // The `_f` parameter is unused but it helps the compiler infer `F`.
+    fn do_test_is_some<'a, F: FnCall<(&'a str,), String>>(_f: &F) {
+        assert_eq!(F::is_some(), true);
+    }
+
+    #[test]
+    fn test_is_some() {
+        do_test_is_some(&str::to_owned);
+    }
 }