diff mbox series

[v8,7/8] rust: add utility procedural macro crate

Message ID 20240823-rust-pl011-v8-7-b9f5746bdaf3@linaro.org (mailing list archive)
State New, archived
Headers show
Series Add Rust build support, ARM PL011 device impl | expand

Commit Message

Manos Pitsidianakis Aug. 23, 2024, 8:11 a.m. UTC
This commit adds a helper crate library, qemu-api-macros for derive (and
other procedural) macros to be used along qemu-api.

It needs to be a separate library because in Rust, procedural macros, or
macros that can generate arbitrary code, need to be special separate
compilation units.

Only one macro is introduced in this patch, #[derive(Object)]. It
generates a constructor to register a QOM TypeInfo on init and it must
be used on types that implement qemu_api::definitions::ObjectImpl trait.

Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
---
 MAINTAINERS                            |   1 +
 rust/meson.build                       |   1 +
 rust/qemu-api-macros/Cargo.lock        |  47 +++++++
 rust/qemu-api-macros/Cargo.toml        |  25 ++++
 rust/qemu-api-macros/README.md         |   1 +
 rust/qemu-api-macros/meson.build       |  25 ++++
 rust/qemu-api-macros/src/cstr/mod.rs   |  55 ++++++++
 rust/qemu-api-macros/src/cstr/parse.rs | 225 +++++++++++++++++++++++++++++++++
 rust/qemu-api-macros/src/lib.rs        |  43 +++++++
 rust/qemu-api/meson.build              |   3 +
 10 files changed, 426 insertions(+)

Comments

Junjie Mao Aug. 26, 2024, 5:15 a.m. UTC | #1
On 8/23/2024 4:11 PM, Manos Pitsidianakis wrote:
> This commit adds a helper crate library, qemu-api-macros for derive (and
> other procedural) macros to be used along qemu-api.
> 
> It needs to be a separate library because in Rust, procedural macros, or
> macros that can generate arbitrary code, need to be special separate
> compilation units.
> 
> Only one macro is introduced in this patch, #[derive(Object)]. It
> generates a constructor to register a QOM TypeInfo on init and it must
> be used on types that implement qemu_api::definitions::ObjectImpl trait.
> 
> Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
> ---
>   MAINTAINERS                            |   1 +
>   rust/meson.build                       |   1 +
>   rust/qemu-api-macros/Cargo.lock        |  47 +++++++
>   rust/qemu-api-macros/Cargo.toml        |  25 ++++
>   rust/qemu-api-macros/README.md         |   1 +
>   rust/qemu-api-macros/meson.build       |  25 ++++
>   rust/qemu-api-macros/src/cstr/mod.rs   |  55 ++++++++
>   rust/qemu-api-macros/src/cstr/parse.rs | 225 +++++++++++++++++++++++++++++++++

Since Rust 1.77.0 C-string literals are stabilized [1]. I don't think we need to 
include this cstr crate as we require Rust >= 1.80.0.

[1] https://crates.io/crates/cstr

