diff mbox series

[14/26] rust: qom: move bridge for TypeInfo functions out of pl011

Message ID 20241209123717.99077-15-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
Allow the ObjectImpl trait to expose Rust functions that avoid raw
pointers (though INSTANCE_INIT for example is still unsafe).
ObjectImpl::TYPE_INFO adds thunks around the functions in
ObjectImpl.

While at it, document `TypeInfo`.

Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
 rust/hw/char/pl011/src/device.rs | 40 +++++++--------------
 rust/qemu-api/src/definitions.rs | 61 +++++++++++++++++++++++++++++---
 2 files changed, 69 insertions(+), 32 deletions(-)

Comments

Zhao Liu Dec. 10, 2024, 3:50 p.m. UTC | #1
On Mon, Dec 09, 2024 at 01:37:05PM +0100, Paolo Bonzini wrote:
> Date: Mon,  9 Dec 2024 13:37:05 +0100
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH 14/26] rust: qom: move bridge for TypeInfo functions out of
>  pl011
> X-Mailer: git-send-email 2.47.1
> 
> Allow the ObjectImpl trait to expose Rust functions that avoid raw
> pointers (though INSTANCE_INIT for example is still unsafe).
> ObjectImpl::TYPE_INFO adds thunks around the functions in
> ObjectImpl.
> 
> While at it, document `TypeInfo`.
> 
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
>  rust/hw/char/pl011/src/device.rs | 40 +++++++--------------
>  rust/qemu-api/src/definitions.rs | 61 +++++++++++++++++++++++++++++---
>  2 files changed, 69 insertions(+), 32 deletions(-)
> 
> diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
> index 56403c36609..b9f8fb134b5 100644
> --- a/rust/hw/char/pl011/src/device.rs
> +++ b/rust/hw/char/pl011/src/device.rs
> @@ -110,7 +110,7 @@ impl ObjectImpl for PL011State {
>      type Class = PL011Class;
>      const TYPE_NAME: &'static CStr = crate::TYPE_PL011;
>      const PARENT_TYPE_NAME: Option<&'static CStr> = Some(TYPE_SYS_BUS_DEVICE);
> -    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = Some(pl011_init);
> +    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = Some(Self::init);

No need to keep `unsafe` here?

> +///
> +/// - the struct must be `#[repr(C)]`
> +///
> +/// - `Class` and `TYPE` must match the data in the `TypeInfo` (this is
> +///   automatic if the class is defined via `ObjectImpl`).
> +///
> +/// - the first field of the struct must be of the instance struct corresponding
> +///   to the superclass declared as `PARENT_TYPE_NAME`
>  pub trait ObjectImpl: ClassInitImpl + Sized {
> +    /// The QOM class object corresponding to this struct.  Not used yet.
>      type Class;
> +
> +    /// The name of the type, which can be passed to `object_new()` to
> +    /// generate an instance of this type.
>      const TYPE_NAME: &'static CStr;
> +
> +    /// The parent of the type.  This should match the first field of
> +    /// the struct that implements `ObjectImpl`:
>      const PARENT_TYPE_NAME: Option<&'static CStr>;
> +
> +    /// Whether the object can be instantiated
>      const ABSTRACT: bool = false;
> -    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = None;
> -    const INSTANCE_POST_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = None;
> +
> +    /// Function that is called to initialize an object.  The parent class will
> +    /// have already been initialized so the type is only responsible for
> +    /// initializing its own members.
> +    ///
> +    /// FIXME: The argument is not really a valid reference. `&mut
> +    /// MaybeUninit<Self>` would be a better description.
> +    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = None;

And here.

> +    /// Function that is called to finish initialization of an object, once
> +    /// `INSTANCE_INIT` functions have been called.
> +    const INSTANCE_POST_INIT: Option<fn(&mut Self)> = None;
>  

Otherwise,

Reviewed-by: Zhao Liu <zhao1.liu@intel.com>
Zhao Liu Dec. 10, 2024, 4:02 p.m. UTC | #2
> +unsafe extern "C" fn rust_instance_init<T: ObjectImpl>(obj: *mut Object) {
> +    // SAFETY: obj is an instance of T, since rust_instance_init<T>
> +    // is called from QOM core as the instance_init function
> +    // for class T
> +    unsafe { T::INSTANCE_INIT.unwrap()(&mut *obj.cast::<T>()) }
> +}

Here's the difference, why doesn't init() narrow the unsafe scope like
post_init() does?

> +unsafe extern "C" fn rust_instance_post_init<T: ObjectImpl>(obj: *mut Object) {
> +    // SAFETY: obj is an instance of T, since rust_instance_post_init<T>
> +    // is called from QOM core as the instance_post_init function
> +    // for class T
> +    //
> +    // FIXME: it's not really guaranteed that there are no backpointers to
> +    // obj; it's quite possible that they have been created by instance_init().
> +    // The receiver should be &self, not &mut self.
> +    T::INSTANCE_POST_INIT.unwrap()(unsafe { &mut *obj.cast::<T>() })
> +}
> +
Paolo Bonzini Dec. 10, 2024, 5:38 p.m. UTC | #3
On 12/10/24 16:50, Zhao Liu wrote:
> On Mon, Dec 09, 2024 at 01:37:05PM +0100, Paolo Bonzini wrote:
>> Date: Mon,  9 Dec 2024 13:37:05 +0100
>> From: Paolo Bonzini <pbonzini@redhat.com>
>> Subject: [PATCH 14/26] rust: qom: move bridge for TypeInfo functions out of
>>   pl011
>> X-Mailer: git-send-email 2.47.1
>>
>> Allow the ObjectImpl trait to expose Rust functions that avoid raw
>> pointers (though INSTANCE_INIT for example is still unsafe).
>> ObjectImpl::TYPE_INFO adds thunks around the functions in
>> ObjectImpl.
>>
>> While at it, document `TypeInfo`.
>>
>> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>> ---
>>   rust/hw/char/pl011/src/device.rs | 40 +++++++--------------
>>   rust/qemu-api/src/definitions.rs | 61 +++++++++++++++++++++++++++++---
>>   2 files changed, 69 insertions(+), 32 deletions(-)
>>
>> diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
>> index 56403c36609..b9f8fb134b5 100644
>> --- a/rust/hw/char/pl011/src/device.rs
>> +++ b/rust/hw/char/pl011/src/device.rs
>> @@ -110,7 +110,7 @@ impl ObjectImpl for PL011State {
>>       type Class = PL011Class;
>>       const TYPE_NAME: &'static CStr = crate::TYPE_PL011;
>>       const PARENT_TYPE_NAME: Option<&'static CStr> = Some(TYPE_SYS_BUS_DEVICE);
>> -    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = Some(pl011_init);
>> +    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = Some(Self::init);
> 
> No need to keep `unsafe` here?

Right now instance_init is called with only the parent initialized, and 
the remaining memory zeroed; its purpose is to prepare things for 
instance_post_init which can then be safe (it's also kind of wrong for 
instance_post_init to receive a &mut Self, because instance_init will 
create other pointers to the object, for example in a MemoryRegion's 
"parent" field).

The right thing to do would be to have an argument of type &mut 
MaybeUninit<Self>.  Then the caller would do something like

     let maybe_uninit = obj as *mut MaybeUninit<Self>;
     unsafe {
         Self::INSTANCE_INIT(&mut *maybe_uninit);
         maybe_uninit.assume_init_mut();
     }

Note however that INSTANCE_INIT would still be unsafe, because its 
safety promise is that it prepares things for the caller's 
assume_init_mut().

The way that this will become safe is to use the pinned_init crate from 
Linux: instance_init returns the initialization as an "impl 
PinInit<Self>", and then instance_post_init can run with a &self.  Until 
then, however, instance_init has to remain unsafe.

Paolo
Zhao Liu Dec. 11, 2024, 7:59 a.m. UTC | #4
> > > -    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = Some(pl011_init);
> > > +    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = Some(Self::init);
> > 
> > No need to keep `unsafe` here?
> 
> Right now instance_init is called with only the parent initialized, and the
> remaining memory zeroed; its purpose is to prepare things for
> instance_post_init which can then be safe (it's also kind of wrong for
> instance_post_init to receive a &mut Self, because instance_init will create
> other pointers to the object, for example in a MemoryRegion's "parent"
> field).

Thank you for explanation. It makes a lot of sense.

> The right thing to do would be to have an argument of type &mut
> MaybeUninit<Self>.  Then the caller would do something like
> 
>     let maybe_uninit = obj as *mut MaybeUninit<Self>;
>     unsafe {
>         Self::INSTANCE_INIT(&mut *maybe_uninit);
>         maybe_uninit.assume_init_mut();
>     }
> 
> Note however that INSTANCE_INIT would still be unsafe, because its safety
> promise is that it prepares things for the caller's assume_init_mut().

Yes, I feel that this approach more clearly explains the purpose of QOM
init.

And since we are talking about the safety of INSTANCE_INIT, I think we
should add some safety guidelines here, like:
 * Proper Initialization of pointers and references
 * Explicit initialization of Non-Zero Fields
 * In placed memory region is correctly initialized.

(Or do you have any additional or clearer guidelines?)

This could be the reference when adding SAFETY comment for the device's
own `unsafe` init(). :-)

And this is also a good explanation to distinguish between initialization
in init() and realize(). For example, HPET attempts to initialize the
timer array in realize().

> The way that this will become safe is to use the pinned_init crate from
> Linux: instance_init returns the initialization as an "impl PinInit<Self>",

Then do we need to place OBJECT in some suitable memory location (Box or
something) for Rust implementation?

> and then instance_post_init can run with a &self.  Until then, however,
> instance_init has to remain unsafe.

Thanks for sharing! It's a very reasonable direction.

Regards,
Zhao
Paolo Bonzini Dec. 11, 2024, 9:11 a.m. UTC | #5
Il mer 11 dic 2024, 08:41 Zhao Liu <zhao1.liu@intel.com> ha scritto:

> And since we are talking about the safety of INSTANCE_INIT, I think we
> should add some safety guidelines here, like:
>  * Proper Initialization of pointers and references
>  * Explicit initialization of Non-Zero Fields
>  * In placed memory region is correctly initialized.
>

Yes that's pretty much it.

(Or do you have any additional or clearer guidelines?)
>
> This could be the reference when adding SAFETY comment for the device's
> own `unsafe` init(). :-)
>
> And this is also a good explanation to distinguish between initialization
> in init() and realize(). For example, HPET attempts to initialize the
> timer array in realize().
>

Generally:

- embedded objects will have to be initialized in instance_init unless they
are Options

- sysbus_init_irq and sysbus_init_mmio can be called in both instance_init
and instance_post_init for now, but they will have to be in post_init once
the signature of init changes to return impl PinInit

- if you don't need properties you can choose between post_init and
realize, if you need properties you need to initialize in realize (and
then, unlike C, you might need to explicitly allow the pre-realize state;
for example using Option<&...> instead of just a reference; or Vec<>
instead of an array).

>
> > The way that this will become safe is to use the pinned_init crate from
> > Linux: instance_init returns the initialization as an "impl
> PinInit<Self>",
>
> Then do we need to place OBJECT in some suitable memory location (Box or
> something) for Rust implementation?
>

Allocation is still done by the C code, so I am not sure I understand the
question. Rust code allocates QOM objects with object_new() so they are
malloc-ed. I discussed it with Manos some time ago and in principle you
could use a Box (QOM supports custom freeing functions) but it's a bit
complex because the freeing function would have to free the memory without
dropping the contents of the Box (the drop is done by QOM via
instance_finalize).

If you want to allocate the HPETTimers at realize time, I think you can
place them in a Vec. I think you won't need NonNull for this, but I am not
100% sure. Alternatively if you want to always prepare all MAX_TIMERS of
them and then only use a subset, you can use an array.

Either way, probably it makes sense for you to have an "fn
timers_iter(&self) -> impl Iter<Item = &BqlRefCell<HPETTimer>>" in
HPETState, or something like that. Then you can easily use for loops to
walk all timers between 0 and self.num_timers-1.

Paolo

> and then instance_post_init can run with a &self.  Until then, however,
> > instance_init has to remain unsafe.
>
> Thanks for sharing! It's a very reasonable direction.
>
> Regards,
> Zhao
>
>
Zhao Liu Dec. 11, 2024, 4:56 p.m. UTC | #6
> Generally:
> 
> - embedded objects will have to be initialized in instance_init unless they
> are Options

I see, at least for HPETTimer array, I need to prepare all of them in
instance_init()...

> - if you don't need properties you can choose between post_init and
> realize, if you need properties you need to initialize in realize (and
> then, unlike C, you might need to explicitly allow the pre-realize state;
> for example using Option<&...> instead of just a reference; or Vec<>
> instead of an array).

...in realize(), also need to handle the "timers" property to enable
the timers that property requires.

> - sysbus_init_irq and sysbus_init_mmio can be called in both instance_init
> and instance_post_init for now, but they will have to be in post_init once
> the signature of init changes to return impl PinInit

make sense, thanks!

> >
> > > The way that this will become safe is to use the pinned_init crate from
> > > Linux: instance_init returns the initialization as an "impl
> > PinInit<Self>",
> >
> > Then do we need to place OBJECT in some suitable memory location (Box or
> > something) for Rust implementation?
> >
> 
> Allocation is still done by the C code, so I am not sure I understand the
> question. Rust code allocates QOM objects with object_new() so they are
> malloc-ed.

Sorry, I'm not familiar enough with this piece...I have the question
because PinInit doc said "you will need a suitable memory location that
can hold a T. This can be KBox<T>, Arc<T>, UniqueArc<T> or even the
stack (see stack_pin_init!)."

I see that the core point is ensuring that structures cannot be moved.
Given that object_new() on the C side ensures that the allocated object
will not be moved, Rust side does not need to worry about pinning, correct?

> I discussed it with Manos some time ago and in principle you
> could use a Box (QOM supports custom freeing functions) but it's a bit
> complex because the freeing function would have to free the memory without
> dropping the contents of the Box (the drop is done by QOM via
> instance_finalize).
> 
> If you want to allocate the HPETTimers at realize time, I think you can
> place them in a Vec. I think you won't need NonNull for this, but I am not
> 100% sure. Alternatively if you want to always prepare all MAX_TIMERS of
> them and then only use a subset, you can use an array.

Vec seems to lack proper vmstate support. I understand that we need to
modify VMSTATE_STRUCT_VARRAY_POINTER_* to introduce a variant for Vec.

Since the array support is already available, I chose to use an array
instead (although vmstate is disabled for now).

> Either way, probably it makes sense for you to have an "fn
> timers_iter(&self) -> impl Iter<Item = &BqlRefCell<HPETTimer>>" in
> HPETState, or something like that. Then you can easily use for loops to
> walk all timers between 0 and self.num_timers-1.

Good idea, yes, will do.

Thanks,
Zhao
Paolo Bonzini Dec. 12, 2024, 9:24 a.m. UTC | #7
On Wed, Dec 11, 2024 at 5:38 PM Zhao Liu <zhao1.liu@intel.com> wrote:
>
> > Generally:
> >
> > - embedded objects will have to be initialized in instance_init unless they
> > are Options
>
> I see, at least for HPETTimer array, I need to prepare all of them in
> instance_init()...
>
> ...in realize(), also need to handle the "timers" property to enable
> the timers that property requires.

I see -- though, thinking more about it, since you have

    fn init_timer(&mut self) {
        let raw_ptr: *mut HPETState = self;

        for i in 0..HPET_MAX_TIMERS {
            let mut timer = self.get_timer(i).borrow_mut();
            timer.init(i, raw_ptr).init_timer_with_state();
        }
    }

It seems to me that you can do everything in instance_init. Later on a
function like the above
will become something like

impl HPETTimer {
    fn init_timer(hpet: NonNull<HPETState>, n: usize) -> impl PinInit<Self> {
        pin_init!(&this in HPETTimer {
            index: n,
            qemu_timer <- Timer::init_ns(...),
            state: hpet,
            config: 0,
            cmp: 0,
            fsb: 0,
            cmp64: 0,
            period: 0,
            wrap_flag: false,
            last: 0,
        }
    }
}

But even now you can write something that takes a &mut self as the
first argument. It's undefined behavior but it's okay as long as we
have a path forward.

> > > > The way that this will become safe is to use the pinned_init crate from
> > > > Linux: instance_init returns the initialization as an "impl
> > > PinInit<Self>",
> > >
> > > Then do we need to place OBJECT in some suitable memory location (Box or
> > > something) for Rust implementation?
> > >
> >
> > Allocation is still done by the C code, so I am not sure I understand the
> > question. Rust code allocates QOM objects with object_new() so they are
> > malloc-ed.
>
> Sorry, I'm not familiar enough with this piece...I have the question
> because PinInit doc said "you will need a suitable memory location that
> can hold a T. This can be KBox<T>, Arc<T>, UniqueArc<T> or even the
> stack (see stack_pin_init!)."

Ah, I see. You can use __pinned_init directly on the memory that is
passed to rust_instance_init. See for example the implementation of
InPlaceWrite for Box
(https://docs.rs/pinned-init/latest/src/pinned_init/lib.rs.html#1307).

> I see that the core point is ensuring that structures cannot be moved.
> Given that object_new() on the C side ensures that the allocated object
> will not be moved, Rust side does not need to worry about pinning, correct?

Sort of... You still need to worry about it for two reasons:

1) if you have &mut Self you can move values out of the object using
e.g. mem::replace or mem::swap. Those would move the value in memory
and cause trouble (think of moving a QEMUTimer while it is pointed to
by the QEMUTimerList). This is solved by 1) using &Self all the time +
interior mutability 2) using pinned_init's "PinnedDrop" functionality,
because &Self can be used in QEMU-specific APIs but (obviously) not in
the built-in Drop trait.

