diff mbox series

[v3,09/16] rust: add `io::Io` base type

Message ID 20241022213221.2383-10-dakr@kernel.org (mailing list archive)
State Handled Elsewhere
Delegated to: Bjorn Helgaas
Headers show
Series Device / Driver PCI / Platform Rust abstractions | expand

Commit Message

Danilo Krummrich Oct. 22, 2024, 9:31 p.m. UTC
I/O memory is typically either mapped through direct calls to ioremap()
or subsystem / bus specific ones such as pci_iomap().

Even though subsystem / bus specific functions to map I/O memory are
based on ioremap() / iounmap() it is not desirable to re-implement them
in Rust.

Instead, implement a base type for I/O mapped memory, which generically
provides the corresponding accessors, such as `Io::readb` or
`Io:try_readb`.

`Io` supports an optional const generic, such that a driver can indicate
the minimal expected and required size of the mapping at compile time.
Correspondingly, calls to the 'non-try' accessors, support compile time
checks of the I/O memory offset to read / write, while the 'try'
accessors, provide boundary checks on runtime.

`Io` is meant to be embedded into a structure (e.g. pci::Bar or
io::IoMem) which creates the actual I/O memory mapping and initializes
`Io` accordingly.

To ensure that I/O mapped memory can't out-live the device it may be
bound to, subsystems should embedd the corresponding I/O memory type
(e.g. pci::Bar) into a `Devres` container, such that it gets revoked
once the device is unbound.

Co-developed-by: Philipp Stanner <pstanner@redhat.com>
Signed-off-by: Philipp Stanner <pstanner@redhat.com>
Signed-off-by: Danilo Krummrich <dakr@kernel.org>
---
 rust/helpers/helpers.c |   1 +
 rust/helpers/io.c      |  91 ++++++++++++++++
 rust/kernel/io.rs      | 234 +++++++++++++++++++++++++++++++++++++++++
 rust/kernel/lib.rs     |   1 +
 4 files changed, 327 insertions(+)
 create mode 100644 rust/helpers/io.c
 create mode 100644 rust/kernel/io.rs

Comments

Alice Ryhl Oct. 28, 2024, 3:43 p.m. UTC | #1
On Tue, Oct 22, 2024 at 11:33 PM Danilo Krummrich <dakr@kernel.org> wrote:
>
> I/O memory is typically either mapped through direct calls to ioremap()
> or subsystem / bus specific ones such as pci_iomap().
>
> Even though subsystem / bus specific functions to map I/O memory are
> based on ioremap() / iounmap() it is not desirable to re-implement them
> in Rust.
>
> Instead, implement a base type for I/O mapped memory, which generically
> provides the corresponding accessors, such as `Io::readb` or
> `Io:try_readb`.
>
> `Io` supports an optional const generic, such that a driver can indicate
> the minimal expected and required size of the mapping at compile time.
> Correspondingly, calls to the 'non-try' accessors, support compile time
> checks of the I/O memory offset to read / write, while the 'try'
> accessors, provide boundary checks on runtime.

And using zero works because the user then statically knows that zero
bytes are available ... ?

> `Io` is meant to be embedded into a structure (e.g. pci::Bar or
> io::IoMem) which creates the actual I/O memory mapping and initializes
> `Io` accordingly.
>
> To ensure that I/O mapped memory can't out-live the device it may be
> bound to, subsystems should embedd the corresponding I/O memory type
> (e.g. pci::Bar) into a `Devres` container, such that it gets revoked
> once the device is unbound.

I wonder if `Io` should be a reference type instead. That is:

struct Io<'a, const SIZE: usize> {
    addr: usize,
    maxsize: usize,
    _lifetime: PhantomData<&'a ()>,
}

and then the constructor requires "addr must be valid I/O mapped
memory for maxsize bytes for the duration of 'a". And instead of
embedding it in another struct, the other struct creates an `Io` on
each access?

