qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: Paolo Bonzini <pbonzini@redhat.com>
To: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>,
	qemu-devel@nongnu.org
Cc: "Peter Maydell" <peter.maydell@linaro.org>,
	"Marc-André Lureau" <marcandre.lureau@redhat.com>,
	"Daniel P. Berrangé" <berrange@redhat.com>,
	"Philippe Mathieu-Daudé" <philmd@linaro.org>,
	"Alex Bennée" <alex.bennee@linaro.org>,
	"Thomas Huth" <thuth@redhat.com>,
	"Junjie Mao" <junjie.mao@hotmail.com>,
	"Junjie Mao" <junjie.mao@intel.com>,
	"Zhao Liu" <zhao1.liu@intel.com>, "Kevin Wolf" <kwolf@redhat.com>,
	"Richard Henderson" <richard.henderson@linaro.org>,
	"Gustavo Romero" <gustavo.romero@linaro.org>,
	"Pierrick Bouvier" <pierrick.bouvier@linaro.org>
Subject: Re: [PATCH 03/11] rust/qemu-api-macros: introduce Device proc macro
Date: Fri, 25 Oct 2024 14:01:37 +0200	[thread overview]
Message-ID: <34f5191b-67d9-4815-a58b-a794fff0294d@redhat.com> (raw)
In-Reply-To: <20241024-rust-round-2-v1-3-051e7a25b978@linaro.org>

On 10/24/24 16:03, Manos Pitsidianakis wrote:
> Add a new derive procedural macro to declare device models. Add
> corresponding DeviceImpl trait after already existing ObjectImpl trait.
> At the same time, add instance_init, instance_post_init,
> instance_finalize methods to the ObjectImpl trait and call them from the
> ObjectImplUnsafe trait, which is generated by the procedural macro.
> 
> This allows all the boilerplate device model registration to be handled
> by macros, and all pertinent details to be declared through proc macro
> attributes or trait associated constants and methods.
> The device class can now be generated automatically and the name can be
> optionally overridden:
> 
>    ------------------------ >8 ------------------------
>   #[repr(C)]
>   #[derive(Debug, qemu_api_macros::Object, qemu_api_macros::Device)]
>   #[device(class_name_override = PL011Class)]
>   /// PL011 Device Model in QEMU
>   pub struct PL011State {
>    ------------------------ >8 ------------------------
> 
> Properties are now marked as field attributes:
> 
>    ------------------------ >8 ------------------------
>   #[property(name = c"chardev", qdev_prop = qdev_prop_chr)]
>   pub char_backend: CharBackend,
>    ------------------------ >8 ------------------------

Let's look at this from the point of view of separating the code into 
multiple patches.

This part is probably the easiest to extract.  We can start the device 
impl trait with just the properties:

pub trait DeviceImpl {
     const PROPERTIES: &[Property] = &[];
}

and then the device_class_init! macro can use <$type as 
DeviceImpl>::PROPERTIES instead of $props.

  qemu_api::device_class_init! {
-    pl011_class_init,
-    props => PL011_PROPERTIES,
+    PL011State, pl011_class_init,
      realize_fn => Some(pl011_realize),
      legacy_reset_fn => Some(pl011_reset),
      vmsd => VMSTATE_PL011,
  }

> Object methods (instance_init, etc) methods are now trait methods:
> 
>    ------------------------ >8 ------------------------
>   /// Trait a type must implement to be registered with QEMU.
>   pub trait ObjectImpl {
>       type Class: ClassImpl;
>       const TYPE_NAME: &'static CStr;
>       const PARENT_TYPE_NAME: Option<&'static CStr>;
>       const ABSTRACT: bool;
> 
>       unsafe fn instance_init(&mut self) {}
>       fn instance_post_init(&mut self) {}
>       fn instance_finalize(&mut self) {}
>   }
>    ------------------------ >8 ------------------------

Some comments here:

- instance_init should take either "instance: *mut self" or even better 
"instance: &mut MaybeUninit<self>".

-  for all Rust objects the implementation of instance_finalize() should 
just be ptr::drop_in_place(obj), so this need not be included in the trait.

TYPE_NAME/PARENT_TYPE_NAME/ABSTRACT could be passed to the 
#[derive(Object)] macro, for example:

#[derive(Object)]
// abstract can be optional
#[object(type_name="pl011", parent_type=DeviceState, abstract=false,
          class_type=PL011Class)]

while the "fn instance_init()" remains as a trait implementation in 
pl011/src/device.rs.

Because a trait can be implemented only once, this suggests separating 
the trait(s) in two: one for the constants that can be generated from 
the macro, one for the functions that form the vtable.  This is true for 
both objects and devices:

pub trait ObjectInfo {
     type Class: ClassImpl;
     const TYPE_NAME: &'static CStr;
     const PARENT_TYPE_NAME: Option<&'static CStr>;
     const ABSTRACT: bool;
}

pub trait ObjectImpl: ObjectInfo {
     unsafe fn instance_init(instance: &mut MaybeUninit<self>) {}
}


and likewise:

> Device methods (realize/reset etc) are now safe idiomatic trait methods:
> 
>    ------------------------ >8 ------------------------
>   /// Implementation methods for device types.
>   pub trait DeviceImpl: ObjectImpl {
>       fn realize(&mut self) {}
>       fn reset(&mut self) {}
>   }
>    ------------------------ >8 ------------------------

pub trait DeviceInfo {
     const PROPERTIES: &[Property] = &[];
}

pub trait DeviceImpl: ObjectImpl + DeviceInfo {
     fn realize(&mut self) {}
     fn reset(&mut self) {}
     const VMSTATE: Option<VMStateDescription> = None;
}

(Generally, don't read too much in the code - the syntax might have 
issues but you get the idea).


Anyhow, going forward to the property attribute:

> +    #[property(name = c"migrate-clk", qdev_prop = qdev_prop_bool)]

There are two issues here:

First, the default is true, so 1) this has to be fixed in QEMU (will 
do), 2) it is important to support it in #[property()].

Second, this provides a false sense of safety, because I could specify 
qdev_prop_chr here.  Instead, the qdev_prop type should be derived by 
the field type.

Third, since we are at it there's no need to use c"" in the attribute. 
The c_str!() macro that I am adding for backwards compatibility to old 
versions of Rust might actually come in handy here.


The part where I have most comments, and some ideas of how to make your 
work a little more maintainable, is the implementation of class_init 
(and all that depends on it).

Let's start with these generated functions:

> +        pub unsafe extern "C" fn #realize_fn(
> +            dev: *mut ::qemu_api::bindings::DeviceState,
> +            errp: *mut *mut ::qemu_api::bindings::Error,
> +        ) {
> +            let mut instance = NonNull::new(dev.cast::<#name>()).expect(concat!("Expected dev to be a non-null pointer of type ", stringify!(#name)));
> +            unsafe {
> +                ::qemu_api::objects::DeviceImpl::realize(instance.as_mut());
> +            }
> +        }
> +
> +        #[no_mangle]
> +        pub unsafe extern "C" fn #reset_fn(
> +            dev: *mut ::qemu_api::bindings::DeviceState,
> +        ) {
> +            let mut instance = NonNull::new(dev.cast::<#name>()).expect(concat!("Expected dev to be a non-null pointer of type ", stringify!(#name)));
> +            unsafe {
> +                ::qemu_api::objects::DeviceImpl::reset(instance.as_mut());
> +            }
> +        }

This can be handled entirely in Rust code, outside the macro.  If you add:

unsafe extern "C" fn realize_fn_unsafe<T: DeviceImpl>(
     dev: *mut DeviceState,
     errp: *mut *mut Error,
) {
     let mut instance = NonNull::new(dev.cast::<T>()).
         expect("Expected dev to be a non-null pointer");
     unsafe {
         ::qemu_api::objects::DeviceImpl::realize(instance.as_mut());
     }
}

unsafe extern "C" fn reset_fn_unsafe<T: DeviceImpl>(
     dev: *mut ::qemu_api::bindings::DeviceState,
) {
     let mut instance = NonNull::new(dev.cast::<T>()).
         expect("Expected dev to be a non-null pointer");
     unsafe {
         ::qemu_api::objects::DeviceImpl::reset(instance.as_mut());
     }
}

then the functions can be used directly instead of #realize_fn and 
#reset_fn with a blanket implementation of DeviceImplUnsafe:


unsafe impl DeviceImplUnsafe for T: DeviceImpl {
     const REALIZE: ... = Some(realize_fn_unsafe::<T>);
     const RESET: ... = Some(realize_fn_unsafe::<T>);
}

Going on to the implementation of the safe functions:

> +impl DeviceImpl for PL011State {
> +    fn realize(&mut self) {

These are not quite traits.  First, you can implement only some of the 
functions.  Second, if you don't implement them they are not overwritten 
by the class_init method.

So this points to a different implementation as an attribute macro, 
which is able to rewrite everything in the body of the impl.  For example:

#[qom_class_init]
impl DeviceImpl for PL011State {
     fn realize(&mut self) { ... }
     fn reset(&mut self) { ... }

     const VMSTATE: ... = {}
     const CATEGORY: ... = {}
}

can be transformed into:

#[qom_class_init]
impl DeviceImpl for PL011State {
     // fns are wrapped and transformed into consts
     const REALIZE: Option<fn(&mut self)> = {
         fn realize(&mut self) { ... }
         Some(realize)
     };
     const RESET: Option<fn(&mut self)> = {
         fn reset(&mut self) { ... }
         Some(reset)
     };

     // while associated consts (and perhaps types?) remain as is
     const VMSTATE: ... = {}
     const CATEGORY: ... = {}
}

The above blanket implementation of DeviceImplUnsafe is easily adjusted 
to support non-overridden methods:

unsafe impl DeviceImplUnsafe for T: DeviceImpl {
     const REALIZE: ... = <T as DeviceImpl>::REALIZE::map(
         || realize_fn_unsafe::<T>);
     const RESET: ... = <T as DeviceImpl>::RESET::map(
         || realize_fn_unsafe::<T>);
}


You can also keep out of the macro the class_init method itself.  Here 
I'm adding it to DeviceImplUnsafe:

pub trait DeviceImplUnsafe {
     unsafe fn class_init(klass: *mut ObjectClass, data: *mut c_void) {
         let mut dc = NonNull::new(klass.cast::<DeviceClass>()).unwrap();
         unsafe {
             dc.as_mut().realize = Self::REALIZE;
             bindings::device_class_set_legacy_reset(
                                  dc.as_mut(), Self::RESET);
             device_class_set_props(dc.as_mut(),
                                    <Self as DeviceInfo>::PROPS);
             if let Some(vmsd) = <Self as DeviceInfo>::VMSTATE {
                 dc.as_mut().vmsd = vmsd;
             }
             if let Some(cat) = <Self as DeviceInfo>::CATEGORY {
                 dc.as_mut().category = cat;
             }

             // maybe in the future for unparent
             // <Self as ObjectImplUnsafe>::class_init(klass, data)
         }
     }
}

And with all this machinery in place the device_class_init! macro 
becomes simply

     device_class_init!(PL011State);

(because the arguments have moved to DeviceInfo or DeviceImpl).


Why is this important? Because the only code transformation is the 
generation of properties and vtables, and the bindings can be developed 
almost entirely in qemu_api instead of qemu_api_macros.  This has 
several advantages:

1) it's much easier: simpler error messages, no macro indirection, no 
super long global crate paths

2) it allows simplifying the pl011 code piecewise, even before 
introducing procedural macro code

3) it's easier to add comments


