On Fri, 25 Oct 2024 at 15:01, Paolo Bonzini <[email protected]> wrote:
>
> (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()].
TODO, it was not ignored just planned as next
>
> 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.
TODO, if you recall we had that discussion about external statics,
that was what I was looking into back then.
> 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.
TODO, you can you use both LitStr and LitCStr in the macro
>
> 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:
Why? I don't understand what this solves. These are *just* trampoline
functions to call the Rust-abi code.
>
> 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:
>
So just rename them and put a generic argument. Still not seeing any gain here.
>
> 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.
This is called "default implementations" in Rust
> Second, if you don't implement them they are not overwritten
> by the class_init method.
WYM overwritten? That we hook up the empty stub instead of a NULL
function pointer?
> 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
Hard no, sorry. Error messages can definitely be generated from proc
macros. Long crate paths; that's just a matter of using imports or
changing names.
>
> 2) it allows simplifying the pl011 code piecewise, even before
> introducing procedural macro code
Not sure how that is relevant can you explain?
>
> 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
I disagree with this approach at the moment. This patch is in an
acceptable state albeit a bit longish while all these suggestions
would merely cause more running around making changes for no real
gain while also delaying review and merging.
>
> 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.
I don't know what this is in reference to :P What work and throwing
away are you talking about?
> Paolo
>