diff mbox series

[v2,08/13] rust: cleanup module_init!, use it from #[derive(Object)]

Message ID 20241021163538.136941-9-pbonzini@redhat.com (mailing list archive)
State New
Headers show
Series rust: miscellaneous cleanups + QOM integration tests | expand

Commit Message

Paolo Bonzini Oct. 21, 2024, 4:35 p.m. UTC
Remove the duplicate code by using the module_init! macro; at the same time,
simplify how module_init! is used, by taking inspiration from the implementation
of #[derive(Object)].

Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
 rust/qemu-api-macros/src/lib.rs  | 33 +++-------------
 rust/qemu-api/src/definitions.rs | 66 ++++++++++++++------------------
 2 files changed, 33 insertions(+), 66 deletions(-)

Comments

Junjie Mao Oct. 22, 2024, 2:02 a.m. UTC | #1
Paolo Bonzini <pbonzini@redhat.com> writes:

> Remove the duplicate code by using the module_init! macro; at the same time,
> simplify how module_init! is used, by taking inspiration from the implementation
> of #[derive(Object)].
>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>

Reviewed-by: Junjie Mao <junjie.mao@hotmail.com>

One minor comment below.

> ---
>  rust/qemu-api-macros/src/lib.rs  | 33 +++-------------
>  rust/qemu-api/src/definitions.rs | 66 ++++++++++++++------------------
>  2 files changed, 33 insertions(+), 66 deletions(-)
>
<snip>
> diff --git a/rust/qemu-api/src/definitions.rs b/rust/qemu-api/src/definitions.rs
> index 3323a665d92..f180c38bfb2 100644
> --- a/rust/qemu-api/src/definitions.rs
> +++ b/rust/qemu-api/src/definitions.rs
> @@ -29,51 +29,40 @@ pub trait Class {
>
>  #[macro_export]
>  macro_rules! module_init {
<snip>
> +    ($type:ident => $body:block) => {
> +        const _: () = {
> +            #[used]
> +            #[cfg_attr(
> +                not(any(target_vendor = "apple", target_os = "windows")),
> +                link_section = ".init_array"
> +            )]
> +            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
> +            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
> +            pub static LOAD_MODULE: extern "C" fn() = {
> +                extern "C" fn init_fn() {

init_fn() should be unsafe fn according to the signature of
register_module_init. Being unsafe fn also makes sense here because it
is the caller, not init_fn() itself, that is responsible for the safety of
the unsafe code in the body.

--
Best Regards
Junjie Mao

>                      $body
>                  }
>
> -                unsafe {
> -                    $crate::bindings::register_module_init(
> -                        Some($func),
> -                        $crate::bindings::module_init_type::MODULE_INIT_QOM,
> -                    );
> +                extern "C" fn ctor_fn() {
> +                    unsafe {
> +                        $crate::bindings::register_module_init(
> +                            Some(init_fn),
> +                            $crate::bindings::module_init_type::$type,
> +                        );
> +                    }
>                  }
> -            }
>
> -            __load
> +                ctor_fn
> +            };
>          };
>      };
> +
> +    // shortcut because it's quite common that $body needs unsafe {}
> +    ($type:ident => unsafe $body:block) => {
> +        $crate::module_init! {
> +            $type => { unsafe { $body } }
> +        }
> +    };
>  }
>
>  #[macro_export]
Paolo Bonzini Oct. 22, 2024, 5:16 a.m. UTC | #2
On Tue, Oct 22, 2024 at 4:12 AM Junjie Mao <junjie.mao@hotmail.com> wrote:
>
>
> Paolo Bonzini <pbonzini@redhat.com> writes:
>
> > Remove the duplicate code by using the module_init! macro; at the same time,
> > simplify how module_init! is used, by taking inspiration from the implementation
> > of #[derive(Object)].
> >
> > Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
>
> Reviewed-by: Junjie Mao <junjie.mao@hotmail.com>
>
> One minor comment below.
>
> > ---
> >  rust/qemu-api-macros/src/lib.rs  | 33 +++-------------
> >  rust/qemu-api/src/definitions.rs | 66 ++++++++++++++------------------
> >  2 files changed, 33 insertions(+), 66 deletions(-)
> >
> <snip>
> > diff --git a/rust/qemu-api/src/definitions.rs b/rust/qemu-api/src/definitions.rs
> > index 3323a665d92..f180c38bfb2 100644
> > --- a/rust/qemu-api/src/definitions.rs
> > +++ b/rust/qemu-api/src/definitions.rs
> > @@ -29,51 +29,40 @@ pub trait Class {
> >
> >  #[macro_export]
> >  macro_rules! module_init {
> <snip>
> > +    ($type:ident => $body:block) => {
> > +        const _: () = {
> > +            #[used]
> > +            #[cfg_attr(
> > +                not(any(target_vendor = "apple", target_os = "windows")),
> > +                link_section = ".init_array"
> > +            )]
> > +            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
> > +            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
> > +            pub static LOAD_MODULE: extern "C" fn() = {
> > +                extern "C" fn init_fn() {
>
> init_fn() should be unsafe fn according to the signature of
> register_module_init.

I think it *can* be unsafe (which bindgen does by default). It's
always okay to pass a non-unsafe function as unsafe function pointer:

fn f() {
    println!("abc");
}

fn g(pf: unsafe fn()) {
    unsafe {
        pf();
    }
}

fn main() {
    g(f);
}

> Being unsafe fn also makes sense here because it
> is the caller, not init_fn() itself, that is responsible for the safety of
> the unsafe code in the body.

Isn't it the opposite? Since the caller of module_init! is responsible
for the safety, init_fn() itself can be safe. With
unsafe_op_in_unsafe_fn it's not a big deal; but without it, an unsafe
init_fn would hide what is safe and what is not in the argument of
module_init!.

It's also relevant in this respect that init_fn is called *after*
main(), only ctor_fn() is called before main.

Thanks,

Paolo
Junjie Mao Oct. 22, 2024, 6 a.m. UTC | #3
Paolo Bonzini <pbonzini@redhat.com> writes:

> On Tue, Oct 22, 2024 at 4:12 AM Junjie Mao <junjie.mao@hotmail.com> wrote:
>> > +    ($type:ident => $body:block) => {
>> > +        const _: () = {
>> > +            #[used]
>> > +            #[cfg_attr(
>> > +                not(any(target_vendor = "apple", target_os = "windows")),
>> > +                link_section = ".init_array"
>> > +            )]
>> > +            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
>> > +            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
>> > +            pub static LOAD_MODULE: extern "C" fn() = {
>> > +                extern "C" fn init_fn() {
>>
>> init_fn() should be unsafe fn according to the signature of
>> register_module_init.
>
> I think it *can* be unsafe (which bindgen does by default). It's
> always okay to pass a non-unsafe function as unsafe function pointer:
>
> fn f() {
>     println!("abc");
> }
>
> fn g(pf: unsafe fn()) {
>     unsafe {
>         pf();
>     }
> }
>
> fn main() {
>     g(f);
> }
>
>> Being unsafe fn also makes sense here because it
>> is the caller, not init_fn() itself, that is responsible for the safety of
>> the unsafe code in the body.
>
> Isn't it the opposite? Since the caller of module_init! is responsible
> for the safety, init_fn() itself can be safe.

My understanding is that:

  1. init_fn() is called by QEMU QOM subsystem with certain timing
     (e.g., called after main()) and concurrency (e.g., all init_fn()
     are called sequentially) preconditions.

  2. The caller of module_init! is responsible for justifying the safety
     of their $body under the preconditions established in #1.

"unsafe fn" in Rust is designed to mark functions with safety
preconditions so that the compiler can raise an error if the caller
forgets that it has those preconditions to uphold [1].

Under that interpretation, a safe "fn init_fn()" reads that init_fn() is
safe to call without the preconditions mentioned in #1. That is rarely
the case to me.

Using "unsafe" to mark the responsibility of device backends in #2 looks
like repurposing the keyword. That may make more sense in this specific
context because:

  1. the callers of init_fn() are in C, so Rust compiler cannot really
     check them,

  2. the caller will by design upholds the safety preconditions
     regardless of whether a specific callback needs it or not, and

  3. without unsafe_op_in_unsafe_fn an unsafe fn hides unsafe operation
     in its body from the compiler.

If that's what we prefer, I would suggest add the considerations above
to the code as comments, so that future readers are not confused by the
usage of "unsafe" here.

[1] https://rust-lang.github.io/rfcs/2585-unsafe-block-in-unsafe-fn.html

> With unsafe_op_in_unsafe_fn it's not a big deal; but without it, an
> unsafe init_fn would hide what is safe and what is not in the argument
> of module_init!.
>
> It's also relevant in this respect that init_fn is called *after*
> main(), only ctor_fn() is called before main.
>
> Thanks,
>
> Paolo

--
Best Regards
Junjie Mao
Paolo Bonzini Oct. 22, 2024, 7:20 a.m. UTC | #4
On Tue, Oct 22, 2024 at 9:12 AM Junjie Mao <junjie.mao@hotmail.com> wrote:
> My understanding is that:
>
>   1. init_fn() is called by QEMU QOM subsystem with certain timing
>      (e.g., called after main()) and concurrency (e.g., all init_fn()
>      are called sequentially) preconditions.
>
>   2. The caller of module_init! is responsible for justifying the safety
>      of their $body under the preconditions established in #1.
>
> "unsafe fn" in Rust is designed to mark functions with safety
> preconditions so that the compiler can raise an error if the caller
> forgets that it has those preconditions to uphold [1].
>
> Under that interpretation, a safe "fn init_fn()" reads that init_fn() is
> safe to call without the preconditions mentioned in #1. That is rarely
> the case to me.
>
> Using "unsafe" to mark the responsibility of device backends in #2 looks
> like repurposing the keyword. That may make more sense in this specific
> context because:
>
>   1. the callers of init_fn() are in C, so Rust compiler cannot really
>      check them,
>
>   2. the caller will by design upholds the safety preconditions
>      regardless of whether a specific callback needs it or not, and

Or at least will try.

>   3. without unsafe_op_in_unsafe_fn an unsafe fn hides unsafe operation
>      in its body from the compiler.

4. In this specific case, init_fn is only used from ctor_fn and even
there only as a function pointer; it is not visible from outside. So
the "unsafe" marker's only purpose is documentation (and in fact, as
you point out in (3), it would even be harmful without
unsafe_op_in_unsafe_fn).

> If that's what we prefer, I would suggest add the considerations above
> to the code as comments, so that future readers are not confused by the
> usage of "unsafe" here.

Yes, I agree that since we're talking about a macro (not a function
which can itself be annotated with "unsafe") that lies at the C<->Rust
boundary, this is mostly a documentation issue.

In general the documentation of qemu-api and qemu-api-macros leaves
something to be desired. This is okay, but it is also the kind of
technical debt that we need to look at as soon as we can actually get
the thing to compile. :)

I'll add it shortly to https://wiki.qemu.org/Features/Rust.

Paolo
Kevin Wolf Oct. 22, 2024, 8:32 p.m. UTC | #5
Am 22.10.2024 um 08:00 hat Junjie Mao geschrieben:
> 
> Paolo Bonzini <pbonzini@redhat.com> writes:
> 
> > On Tue, Oct 22, 2024 at 4:12 AM Junjie Mao <junjie.mao@hotmail.com> wrote:
> >> > +    ($type:ident => $body:block) => {
> >> > +        const _: () = {
> >> > +            #[used]
> >> > +            #[cfg_attr(
> >> > +                not(any(target_vendor = "apple", target_os = "windows")),
> >> > +                link_section = ".init_array"
> >> > +            )]
> >> > +            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
> >> > +            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
> >> > +            pub static LOAD_MODULE: extern "C" fn() = {
> >> > +                extern "C" fn init_fn() {
> >>
> >> init_fn() should be unsafe fn according to the signature of
> >> register_module_init.
> >
> > I think it *can* be unsafe (which bindgen does by default). It's
> > always okay to pass a non-unsafe function as unsafe function pointer:
> >
> > fn f() {
> >     println!("abc");
> > }
> >
> > fn g(pf: unsafe fn()) {
> >     unsafe {
> >         pf();
> >     }
> > }
> >
> > fn main() {
> >     g(f);
> > }
> >
> >> Being unsafe fn also makes sense here because it
> >> is the caller, not init_fn() itself, that is responsible for the safety of
> >> the unsafe code in the body.
> >
> > Isn't it the opposite? Since the caller of module_init! is responsible
> > for the safety, init_fn() itself can be safe.
> 
> My understanding is that:
> 
>   1. init_fn() is called by QEMU QOM subsystem with certain timing
>      (e.g., called after main()) and concurrency (e.g., all init_fn()
>      are called sequentially) preconditions.

Though these conditions are not a matter of safety, but of correctness.

>   2. The caller of module_init! is responsible for justifying the safety
>      of their $body under the preconditions established in #1.
> 
> "unsafe fn" in Rust is designed to mark functions with safety
> preconditions so that the compiler can raise an error if the caller
> forgets that it has those preconditions to uphold [1].

I don't think we expect to have preconditions here that are required for
safety (in the sense with which the term is used in Rust).

But safe code is not automatically correct.

If you added "unsafe" for every function that requires some
preconditions to be met so that it functions correctly (instead of only
so that it doesn't have undefined behaviour on the language level), then
you would annotate almost every function as "unsafe".

I think the rule of thumb for marking a function unsafe is when you have
unsafe code inside of it whose safety (not correctness!) depends on a
condition that the caller must reason about because you can't guarantee
it in the function itself.

This macro should probably only be used with code that can guarantee the
safety of its unsafe blocks in itself. The C side of constructors can't
make many guarantees anyway, and there is nobody who would reason about
them.

Kevin
Junjie Mao Oct. 23, 2024, 6:46 a.m. UTC | #6
Kevin Wolf <kwolf@redhat.com> writes:

> Am 22.10.2024 um 08:00 hat Junjie Mao geschrieben:
>>
>> Paolo Bonzini <pbonzini@redhat.com> writes:
>>
>> > On Tue, Oct 22, 2024 at 4:12 AM Junjie Mao <junjie.mao@hotmail.com> wrote:
>> >> > +    ($type:ident => $body:block) => {
>> >> > +        const _: () = {
>> >> > +            #[used]
>> >> > +            #[cfg_attr(
>> >> > +                not(any(target_vendor = "apple", target_os = "windows")),
>> >> > +                link_section = ".init_array"
>> >> > +            )]
>> >> > +            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
>> >> > +            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
>> >> > +            pub static LOAD_MODULE: extern "C" fn() = {
>> >> > +                extern "C" fn init_fn() {
>> >>
>> >> init_fn() should be unsafe fn according to the signature of
>> >> register_module_init.
>> >
>> > I think it *can* be unsafe (which bindgen does by default). It's
>> > always okay to pass a non-unsafe function as unsafe function pointer:
>> >
>> > fn f() {
>> >     println!("abc");
>> > }
>> >
>> > fn g(pf: unsafe fn()) {
>> >     unsafe {
>> >         pf();
>> >     }
>> > }
>> >
>> > fn main() {
>> >     g(f);
>> > }
>> >
>> >> Being unsafe fn also makes sense here because it
>> >> is the caller, not init_fn() itself, that is responsible for the safety of
>> >> the unsafe code in the body.
>> >
>> > Isn't it the opposite? Since the caller of module_init! is responsible
>> > for the safety, init_fn() itself can be safe.
>>
>> My understanding is that:
>>
>>   1. init_fn() is called by QEMU QOM subsystem with certain timing
>>      (e.g., called after main()) and concurrency (e.g., all init_fn()
>>      are called sequentially) preconditions.
>
> Though these conditions are not a matter of safety, but of correctness.
>

The "no concurrency" condition is related to Rust safety. Deep in
type_register_static(), which is commonly used in such init_fn(), there
is a modification to mutable statics in type_table_get():

    static GHashTable *type_table_get(void)
    {
        static GHashTable *type_table;

        if (type_table == NULL) {
            type_table = g_hash_table_new(g_str_hash, g_str_equal);
        }

        return type_table;
    }

It'll cause data race when multiple init_fn() are concurrently called,
and data races has UB.

Also, glib hashtables are not automatically thread-safe [1]:

    ... individual data structure instances are not automatically locked
    for performance reasons. For example, you must coordinate accesses
    to the same GHashTable from multiple threads.

[1] https://docs.gtk.org/glib/threads.html

I don't have evidence yet for the relationship between timing condition
and Rust safety, though.

>>   2. The caller of module_init! is responsible for justifying the safety
>>      of their $body under the preconditions established in #1.
>>
>> "unsafe fn" in Rust is designed to mark functions with safety
>> preconditions so that the compiler can raise an error if the caller
>> forgets that it has those preconditions to uphold [1].
>
> I don't think we expect to have preconditions here that are required for
> safety (in the sense with which the term is used in Rust).
>
> But safe code is not automatically correct.
>
> If you added "unsafe" for every function that requires some
> preconditions to be met so that it functions correctly (instead of only
> so that it doesn't have undefined behaviour on the language level), then
> you would annotate almost every function as "unsafe".
>
> I think the rule of thumb for marking a function unsafe is when you have
> unsafe code inside of it whose safety (not correctness!) depends on a
> condition that the caller must reason about because you can't guarantee
> it in the function itself.

I fully agree with your rules for "unsafe fn" uses.

--
Best Regards
Junjie Mao

>
> This macro should probably only be used with code that can guarantee
> the safety of its unsafe blocks in itself. The C side of constructors
> can't make many guarantees anyway, and there is nobody who would
> reason about them.
>
> Kevin
Zhao Liu Oct. 25, 2024, 8:59 a.m. UTC | #7
On Mon, Oct 21, 2024 at 06:35:33PM +0200, Paolo Bonzini wrote:
> Date: Mon, 21 Oct 2024 18:35:33 +0200
> From: Paolo Bonzini <pbonzini@redhat.com>
> Subject: [PATCH v2 08/13] rust: cleanup module_init!, use it from
>  #[derive(Object)]
> X-Mailer: git-send-email 2.46.2
> 
> Remove the duplicate code by using the module_init! macro; at the same time,
> simplify how module_init! is used, by taking inspiration from the implementation
> of #[derive(Object)].
> 
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
>  rust/qemu-api-macros/src/lib.rs  | 33 +++-------------
>  rust/qemu-api/src/definitions.rs | 66 ++++++++++++++------------------
>  2 files changed, 33 insertions(+), 66 deletions(-)

LGTM (with some questions related type_init usage inline)

Reviewed-by: Zhao Liu <zhao1.liu@intel>

> diff --git a/rust/qemu-api-macros/src/lib.rs b/rust/qemu-api-macros/src/lib.rs
> index 70e3f920460..a4bc5d01ee8 100644
> --- a/rust/qemu-api-macros/src/lib.rs
> +++ b/rust/qemu-api-macros/src/lib.rs
> @@ -3,43 +3,20 @@
>  // SPDX-License-Identifier: GPL-2.0-or-later
>  #[proc_macro_derive(Object)]
>  pub fn derive_object(input: TokenStream) -> TokenStream {

[snip]

> +            MODULE_INIT_QOM => unsafe {
> +                ::qemu_api::bindings::type_register_static(&<#name as ::qemu_api::definitions::ObjectImpl>::TYPE_INFO);
>              }

I want to see how general this macro could be, so I checked current
type_init() cases for TypeInfo. In most cases, only type_register_static()
is called directly in the init_fn() callback.

There are only two exceptions:

1. Some init_fn callbacks contain more complex validation or register
logic.

  For example, in backends/hostmem-epc.c, sgx_epc_backed_info involves
  extra check before type_register_static().

  static void register_types(void)
  {
      int fd = qemu_open_old("/dev/sgx_vepc", O_RDWR);
      if (fd >= 0) {
          close(fd);

          type_register_static(&sgx_epc_backed_info);
      }
  }


  And in hw/audio/intel-hda.c, there's extra pci_register_soundhw afer
  type_register_static():

  static void intel_hda_register_types(void)
  {
      type_register_static(&hda_codec_bus_info);
      type_register_static(&intel_hda_info);
      type_register_static(&intel_hda_info_ich6);
      type_register_static(&intel_hda_info_ich9);
      type_register_static(&hda_codec_device_type_info);
      pci_register_soundhw("hda", "Intel HD Audio", intel_hda_and_codec_init);
  }

  The device can define a custom init_fn() for TypeInfo based on
  module_init!, but I wonder if the examples above are valid. Is it
  allowed to include other logic in init_fn()?


2. Some init_fn callbacks use type_register() instead of
type_register_static().

  TypeImpl *type_register_static(const TypeInfo *info)
  {
      return type_register(info);
  }

  It seems that type_register() and type_register_static() are the same.
  I guess I could clean up one of them, right? (type_register() was added
  by your earlie commit 049cb3cfdac1 :-) ).

Thanks,
Zhao
Paolo Bonzini Oct. 25, 2024, 9:12 a.m. UTC | #8
On 10/25/24 10:59, Zhao Liu wrote:
> I want to see how general this macro could be, so I checked current
> type_init() cases for TypeInfo. In most cases, only type_register_static()
> is called directly in the init_fn() callback.

First of all, it's okay if the Rust code does not cover everything.  But 
looking at your cases:

> There are only two exceptions:
> 
> 1. Some init_fn callbacks contain more complex validation or register
> logic.
> 
>    For example, in backends/hostmem-epc.c, sgx_epc_backed_info involves
>    extra check before type_register_static().
> 
>    static void register_types(void)
>    {
>        int fd = qemu_open_old("/dev/sgx_vepc", O_RDWR);
>        if (fd >= 0) {
>            close(fd);
> 
>            type_register_static(&sgx_epc_backed_info);
>        }
>    }

This one is okay to just change to type_register_static(), moving the 
/dev/sgx_vepc code to the "complete" callback.

> 
>    And in hw/audio/intel-hda.c, there's extra pci_register_soundhw afer
>    type_register_static():
> 
>    static void intel_hda_register_types(void)
>    {
>        type_register_static(&hda_codec_bus_info);
>        type_register_static(&intel_hda_info);
>        type_register_static(&intel_hda_info_ich6);
>        type_register_static(&intel_hda_info_ich9);
>        type_register_static(&hda_codec_device_type_info);
>        pci_register_soundhw("hda", "Intel HD Audio", intel_hda_and_codec_init);
>    }
> 
>    The device can define a custom init_fn() for TypeInfo based on
>    module_init!, but I wonder if the examples above are valid. Is it
>    allowed to include other logic in init_fn()?

Yes, it is but it is also possible to use your own module_init! 
invocation outside #[derive(Object)].
> 2. Some init_fn callbacks use type_register() instead of
> type_register_static().
> 
>    TypeImpl *type_register_static(const TypeInfo *info)
>    {
>        return type_register(info);
>    }
> 
>    It seems that type_register() and type_register_static() are the same.
>    I guess I could clean up one of them, right? (type_register() was added
>    by your earlie commit 049cb3cfdac1 :-) ).

Yeah, you can!

Paolo
diff mbox series

Patch

diff --git a/rust/qemu-api-macros/src/lib.rs b/rust/qemu-api-macros/src/lib.rs
index 70e3f920460..a4bc5d01ee8 100644
--- a/rust/qemu-api-macros/src/lib.rs
+++ b/rust/qemu-api-macros/src/lib.rs
@@ -3,43 +3,20 @@ 
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 use proc_macro::TokenStream;
-use quote::{format_ident, quote};
+use quote::quote;
 use syn::{parse_macro_input, DeriveInput};
 
 #[proc_macro_derive(Object)]
 pub fn derive_object(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
-
     let name = input.ident;
-    let module_static = format_ident!("__{}_LOAD_MODULE", name);
 
     let expanded = quote! {
-        #[allow(non_upper_case_globals)]
-        #[used]
-        #[cfg_attr(
-            not(any(target_vendor = "apple", target_os = "windows")),
-            link_section = ".init_array"
-        )]
-        #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
-        #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
-        pub static #module_static: extern "C" fn() = {
-            extern "C" fn __register() {
-                unsafe {
-                    ::qemu_api::bindings::type_register_static(&<#name as ::qemu_api::definitions::ObjectImpl>::TYPE_INFO);
-                }
+        ::qemu_api::module_init! {
+            MODULE_INIT_QOM => unsafe {
+                ::qemu_api::bindings::type_register_static(&<#name as ::qemu_api::definitions::ObjectImpl>::TYPE_INFO);
             }
-
-            extern "C" fn __load() {
-                unsafe {
-                    ::qemu_api::bindings::register_module_init(
-                        Some(__register),
-                        ::qemu_api::bindings::module_init_type::MODULE_INIT_QOM
-                    );
-                }
-            }
-
-            __load
-        };
+        }
     };
 
     TokenStream::from(expanded)
diff --git a/rust/qemu-api/src/definitions.rs b/rust/qemu-api/src/definitions.rs
index 3323a665d92..f180c38bfb2 100644
--- a/rust/qemu-api/src/definitions.rs
+++ b/rust/qemu-api/src/definitions.rs
@@ -29,51 +29,40 @@  pub trait Class {
 
 #[macro_export]
 macro_rules! module_init {
-    ($func:expr, $type:expr) => {
-        #[used]
-        #[cfg_attr(
-            not(any(target_vendor = "apple", target_os = "windows")),
-            link_section = ".init_array"
-        )]
-        #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
-        #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
-        pub static LOAD_MODULE: extern "C" fn() = {
-            extern "C" fn __load() {
-                unsafe {
-                    $crate::bindings::register_module_init(Some($func), $type);
-                }
-            }
-
-            __load
-        };
-    };
-    (qom: $func:ident => $body:block) => {
-        // NOTE: To have custom identifiers for the ctor func we need to either supply
-        // them directly as a macro argument or create them with a proc macro.
-        #[used]
-        #[cfg_attr(
-            not(any(target_vendor = "apple", target_os = "windows")),
-            link_section = ".init_array"
-        )]
-        #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
-        #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
-        pub static LOAD_MODULE: extern "C" fn() = {
-            extern "C" fn __load() {
-                unsafe extern "C" fn $func() {
+    ($type:ident => $body:block) => {
+        const _: () = {
+            #[used]
+            #[cfg_attr(
+                not(any(target_vendor = "apple", target_os = "windows")),
+                link_section = ".init_array"
+            )]
+            #[cfg_attr(target_vendor = "apple", link_section = "__DATA,__mod_init_func")]
+            #[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
+            pub static LOAD_MODULE: extern "C" fn() = {
+                extern "C" fn init_fn() {
                     $body
                 }
 
-                unsafe {
-                    $crate::bindings::register_module_init(
-                        Some($func),
-                        $crate::bindings::module_init_type::MODULE_INIT_QOM,
-                    );
+                extern "C" fn ctor_fn() {
+                    unsafe {
+                        $crate::bindings::register_module_init(
+                            Some(init_fn),
+                            $crate::bindings::module_init_type::$type,
+                        );
+                    }
                 }
-            }
 
-            __load
+                ctor_fn
+            };
         };
     };
+
+    // shortcut because it's quite common that $body needs unsafe {}
+    ($type:ident => unsafe $body:block) => {
+        $crate::module_init! {
+            $type => { unsafe { $body } }
+        }
+    };
 }
 
 #[macro_export]