[PATCH v2] rust: introduce alternative implementation of offset_of!

Junjie Mao posted 1 patch 10 hours ago
rust/hw/char/pl011/src/device.rs              |  2 +-
rust/qemu-api-macros/Cargo.toml               |  6 +-
rust/qemu-api-macros/src/lib.rs               | 71 +++++++++++++-
rust/qemu-api/Cargo.lock                      | 47 ++++++++++
rust/qemu-api/Cargo.toml                      |  1 +
rust/qemu-api/meson.build                     | 12 ++-
rust/qemu-api/src/device_class.rs             |  8 +-
rust/qemu-api/src/lib.rs                      |  4 +
rust/qemu-api/src/offset_of.rs                | 94 +++++++++++++++++++
rust/qemu-api/tests/tests.rs                  | 31 +++++-
subprojects/packagefiles/syn-2-rs/meson.build |  1 +
11 files changed, 261 insertions(+), 16 deletions(-)
create mode 100644 rust/qemu-api/src/offset_of.rs
[PATCH v2] rust: introduce alternative implementation of offset_of!
Posted by Junjie Mao 10 hours ago
offset_of! was stabilized in Rust 1.77.0. Use an alternative implemenation
that was found on the Rust forums, and whose author agreed to license as
MIT for use in QEMU.

The alternative allows only one level of field access, but apart from this
can be used just by replacing core::mem::offset_of! with
qemu_api::offset_of!.

Using offset_of! prior Rust 1.77.0 requires the structure to have the
derive(qemu_api_macros::Offsets) attribute.

Base on Paolo's fix series [1].

Make sure to remove subprojects/syn-2.0.66 if there is an existing
build. An additional feature cfg is added to
packagefiles/syn-2-rs/meson.build, which requires meson to re-checkout the
subproject.

TODO:

1. For each type of error detected in the proc macro:
    a. Determine the proper span in the error reports.
    b. Add tests.

v1 -> v2:

* Rebase to [1].
* The proc macro now generates a call to the with_offsets! macro rule
  instead of generating everything by itself.
* Use quote_spanned! when reporting errors in the proc macro.
* Add qemu-api-macro as a dependency of qemu-api for cargo expand.

[1] https://lore.kernel.org/qemu-devel/20241021163538.136941-1-pbonzini@redhat.com

Co-authored-by: Paolo Bonzini <pbonzini@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Signed-off-by: Junjie Mao <junjie.mao@hotmail.com>
---
 rust/hw/char/pl011/src/device.rs              |  2 +-
 rust/qemu-api-macros/Cargo.toml               |  6 +-
 rust/qemu-api-macros/src/lib.rs               | 71 +++++++++++++-
 rust/qemu-api/Cargo.lock                      | 47 ++++++++++
 rust/qemu-api/Cargo.toml                      |  1 +
 rust/qemu-api/meson.build                     | 12 ++-
 rust/qemu-api/src/device_class.rs             |  8 +-
 rust/qemu-api/src/lib.rs                      |  4 +
 rust/qemu-api/src/offset_of.rs                | 94 +++++++++++++++++++
 rust/qemu-api/tests/tests.rs                  | 31 +++++-
 subprojects/packagefiles/syn-2-rs/meson.build |  1 +
 11 files changed, 261 insertions(+), 16 deletions(-)
 create mode 100644 rust/qemu-api/src/offset_of.rs

diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
index 0f6918dd22..fdb15e42d8 100644
--- a/rust/hw/char/pl011/src/device.rs
+++ b/rust/hw/char/pl011/src/device.rs
@@ -26,7 +26,7 @@
 pub const PL011_FIFO_DEPTH: usize = 16_usize;
 
 #[repr(C)]