>   rust/qemu-api-macros/src/lib.rs        |  43 +++++++
>   rust/qemu-api/meson.build              |   3 +
>   10 files changed, 426 insertions(+)
> 
[snip]
> diff --git a/rust/qemu-api/meson.build b/rust/qemu-api/meson.build
> index 85838d31b4..a0802ad858 100644
> --- a/rust/qemu-api/meson.build
> +++ b/rust/qemu-api/meson.build
> @@ -13,6 +13,9 @@ _qemu_api_rs = static_library(
>     rust_args: [
>       '--cfg', 'MESON',
>     ],
> +  dependencies: [
> +    qemu_api_macros,
> +  ],
>   )
>   
>   qemu_api = declare_dependency(
> 

qemu-api does not use macros provided by qemu-api-macros, but the later 
generates code that uses types defined by the former. So to me qemu-api-macros 
should depend on qemu-api, not vice versa.

---
Best Regards
Junjie Mao
Manos Pitsidianakis Aug. 26, 2024, 6:02 a.m. UTC | #2
Hello Junjie,

On Mon, 26 Aug 2024 08:15, Junjie Mao <junjie.mao@intel.com> wrote:
>On 8/23/2024 4:11 PM, Manos Pitsidianakis wrote:
>> This commit adds a helper crate library, qemu-api-macros for derive (and
>> other procedural) macros to be used along qemu-api.
>> 
>> It needs to be a separate library because in Rust, procedural macros, or
>> macros that can generate arbitrary code, need to be special separate
>> compilation units.
>> 
>> Only one macro is introduced in this patch, #[derive(Object)]. It
>> generates a constructor to register a QOM TypeInfo on init and it must
>> be used on types that implement qemu_api::definitions::ObjectImpl trait.
>> 
>> Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
>> ---
>>   MAINTAINERS                            |   1 +
>>   rust/meson.build                       |   1 +
>>   rust/qemu-api-macros/Cargo.lock        |  47 +++++++
>>   rust/qemu-api-macros/Cargo.toml        |  25 ++++
>>   rust/qemu-api-macros/README.md         |   1 +
>>   rust/qemu-api-macros/meson.build       |  25 ++++
>>   rust/qemu-api-macros/src/cstr/mod.rs   |  55 ++++++++
>>   rust/qemu-api-macros/src/cstr/parse.rs | 225 +++++++++++++++++++++++++++++++++
>
>Since Rust 1.77.0 C-string literals are stabilized [1]. I don't think we need to 
>include this cstr crate as we require Rust >= 1.80.0.


Many thanks! I got the qemu-api-macros from my git stash, I tried to 
bundle cstr in a previous version before we had proper meson 
dependencies and I hadn't raised the Rust version. So I just forgot to 
remove these files (they are not even declared in lib.rs). Oops 
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index d35e9f6b20..727f3a7a2c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3352,6 +3352,7 @@  Rust
 M: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
 S: Maintained
 F: rust/qemu-api
+F: rust/qemu-api-macros
 F: rust/rustfmt.toml
 
 SLIRP
diff --git a/rust/meson.build b/rust/meson.build
index 4a58d106b1..7a32b1b195 100644
--- a/rust/meson.build
+++ b/rust/meson.build
@@ -1 +1,2 @@ 
+subdir('qemu-api-macros')
 subdir('qemu-api')
diff --git a/rust/qemu-api-macros/Cargo.lock b/rust/qemu-api-macros/Cargo.lock
new file mode 100644
index 0000000000..fdc0fce116
--- /dev/null
+++ b/rust/qemu-api-macros/Cargo.lock
@@ -0,0 +1,47 @@ 
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[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.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
diff --git a/rust/qemu-api-macros/Cargo.toml b/rust/qemu-api-macros/Cargo.toml
new file mode 100644
index 0000000000..144cc3650f
--- /dev/null
+++ b/rust/qemu-api-macros/Cargo.toml
@@ -0,0 +1,25 @@ 
+[package]
+name = "qemu_api_macros"
+version = "0.1.0"
+edition = "2021"
+authors = ["Manos Pitsidianakis <manos.pitsidianakis@linaro.org>"]
+license = "GPL-2.0-or-later"
+readme = "README.md"
+homepage = "https://www.qemu.org"
+description = "Rust bindings for QEMU - Utility macros"
+repository = "https://gitlab.com/qemu-project/qemu/"
+resolver = "2"
+publish = false
+keywords = []
+categories = []
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1"
+quote = "1"
+syn = "2"
+
+# Do not include in any global workspace
+[workspace]
diff --git a/rust/qemu-api-macros/README.md b/rust/qemu-api-macros/README.md
new file mode 100644
index 0000000000..f60f54ac4b
--- /dev/null
+++ b/rust/qemu-api-macros/README.md
@@ -0,0 +1 @@ 
+# `qemu-api-macros` - Utility macros for defining QEMU devices
diff --git a/rust/qemu-api-macros/meson.build b/rust/qemu-api-macros/meson.build
new file mode 100644
index 0000000000..48af91ed38
--- /dev/null
+++ b/rust/qemu-api-macros/meson.build
@@ -0,0 +1,25 @@ 
+add_languages('rust', required: true, native: true)
+
+quote_dep = dependency('quote-1-rs', native: true)
+syn_dep = dependency('syn-2-rs', native: true)
+proc_macro2_dep = dependency('proc-macro2-1-rs', native: true)
+
+_qemu_api_macros_rs = import('rust').proc_macro(
+  'qemu_api_macros',
+  files('src/lib.rs'),
+  override_options: ['rust_std=2021', 'build.rust_std=2021'],
+  rust_args: [
+    '--cfg', 'use_fallback',
+    '--cfg', 'feature="syn-error"',
+    '--cfg', 'feature="proc-macro"',
+  ],
+  dependencies: [
+    proc_macro2_dep,
+    quote_dep,
+    syn_dep,
+  ],
+)
+
+qemu_api_macros = declare_dependency(
+  link_with: _qemu_api_macros_rs,
+)
diff --git a/rust/qemu-api-macros/src/cstr/mod.rs b/rust/qemu-api-macros/src/cstr/mod.rs
new file mode 100644
index 0000000000..b88e5fd10b
--- /dev/null
+++ b/rust/qemu-api-macros/src/cstr/mod.rs
@@ -0,0 +1,55 @@ 
+// Copyright (c) 2018-2020 Xidorn Quan
+//
+// Permission is hereby granted, free of charge, to any
+// person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the
+// Software without restriction, including without
+// limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions
+// of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+use self::parse::parse_input;
+use proc_macro::TokenStream as RawTokenStream;
+use proc_macro2::{Literal, Span, TokenStream};
+use quote::{quote, quote_spanned};
+use std::ffi::CString;
+
+mod parse;
+
+struct Error(Span, &'static str);
+
+pub(crate) fn cstr(input: RawTokenStream) -> RawTokenStream {
+    let tokens = match build_byte_str(input.into()) {
+        Ok(s) => quote!(unsafe { ::core::ffi::CStr::from_bytes_with_nul_unchecked(#s) }),
+        Err(Error(span, msg)) => quote_spanned!(span => compile_error!(#msg)),
+    };
+    tokens.into()
+}
+
+fn build_byte_str(input: TokenStream) -> Result<Literal, Error> {
+    let (bytes, span) = parse_input(input)?;
+    match CString::new(bytes) {
+        Ok(s) => {
+            let mut lit = Literal::byte_string(s.as_bytes_with_nul());
+            lit.set_span(span);
+            Ok(lit)
+        }
+        Err(_) => Err(Error(span, "nul byte found in the literal")),
+    }
+}
diff --git a/rust/qemu-api-macros/src/cstr/parse.rs b/rust/qemu-api-macros/src/cstr/parse.rs
new file mode 100644
index 0000000000..1d495296ad
--- /dev/null
+++ b/rust/qemu-api-macros/src/cstr/parse.rs
@@ -0,0 +1,225 @@ 
+use super::Error;
+use core::char;
+use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream, TokenTree};
+
+macro_rules! unexpected_content {
+    () => {
+        "expected one of: byte string literal, string literal, identifier"
+    };
+}
+
+pub(crate) fn parse_input(mut input: TokenStream) -> Result<(Vec<u8>, Span), Error> {
+    loop {
+        let mut tokens = input.into_iter();
+        let token = match tokens.next() {
+            Some(token) => token,
+            None => {
+                return Err(Error(
+                    Span::call_site(),
+                    concat!("unexpected end of input, ", unexpected_content!()),
+                ))
+            }
+        };
+        let span = token.span();
+        let result = match token {
+            // Unwrap any empty group which may be created from macro expansion.
+            TokenTree::Group(group) if group.delimiter() == Delimiter::None => Err(group),
+            TokenTree::Literal(literal) => match parse_literal(literal) {
+                Ok(result) => Ok(result),
+                Err(msg) => return Err(Error(span, msg)),
+            },
+            TokenTree::Ident(ident) => Ok(parse_ident(ident)),
+            _ => return Err(Error(span, unexpected_content!())),
+        };
+        if let Some(token) = tokens.next() {
+            return Err(Error(token.span(), "unexpected token"));
+        }
+        match result {
+            Ok(result) => return Ok((result, span)),
+            Err(group) => input = group.stream(),
+        }
+    }
+}
+
+fn parse_literal(literal: Literal) -> Result<Vec<u8>, &'static str> {
+    let s = literal.to_string();
+    let s = s.as_bytes();
+    match s[0] {
+        b'"' => Ok(parse_cooked_content(s)),
+        b'r' => Ok(parse_raw_content(&s[1..])),
+        b'b' => match s[1] {
+            b'"' => Ok(parse_cooked_content(&s[1..])),
+            b'r' => Ok(parse_raw_content(&s[2..])),
+            _ => Err(unexpected_content!()),
+        },
+        _ => Err(unexpected_content!()),
+    }
+}
+
+fn all_pounds(bytes: &[u8]) -> bool {
+    bytes.iter().all(|b| *b == b'#')
+}
+
+/// Parses raw string / bytes content after `r` prefix.
+fn parse_raw_content(s: &[u8]) -> Vec<u8> {
+    let q_start = s.iter().position(|b| *b == b'"').unwrap();
+    let q_end = s.iter().rposition(|b| *b == b'"').unwrap();
+    assert!(all_pounds(&s[0..q_start]));
+    assert!(all_pounds(&s[q_end + 1..q_end + q_start + 1]));
+    Vec::from(&s[q_start + 1..q_end])
+}
+
+/// Parses the cooked string / bytes content within quotes.
+fn parse_cooked_content(mut s: &[u8]) -> Vec<u8> {
+    s = &s[1..s.iter().rposition(|b| *b == b'"').unwrap()];
+    let mut result = Vec::new();
+    while !s.is_empty() {
+        match s[0] {
+            b'\\' => {}
+            b'\r' => {
+                assert_eq!(s[1], b'\n');
+                result.push(b'\n');
+                s = &s[2..];
+                continue;
+            }
+            b => {
+                result.push(b);
+                s = &s[1..];
+                continue;
+            }
+        }
+        let b = s[1];
+        s = &s[2..];
+        match b {
+            b'x' => {
+                let (b, rest) = backslash_x(s);
+                result.push(b);
+                s = rest;
+            }
+            b'u' => {
+                let (c, rest) = backslash_u(s);
+                result.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
+                s = rest;
+            }
+            b'n' => result.push(b'\n'),
+            b'r' => result.push(b'\r'),
+            b't' => result.push(b'\t'),
+            b'\\' => result.push(b'\\'),
+            b'0' => result.push(b'\0'),
+            b'\'' => result.push(b'\''),
+            b'"' => result.push(b'"'),
+            b'\r' | b'\n' => {
+                let next = s.iter().position(|b| {
+                    let ch = char::from_u32(u32::from(*b)).unwrap();
+                    !ch.is_whitespace()
+                });
+                match next {
+                    Some(pos) => s = &s[pos..],
+                    None => s = b"",
+                }
+            }
+            b => panic!("unexpected byte {:?} after \\", b),
+        }
+    }
+    result
+}
+
+fn backslash_x(s: &[u8]) -> (u8, &[u8]) {
+    let ch = hex_to_u8(s[0]) * 0x10 + hex_to_u8(s[1]);
+    (ch, &s[2..])
+}
+
+fn hex_to_u8(b: u8) -> u8 {
+    match b {
+        b'0'..=b'9' => b - b'0',
+        b'a'..=b'f' => b - b'a' + 10,
+        b'A'..=b'F' => b - b'A' + 10,
+        _ => unreachable!("unexpected non-hex character {:?} after \\x", b),
+    }
+}
+
+fn backslash_u(s: &[u8]) -> (char, &[u8]) {
+    assert_eq!(s[0], b'{');
+    let end = s[1..].iter().position(|b| *b == b'}').unwrap();
+    let mut ch = 0;
+    for b in &s[1..=end] {
+        ch *= 0x10;
+        ch += u32::from(hex_to_u8(*b));
+    }
+    (char::from_u32(ch).unwrap(), &s[end + 2..])
+}
+
+fn parse_ident(ident: Ident) -> Vec<u8> {
+    ident.to_string().into_bytes()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::str::FromStr;
+
+    // Tests below were modified from
+    // https://github.com/dtolnay/syn/blob/cd5fdc0f530f822446fccaf831669cd0cf4a0fc9/tests/test_lit.rs
+
+    fn lit(s: &str) -> Vec<u8> {
+        match TokenStream::from_str(s)
+            .unwrap()
+            .into_iter()
+            .next()
+            .unwrap()
+        {
+            TokenTree::Literal(lit) => parse_literal(lit).unwrap(),
+            _ => panic!(),
+        }
+    }
+
+    #[test]
+    fn strings() {
+        #[track_caller]
+        fn test_string(s: &str, value: &[u8]) {
+            assert_eq!(lit(s), value);
+        }
+
+        test_string("\"a\"", b"a");
+        test_string("\"\\n\"", b"\n");
+        test_string("\"\\r\"", b"\r");
+        test_string("\"\\t\"", b"\t");
+        test_string("\"