> Co-developed-by: Philipp Stanner <pstanner@redhat.com>
> Signed-off-by: Philipp Stanner <pstanner@redhat.com>
> Signed-off-by: Danilo Krummrich <dakr@kernel.org>

[...]

> diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
> new file mode 100644
> index 000000000000..750af938f83e
> --- /dev/null
> +++ b/rust/kernel/io.rs
> @@ -0,0 +1,234 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +//! Memory-mapped IO.
> +//!
> +//! C header: [`include/asm-generic/io.h`](srctree/include/asm-generic/io.h)
> +
> +use crate::error::{code::EINVAL, Result};
> +use crate::{bindings, build_assert};
> +
> +/// IO-mapped memory, starting at the base address @addr and spanning @maxlen bytes.
> +///
> +/// The creator (usually a subsystem / bus such as PCI) is responsible for creating the
> +/// mapping, performing an additional region request etc.
> +///
> +/// # Invariant
> +///
> +/// `addr` is the start and `maxsize` the length of valid I/O mapped memory region of size
> +/// `maxsize`.

Do you not also need an invariant that `SIZE <= maxsize`?

Alice
Danilo Krummrich Oct. 29, 2024, 9:20 a.m. UTC | #2
On Mon, Oct 28, 2024 at 04:43:02PM +0100, Alice Ryhl wrote:
> On Tue, Oct 22, 2024 at 11:33 PM Danilo Krummrich <dakr@kernel.org> wrote:
> >
> > I/O memory is typically either mapped through direct calls to ioremap()
> > or subsystem / bus specific ones such as pci_iomap().
> >
> > Even though subsystem / bus specific functions to map I/O memory are
> > based on ioremap() / iounmap() it is not desirable to re-implement them
> > in Rust.
> >
> > Instead, implement a base type for I/O mapped memory, which generically
> > provides the corresponding accessors, such as `Io::readb` or
> > `Io:try_readb`.
> >
> > `Io` supports an optional const generic, such that a driver can indicate
> > the minimal expected and required size of the mapping at compile time.
> > Correspondingly, calls to the 'non-try' accessors, support compile time
> > checks of the I/O memory offset to read / write, while the 'try'
> > accessors, provide boundary checks on runtime.
> 
> And using zero works because the user then statically knows that zero
> bytes are available ... ?

Zero would mean that the (minimum) resource size is unknown at compile time.
Correspondingly, any call to `read` and `write` would not compile, since the
compile time check requires that `offset + type_size <= SIZE`.

(Hope this answers the questions, I'm not sure I got it correctly.)

> 
> > `Io` is meant to be embedded into a structure (e.g. pci::Bar or
> > io::IoMem) which creates the actual I/O memory mapping and initializes
> > `Io` accordingly.
> >
> > To ensure that I/O mapped memory can't out-live the device it may be
> > bound to, subsystems should embedd the corresponding I/O memory type
> > (e.g. pci::Bar) into a `Devres` container, such that it gets revoked
> > once the device is unbound.
> 
> I wonder if `Io` should be a reference type instead. That is:
> 
> struct Io<'a, const SIZE: usize> {
>     addr: usize,
>     maxsize: usize,
>     _lifetime: PhantomData<&'a ()>,
> }
> 
> and then the constructor requires "addr must be valid I/O mapped
> memory for maxsize bytes for the duration of 'a". And instead of
> embedding it in another struct, the other struct creates an `Io` on
> each access?

So, we'd create the `Io` instance in `deref` of the parent structure, right?
What would be the advantage?