2) right now marking something as pinned is an indirect way to tell
the compiler and miri that there are external references to it. For a
longer discussion you can read
https://crates.io/crates/pinned-aliasable or
https://gist.github.com/Darksonn/1567538f56af1a8038ecc3c664a42462.

Linux does this with a wrapper type similar to the one in pinned-aliasable:

/// Stores an opaque value.
///
/// This is meant to be used with FFI objects that are never
interpreted by Rust code.
#[repr(transparent)]
pub struct Opaque<T> {
    value: UnsafeCell<MaybeUninit<T>>,
    _pin: PhantomPinned,
}

It's on my todo list to introduce it in qemu_api::cell and (for
example) change qom::Object from

    pub use bindings::Object

to

    pub type Object = Opaque<bindings::Object>;

Or something like that.

> Vec seems to lack proper vmstate support. I understand that we need to
> modify VMSTATE_STRUCT_VARRAY_POINTER_* to introduce a variant for Vec.
>
> Since the array support is already available, I chose to use an array
> instead (although vmstate is disabled for now).

Yes, you're right.

Palo
Zhao Liu Dec. 13, 2024, 8:53 a.m. UTC | #8
> I see -- though, thinking more about it, since you have
> 
>     fn init_timer(&mut self) {
>         let raw_ptr: *mut HPETState = self;
> 
>         for i in 0..HPET_MAX_TIMERS {
>             let mut timer = self.get_timer(i).borrow_mut();
>             timer.init(i, raw_ptr).init_timer_with_state();
>         }
>     }
> 
> It seems to me that you can do everything in instance_init. Later on a
> function like the above
> will become something like
> 
> impl HPETTimer {
>     fn init_timer(hpet: NonNull<HPETState>, n: usize) -> impl PinInit<Self> {

Thank you! I should pass NonNull type other than `*mut HPETState` for now :-)

>         pin_init!(&this in HPETTimer {
>             index: n,
>             qemu_timer <- Timer::init_ns(...),
>             state: hpet,
>             config: 0,
>             cmp: 0,
>             fsb: 0,
>             cmp64: 0,
>             period: 0,
>             wrap_flag: false,
>             last: 0,
>         }
>     }
> }