It also becomes much easier to separate the work in separate patches, or 
even separate series.  Replacing the arguments to device_class_init!() 
with DeviceImpl + DeviceImplUnsafe can be introduced first: posted, 
reviewed, merged.  All the remaining tasks are pretty much independent:

1) splitting out ObjectInfo and introducing #[object] to automate it 
(i.e. extending derive(Object))

2) splitting out DeviceInfo and introducing #[property] to automate it 
(i.e. derive(Device))

3) the #[qom_class_init] macro


A final word: I absolutely don't want you to think that your work is of 
no value.  It's totally okay to throw away the first version of 
something.  You showed that it _is_ possible to have idiomatic code with 
the help of procedural macros.  Even if the implementation can be 
improved, the _idea_ remains yours.

Paolo



  parent reply	other threads:[~2024-10-25 12:02 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-10-24 14:02 [PATCH 00/11] Rust device model patches and misc cleanups Manos Pitsidianakis
2024-10-24 14:02 ` [PATCH 01/11] Revert "rust: add PL011 device model" Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 02/11] rust: add PL011 device model Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 03/11] rust/qemu-api-macros: introduce Device proc macro Manos Pitsidianakis
2024-10-24 15:13   ` Alex Bennée
2024-10-24 17:06     ` Manos Pitsidianakis
2024-10-25 12:01   ` Paolo Bonzini [this message]
2024-10-25 14:04     ` Manos Pitsidianakis
2024-10-25 15:22       ` Paolo Bonzini
2024-10-25 16:22         ` Manos Pitsidianakis
2024-10-27 20:58   ` Paolo Bonzini
2024-10-27 22:39     ` Manos Pitsidianakis
2024-10-28  7:07       ` Paolo Bonzini
2024-10-24 14:03 ` [PATCH 04/11] rust: add support for migration in device models Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 05/11] rust/pl011: move CLK_NAME static to function scope Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 06/11] rust/pl011: add TYPE_PL011_LUMINARY device Manos Pitsidianakis
2024-10-24 17:27   ` Zhao Liu
2024-10-24 14:03 ` [PATCH 07/11] rust/pl011: move pub callback decl to local scope Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 08/11] rust/pl011: remove commented out C code Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 09/11] rust/pl011: Use correct masks for IBRD and FBRD Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 10/11] rust/qemu-api: add log module Manos Pitsidianakis
2024-10-24 14:03 ` [PATCH 11/11] rust/pl011: log guest/unimp errors Manos Pitsidianakis
2024-10-25  9:33 ` [PATCH 00/11] Rust device model patches and misc cleanups Paolo Bonzini
2024-10-26 10:06   ` Manos Pitsidianakis
2024-10-27  8:13     ` Paolo Bonzini

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=34f5191b-67d9-4815-a58b-a794fff0294d@redhat.com \
    --to=pbonzini@redhat.com \
    --cc=alex.bennee@linaro.org \
    --cc=berrange@redhat.com \
    --cc=gustavo.romero@linaro.org \
    --cc=junjie.mao@hotmail.com \
    --cc=junjie.mao@intel.com \
    --cc=kwolf@redhat.com \
    --cc=manos.pitsidianakis@linaro.org \
    --cc=marcandre.lureau@redhat.com \
    --cc=peter.maydell@linaro.org \
    --cc=philmd@linaro.org \
    --cc=pierrick.bouvier@linaro.org \
    --cc=qemu-devel@nongnu.org \
    --cc=richard.henderson@linaro.org \
    --cc=thuth@redhat.com \
    --cc=zhao1.liu@intel.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).