> 
> > Co-developed-by: Philipp Stanner <pstanner@redhat.com>
> > Signed-off-by: Philipp Stanner <pstanner@redhat.com>
> > Signed-off-by: Danilo Krummrich <dakr@kernel.org>
> 
> [...]
> 
> > diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
> > new file mode 100644
> > index 000000000000..750af938f83e
> > --- /dev/null
> > +++ b/rust/kernel/io.rs
> > @@ -0,0 +1,234 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +
> > +//! Memory-mapped IO.
> > +//!
> > +//! C header: [`include/asm-generic/io.h`](srctree/include/asm-generic/io.h)
> > +
> > +use crate::error::{code::EINVAL, Result};
> > +use crate::{bindings, build_assert};
> > +
> > +/// IO-mapped memory, starting at the base address @addr and spanning @maxlen bytes.
> > +///
> > +/// The creator (usually a subsystem / bus such as PCI) is responsible for creating the
> > +/// mapping, performing an additional region request etc.
> > +///
> > +/// # Invariant
> > +///
> > +/// `addr` is the start and `maxsize` the length of valid I/O mapped memory region of size
> > +/// `maxsize`.
> 
> Do you not also need an invariant that `SIZE <= maxsize`?

I guess so, yes. It's enforced by `Io::new`, which fails if `SIZE > maxsize`.

> 
> Alice
>
Alice Ryhl Oct. 29, 2024, 10:18 a.m. UTC | #3
On Tue, Oct 29, 2024 at 10:21 AM Danilo Krummrich <dakr@kernel.org> wrote:
>
> On Mon, Oct 28, 2024 at 04:43:02PM +0100, Alice Ryhl wrote:
> > On Tue, Oct 22, 2024 at 11:33 PM Danilo Krummrich <dakr@kernel.org> wrote:
> > >
> > > I/O memory is typically either mapped through direct calls to ioremap()
> > > or subsystem / bus specific ones such as pci_iomap().
> > >
> > > Even though subsystem / bus specific functions to map I/O memory are
> > > based on ioremap() / iounmap() it is not desirable to re-implement them
> > > in Rust.
> > >
> > > Instead, implement a base type for I/O mapped memory, which generically
> > > provides the corresponding accessors, such as `Io::readb` or
> > > `Io:try_readb`.
> > >
> > > `Io` supports an optional const generic, such that a driver can indicate
> > > the minimal expected and required size of the mapping at compile time.
> > > Correspondingly, calls to the 'non-try' accessors, support compile time
> > > checks of the I/O memory offset to read / write, while the 'try'
> > > accessors, provide boundary checks on runtime.
> >
> > And using zero works because the user then statically knows that zero
> > bytes are available ... ?
>
> Zero would mean that the (minimum) resource size is unknown at compile time.
> Correspondingly, any call to `read` and `write` would not compile, since the
> compile time check requires that `offset + type_size <= SIZE`.
>
> (Hope this answers the questions, I'm not sure I got it correctly.)

Yeah, thanks! I got it now.

> > > `Io` is meant to be embedded into a structure (e.g. pci::Bar or
> > > io::IoMem) which creates the actual I/O memory mapping and initializes
> > > `Io` accordingly.
> > >
> > > To ensure that I/O mapped memory can't out-live the device it may be
> > > bound to, subsystems should embedd the corresponding I/O memory type
> > > (e.g. pci::Bar) into a `Devres` container, such that it gets revoked
> > > once the device is unbound.
> >
> > I wonder if `Io` should be a reference type instead. That is:
> >
> > struct Io<'a, const SIZE: usize> {
> >     addr: usize,
> >     maxsize: usize,
> >     _lifetime: PhantomData<&'a ()>,
> > }
> >
> > and then the constructor requires "addr must be valid I/O mapped
> > memory for maxsize bytes for the duration of 'a". And instead of
> > embedding it in another struct, the other struct creates an `Io` on
> > each access?
>
> So, we'd create the `Io` instance in `deref` of the parent structure, right?
> What would be the advantage?

What you're doing now is a bit awkward to use. You have to make sure
that it never escapes the struct it's created for, so e.g. you can't
give out a mutable reference as the user could otherwise `mem::swap`
it with another Io. Similarly, the Io can never be in a public field.
Your safety comment on Io::new really needs to say something like
"while this struct exists, the `addr` must be a valid I/O region",
since I assume such regions are not valid forever? Similarly if we
look at [1], the I/O region actually gets unmapped *before* the Io is
destroyed since IoMem::drop runs before the fields of IoMem are
destroyed, so you really need something like "until the last use of
this Io" and not "until this Io is destroyed" in the safety comment.