That's the right and ideal way, and I like it.

> But even now you can write something that takes a &mut self as the
> first argument. It's undefined behavior but it's okay as long as we
> have a path forward.

Yes, I agree. In the next version, I can follow your suggestion and put
these embedded items into instance_init(), to be better prepared for the
next step.

> > > > > The way that this will become safe is to use the pinned_init crate from
> > > > > Linux: instance_init returns the initialization as an "impl
> > > > PinInit<Self>",
> > > >
> > > > Then do we need to place OBJECT in some suitable memory location (Box or
> > > > something) for Rust implementation?
> > > >
> > >
> > > Allocation is still done by the C code, so I am not sure I understand the
> > > question. Rust code allocates QOM objects with object_new() so they are
> > > malloc-ed.
> >
> > Sorry, I'm not familiar enough with this piece...I have the question
> > because PinInit doc said "you will need a suitable memory location that
> > can hold a T. This can be KBox<T>, Arc<T>, UniqueArc<T> or even the
> > stack (see stack_pin_init!)."
> 
> Ah, I see. You can use __pinned_init directly on the memory that is
> passed to rust_instance_init. See for example the implementation of
> InPlaceWrite for Box
> (https://docs.rs/pinned-init/latest/src/pinned_init/lib.rs.html#1307).

Thank you! I understand your intention. A similar implementation would
also be quite natural in rust_instance_init.

> > I see that the core point is ensuring that structures cannot be moved.
> > Given that object_new() on the C side ensures that the allocated object
> > will not be moved, Rust side does not need to worry about pinning, correct?
> 
> Sort of... You still need to worry about it for two reasons:
> 
> 1) if you have &mut Self you can move values out of the object using
> e.g. mem::replace or mem::swap. Those would move the value in memory
> and cause trouble (think of moving a QEMUTimer while it is pointed to
> by the QEMUTimerList). This is solved by 1) using &Self all the time +
> interior mutability

