diff --git a/qemu/src/hw/core/device_impl.rs b/qemu/src/hw/core/device_impl.rs
new file mode 100644
index 0000000..80b0e5e
--- /dev/null
+++ b/qemu/src/hw/core/device_impl.rs
@@ -0,0 +1,140 @@
+//! Macros and traits to implement subclasses of Device in Rust
+//!
+//! @author Paolo Bonzini
+
+#![allow(clippy::missing_safety_doc)]
+
+use std::ffi::c_void;
+
+use crate::bindings;
+use crate::bindings::DeviceClass;
+use crate::bindings::DeviceState;
+use crate::bindings::Property;
+
+use crate::qom::object_impl::ObjectImpl;
+use crate::qom::object_impl::TypeImpl;
+
+use crate::qom::refs::ObjectCast;
+
+use crate::util::error::Error;
+
+/// Information on which superclass methods are overridden
+/// by a Rust-implemented subclass of Device.
+pub trait DeviceImpl: ObjectImpl + DeviceTypeImpl {
+    /// If not `None`, a function that implements the `realize` member
+    /// of the QOM `DeviceClass`.
+    const REALIZE: Option<fn(obj: &Self) -> crate::Result<()>> = None;
+
+    /// If not `None`, a function that implements the `unrealize` member
+    /// of the QOM `DeviceClass`.
+    const UNREALIZE: Option<fn(obj: &Self)> = None;
+
+    /// If not `None`, a function that implements the `cold_reset` member
+    /// of the QOM `DeviceClass`.
+    const COLD_RESET: Option<fn(obj: &Self)> = None;
+}
+
+impl DeviceClass {
+    pub fn class_init<T: DeviceImpl>(&mut self) {
+        unsafe extern "C" fn rust_cold_reset<T: DeviceImpl>(obj: *mut DeviceState) {
+            let f = T::COLD_RESET.unwrap();
+            f((&*obj).unsafe_cast::<T>())
+        }
+        self.cold_reset = T::COLD_RESET.map(|_| rust_cold_reset::<T> as _);
+
+        unsafe extern "C" fn rust_realize<T: DeviceImpl>(
+            obj: *mut DeviceState,
+            errp: *mut *mut bindings::Error,
+        ) {
+            let f = T::REALIZE.unwrap();
+            let result = f((&*obj).unsafe_cast::<T>());
+            Error::ok_or_propagate(result, errp);
+        }
+        self.realize = T::REALIZE.map(|_| rust_realize::<T> as _);
+
+        unsafe extern "C" fn rust_unrealize<T: DeviceImpl>(obj: *mut DeviceState) {
+            let f = T::UNREALIZE.unwrap();
+            f((&*obj).unsafe_cast::<T>())
+        }
+        self.unrealize = T::UNREALIZE.map(|_| rust_unrealize::<T> as _);
+
+        self.properties = <T as DeviceTypeImpl>::properties();
+
+        // Now initialize the ObjectClass from the ObjectImpl.
+        self.oc.class_init::<T>();
+    }
+}
+
+impl DeviceState {
+    pub unsafe extern "C" fn rust_class_init<T: DeviceImpl>(
+        klass: *mut c_void,
+        _data: *mut c_void,
+    ) {
+        let dc: &mut DeviceClass = &mut *(klass.cast());
+        dc.class_init::<T>();
+    }
+}
+
+/// Internal information on a Rust-implemented subclass of Device.
+/// Only public because it is used by macros.
+pub unsafe trait DeviceTypeImpl: TypeImpl {
+    const CONF_OFFSET: usize;
+
+    // This needs to be here, and not in DeviceImpl, because properties
+    // reference statics (for globals defined in C, e.g. qdev_prop_bool)
+    // which is unstable (see https://github.com/rust-lang/rust/issues/119618,
+    // feature const_refs_to_static)
+    fn properties() -> *const Property;
+}
+
+pub struct QdevPropBool;
+impl QdevPropBool {
+    pub const fn convert(value: &bool) -> u64 {
+        *value as u64
+    }
+}
+
+#[macro_export]
+macro_rules! qdev_prop {
+    (@internal bool, $name:expr, $default:expr, $offset:expr) => {
+        $crate::Property {
+            name: $name.as_ptr(),
+            offset: $offset,
+            default: $crate::hw::core::device_impl::QdevPropBool::convert(&($default)),
+            info: unsafe { &$crate::bindings::qdev_prop_bool },
+        }
+    };
+
+    // Replace field with typechecking expression and offset
+    ($kind:tt, $name:expr, $type:ty, $default:expr, $field:ident) => {
+        qdev_prop!(@internal
+            $kind,
+            $name,
+            (<$crate::conf_type!($type) as ConstDefault>::DEFAULT).$field,
+            <$type as $crate::DeviceTypeImpl>::CONF_OFFSET + std::mem::offset_of!($crate::conf_type!($type), $field)
+        )
+    };
+}
+
+#[macro_export]
+macro_rules! qdev_define_type {
+    ($name:expr, $struct:ident, $conf_ty:ty, $state_ty:ty;
+     @extends $super:ty $(,$supers:ty)*;
+     @properties [$($props: expr),+]) => {
+        $crate::qom_define_type!(
+            $name, $struct, $conf_ty, $state_ty;
+            @extends $super $(,$supers)*, $crate::Object);
+
+        unsafe impl $crate::DeviceTypeImpl for $struct {
+            const CONF_OFFSET: usize = std::mem::offset_of!($struct, conf);
+
+            fn properties() -> *const $crate::Property {
+                static mut PROPERTIES: &'static [$crate::Property] = &[$($props),+];
+
+                // SAFETY: The only reference is created here; mut is needed to refer to
+                // &qdev_prop_xxx.
+                unsafe { PROPERTIES.as_ptr() }
+            }
+        }
+    }
+}
diff --git a/qemu/src/hw/core/mod.rs b/qemu/src/hw/core/mod.rs
index 5458924..6cd9197 100644
--- a/qemu/src/hw/core/mod.rs
+++ b/qemu/src/hw/core/mod.rs
@@ -1 +1,2 @@
 pub mod device;