If you compare similar cases in Rust, then they also do what I
suggested. For example, Vec<T> holds a raw pointer, and it uses unsafe
to assert that it's valid on each use of the raw pointer - it does not
create e.g. an `&'static mut [T]` to hold in a field of the Vec<T>.
Having an IoRaw<S> and an Io<'a, S> corresponds to what Vec<T> does
with IoRaw being like NonNull<T> and Io<'a, S> being like &'a T.

[1]: https://lore.kernel.org/all/20241024-topic-panthor-rs-platform_io_support-v1-1-3d1addd96e30@collabora.com/

> > > diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
> > > new file mode 100644
> > > index 000000000000..750af938f83e
> > > --- /dev/null
> > > +++ b/rust/kernel/io.rs
> > > @@ -0,0 +1,234 @@
> > > +// SPDX-License-Identifier: GPL-2.0
> > > +
> > > +//! Memory-mapped IO.
> > > +//!
> > > +//! C header: [`include/asm-generic/io.h`](srctree/include/asm-generic/io.h)
> > > +
> > > +use crate::error::{code::EINVAL, Result};
> > > +use crate::{bindings, build_assert};
> > > +
> > > +/// IO-mapped memory, starting at the base address @addr and spanning @maxlen bytes.
> > > +///
> > > +/// The creator (usually a subsystem / bus such as PCI) is responsible for creating the
> > > +/// mapping, performing an additional region request etc.
> > > +///
> > > +/// # Invariant
> > > +///
> > > +/// `addr` is the start and `maxsize` the length of valid I/O mapped memory region of size
> > > +/// `maxsize`.
> >
> > Do you not also need an invariant that `SIZE <= maxsize`?
>
> I guess so, yes. It's enforced by `Io::new`, which fails if `SIZE > maxsize`.

Sure. It's still an invariant.

Alice
Daniel Almeida Nov. 6, 2024, 11:31 p.m. UTC | #4
Hi,

> On 28 Oct 2024, at 12:43, Alice Ryhl <aliceryhl@google.com> wrote:
> 
> On Tue, Oct 22, 2024 at 11:33 PM Danilo Krummrich <dakr@kernel.org> wrote:
>> 
>> I/O memory is typically either mapped through direct calls to ioremap()
>> or subsystem / bus specific ones such as pci_iomap().
>> 
>> Even though subsystem / bus specific functions to map I/O memory are
>> based on ioremap() / iounmap() it is not desirable to re-implement them
>> in Rust.
>> 
>> Instead, implement a base type for I/O mapped memory, which generically
>> provides the corresponding accessors, such as `Io::readb` or
>> `Io:try_readb`.
>> 
>> `Io` supports an optional const generic, such that a driver can indicate
>> the minimal expected and required size of the mapping at compile time.
>> Correspondingly, calls to the 'non-try' accessors, support compile time
>> checks of the I/O memory offset to read / write, while the 'try'
>> accessors, provide boundary checks on runtime.
> 
> And using zero works because the user then statically knows that zero
> bytes are available ... ?
> 
>> `Io` is meant to be embedded into a structure (e.g. pci::Bar or
>> io::IoMem) which creates the actual I/O memory mapping and initializes
>> `Io` accordingly.
>> 
>> To ensure that I/O mapped memory can't out-live the device it may be
>> bound to, subsystems should embedd the corresponding I/O memory type
>> (e.g. pci::Bar) into a `Devres` container, such that it gets revoked
>> once the device is unbound.
> 
> I wonder if `Io` should be a reference type instead. That is:
> 
> struct Io<'a, const SIZE: usize> {
>    addr: usize,
>    maxsize: usize,
>    _lifetime: PhantomData<&'a ()>,
> }
> 
> and then the constructor requires "addr must be valid I/O mapped
> memory for maxsize bytes for the duration of 'a". And instead of
> embedding it in another struct, the other struct creates an `Io` on
> each access?