With your help and through our discussions, I have gained a clearer
understanding of this intention.

> 2) using pinned_init's "PinnedDrop" functionality,
> because &Self can be used in QEMU-specific APIs but (obviously) not in
> the built-in Drop trait.
> 
> 2) right now marking something as pinned is an indirect way to tell
> the compiler and miri that there are external references to it. For a
> longer discussion you can read
> https://crates.io/crates/pinned-aliasable or
> https://gist.github.com/Darksonn/1567538f56af1a8038ecc3c664a42462.

Thanks for sharing!

> Linux does this with a wrapper type similar to the one in pinned-aliasable:
> 
> /// Stores an opaque value.
> ///
> /// This is meant to be used with FFI objects that are never
> interpreted by Rust code.
> #[repr(transparent)]
> pub struct Opaque<T> {
>     value: UnsafeCell<MaybeUninit<T>>,
>     _pin: PhantomPinned,
> }
> 
> It's on my todo list to introduce it in qemu_api::cell and (for
> example) change qom::Object from
> 
>     pub use bindings::Object
> 
> to
> 
>     pub type Object = Opaque<bindings::Object>;
> 
> Or something like that.

Yes, I agree with this idea. It's what we need.

Regards,
Zhao
diff mbox series

Patch

diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
index 56403c36609..b9f8fb134b5 100644
--- a/rust/hw/char/pl011/src/device.rs
+++ b/rust/hw/char/pl011/src/device.rs
@@ -110,7 +110,7 @@  impl ObjectImpl for PL011State {
     type Class = PL011Class;
     const TYPE_NAME: &'static CStr = crate::TYPE_PL011;
     const PARENT_TYPE_NAME: Option<&'static CStr> = Some(TYPE_SYS_BUS_DEVICE);
-    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = Some(pl011_init);
+    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = Some(Self::init);
 }
 
 #[repr(C)]
