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

Paolo Bonzini posted 14 patches 13 hours ago
[PATCH v2 07/14] rust: introduce alternative implementation of offset_of!
Posted by Paolo Bonzini 13 hours ago
From: Junjie Mao <junjie.mao@hotmail.com>

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!.

The actual implementation of offset_of! is done in a declarative macro,
but for simplicity and to avoid introducing an extra level of indentation,
the trigger is a procedural macro #[derive(offsets)].

Signed-off-by: Junjie Mao <junjie.mao@hotmail.com>
Co-developed-by: Paolo Bonzini <pbonzini@redhat.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
 rust/hw/char/pl011/Cargo.lock                 |   1 +
 rust/hw/char/pl011/src/device.rs              |   2 +-
 rust/qemu-api-macros/Cargo.toml               |   5 +-
 rust/qemu-api-macros/src/lib.rs               |  29 +++-
 rust/qemu-api/Cargo.lock                      |   7 +
 rust/qemu-api/Cargo.toml                      |   6 +-
 rust/qemu-api/build.rs                        |   8 +
 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                | 161 ++++++++++++++++++
 rust/qemu-api/tests/tests.rs                  |   1 +
 subprojects/packagefiles/syn-2-rs/meson.build |   1 +
 13 files changed, 232 insertions(+), 13 deletions(-)
 create mode 100644 rust/qemu-api/src/offset_of.rs

diff --git a/rust/hw/char/pl011/Cargo.lock b/rust/hw/char/pl011/Cargo.lock
index 82028ddf793..087658ca72b 100644
--- a/rust/hw/char/pl011/Cargo.lock
+++ b/rust/hw/char/pl011/Cargo.lock
@@ -93,6 +93,7 @@ name = "qemu_api"
 version = "0.1.0"
 dependencies = [
  "qemu_api_macros",
+ "version_check",
 ]
 
 [[package]]
diff --git a/rust/hw/char/pl011/src/device.rs b/rust/hw/char/pl011/src/device.rs
index 5ffe1911376..c26d21e079d 100644
--- a/rust/hw/char/pl011/src/device.rs
+++ b/rust/hw/char/pl011/src/device.rs
@@ -29,7 +29,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 144cc3650fa..47fec2e4637 100644
--- a/rust/qemu-api-macros/Cargo.toml
+++ b/rust/qemu-api-macros/Cargo.toml
@@ -19,7 +19,10 @@ proc-macro = true
 [dependencies]
 proc-macro2 = "1"
 quote = "1"
