On 10/28/2025 8:14 PM, Paolo Bonzini wrote:
> On Tue, Oct 28, 2025 at 12:26 PM Paolo Bonzini <[email protected]> wrote:
>> If you were implementing a new serial device, for example, you would
>> just write a new directory in rust/hw/char.
>>
>> Even in that case, creating the meson.build file is more complex than
>> in C; I'm working on simplifying that by adding new features to Meson.
>>
>> In other words, you're not wrong at all but note that you are not
> Very important typo: you are *now* seeing the code from the point of
> view of someone implementing the Rust-to-C bridges.
>
> Paolo

Yes, I' ve come to realize this very clearly; I' ve found that many of the 
suggestions you gave actually stem from my failure to fully grasp the 
underlying design principles!

Chen Miao

>> seeing the code from the point of view of someone implementing the
>> Rust-to-C bridges, not someone implementing a new device, because
>> there are no I2C devices yet in QEMU that are written in Rust. I agree
>> that this is a relatively large work, on the other it only has to be
>> done once.
>>
>>> Moreover, the mechanisms of bindings and wrappers are overly redundant, and
>>> there is no clear indication that they are mandatory. These constructs 
>>> generate
>>> a large number of similar structures in the final compilation artifacts. 
>>> Could
>>> we design a more generic approach to bindings and wrappers?
>> Bindings are redundant across crates, indeed. It is probably possible
>> to improve how they work.
>>
>>> Additionally, vmstate has not been fully implemented for the PCF8574 (to be
>>> precise, it is not yet complete). During the implementation, I encountered 
>>> an
>>> issue: initially, I tried to directly transliterate the C struct — that is, 
>>> I
>>> attempted to configure vmstate and handle other aspects directly via field
>>> mappings. However, I eventually found that some internal APIs were 
>>> incompatible.
>>> To make it work, I had to isolate those "potentially mutable values" into a
>>> separate structure, akin to something like a "PL011Register", in order for 
>>> it
>>> to function correctly.
>> This is correct and intended. It isolates the interior-mutable parts
>> of the struct and in the future it can be used to support
>> multi-threaded devices too.
>>
>>> +// impl_vmstate_struct!(
>>> +//     I2CSlave,
>>> +//     VMStateDescriptionBuilder::<I2CSlave>::new()
>>> +//         .name(c"I2CSlave")
>>> +//         .version_id(1)
>>> +//         .minimum_version_id(1)
>>> +//         .post_load(&I2CSlave::post_load)
>>> +//         .fields(vmstate_fields! {
>>> +//             vmstate_of!(I2CSlave, address),
>>> +//         })
>>> +//         .build()
>>> +// );
>> This is not needed, instead you can implement VMState for I2CSlave:
>>
>> impl_vmstate_c_struct!(I2CSlave, bindings::vmstate_i2c_slave);
>>
>>> +bilge = { version = "0.2.0" }
>>> +bilge-impl = { version = "0.2.0" }
>>> +chardev = { path = "../../../chardev" }
>> You don't need these.
>>
>>> +_libpcf8574_bindings_inc_rs = rust.bindgen(
>>> +  input: 'wrapper.h',
>>> +  dependencies: common_ss.all_dependencies(),
>>> +  output: 'bindings.inc.rs',
>>> +  include_directories: bindings_incdir,
>>> +  bindgen_version: ['>=0.60.0'],
>>> +  args: bindgen_args_common,
>>> +  c_args: bindgen_c_args,
>>> +)
>> You also don't need bindgen for this simple device...
>>
>>> +_libpcf8574_rs = static_library(
>>> +  'pcf8574',
>>> +  structured_sources(
>>> +    [
>>> +      'src/lib.rs',
>>> +      'src/bindings.rs',
>>> +      'src/device.rs',
>>> +    ],
>>> +    {'.' : _libpcf8574_bindings_inc_rs},
>> ... and therefore you don't need structured_sources, just 'src/lib.rs'.
>>
>>> +    bilge_rs,
>>> +    bilge_impl_rs,
>>> +    chardev_rs,
>> Also remove unneeded crates from here.
>>
>>> --- /dev/null
>>> +++ b/rust/hw/gpio/pcf8574/src/device.rs
>>> @@ -0,0 +1,180 @@
>>> +// Copyright 2025 HUST OpenAtom Open Source Club.
>>> +// Author(s): Chen Miao <[email protected]>
>>> +// SPDX-License-Identifier: GPL-2.0-or-later
>>> +
>>> +use std::slice::from_ref;
>>> +
>>> +// use common::static_assert;
>>> +use bql::BqlRefCell;
>>> +use hwcore::{
>>> +    bindings, DeviceClass, DeviceImpl, DeviceMethods, DeviceState, 
>>> I2CSlave, I2CSlaveImpl,
>>> +    InterruptSource, ResetType, ResettablePhasesImpl,
>>> +};
>>> +use migration::{
>>> +    self, impl_vmstate_struct, vmstate_fields, vmstate_of, 
>>> VMStateDescription,
>>> +    VMStateDescriptionBuilder,
>>> +};
>>> +use qom::{qom_isa, IsA, Object, ObjectImpl, ObjectType, ParentField};
>>> +use util::Result;
>>> +
>>> +const PORTS_COUNT: usize = 8;
>>> +
>>> +#[repr(C)]
>>> +#[derive(Debug, Default)]
>>> +pub struct PCF8574StateInner {
>>> +    pub lastrq: u8,
>>> +    pub input: u8,
>>> +    pub output: u8,
>>> +}
>> This is small and simple enough that it can be Copy.
>>
>>> +#[repr(C)]
>>> +#[derive(qom::Object, hwcore::Device)]
>>> +pub struct PCF8574State {
>>> +    pub parent_obj: ParentField<I2CSlave>,
>>> +    pub inner: BqlRefCell<PCF8574StateInner>,
>>> +    pub handler: [InterruptSource; PORTS_COUNT],
>>> +    pub intrq: InterruptSource,
>>> +}
>>> +
>>> +// static_assert!(size_of::<PCF8574State>() <= 
>>> size_of::<crate::bindings::PCF8574State>());
>>> +
>>> +qom_isa!(PCF8574State: I2CSlave, DeviceState, Object);
>>> +
>>> +#[allow(dead_code)]
>>> +trait PCF8574Impl: I2CSlaveImpl + IsA<PCF8574State> {}
>>> +
>>> +unsafe impl ObjectType for PCF8574State {
>>> +    type Class = DeviceClass;
>>> +    const TYPE_NAME: &'static std::ffi::CStr = crate::TYPE_PCF8574;
>>> +}
>>> +
>>> +impl PCF8574Impl for PCF8574State {}
>>> +
>>> +impl ObjectImpl for PCF8574State {
>>> +    type ParentType = I2CSlave;
>>> +    const CLASS_INIT: fn(&mut Self::Class) = 
>>> Self::Class::class_init::<Self>;
>>> +}
>>> +
>>> +impl DeviceImpl for PCF8574State {
>>> +    const VMSTATE: Option<migration::VMStateDescription<Self>> = 
>>> Some(VMSTATE_PCF8574);
>>> +    const REALIZE: Option<fn(&Self) -> util::Result<()>> = 
>>> Some(Self::realize);
>>> +}
>>> +
>>> +impl ResettablePhasesImpl for PCF8574State {
>>> +    const HOLD: Option<fn(&Self, ResetType)> = Some(Self::reset_hold);
>>> +}
>>> +
>>> +impl I2CSlaveImpl for PCF8574State {
>>> +    const SEND: Option<fn(&Self, data: u8) -> util::Result<bool>> = 
>>> Some(Self::send);
>>> +    const RECV: Option<fn(&Self) -> util::Result<bool>> = Some(Self::recv);
>>> +    const SEND_ASYNC: Option<fn(&Self, data: u8)> = None;
>>> +    const EVENT: Option<fn(&Self, event: hwcore::I2CEvent) -> 
>>> util::Result<hwcore::I2CEvent>> =
>>> +        None;
>>> +    const MATCH_AND_ADD: Option<
>>> +        fn(
>>> +            &Self,
>>> +            address: u8,
>>> +            broadcast: bool,
>>> +            current_devs: *mut bindings::I2CNodeList,
>>> +        ) -> Result<bool>,
>>> +    > = None;
>> You don't need to redeclare the ones that are "None".
>>
>>> +}
>>> +
>>> +impl PCF8574State {
>>> +    fn send(&self, data: u8) -> Result<bool> {
>>> +        let prev = self.line_state();
>>> +        self.inner.borrow_mut().output = data;
>> I think you can use a function like
>>
>> impl PCF8574Inner {
>>      pub fn line_state(&self) {
>>           self.input & self.output
>>      }
>>
>>      pub fn set_outputs(&mut self, data: u32) -> PCF8574Inner {
>>           let prev = self.line_state();
>>           self.output = data;
>>           let actual = self.line_state();
>>           self
>>       }
>> }
>>
>> by making PCF8574Inner "Copy". This removes a lot of the borrows you need 
>> below.
>>
>>> +        let actual = self.line_state();
>>> +
>>> +        let mut diff = actual ^ prev;
>>> +        while diff != 0 {
>>> +            let line = diff.trailing_zeros() as u8;
>>> +            if let Some(handler) = self.handler.get(line as usize) {
>>> +                if handler.get() {
>>> +                    handler.set((actual >> line) & 1 == 1);
>>> +                }
>> There's no need to check if a GPIO line is connected, InterruptSource
>> handles it. (This can also be changed in the C version).
>>
>>> +    fn recv(&self) -> Result<bool> {
>>> +        let line_state = self.line_state();
>>> +        if self.inner.borrow().lastrq != line_state {
>>> +            self.inner.borrow_mut().lastrq = line_state;
>> Same here, part of the logic can be moved to PCF8574Inner:
>>
>>      pub fn receive(&mut self, data: u32) -> (bool, u32) {
>>           let prev = self.lastrq;
>>           let actual = self.line_state();
>>           self.lastrq = actual;
>>           (prev != actual, actual)
>>       }
>>
>> The second element of the tuple is needed because...
>>
>>> +            if self.intrq.get() {
>>> +                self.intrq.raise();
>>> +            }
>>> +        }
>>> +
>>> +        Ok(true)
>> ... RECV should return a byte, not a bool.
>>
>>> +    }
>>> +
>>> +    fn realize(&self) -> util::Result<()> {
>>> +        self.init_gpio_in(self.handler_size(), PCF8574State::gpio_set);
>>> +        self.init_gpio_out(from_ref(&self.handler[0]));
>>> +        self.init_gpio_out_named(from_ref(&self.intrq), "nINT", 1);
>>> +        Ok(())
>>> +        // unsafe {
>>> +        //   bindings::qdev_init_gpio_in(addr_of_mut!(*self), 
>>> Self::gpio_set, Self::handler_size as c_int);
>>> +        //   bindings::qdev_init_gpio_out(addr_of_mut!(*self), 
>>> InterruptSource::slice_as_ptr(self.handler), Self::handler_size as c_int);
>>> +        //   bindings::qdev_init_gpio_out_named(addr_of_mut!(*self), 
>>> self.intrq, c"nINT", 1);
>>> +        // }
>>> +    }
>>> +
>>> +    fn gpio_set(&self, line: u32, level: u32) {
>>> +        assert!(line < self.handler_size());
>>> +
>>> +        match level {
>>> +            0 => self.inner.borrow_mut().input &= !(1 << line),
>>> +            _ => self.inner.borrow_mut().input |= 1 << line,
>>> +        }
>> Again, move this to PCF8574Inner. deposit is in
>> common::bitops::IntegerExt and lets you write this:
>>
>>      pub fn set_input(&mut self, n: u32, value: u32) -> bool {
>>           self.input = self.input.deposit(n, 1, value);
>>           self.line_state() != self.lastrq
>>       }
>>
>>> +        if self.line_state() != self.inner.borrow().lastrq && 
>>> self.intrq.get() {
>>> +            self.intrq.raise();
>>> +        }
>>> +    }
>>> +
>>> +    fn handler_size(&self) -> u32 {
>>> +        self.handler.len() as u32
>>> +    }
>>> +!(
>>> +    fn line_state(&self) -> u8 {
>>> +        let inner = self.inner.borrow();
>>> +        inner.input & inner.output
>>> +    }
>>> +
>>> +    fn reset_hold(&self, _type: ResetType) {}
>> This should clear PCF8574Inner (it can be another method in there).
>>
>>> +}
>>> +
>>> +impl_vmstate_struct!(
>>> +    PCF8574StateInner,
>>> +    VMStateDescriptionBuilder::<PCF8574StateInner>::new()
>>> +        .name(c"pcf8574/inner")
>>> +        .version_id(0)
>>> +        .minimum_version_id(0)
>>> +        .fields(vmstate_fields! {
>>> +            vmstate_of!(PCF8574StateInner, lastrq),
>>> +            vmstate_of!(PCF8574StateInner, input),
>>> +            vmstate_of!(PCF8574StateInner, output),
>>> +        })
>>> +        .build()
>>> +);
>>> +
>>> +pub const VMSTATE_PCF8574: VMStateDescription<PCF8574State> =
>>> +    VMStateDescriptionBuilder::<PCF8574State>::new()
>>> +        .name(c"pcf8574")
>>> +        .version_id(0)
>>> +        .minimum_version_id(0)
>>> +        .fields(vmstate_fields! {
>>> +            // TODO
>>> +            // vmstate_of!(PCF8574State, parent_obj),
>> You're right, for this to work you'll need to add
>>
>> impl_vmstate_transparent!(ParentField<T> where T: VMState);
>>
>> to rust/qom/src/qom.rs and
>>
>> impl_vmstate_transparent!(std::mem::ManuallyDrop<T> where T: VMState);
>>
>> to rust/migration/src/vmstate.rs.  I'll send a patch for this.
>>
>> But really -- not a bad job at all!  Thanks very much for trying,
>> remember that Zhao and Manos and myself have been working on this for
>> over a year.  Your experience and your suggestions will be precious,
>> so please let us know what can be done better.
>>
>> Paolo

Reply via email to