Please let’s not add a lifetime here. Drivers will usually have this mapped
at probe time and it will usually remain mapped until remove(), so I think this
works fine the way it is.

> 
>> Co-developed-by: Philipp Stanner <pstanner@redhat.com>
>> Signed-off-by: Philipp Stanner <pstanner@redhat.com>
>> Signed-off-by: Danilo Krummrich <dakr@kernel.org>
> 
> [...]
> 
>> diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
>> new file mode 100644
>> index 000000000000..750af938f83e
>> --- /dev/null
>> +++ b/rust/kernel/io.rs
>> @@ -0,0 +1,234 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +
>> +//! Memory-mapped IO.
>> +//!
>> +//! C header: [`include/asm-generic/io.h`](srctree/include/asm-generic/io.h)
>> +
>> +use crate::error::{code::EINVAL, Result};
>> +use crate::{bindings, build_assert};
>> +
>> +/// IO-mapped memory, starting at the base address @addr and spanning @maxlen bytes.
>> +///
>> +/// The creator (usually a subsystem / bus such as PCI) is responsible for creating the
>> +/// mapping, performing an additional region request etc.
>> +///
>> +/// # Invariant
>> +///
>> +/// `addr` is the start and `maxsize` the length of valid I/O mapped memory region of size
>> +/// `maxsize`.
> 
> Do you not also need an invariant that `SIZE <= maxsize`?
> 
> Alice
Daniel Almeida Nov. 6, 2024, 11:44 p.m. UTC | #5
Sorry, I didn’t see this:

> On 29 Oct 2024, at 07:18, Alice Ryhl <aliceryhl@google.com> wrote:
> 
> What you're doing now is a bit awkward to use. You have to make sure
> that it never escapes the struct it's created for, so e.g. you can't
> give out a mutable reference as the user could otherwise `mem::swap`
> it with another Io. Similarly, the Io can never be in a public field.
> Your safety comment on Io::new really needs to say something like
> "while this struct exists, the `addr` must be a valid I/O region",
> since I assume such regions are not valid forever? Similarly if we

Io is meant to be a private member within a wrapper type that actually
acquires the underlying I/O region, like `pci::Bar` or `Platform::IoMem`.

Doesn’t that fix the above?

> look at [1], the I/O region actually gets unmapped *before* the Io is
> destroyed since IoMem::drop runs before the fields of IoMem are
> destroyed, so you really need something like "until the last use of
> this Io" and not "until this Io is destroyed" in the safety comment.
> 
> If you compare similar cases in Rust, then they also do what I
> suggested. For example, Vec<T> holds a raw pointer, and it uses unsafe
> to assert that it's valid on each use of the raw pointer - it does not
> create e.g. an `&'static mut [T]` to hold in a field of the Vec<T>.
> Having an IoRaw<S> and an Io<'a, S> corresponds to what Vec<T> does
> with IoRaw being like NonNull<T> and Io<'a, S> being like &'a T.
> 
> [1]: https://lore.kernel.org/all/20241024-topic-panthor-rs-platform_io_support-v1-1-3d1addd96e30@collabora.com/

What I was trying to say in my last message is that the wrapper type, i.e.:
IoMem and so on, should not have a lifetime parameter, but IIUC this is not
what you’re suggesting here, right?

— Daniel
diff mbox series

Patch

diff --git a/rust/helpers/helpers.c b/rust/helpers/helpers.c
index 0720debccdd4..e2f6b2197061 100644
--- a/rust/helpers/helpers.c
+++ b/rust/helpers/helpers.c
@@ -12,6 +12,7 @@ 
 #include "build_assert.c"
 #include "build_bug.c"
 #include "err.c"