-syn = "2"
+
+[dependencies.syn]
+version = "2"
+features= ["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 a4bc5d01ee8..472264159d6 100644
--- a/rust/qemu-api-macros/src/lib.rs
+++ b/rust/qemu-api-macros/src/lib.rs
@@ -3,8 +3,8 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 use proc_macro::TokenStream;
-use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use quote::{quote, quote_spanned};
+use syn::{parse_macro_input, parse_quote, spanned::Spanned, Attribute, DeriveInput};
 
 #[proc_macro_derive(Object)]
 pub fn derive_object(input: TokenStream) -> TokenStream {
@@ -21,3 +21,28 @@ pub fn derive_object(input: TokenStream) -> TokenStream {
 
     TokenStream::from(expanded)
 }
+
+#[proc_macro_derive(offsets)]
+pub fn derive_offsets(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let repr_c: Attribute = parse_quote! { #[repr(C)] };
+
+    // All the work for expanding this macro is done in "with_offsets!",
+    // a macro in the qemu_api crate which re-parses the struct declaration;
+    // this derive macro is just a convenience.
+    //
+    // The with_offsets! macro expands to nothing if the compiler is new enough
+    // to have a stable offset_of.
+    let expanded = if !input.attrs.iter().any(|x| *x == repr_c) {
+        quote_spanned! {
+            input.span() =>
+            compile_error!("expected #[repr(C)] together with #[derive(offsets)]");
+        }
+    } else {
+        quote! {
+            ::qemu_api::with_offsets! { #input }
+        }
+    };
+
+    TokenStream::from(expanded)
+}
diff --git a/rust/qemu-api/Cargo.lock b/rust/qemu-api/Cargo.lock
index 468293feedd..371c6167918 100644
--- a/rust/qemu-api/Cargo.lock
+++ b/rust/qemu-api/Cargo.lock
@@ -16,6 +16,7 @@ name = "qemu_api"
 version = "0.1.0"
 dependencies = [
  "qemu_api_macros",
+ "version_check",
 ]
 
 [[package]]
@@ -52,3 +53,9 @@ name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
diff --git a/rust/qemu-api/Cargo.toml b/rust/qemu-api/Cargo.toml
index db594c64083..9c3a99522a9 100644
--- a/rust/qemu-api/Cargo.toml
+++ b/rust/qemu-api/Cargo.toml
@@ -16,6 +16,9 @@ categories = []
 [dependencies]
 qemu_api_macros = { path = "../qemu-api-macros" }
 
+[build-dependencies]
+version_check = "~0.9"
+
 [features]
 default = []
 allocator = []
@@ -24,4 +27,5 @@ allocator = []
 [workspace]
 
 [lints.rust]
-unexpected_cfgs = { level = "warn", check-cfg = ['cfg(MESON)', 'cfg(HAVE_GLIB_WITH_ALIGNED_ALLOC)'] }
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(MESON)', 'cfg(HAVE_GLIB_WITH_ALIGNED_ALLOC)',
+                                                 'cfg(has_offset_of)'] }
diff --git a/rust/qemu-api/build.rs b/rust/qemu-api/build.rs
index 419b154c2d2..0a68c3712f1 100644
--- a/rust/qemu-api/build.rs
+++ b/rust/qemu-api/build.rs
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 use std::path::Path;
+use version_check as rustc;
 
 fn main() {
     if !Path::new("src/bindings.rs").exists() {
@@ -11,4 +12,11 @@ fn main() {
              (`ninja bindings.rs`) and copy them to src/bindings.rs, or build through meson."
         );
     }
+
+    // Check for available rustc features
+    if rustc::is_min_version("1.77.0").unwrap_or(false) {
+        println!("cargo:rustc-cfg=has_offset_of");
+    }
+
+    println!("cargo:rerun-if-changed=build.rs");
 }
diff --git a/rust/qemu-api/meson.build b/rust/qemu-api/meson.build
index 2b3dea59fb6..4ac33acced3 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,16 +12,14 @@ _qemu_api_rs = static_library(
       'src/c_str.rs',
       'src/definitions.rs',
       'src/device_class.rs',
+      'src/offset_of.rs',
       'src/zeroable.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,
 )
 
 rust.test('rust-qemu-api-tests', _qemu_api_rs,
diff --git a/rust/qemu-api/src/device_class.rs b/rust/qemu-api/src/device_class.rs
index 5c305d945d3..ce3d5f50507 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: ::std::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: ::std::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 9c3be4f6489..92f1785d891 100644
--- a/rust/qemu-api/src/lib.rs
+++ b/rust/qemu-api/src/lib.rs
@@ -30,6 +30,7 @@ unsafe impl Sync for bindings::VMStateDescription {}
 pub mod c_str;
 pub mod definitions;
 pub mod device_class;
+pub mod offset_of;
 pub mod zeroable;
 
 use std::{
@@ -166,3 +167,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 00000000000..87db8607592
--- /dev/null
+++ b/rust/qemu-api/src/offset_of.rs
@@ -0,0 +1,161 @@
+// 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 wrapped with `with_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
+#[macro_export]
+macro_rules! with_offsets {
+    // This method to generate field offset constants comes from:
+    //
+    //     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_meta:meta])*
+        $struct_vis:vis
+        struct $StructName:ident {
+            $(
+                $(#[$field_meta:meta])*
+                $field_vis:vis
+                $field_name:ident : $field_ty:ty
+            ),*
+            $(,)?
+        }
+    ) => (
+        #[cfg(not(has_offset_of))]
+        const _: () = {
+            struct StructOffsetsHelper<T>(std::marker::PhantomData<T>);
+            const END_OF_PREV_FIELD: usize = 0;
+
+            // populate StructOffsetsHelper<T> with associated consts,
+            // one for each field
+            $crate::with_offsets! {
+                @struct $StructName
+                @names [ $($field_name)* ]
+                @tys [ $($field_ty ,)*]
+            }
+
+            // now turn StructOffsetsHelper<T>'s consts into a single struct,
+            // applying field visibility.  This provides better error messages
+            // than if offset_of! used StructOffsetsHelper::<T> directly.
+            pub
+            struct StructOffsets {
+                $(
+                    $field_vis
+                    $field_name: usize,
+                )*
+            }
+            impl $StructName {
+                pub
+                const OFFSET_TO__: StructOffsets = StructOffsets {
+                    $(
+                        $field_name: StructOffsetsHelper::<$StructName>::$field_name,
+                    )*
+                };
+            }
+        };
+    );
+
+    (
+        @struct $StructName:ident
+        @names []
+        @tys []
+    ) => ();
+
+    (
+        @struct $StructName:ident
+        @names [$field_name:ident $($other_names:tt)*]
+        @tys [$field_ty:ty , $($other_tys:tt)*]
+    ) => (
+        #[allow(non_local_definitions)]
+        #[allow(clippy::modulo_one)]
+        impl StructOffsetsHelper<$StructName> {
+            #[allow(nonstandard_style)]
+            const $field_name: usize = {
+                const ALIGN: usize = std::mem::align_of::<$field_ty>();
+                const TRAIL: usize = END_OF_PREV_FIELD % ALIGN;
+                END_OF_PREV_FIELD + (if TRAIL == 0 { 0usize } else { ALIGN - TRAIL })
+            };
+        }
+        const _: () = {
+            const END_OF_PREV_FIELD: usize =
+                StructOffsetsHelper::<$StructName>::$field_name +
+                std::mem::size_of::<$field_ty>()
+            ;
+            $crate::with_offsets! {
+                @struct $StructName
+                @names [$($other_names)*]
+                @tys [$($other_tys)*]
+            }
+        };
+    );
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::offset_of;
+
+    #[repr(C)]
+    struct Foo {
+        a: u16,
+        b: u32,
+        c: u64,
+        d: u16,
+    }
+
+    #[repr(C)]
+    struct Bar {
+        pub a: u16,
+        pub b: u64,
+        c: Foo,
+        d: u64,
+    }
+
+    crate::with_offsets! {
+        #[repr(C)]
+        struct Bar {
+            pub a: u16,
+            pub b: u64,
+            c: Foo,
+            d: u64,
+        }
+    }
+
+    #[repr(C)]
+    pub struct Baz {
+        b: u32,
+        a: u8,
+    }
+    crate::with_offsets! {
+        #[repr(C)]
+        pub struct Baz {
+            b: u32,
+            a: u8,
+        }
+    }
+
+    #[test]
+    fn test_offset_of() {
+        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);
+
+        assert_eq!(offset_of!(Baz, b), 0);
+        assert_eq!(offset_of!(Baz, a), 4);
+    }
+}
diff --git a/rust/qemu-api/tests/tests.rs b/rust/qemu-api/tests/tests.rs
index 605bc99ed4c..96e413b75ee 100644
--- a/rust/qemu-api/tests/tests.rs
+++ b/rust/qemu-api/tests/tests.rs
@@ -20,6 +20,7 @@ fn test_device_decl_macros() {
         unmigratable: true,
     }
 
+    #[derive(qemu_api_macros::offsets)]
     #[repr(C)]
     #[derive(qemu_api_macros::Object)]
     pub struct DummyState {
diff --git a/subprojects/packagefiles/syn-2-rs/meson.build b/subprojects/packagefiles/syn-2-rs/meson.build
index a53335f3092..9f56ce1c24d 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.46.2