+pub mod device_impl;
diff --git a/qemu/src/lib.rs b/qemu/src/lib.rs
index 81abf9c..3f0491c 100644
--- a/qemu/src/lib.rs
+++ b/qemu/src/lib.rs
@@ -2,12 +2,17 @@
 #![allow(dead_code)]
 
 pub mod bindings;
+pub use bindings::DeviceClass;
 pub use bindings::DeviceState;
 pub use bindings::Object;
+pub use bindings::Property;
+pub use bindings::PropertyInfo;
 pub use bindings::TypeInfo;
 
 pub mod hw;
 pub use hw::core::device::DeviceMethods;
+pub use hw::core::device_impl::DeviceImpl;
+pub use hw::core::device_impl::DeviceTypeImpl;
 
 pub mod qom;
 pub use qom::object::ObjectClassMethods;
diff --git a/qemu/tests/main.rs b/qemu/tests/main.rs
index a7cbeed..e499c14 100644
--- a/qemu/tests/main.rs
+++ b/qemu/tests/main.rs
@@ -5,9 +5,18 @@ use qemu::Object;
 use qemu::ObjectClassMethods;
 use qemu::ObjectImpl;
 
+use qemu::qdev_define_type;
+use qemu::qdev_prop;
+use qemu::DeviceImpl;
+use qemu::DeviceMethods;
+use qemu::DeviceState;
+
+use qemu::Result;
+
+use std::cell::RefCell;
+
 #[derive(Default, ConstDefault)]
 struct TestConf {
-    #[allow(dead_code)]
     foo: bool,
 }
 
@@ -27,6 +36,47 @@ qom_define_type!(
 
 impl ObjectImpl for TestObject {}
 
+qdev_define_type!(
+    c"test-device",
+    TestDevice,
+    TestConf,
+    RefCell<TestState>;
+    @extends DeviceState;
+    @properties [qdev_prop!(bool, c"foo", TestDevice, true, foo)]
+);
+
+impl TestDevice {
+    #[allow(clippy::unused_self)]
+    fn unparent(&self) {
+        println!("unparent");
+    }
+
+    #[allow(clippy::unused_self)]
+    fn realize(&self) -> Result<()> {
+        println!("realize");
+        Ok(())
+    }
+
+    #[allow(clippy::unused_self)]
+    fn unrealize(&self) {
+        println!("unrealize");
+    }
+}
+
+impl ObjectImpl for TestDevice {
+    const UNPARENT: Option<fn(&TestDevice)> = Some(TestDevice::unparent);
+}
+
+impl DeviceImpl for TestDevice {
+    const REALIZE: Option<fn(&TestDevice) -> Result<()>> = Some(TestDevice::realize);
+    const UNREALIZE: Option<fn(&TestDevice)> = Some(TestDevice::unrealize);
+}
+
 fn main() {
     drop(TestObject::new());
+
+    let d = TestDevice::new();
+    d.realize().unwrap();
+    d.cold_reset();
+    d.unparent();
 }