@@ -615,19 +615,6 @@  pub fn post_load(&mut self, _version_id: u32) -> Result<(), ()> {
     }
 }
 
-/// # Safety
-///
-/// We expect the FFI user of this function to pass a valid pointer, that has
-/// the same size as [`PL011State`]. We also expect the device is
-/// readable/writeable from one thread at any time.
-pub unsafe extern "C" fn pl011_init(obj: *mut Object) {
-    unsafe {
-        debug_assert!(!obj.is_null());
-        let mut state = NonNull::new_unchecked(obj.cast::<PL011State>());
-        state.as_mut().init();
-    }
-}
-
 #[repr(C)]
 #[derive(Debug, qemu_api_macros::Object)]
 /// PL011 Luminary device model.
@@ -640,19 +627,16 @@  pub struct PL011LuminaryClass {
     _inner: [u8; 0],
 }
 
-/// Initializes a pre-allocated, unitialized instance of `PL011Luminary`.
-///
-/// # Safety
-///
-/// We expect the FFI user of this function to pass a valid pointer, that has
-/// the same size as [`PL011Luminary`]. We also expect the device is
-/// readable/writeable from one thread at any time.
-pub unsafe extern "C" fn pl011_luminary_init(obj: *mut Object) {
-    unsafe {
-        debug_assert!(!obj.is_null());
-        let mut state = NonNull::new_unchecked(obj.cast::<PL011Luminary>());
-        let state = state.as_mut();
-        state.parent_obj.device_id = DeviceId::Luminary;
+impl PL011Luminary {
+    /// Initializes a pre-allocated, unitialized instance of `PL011Luminary`.
+    ///
+    /// # Safety
+    ///
+    /// We expect the FFI user of this function to pass a valid pointer, that
+    /// has the same size as [`PL011Luminary`]. We also expect the device is
+    /// readable/writeable from one thread at any time.
+    unsafe fn init(&mut self) {
+        self.parent_obj.device_id = DeviceId::Luminary;
     }
 }
 
@@ -660,7 +644,7 @@  impl ObjectImpl for PL011Luminary {
     type Class = PL011LuminaryClass;
     const TYPE_NAME: &'static CStr = crate::TYPE_PL011_LUMINARY;
     const PARENT_TYPE_NAME: Option<&'static CStr> = Some(crate::TYPE_PL011);
-    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = Some(pl011_luminary_init);
+    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = Some(Self::init);
 }
 
 impl DeviceImpl for PL011Luminary {}
diff --git a/rust/qemu-api/src/definitions.rs b/rust/qemu-api/src/definitions.rs
index d64a581a5cc..078ba30d61a 100644
--- a/rust/qemu-api/src/definitions.rs
+++ b/rust/qemu-api/src/definitions.rs
@@ -8,6 +8,24 @@ 
 
 use crate::bindings::{Object, ObjectClass, TypeInfo};
 
+unsafe extern "C" fn rust_instance_init<T: ObjectImpl>(obj: *mut Object) {
+    // SAFETY: obj is an instance of T, since rust_instance_init<T>
+    // is called from QOM core as the instance_init function
+    // for class T
+    unsafe { T::INSTANCE_INIT.unwrap()(&mut *obj.cast::<T>()) }
+}
+
+unsafe extern "C" fn rust_instance_post_init<T: ObjectImpl>(obj: *mut Object) {
+    // SAFETY: obj is an instance of T, since rust_instance_post_init<T>
+    // is called from QOM core as the instance_post_init function
+    // for class T
+    //
+    // FIXME: it's not really guaranteed that there are no backpointers to
+    // obj; it's quite possible that they have been created by instance_init().
+    // The receiver should be &self, not &mut self.
+    T::INSTANCE_POST_INIT.unwrap()(unsafe { &mut *obj.cast::<T>() })
+}
+
 unsafe extern "C" fn drop_object<T: ObjectImpl>(obj: *mut Object) {
     // SAFETY: obj is an instance of T, since drop_object<T>
     // is called from QOM core as the instance_finalize function
@@ -16,13 +34,42 @@ 
 }
 
 /// Trait a type must implement to be registered with QEMU.
+///
+/// # Safety
+///
+/// - the struct must be `#[repr(C)]`
+///
+/// - `Class` and `TYPE` must match the data in the `TypeInfo` (this is
+///   automatic if the class is defined via `ObjectImpl`).
+///
+/// - the first field of the struct must be of the instance struct corresponding
+///   to the superclass declared as `PARENT_TYPE_NAME`
 pub trait ObjectImpl: ClassInitImpl + Sized {
+    /// The QOM class object corresponding to this struct.  Not used yet.
     type Class;
+
+    /// The name of the type, which can be passed to `object_new()` to
+    /// generate an instance of this type.
     const TYPE_NAME: &'static CStr;
+
+    /// The parent of the type.  This should match the first field of
+    /// the struct that implements `ObjectImpl`:
     const PARENT_TYPE_NAME: Option<&'static CStr>;
+
+    /// Whether the object can be instantiated
     const ABSTRACT: bool = false;
-    const INSTANCE_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = None;
-    const INSTANCE_POST_INIT: Option<unsafe extern "C" fn(obj: *mut Object)> = None;
+
+    /// Function that is called to initialize an object.  The parent class will
+    /// have already been initialized so the type is only responsible for
+    /// initializing its own members.
+    ///
+    /// FIXME: The argument is not really a valid reference. `&mut
+    /// MaybeUninit<Self>` would be a better description.
+    const INSTANCE_INIT: Option<unsafe fn(&mut Self)> = None;
+
+    /// Function that is called to finish initialization of an object, once
+    /// `INSTANCE_INIT` functions have been called.
+    const INSTANCE_POST_INIT: Option<fn(&mut Self)> = None;
 
     const TYPE_INFO: TypeInfo = TypeInfo {
         name: Self::TYPE_NAME.as_ptr(),
@@ -33,8 +80,14 @@  pub trait ObjectImpl: ClassInitImpl + Sized {
         },
         instance_size: core::mem::size_of::<Self>(),
         instance_align: core::mem::align_of::<Self>(),
-        instance_init: Self::INSTANCE_INIT,
-        instance_post_init: Self::INSTANCE_POST_INIT,
+        instance_init: match Self::INSTANCE_INIT {
+            None => None,
+            Some(_) => Some(rust_instance_init::<Self>),
+        },
+        instance_post_init: match Self::INSTANCE_POST_INIT {
+            None => None,
+            Some(_) => Some(rust_instance_post_init::<Self>),
+        },
         instance_finalize: Some(drop_object::<Self>),
         abstract_: Self::ABSTRACT,
         class_size: core::mem::size_of::<Self::Class>(),