-#[derive(Debug, qemu_api_macros::Object)]
+#[derive(Debug, qemu_api_macros::Object, qemu_api_macros::Offsets)]
 /// PL011 Device Model in QEMU
 pub struct PL011State {
     pub parent_obj: SysBusDevice,
diff --git a/rust/qemu-api-macros/Cargo.toml b/rust/qemu-api-macros/Cargo.toml
index 144cc3650f..19ea1e84ea 100644
--- a/rust/qemu-api-macros/Cargo.toml
+++ b/rust/qemu-api-macros/Cargo.toml
@@ -17,9 +17,9 @@ categories = []
 proc-macro = true
 
 [dependencies]
-proc-macro2 = "1"
-quote = "1"
-syn = "2"
+proc-macro2 = "=1.0.84"
+quote = "=1.0.36"
+syn = { version = "=2.0.66", features = ["full", "derive", "parsing", "printing", "clone-impls", "proc-macro", "extra-traits"] }
 
 # Do not include in any global workspace
 [workspace]
diff --git a/rust/qemu-api-macros/src/lib.rs b/rust/qemu-api-macros/src/lib.rs
index a4bc5d01ee..055c71efba 100644
--- a/rust/qemu-api-macros/src/lib.rs
+++ b/rust/qemu-api-macros/src/lib.rs
@@ -3,8 +3,12 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 use proc_macro::TokenStream;
-use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use proc_macro2::Span;
+use quote::{quote, quote_spanned};
+use syn::{
+    parse_macro_input, parse_quote, punctuated::Punctuated, token::Comma, Data, DeriveInput, Field,
+    Fields, Ident, Type, Visibility,
+};
 
 #[proc_macro_derive(Object)]
 pub fn derive_object(input: TokenStream) -> TokenStream {
@@ -21,3 +25,66 @@ pub fn derive_object(input: TokenStream) -> TokenStream {
 
     TokenStream::from(expanded)
 }
+
+fn is_c_repr(input: &DeriveInput) -> Result<(), (String, Span)> {
+    let expected = parse_quote! { #[repr(C)] };
+
+    if input.attrs.iter().any(|attr| attr == &expected) {
+        Ok(())
+    } else {
+        Err((
+            "Can only generate offsets for structs with a C representation.".to_string(),
+            input.ident.span(),
+        ))
+    }
+}
+
+fn get_fields(input: &DeriveInput) -> Result<&Punctuated<Field, Comma>, (String, Span)> {
+    if let Data::Struct(s) = &input.data {
+        if let Fields::Named(fs) = &s.fields {
+            Ok(&fs.named)
+        } else {
+            Err((
+                "Cannot generate offsets for unnamed fields.".to_string(),
+                input.ident.span(),
+            ))
+        }
+    } else {
+        Err((
+            "Cannot generate offsets for union or enum.".to_string(),
+            input.ident.span(),
+        ))
+    }
+}
+
+#[rustfmt::skip::macros(quote)]
+fn derive_offsets_or_error(input: DeriveInput) -> Result<proc_macro2::TokenStream, (String, Span)> {
+    is_c_repr(&input)?;
+
+    let name = &input.ident;
+    let fields = get_fields(&input)?;
+    let field_names: Vec<&Ident> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
+    let field_types: Vec<&Type> = fields.iter().map(|f| &f.ty).collect();
+    let field_vis: Vec<&Visibility> = fields.iter().map(|f| &f.vis).collect();
+
+    Ok(quote! {
+	::qemu_api::with_offsets! {
+	    struct #name {
+		#(#field_vis #field_names: #field_types,)*
+	    }
+	}
+    })
+}
+
+/// A derive macro that generate field offsets for using `offset_of!` in
+/// versions of Rust prior to 1.77
+#[proc_macro_derive(Offsets)]
+pub fn derive_offsets(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let expanded = match derive_offsets_or_error(input) {
+        Ok(ts) => ts,
+        Err((msg, span)) => quote_spanned! {span=> compile_error!(#msg); },
+    };
+
+    proc_macro::TokenStream::from(expanded)
+}
diff --git a/rust/qemu-api/Cargo.lock b/rust/qemu-api/Cargo.lock
index e9c51a243a..3886f58cd0 100644
--- a/rust/qemu-api/Cargo.lock
+++ b/rust/qemu-api/Cargo.lock
@@ -2,6 +2,53 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "proc-macro2"
+version = "1.0.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
+dependencies = [
+ "unicode-ident",
+]
+
 [[package]]
 name = "qemu_api"
 version = "0.1.0"
+dependencies = [
+ "qemu_api_macros",
+]
+
+[[package]]
+name = "qemu_api_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
diff --git a/rust/qemu-api/Cargo.toml b/rust/qemu-api/Cargo.toml
index 3677def3fe..db594c6408 100644
--- a/rust/qemu-api/Cargo.toml
+++ b/rust/qemu-api/Cargo.toml
@@ -14,6 +14,7 @@ keywords = []
 categories = []
 
 [dependencies]
+qemu_api_macros = { path = "../qemu-api-macros" }
 
 [features]
 default = []
diff --git a/rust/qemu-api/meson.build b/rust/qemu-api/meson.build
index 1b0fd40637..5736a25181 100644
--- a/rust/qemu-api/meson.build
+++ b/rust/qemu-api/meson.build
@@ -1,3 +1,9 @@
+_qemu_api_cfg = ['--cfg', 'MESON']
+# _qemu_api_cfg += ['--cfg', 'feature="allocator"']
+if rustc.version().version_compare('>=1.77.0')
+  _qemu_api_cfg += ['--cfg', 'has_offset_of']
+endif
+
 _qemu_api_rs = static_library(
   'qemu_api',
   structured_sources(
@@ -6,15 +12,13 @@ _qemu_api_rs = static_library(
       'src/definitions.rs',
       'src/device_class.rs',
       'src/zeroable.rs',
+      'src/offset_of.rs',
     ],
     {'.' : bindings_rs},
   ),
   override_options: ['rust_std=2021', 'build.rust_std=2021'],
   rust_abi: 'rust',
-  rust_args: [
-    '--cfg', 'MESON',
-    # '--cfg', 'feature="allocator"',
-  ],
+  rust_args: _qemu_api_cfg,
 )
 
 qemu_api = declare_dependency(
diff --git a/rust/qemu-api/src/device_class.rs b/rust/qemu-api/src/device_class.rs
index ed2d7ce1a5..ed1b14c677 100644
--- a/rust/qemu-api/src/device_class.rs
+++ b/rust/qemu-api/src/device_class.rs
@@ -23,23 +23,23 @@ macro_rules! device_class_init {
 
 #[macro_export]
 macro_rules! define_property {
-    ($name:expr, $state:ty, $field:expr, $prop:expr, $type:expr, default = $defval:expr$(,)*) => {
+    ($name:expr, $state:ty, $field:ident, $prop:expr, $type:expr, default = $defval:expr$(,)*) => {
         $crate::bindings::Property {
             // use associated function syntax for type checking
             name: ::core::ffi::CStr::as_ptr($name),
             info: $prop,
-            offset: ::core::mem::offset_of!($state, $field) as isize,
+            offset: $crate::offset_of!($state, $field) as isize,
             set_default: true,
             defval: $crate::bindings::Property__bindgen_ty_1 { u: $defval.into() },
             ..$crate::zeroable::Zeroable::ZERO
         }
     };
-    ($name:expr, $state:ty, $field:expr, $prop:expr, $type:expr$(,)*) => {
+    ($name:expr, $state:ty, $field:ident, $prop:expr, $type:expr$(,)*) => {
         $crate::bindings::Property {
             // use associated function syntax for type checking
             name: ::core::ffi::CStr::as_ptr($name),
             info: $prop,
-            offset: ::core::mem::offset_of!($state, $field) as isize,
+            offset: $crate::offset_of!($state, $field) as isize,
             set_default: false,
             ..$crate::zeroable::Zeroable::ZERO
         }
diff --git a/rust/qemu-api/src/lib.rs b/rust/qemu-api/src/lib.rs
index e94a15bb82..9b2f580756 100644
--- a/rust/qemu-api/src/lib.rs
+++ b/rust/qemu-api/src/lib.rs
@@ -29,6 +29,7 @@ unsafe impl Sync for bindings::VMStateDescription {}
 
 pub mod definitions;
 pub mod device_class;
+pub mod offset_of;
 pub mod zeroable;
 
 use std::alloc::{GlobalAlloc, Layout};
@@ -162,3 +163,6 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
         }
     }
 }
+
+#[cfg(has_offset_of)]
+pub use std::mem::offset_of;
diff --git a/rust/qemu-api/src/offset_of.rs b/rust/qemu-api/src/offset_of.rs
new file mode 100644
index 0000000000..f914fda648
--- /dev/null
+++ b/rust/qemu-api/src/offset_of.rs
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: MIT
+
+/// This macro provides the same functionality as `core::mem::offset_of`,
+/// except that only one level of field access is supported. The declaration
+/// of the struct must be attributed `derive(qemu_api_macros::Offsets)`.
+///
+/// It is needed because `offset_of!` was only stabilized in Rust 1.77.
+#[cfg(not(has_offset_of))]
+#[macro_export]
+macro_rules! offset_of {
+    ($Container:ty, $field:ident) => {
+        <$Container>::offset_to.$field
+    };
+}
+
+/// A wrapper for struct declarations, that allows using `offset_of!` in
+/// versions of Rust prior to 1.77.
+///
+/// Do not use this macro directly. Add the derive(qemu_api_macros::Offsets)
+/// attribute to the structure instead.
+#[macro_export]
+macro_rules! with_offsets {
+    // source: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=10a22a9b8393abd7b541d8fc844bc0df
+    // used under MIT license with permission of Yandros aka Daniel Henry-Mantilla
+    (
+        struct $StructName:ident {
+            $(
+                $field_vis:vis
+                $field_name:ident : $field_ty:ty
+            ),*
+            $(,)?
+        }
+    ) => (
+        #[cfg(not(has_offset_of))]
+        #[allow(nonstandard_style)]
+        const _: () = {
+            pub
+            struct StructOffsets {
+                $(
+                    $field_vis
+                    $field_name: usize,
+                )*
+            }
+            struct Helper;
+            impl $StructName {
+                pub
+                const offset_to: StructOffsets = StructOffsets {
+                    $(
+                        $field_name: Helper::$field_name,
+                    )*
+                };
+            }
+            const END_OF_PREV_FIELD: usize = 0;
+            $crate::with_offsets! {
+                @names [ $($field_name)* ]
+                @tys [ $($field_ty ,)*]
+            }
+        };
+    );
+
+    (
+        @names []
+        @tys []
+    ) => ();
+
+    (
+        @names [$field_name:ident $($other_names:tt)*]
+        @tys [$field_ty:ty , $($other_tys:tt)*]
+    ) => (
+        impl Helper {
+            const $field_name: usize = {
+                let align =
+                    std::mem::align_of::<$field_ty>()
+                ;
+                let trail =
+                    END_OF_PREV_FIELD % align
+                ;
+                0   + END_OF_PREV_FIELD
+                    + (align - trail)
+                        * [1, 0][(trail == 0) as usize]
+            };
+        }
+        const _: () = {
+            const END_OF_PREV_FIELD: usize =
+                Helper::$field_name +
+                std::mem::size_of::<$field_ty>()
+            ;
+            $crate::with_offsets! {
+                @names [$($other_names)*]
+                @tys [$($other_tys)*]
+            }
+        };
+    );
+}
diff --git a/rust/qemu-api/tests/tests.rs b/rust/qemu-api/tests/tests.rs
index aa1e0568c6..22ce1b5628 100644
--- a/rust/qemu-api/tests/tests.rs
+++ b/rust/qemu-api/tests/tests.rs
@@ -8,7 +8,7 @@
     bindings::*,
     declare_properties, define_property,
     definitions::{Class, ObjectImpl},
-    device_class_init, vm_state_description,
+    device_class_init, offset_of, vm_state_description,
 };
 
 #[test]
@@ -21,7 +21,7 @@ fn test_device_decl_macros() {
     }
 
     #[repr(C)]
-    #[derive(qemu_api_macros::Object)]
+    #[derive(qemu_api_macros::Object, qemu_api_macros::Offsets)]
     pub struct DummyState {
         pub _parent: DeviceState,
         pub migrate_clock: bool,
@@ -76,3 +76,30 @@ impl Class for DummyClass {
         object_unref(object_new(DummyState::TYPE_NAME.as_ptr()) as *mut _);
     }
 }
+
+#[test]
+fn test_offset_of() {
+    #[repr(C)]
+    struct Foo {
+        a: u16,
+        b: u32,
+        c: u64,
+        d: u16,
+    }
+
+    #[repr(C)]
+    #[derive(qemu_api_macros::Offsets)]
+    struct Bar {
+        pub a: u16,
+        pub b: u64,
+        c: Foo,
+        d: u64,
+    }
+
+    const OFFSET_TO_C: usize = offset_of!(Bar, c);
+
+    assert_eq!(offset_of!(Bar, a), 0);
+    assert_eq!(offset_of!(Bar, b), 8);
+    assert_eq!(OFFSET_TO_C, 16);
+    assert_eq!(offset_of!(Bar, d), 40);
+}
diff --git a/subprojects/packagefiles/syn-2-rs/meson.build b/subprojects/packagefiles/syn-2-rs/meson.build
index a53335f309..9f56ce1c24 100644
--- a/subprojects/packagefiles/syn-2-rs/meson.build
+++ b/subprojects/packagefiles/syn-2-rs/meson.build
@@ -24,6 +24,7 @@ _syn_rs = static_library(
     '--cfg', 'feature="printing"',
     '--cfg', 'feature="clone-impls"',
     '--cfg', 'feature="proc-macro"',
+    '--cfg', 'feature="extra-traits"',
   ],
   dependencies: [
     quote_dep,
-- 
2.34.1
Re: [PATCH v2] rust: introduce alternative implementation of offset_of!
Posted by Junjie Mao 10 hours ago
Junjie Mao <junjie.mao@hotmail.com> writes:

> offset_of! was stabilized in Rust 1.77.0. Use an alternative implemenation
> that was found on the Rust forums, and whose author agreed to license as
> MIT for use in QEMU.
>
> The alternative allows only one level of field access, but apart from this
> can be used just by replacing core::mem::offset_of! with
> qemu_api::offset_of!.
>
> Using offset_of! prior Rust 1.77.0 requires the structure to have the
> derive(qemu_api_macros::Offsets) attribute.
>
> Base on Paolo's fix series [1].
>
> Make sure to remove subprojects/syn-2.0.66 if there is an existing
> build. An additional feature cfg is added to
> packagefiles/syn-2-rs/meson.build, which requires meson to re-checkout the
> subproject.
>
> TODO:
>
> 1. For each type of error detected in the proc macro:
>     a. Determine the proper span in the error reports.
>     b. Add tests.
>
> v1 -> v2:
>
> * Rebase to [1].
> * The proc macro now generates a call to the with_offsets! macro rule
>   instead of generating everything by itself.
> * Use quote_spanned! when reporting errors in the proc macro.
> * Add qemu-api-macro as a dependency of qemu-api for cargo expand.
>
> [1] https://lore.kernel.org/qemu-devel/20241021163538.136941-1-pbonzini@redhat.com
>
> Co-authored-by: Paolo Bonzini <pbonzini@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> Signed-off-by: Junjie Mao <junjie.mao@hotmail.com>

Just noticed Paolo has a refreshed version in his v2 series.

Kindly ignore this patch.

--
Best Regards
Junjie Mao
Re: [PATCH v2] rust: introduce alternative implementation of offset_of!
Posted by Paolo Bonzini 9 hours ago
On 10/22/24 13:01, Junjie Mao wrote:
> Just noticed Paolo has a refreshed version in his v2 series.

I like your improved error reporting actually.  If (as will almost 
certainly be the case) there will be a v3, I will incorporate it.

Paolo