+#include "io.c"
 #include "kunit.c"
 #include "mutex.c"
 #include "page.c"
diff --git a/rust/helpers/io.c b/rust/helpers/io.c
new file mode 100644
index 000000000000..f9bb1bbf1fd5
--- /dev/null
+++ b/rust/helpers/io.c
@@ -0,0 +1,91 @@ 
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/io.h>
+
+u8 rust_helper_readb(const volatile void __iomem *addr)
+{
+	return readb(addr);
+}
+
+u16 rust_helper_readw(const volatile void __iomem *addr)
+{
+	return readw(addr);
+}
+
+u32 rust_helper_readl(const volatile void __iomem *addr)
+{
+	return readl(addr);
+}
+
+#ifdef CONFIG_64BIT
+u64 rust_helper_readq(const volatile void __iomem *addr)
+{
+	return readq(addr);
+}
+#endif
+
+void rust_helper_writeb(u8 value, volatile void __iomem *addr)
+{
+	writeb(value, addr);
+}
+
+void rust_helper_writew(u16 value, volatile void __iomem *addr)
+{
+	writew(value, addr);
+}
+
+void rust_helper_writel(u32 value, volatile void __iomem *addr)
+{
+	writel(value, addr);
+}
+
+#ifdef CONFIG_64BIT
+void rust_helper_writeq(u64 value, volatile void __iomem *addr)
+{
+	writeq(value, addr);
+}
+#endif
+
+u8 rust_helper_readb_relaxed(const volatile void __iomem *addr)
+{
+	return readb_relaxed(addr);
+}
+
+u16 rust_helper_readw_relaxed(const volatile void __iomem *addr)
+{
+	return readw_relaxed(addr);
+}
+
+u32 rust_helper_readl_relaxed(const volatile void __iomem *addr)
+{
+	return readl_relaxed(addr);
+}
+
+#ifdef CONFIG_64BIT
+u64 rust_helper_readq_relaxed(const volatile void __iomem *addr)
+{
+	return readq_relaxed(addr);
+}
+#endif
+
+void rust_helper_writeb_relaxed(u8 value, volatile void __iomem *addr)
+{
+	writeb_relaxed(value, addr);
+}
+
+void rust_helper_writew_relaxed(u16 value, volatile void __iomem *addr)
+{
+	writew_relaxed(value, addr);
+}
+
+void rust_helper_writel_relaxed(u32 value, volatile void __iomem *addr)
+{
+	writel_relaxed(value, addr);
+}
+
+#ifdef CONFIG_64BIT
+void rust_helper_writeq_relaxed(u64 value, volatile void __iomem *addr)
+{
+	writeq_relaxed(value, addr);
+}
+#endif
diff --git a/rust/kernel/io.rs b/rust/kernel/io.rs
new file mode 100644
index 000000000000..750af938f83e
--- /dev/null
+++ b/rust/kernel/io.rs
@@ -0,0 +1,234 @@ 
+// SPDX-License-Identifier: GPL-2.0
+
+//! Memory-mapped IO.
+//!
+//! C header: [`include/asm-generic/io.h`](srctree/include/asm-generic/io.h)
+
+use crate::error::{code::EINVAL, Result};
+use crate::{bindings, build_assert};
+
+/// IO-mapped memory, starting at the base address @addr and spanning @maxlen bytes.
+///
+/// The creator (usually a subsystem / bus such as PCI) is responsible for creating the
+/// mapping, performing an additional region request etc.
+///
+/// # Invariant
+///
+/// `addr` is the start and `maxsize` the length of valid I/O mapped memory region of size
+/// `maxsize`.
+///
+/// # Examples
+///
+/// ```no_run
+/// # use kernel::{bindings, io::Io};
+/// # use core::ops::Deref;
+///
+/// // See also [`pci::Bar`] for a real example.
+/// struct IoMem<const SIZE: usize>(Io<SIZE>);
+///
+/// impl<const SIZE: usize> IoMem<SIZE> {
+///     /// # Safety
+///     ///
+///     /// [`paddr`, `paddr` + `SIZE`) must be a valid MMIO region that is mappable into the CPUs
+///     /// virtual address space.
+///     unsafe fn new(paddr: usize) -> Result<Self>{
+///
+///         // SAFETY: By the safety requirements of this function [`paddr`, `paddr` + `SIZE`) is
+///         // valid for `ioremap`.
+///         let addr = unsafe { bindings::ioremap(paddr as _, SIZE.try_into().unwrap()) };
+///         if addr.is_null() {
+///             return Err(ENOMEM);
+///         }
+///
+///         // SAFETY: `addr` is guaranteed to be the start of a valid I/O mapped memory region of
+///         // size `SIZE`.
+///         let io = unsafe { Io::new(addr as _, SIZE)? };
+///
+///         Ok(IoMem(io))
+///     }
+/// }
+///
+/// impl<const SIZE: usize> Drop for IoMem<SIZE> {
+///     fn drop(&mut self) {
+///         // SAFETY: Safe as by the invariant of `Io`.
+///         unsafe { bindings::iounmap(self.0.base_addr() as _); };
+///     }
+/// }
+///
+/// impl<const SIZE: usize> Deref for IoMem<SIZE> {
+///    type Target = Io<SIZE>;
+///
+///    fn deref(&self) -> &Self::Target {
+///        &self.0
+///    }
+/// }
+///
+///# fn no_run() -> Result<(), Error> {
+/// // SAFETY: Invalid usage for example purposes.
+/// let iomem = unsafe { IoMem::<{ core::mem::size_of::<u32>() }>::new(0xBAAAAAAD)? };
+/// iomem.writel(0x42, 0x0);
+/// assert!(iomem.try_writel(0x42, 0x0).is_ok());
+/// assert!(iomem.try_writel(0x42, 0x4).is_err());
+/// # Ok(())
+/// # }
+/// ```
+pub struct Io<const SIZE: usize = 0> {
+    addr: usize,
+    maxsize: usize,
+}
+
+macro_rules! define_read {
+    ($(#[$attr:meta])* $name:ident, $try_name:ident, $type_name:ty) => {
+        /// Read IO data from a given offset known at compile time.
+        ///
+        /// Bound checks are performed on compile time, hence if the offset is not known at compile
+        /// time, the build will fail.
+        $(#[$attr])*
+        #[inline]
+        pub fn $name(&self, offset: usize) -> $type_name {
+            let addr = self.io_addr_assert::<$type_name>(offset);
+
+            // SAFETY: By the type invariant `addr` is a valid address for MMIO operations.
+            unsafe { bindings::$name(addr as _) }
+        }
+
+        /// Read IO data from a given offset.
+        ///
+        /// Bound checks are performed on runtime, it fails if the offset (plus the type size) is
+        /// out of bounds.
+        $(#[$attr])*
+        pub fn $try_name(&self, offset: usize) -> Result<$type_name> {
+            let addr = self.io_addr::<$type_name>(offset)?;
+
+            // SAFETY: By the type invariant `addr` is a valid address for MMIO operations.
+            Ok(unsafe { bindings::$name(addr as _) })
+        }
+    };
+}
+
+macro_rules! define_write {
+    ($(#[$attr:meta])* $name:ident, $try_name:ident, $type_name:ty) => {
+        /// Write IO data from a given offset known at compile time.
+        ///
+        /// Bound checks are performed on compile time, hence if the offset is not known at compile
+        /// time, the build will fail.
+        $(#[$attr])*
+        #[inline]
+        pub fn $name(&self, value: $type_name, offset: usize) {
+            let addr = self.io_addr_assert::<$type_name>(offset);
+
+            // SAFETY: By the type invariant `addr` is a valid address for MMIO operations.
+            unsafe { bindings::$name(value, addr as _, ) }
+        }
+
+        /// Write IO data from a given offset.
+        ///
+        /// Bound checks are performed on runtime, it fails if the offset (plus the type size) is
+        /// out of bounds.
+        $(#[$attr])*
+        pub fn $try_name(&self, value: $type_name, offset: usize) -> Result {
+            let addr = self.io_addr::<$type_name>(offset)?;
+
+            // SAFETY: By the type invariant `addr` is a valid address for MMIO operations.
+            unsafe { bindings::$name(value, addr as _) }
+            Ok(())
+        }
+    };
+}
+
+impl<const SIZE: usize> Io<SIZE> {
+    ///
+    ///
+    /// # Safety
+    ///
+    /// Callers must ensure that `addr` is the start of a valid I/O mapped memory region of size
+    /// `maxsize`.
+    pub unsafe fn new(addr: usize, maxsize: usize) -> Result<Self> {
+        if maxsize < SIZE {
+            return Err(EINVAL);
+        }
+
+        // INVARIANT: Covered by the safety requirements of this function.
+        Ok(Self { addr, maxsize })
+    }
+
+    /// Returns the base address of this mapping.
+    #[inline]
+    pub fn base_addr(&self) -> usize {
+        self.addr
+    }
+
+    /// Returns the size of this mapping.
+    #[inline]
+    pub fn maxsize(&self) -> usize {
+        self.maxsize
+    }
+
+    #[inline]
+    const fn offset_valid<U>(offset: usize, size: usize) -> bool {
+        let type_size = core::mem::size_of::<U>();
+        if let Some(end) = offset.checked_add(type_size) {
+            end <= size && offset % type_size == 0
+        } else {
+            false
+        }
+    }
+
+    #[inline]
+    fn io_addr<U>(&self, offset: usize) -> Result<usize> {
+        if !Self::offset_valid::<U>(offset, self.maxsize()) {
+            return Err(EINVAL);
+        }
+
+        // Probably no need to check, since the safety requirements of `Self::new` guarantee that
+        // this can't overflow.
+        self.base_addr().checked_add(offset).ok_or(EINVAL)
+    }
+
+    #[inline]
+    fn io_addr_assert<U>(&self, offset: usize) -> usize {
+        build_assert!(Self::offset_valid::<U>(offset, SIZE));
+
+        self.base_addr() + offset
+    }
+
+    define_read!(readb, try_readb, u8);
+    define_read!(readw, try_readw, u16);
+    define_read!(readl, try_readl, u32);
+    define_read!(
+        #[cfg(CONFIG_64BIT)]
+        readq,
+        try_readq,
+        u64
+    );
+
+    define_read!(readb_relaxed, try_readb_relaxed, u8);
+    define_read!(readw_relaxed, try_readw_relaxed, u16);
+    define_read!(readl_relaxed, try_readl_relaxed, u32);
+    define_read!(
+        #[cfg(CONFIG_64BIT)]
+        readq_relaxed,
+        try_readq_relaxed,
+        u64
+    );
+
+    define_write!(writeb, try_writeb, u8);
+    define_write!(writew, try_writew, u16);
+    define_write!(writel, try_writel, u32);
+    define_write!(
+        #[cfg(CONFIG_64BIT)]
+        writeq,
+        try_writeq,
+        u64
+    );
+
+    define_write!(writeb_relaxed, try_writeb_relaxed, u8);
+    define_write!(writew_relaxed, try_writew_relaxed, u16);
+    define_write!(writel_relaxed, try_writel_relaxed, u32);
+    define_write!(
+        #[cfg(CONFIG_64BIT)]
+        writeq_relaxed,
+        try_writeq_relaxed,
+        u64
+    );
+}
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index b603b67dcd71..fa3b7514e6ae 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -70,6 +70,7 @@ 
 
 #[doc(hidden)]
 pub use bindings;
+pub mod io;
 pub use macros;
 pub use uapi;