Un driver completo en Rust: de probe a Drop
Un platform driver de ejemplo comentado de principio a fin: la tabla de compatibles, el probe que da vida al dispositivo, el estado como objeto con dueño, y el Drop que reemplaza a remove. El contrato probe/remove del nivel 29, ahora con la limpieza garantizada por el lenguaje.
Es hora de juntarlo todo. En el nivel 29 escribiste un platform driver en C: un struct platform_driver, un probe que reclama recursos y un remove que los suelta, con devm emulando la limpieza automática. Aquí tienes el mismo driver en Rust for Linux, comentado de principio a fin. El contrato es idéntico —el core empareja dispositivo y driver, y notifica su aparición y desaparición— pero la limpieza deja de ser una emulación para ser una propiedad del lenguaje.
- Declarar la tabla de compatibles y registrar un
platform::Driveren Rust. - Escribir un
probeque construye el estado del dispositivo como un objeto. - Entender el
Dropcomo elremoveque el kernel ejecuta solo. - Comparar el flujo y la seguridad con el driver C del nivel 29.
El driver de principio a fin
// SPDX-License-Identifier: GPL-2.0
//! Un platform driver de ejemplo en Rust.
use kernel::{c_str, device::Core, of, platform, prelude::*, types::ARef};
// El estado del dispositivo: un objeto cuyo dueno es el core del kernel.
struct DriverEjemplo {
pdev: ARef<platform::Device>,
}
// Dato asociado a cada entrada de la tabla de compatibles.
struct Info(u32);
// La tabla de Open Firmware: de que nodos del device tree se hace cargo
// este driver. Es el of_match_table del nivel 29.4, ahora tipado.
kernel::of_device_table!(
OF_TABLE,
MODULE_OF_TABLE,
<DriverEjemplo as platform::Driver>::IdInfo,
[(of::DeviceId::new(c_str!("acme,rust-device")), Info(42))]
);
impl platform::Driver for DriverEjemplo {
type IdInfo = Info;
const OF_ID_TABLE: Option<of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
// probe: se invoca UNA vez por cada dispositivo que casa con la tabla.
// Devuelve el estado ya construido; el core lo adopta y lo mantiene vivo.
fn probe(
pdev: &platform::Device<Core>,
info: Option<&Self::IdInfo>,
) -> Result<Pin<KBox<Self>>> {
dev_info!(pdev.as_ref(), "probe del driver Rust\n");
if let Some(info) = info {
dev_info!(pdev.as_ref(), "casado con info = {}\n", info.0);
}
// Aqui reclamarias registros, IRQ o relojes: cada recurso seria un
// objeto guard cuyo Drop lo libera. Un fallo con ? deshace lo hecho.
let datos = KBox::new(Self { pdev: pdev.into() }, GFP_KERNEL)?;
Ok(datos.into())
}
}
// Drop: el equivalente de remove. Corre cuando el dispositivo se desliga,
// por rmmod o por unbind. No hay que registrarlo: el core destruye el
// objeto que probe devolvio, y destruirlo ejecuta este Drop.
impl Drop for DriverEjemplo {
fn drop(&mut self) {
dev_info!(self.pdev.as_ref(), "remove del driver Rust\n");
}
}
// Genera el module_init y el module_exit que registran y retiran el driver,
// como el module_platform_driver del nivel 29.
kernel::module_platform_driver! {
type: DriverEjemplo,
name: "driver_ejemplo",
author: "William",
description: "Platform driver de ejemplo en Rust",
license: "GPL v2",
}
El flujo: probe, operaciones, cleanup
El ciclo de vida es el mismo del nivel 29, con una diferencia en el último paso:
flowchart LR MATCH[Match por compatible] --> PROBE[probe construye el objeto] PROBE --> OK[Ok con el estado del driver] OK --> VIVE[dispositivo activo, el core es el dueno] VIVE --> UNBIND[rmmod o unbind] UNBIND --> DROP[Drop libera todo en orden inverso] style PROBE fill:#89b4fa,color:#11111b style VIVE fill:#f9e2af,color:#11111b style DROP fill:#a6e3a1,color:#11111b
Lee el diagrama contra el del nivel 29. El match por compatible es idéntico: el core busca en la tabla de Open Firmware qué driver reclama cada nodo. El probe también: se llama una vez por dispositivo y tiene el mismo trabajo, reservar el estado y dejar el hardware operativo. La primera diferencia está en el tipo de retorno. En C, probe devolvía un int y guardabas tu estado con platform_set_drvdata; aquí probe devuelve Pin<KBox<Self>>, el estado mismo, y el core lo adopta como driver-data sin que tú lo guardes.
La diferencia decisiva está al final. En C, el desligado llamaba a tu remove, y tú te encargabas de deshacer lo que probe hizo —o confiabas en que devm lo hiciera por ti—. En Rust no hay función remove: el core destruye el objeto que probe devolvió, y esa destrucción ejecuta el Drop del struct y, en cascada, el Drop de cada campo. Si probe hubiera reclamado un mapeo de registros y una IRQ, cada uno sería un objeto guard cuyo Drop lo libera, y todos se soltarían en orden inverso al de adquisición, solos.
probe
Nace el dispositivo. Reserva el estado y reclama recursos; devuelve el objeto que el core adopta como driver-data.
operaciones
El dispositivo vive. Interrupciones, ioctls o lecturas usan el estado a través de Mutex o SpinLock.
Drop
Muere el dispositivo. El core destruye el objeto y cada recurso se libera en orden inverso, sin remove que escribir.
En C guardabas tu estado con platform_set_drvdata(pdev, priv) y lo recuperabas en remove con platform_get_drvdata. En Rust ese paso desaparece: probe devuelve el Pin<KBox<Self>> y el core lo custodia como driver-data. No hay un puntero que guardar ni recuperar a mano, y por tanto tampoco forma de recuperarlo con el tipo equivocado o de usarlo después de que el dispositivo se haya ido.
Por qué es más seguro que el equivalente en C
El devm del nivel 29 era la mejor respuesta de C a la limpieza: atar cada recurso a la vida del struct device para que se libere solo. Pero devm es una convención opcional injertada sobre un lenguaje sin destructores; nada te impide mezclar un devm_kzalloc con un kmalloc manual y olvidar el kfree en la ruta de error. En Rust, la limpieza no es una API que eliges usar: es el comportamiento por defecto e inevitable de todo objeto. Un probe que falla en su quinta línea con un ? deshace las cuatro primeras sin una sola etiqueta goto, porque cada objeto ya construido se destruye al desenrollarse la función.
Y se cierra un segundo tipo de bug clásico: el use-after-remove. En C, si algún callback guardaba un puntero al estado del driver y lo usaba tras el desligado, tenías un use-after-free. En Rust el estado es un objeto cuya propiedad tiene el core; nadie más conserva un puntero crudo a él, y cuando el core lo destruye no queda referencia viva que pueda tocarlo, porque el compilador nunca te dejó guardar una referencia que sobreviviera al objeto.
Da un paso atrás y mira la forma completa, porque es la síntesis de todo el nivel 55. Un driver, en cualquier lenguaje, es la respuesta a un acontecimiento: aparece un dispositivo, hay que darle vida; desaparece, hay que retirarlo con limpieza. El modelo de dispositivos de Linux lleva décadas expresando eso con el par probe/remove, y el nivel 29 te enseñó que el reto no está en probe —crear siempre es fácil— sino en remove y en las rutas de error, donde hay que deshacer exactamente lo hecho, en orden inverso, sin olvidar ni repetir, en un sistema vivo donde un descuido es una fuga o una corrupción. C ofrecía disciplina y, más tarde, devm, una emulación de destructores para aliviar esa carga. Rust for Linux hace algo distinto de grado: identifica el driver con un objeto cuyo tiempo de vida es el intervalo en que el dispositivo está ligado, hace que probe sea el constructor que lo trae al mundo y Drop el destructor que lo retira, y delega en el compilador la garantía de que cada recurso adquirido se libera una vez y solo una, en orden inverso, pase lo que pase en el camino. La categoría de bugs que dominó la depuración de drivers en C —la fuga en la ruta de error, el doble free en el remove, el use-after-free del recurso que se soltó dos veces, el goto que salta a la etiqueta equivocada— no se vuelve más fácil de encontrar: se vuelve imposible de escribir en el código seguro. Ese es el sentido de todo el track de Rust en el kernel. No aprendiste un lenguaje nuevo para hacer lo mismo; aprendiste a mover el contrato del kernel —cargar y descargar con simetría, propagar el errno, no tocar memoria ajena, no compartir sin sincronizar, limpiar en orden inverso— desde tu cabeza, donde vivía como disciplina, hasta el sistema de tipos, donde vive como garantía. El dispositivo aparece, el objeto nace; el dispositivo se va, el objeto muere y se lleva sus recursos consigo. No hay nada más que recordar.
- Compila el platform driver y cárgalo; provoca el match con un nodo de device tree cuyo
compatibleseaacme,rust-devicey confirma el mensaje deprobeendmesg. - Desliga el dispositivo con
unbindo descarga el módulo y observa el mensaje delDrophaciendo deremove. - Añade al
structdel driver un campo que sea un recurso conDroppropio y comprueba que se libera sin que escribas su liberación. - Introduce un
?que falle a mitad deprobey razona qué se libera y en qué orden, comparándolo con losgoto errdel driver C del nivel 29. - Pon lado a lado este archivo y el platform driver C del nivel 29 y señala, línea por línea, dónde el
devmy elgotode C se han vueltoDropy?en Rust.