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.pitsidiana...@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(+) diff --git a/MAINTAINERS b/MAINTAINERS index d35e9f6b20..727f3a7a2c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3352,6 +3352,7 @@ Rust M: Manos Pitsidianakis <manos.pitsidiana...@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.pitsidiana...@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("\"🐕\"", b"\xf0\x9f\x90\x95"); // NOTE: This is an emoji + test_string("\"\\\"\"", b"\""); + test_string("\"'\"", b"'"); + test_string("\"\"", b""); + test_string("\"\\u{1F415}\"", b"\xf0\x9f\x90\x95"); + test_string( + "\"contains\nnewlines\\\nescaped newlines\"", + b"contains\nnewlinesescaped newlines", + ); + test_string("r\"raw\nstring\\\nhere\"", b"raw\nstring\\\nhere"); + test_string("\"...\"q", b"..."); + test_string("r\"...\"q", b"..."); + test_string("r##\"...\"##q", b"..."); + } + + #[test] + fn byte_strings() { + #[track_caller] + fn test_byte_string(s: &str, value: &[u8]) { + assert_eq!(lit(s), value); + } + + test_byte_string("b\"a\"", b"a"); + test_byte_string("b\"\\n\"", b"\n"); + test_byte_string("b\"\\r\"", b"\r"); + test_byte_string("b\"\\t\"", b"\t"); + test_byte_string("b\"\\\"\"", b"\""); + test_byte_string("b\"'\"", b"'"); + test_byte_string("b\"\"", b""); + test_byte_string( + "b\"contains\nnewlines\\\nescaped newlines\"", + b"contains\nnewlinesescaped newlines", + ); + test_byte_string("br\"raw\nstring\\\nhere\"", b"raw\nstring\\\nhere"); + test_byte_string("b\"...\"q", b"..."); + test_byte_string("br\"...\"q", b"..."); + test_byte_string("br##\"...\"##q", b"..."); + } +} diff --git a/rust/qemu-api-macros/src/lib.rs b/rust/qemu-api-macros/src/lib.rs new file mode 100644 index 0000000000..331bc9e215 --- /dev/null +++ b/rust/qemu-api-macros/src/lib.rs @@ -0,0 +1,43 @@ +// Copyright 2024, Linaro Limited +// Author(s): Manos Pitsidianakis <manos.pitsidiana...@linaro.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use proc_macro::TokenStream; +use quote::{format_ident, 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(target_os = "linux", link_section = ".ctors")] + #[cfg_attr(target_os = "macos", 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); + } + } + + 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/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( -- 2.44.0