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