[PATCH v1] rust: console: add abstraction for kernel console drivers

From: Matthew Wood

Date: Fri Feb 27 2026 - 16:54:12 EST


Add a safe Rust abstraction for the kernel's `struct console`,
enabling console drivers to be implemented in Rust that provides:

- `ConsoleOps` trait with a required `write` callback and an
optional `setup` callback, mirroring the C console_operations
interface. The trait requires `Send + Sync` to reflect that
console write may be called from any context including IRQ.

- `Console<T>` struct that wraps `struct console` using pin-init
and `Opaque<T>`, with automatic unregistration via `PinnedDrop`.
Registration is performed through a pin-initializer returned by
`Console::register()`, which uses `pin_chain` to set the data
pointer and call `register_console()` after the struct is pinned.

- `ConsoleOpsAdapter<T>` that provides the extern "C" callbacks
bridging from the kernel's function pointers to the Rust trait
methods. The `#[vtable]` attribute on `ConsoleOps` enables
compile-time detection of whether `setup` is implemented via
`T::HAS_SETUP`.

- Console flag constants re-exported from the C `enum cons_flags`.

C helper functions are added for `register_console()`,
`unregister_console()`, and `console_is_registered()` as these are
either inlines or macros that cannot be called directly from Rust
through bindgen.

This abstraction is a dependency for a Rust netconsole implementation
I am working on.

Signed-off-by: Matthew Wood <thepacketgeek@xxxxxxxxx>
---
rust/bindings/bindings_helper.h | 1 +
rust/helpers/console.c | 22 +++
rust/helpers/helpers.c | 1 +
rust/kernel/console.rs | 230 ++++++++++++++++++++++++++++++++
rust/kernel/lib.rs | 1 +
5 files changed, 255 insertions(+)
create mode 100644 rust/helpers/console.c
create mode 100644 rust/kernel/console.rs

diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index 083cc44aa952..eeddf2374f00 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -43,6 +43,7 @@
#include <linux/clk.h>
#include <linux/completion.h>
#include <linux/configfs.h>
+#include <linux/console.h>
#include <linux/cpu.h>
#include <linux/cpufreq.h>
#include <linux/cpumask.h>
diff --git a/rust/helpers/console.c b/rust/helpers/console.c
new file mode 100644
index 000000000000..b101c6c749fd
--- /dev/null
+++ b/rust/helpers/console.c
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * Rust helpers for console.
+ */
+
+#include <linux/console.h>
+
+void rust_helper_register_console(struct console *console)
+{
+ register_console(console);
+}
+
+int rust_helper_unregister_console(struct console *console)
+{
+ return unregister_console(console);
+}
+
+bool rust_helper_console_is_registered(struct console *console)
+{
+ return console_is_registered(console);
+}
diff --git a/rust/helpers/helpers.c b/rust/helpers/helpers.c
index a3c42e51f00a..2b818126ce02 100644
--- a/rust/helpers/helpers.c
+++ b/rust/helpers/helpers.c
@@ -22,6 +22,7 @@
#include "build_bug.c"
#include "clk.c"
#include "completion.c"
+#include "console.c"
#include "cpu.c"
#include "cpufreq.c"
#include "cpumask.c"
diff --git a/rust/kernel/console.rs b/rust/kernel/console.rs
new file mode 100644
index 000000000000..d78b04a9846b
--- /dev/null
+++ b/rust/kernel/console.rs
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Console driver abstraction.
+//!
+//! This module provides safe Rust wrappers for implementing kernel console
+//! drivers, which receive kernel log messages and output them to a device.
+//!
+//! C header: [`include/linux/console.h`](srctree/include/linux/console.h)
+
+use crate::{bindings, error::Result, prelude::*, str::CStr, types::Opaque};
+use core::marker::PhantomData;
+
+/// Console flags from `enum cons_flags`.
+pub mod flags {
+ /// Used by newly registered consoles to avoid duplicate output.
+ pub const CON_PRINTBUFFER: u16 = bindings::cons_flags_CON_PRINTBUFFER as u16;
+ /// Indicates console is backing /dev/console.
+ pub const CON_CONSDEV: u16 = bindings::cons_flags_CON_CONSDEV as u16;
+ /// Console is enabled.
+ pub const CON_ENABLED: u16 = bindings::cons_flags_CON_ENABLED as u16;
+ /// Early boot console.
+ pub const CON_BOOT: u16 = bindings::cons_flags_CON_BOOT as u16;
+ /// Console can be called from any context.
+ pub const CON_ANYTIME: u16 = bindings::cons_flags_CON_ANYTIME as u16;
+ /// Braille device.
+ pub const CON_BRL: u16 = bindings::cons_flags_CON_BRL as u16;
+ /// Console supports extended output format.
+ pub const CON_EXTENDED: u16 = bindings::cons_flags_CON_EXTENDED as u16;
+ /// Console is suspended.
+ pub const CON_SUSPENDED: u16 = bindings::cons_flags_CON_SUSPENDED as u16;
+}
+
+/// Operations that a console driver must implement.
+///
+/// The `write` callback is the only required operation. It will be called
+/// to output kernel log messages.
+#[vtable]
+pub trait ConsoleOps: Sized + Send + Sync {
+ /// Writes a message to the console.
+ ///
+ /// This is called with a buffer containing the message to output.
+ /// The implementation should send the message to the console device.
+ ///
+ /// # Context
+ ///
+ /// This may be called from any context, including IRQ context.
+ /// Implementations must not sleep.
+ fn write(&self, msg: &[u8]);
+
+ /// Sets up the console.
+ ///
+ /// This is called when the console is registered.
+ /// The `options` parameter contains any boot command line options.
+ fn setup(&self, _options: Option<&CStr>) -> Result {
+ Ok(())
+ }
+}
+
+/// Adapter for console operations vtable.
+struct ConsoleOpsAdapter<T: ConsoleOps>(PhantomData<T>);
+
+impl<T: ConsoleOps> ConsoleOpsAdapter<T> {
+ /// Write callback for the console.
+ ///
+ /// # Safety
+ ///
+ /// `con` must be a valid pointer to a `bindings::console` that was
+ /// created by `Console<T>` and has valid `data` pointing to `T`.
+ unsafe extern "C" fn write_callback(
+ con: *mut bindings::console,
+ s: *const u8,
+ count: core::ffi::c_uint,
+ ) {
+ // SAFETY: By function safety requirements, `con` is valid.
+ let data = unsafe { (*con).data };
+ if data.is_null() {
+ return;
+ }
+
+ // SAFETY: `data` points to a valid `T` per the type invariants.
+ let ops = unsafe { &*(data as *const T) };
+
+ // SAFETY: `s` is valid for `count` bytes.
+ let msg = unsafe { core::slice::from_raw_parts(s, count as usize) };
+
+ ops.write(msg);
+ }
+
+ /// Setup callback for the console.
+ ///
+ /// # Safety
+ ///
+ /// `con` must be a valid pointer to a `bindings::console`.
+ unsafe extern "C" fn setup_callback(
+ con: *mut bindings::console,
+ options: *mut u8,
+ ) -> core::ffi::c_int {
+ // SAFETY: By function safety requirements, `con` is valid.
+ let data = unsafe { (*con).data };
+ if data.is_null() {
+ return 0;
+ }
+
+ // SAFETY: `data` points to a valid `T` per the type invariants.
+ let ops = unsafe { &*(data as *const T) };
+
+ let options_cstr = if options.is_null() {
+ None
+ } else {
+ // SAFETY: If not null, `options` points to a null-terminated string.
+ Some(unsafe { CStr::from_char_ptr(options.cast()) })
+ };
+
+ match ops.setup(options_cstr) {
+ Ok(()) => 0,
+ Err(e) => e.to_errno(),
+ }
+ }
+}
+
+/// A registered kernel console.
+///
+/// This struct wraps the kernel's `struct console` and provides safe
+/// registration and unregistration of console drivers.
+///
+/// # Invariants
+///
+/// - `inner` contains a valid `console` structure.
+/// - When registered, the console's `data` field points to valid `T`.
+#[pin_data(PinnedDrop)]
+pub struct Console<T: ConsoleOps> {
+ #[pin]
+ inner: Opaque<bindings::console>,
+ #[pin]
+ data: T,
+ registered: bool,
+}
+
+// SAFETY: Console can be sent between threads if T can.
+unsafe impl<T: ConsoleOps + Send> Send for Console<T> {}
+
+// SAFETY: Console operations are synchronized by the caller.
+unsafe impl<T: ConsoleOps + Sync> Sync for Console<T> {}
+
+impl<T: ConsoleOps> Console<T> {
+ /// Creates an initializer for registering a new console.
+ ///
+ /// # Arguments
+ ///
+ /// * `name` - The name of the console (up to 15 characters).
+ /// * `flags` - Console flags from the `flags` module.
+ /// * `data` - The console operations implementation.
+ pub fn register(
+ name: &'static CStr,
+ console_flags: u16,
+ data: impl PinInit<T, Error>,
+ ) -> impl PinInit<Self, Error> {
+ try_pin_init!(Self {
+ inner <- Opaque::try_ffi_init(|slot: *mut bindings::console| {
+ // SAFETY: `slot` is valid for writing.
+ unsafe {
+ // Zero-initialize the struct.
+ core::ptr::write_bytes(slot, 0, 1);
+
+ // Copy the name (up to 15 chars + null).
+ let name_bytes = name.to_bytes();
+ let name_len = core::cmp::min(name_bytes.len(), 15);
+ core::ptr::copy_nonoverlapping(
+ name_bytes.as_ptr().cast(),
+ (*slot).name.as_mut_ptr(),
+ name_len,
+ );
+
+ // Set flags.
+ (*slot).flags = console_flags as i16;
+
+ // Set the write callback.
+ (*slot).write = Some(ConsoleOpsAdapter::<T>::write_callback);
+
+ // Set the setup callback if T implements it.
+ if T::HAS_SETUP {
+ (*slot).setup = Some(ConsoleOpsAdapter::<T>::setup_callback);
+ }
+ }
+ Ok::<(), Error>(())
+ }),
+ data <- data,
+ registered: true, // Will be set after registration in pin_chain
+ })
+ .pin_chain(|this| {
+ // Set the data pointer to our ops.
+ // SAFETY: `this` is pinned and valid.
+ unsafe {
+ let con = this.inner.get();
+ (*con).data = &this.data as *const T as *mut core::ffi::c_void;
+ }
+
+ // Register the console.
+ // SAFETY: The console structure is properly initialized.
+ unsafe { bindings::register_console(this.inner.get()) };
+
+ Ok(())
+ })
+ }
+
+ /// Returns whether the console is currently registered.
+ pub fn is_registered(&self) -> bool {
+ self.registered
+ }
+
+ /// Returns a pointer to the underlying console struct.
+ pub fn as_ptr(&self) -> *mut bindings::console {
+ self.inner.get()
+ }
+
+ /// Returns a reference to the console data.
+ pub fn data(&self) -> &T {
+ &self.data
+ }
+}
+
+#[pinned_drop]
+impl<T: ConsoleOps> PinnedDrop for Console<T> {
+ fn drop(self: Pin<&mut Self>) {
+ if self.registered {
+ // SAFETY: The console was registered during initialization.
+ unsafe { bindings::unregister_console(self.inner.get()) };
+ }
+ }
+}
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 3da92f18f4ee..6381225cbe9c 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -78,6 +78,7 @@
pub mod clk;
#[cfg(CONFIG_CONFIGFS_FS)]
pub mod configfs;
+pub mod console;
pub mod cpu;
#[cfg(CONFIG_CPU_FREQ)]
pub mod cpufreq;
--
2.52.0