C o Rust: cuál elegir para la programación de abstracción de hardware
Rust es un lenguaje de programación cada vez más popular considerado la mejor opción para la interfaz de hardware. Esto a menudo se compara con el nivel de abstracción de C. Este artículo describe cómo Rust maneja las operaciones bit a bit de varias maneras y proporciona soluciones que son seguras y fáciles de usar.
En el mundo de la programación de sistemas, es posible que a menudo necesite escribir controladores de hardware o interactuar directamente con dispositivos mapeados en memoria, y estas interacciones son casi siempre a través de Esto se logra utilizando registros mapeados en memoria proporcionados por el hardware. Normalmente, interactúa con estos registros realizando operaciones bit a bit en algún tipo numérico de ancho fijo.
Por ejemplo, considere un registro de 8 bits con tres campos:
El número debajo del nombre del campo especifica qué bits se utilizan en el registro para ese campo. Para habilitar este registro, escribiría el valor 1 (0000_0001 en binario) para configurar los bits del campo Habilitado. Sin embargo, normalmente tampoco desea alterar la configuración existente en los registros. Digamos que desea habilitar las interrupciones en el dispositivo, pero también asegurarse de que el dispositivo permanezca habilitado. Para hacer esto, el valor del campo Interrupción debe combinarse con el valor del campo Habilitado. Puedes hacer esto usando operaciones bit a bit:
El valor binario 0000_0011 se obtiene haciendo una operación OR (|) 1 y 2 (que se obtiene desplazando 1 un bit hacia la izquierda). Puede escribirlo en un registro para mantenerlo habilitado pero también habilitar la funcionalidad de interrupción.
Hay mucho que tener en cuenta, especialmente cuando se trata de cientos de registros potenciales en un sistema completo. En la práctica, puedes hacer esto usando mnemotécnicos, que realizan un seguimiento de dónde está el campo en el registro y el ancho del campo (es decir, cuál es su límite superior).
Aquí están los mnemotécnicos. Un ejemplo de uno de las fichas. Son macros de C que reemplazan sus apariciones con el código de la derecha. Esta es una abreviatura de los registros enumerados anteriormente. El lado izquierdo de & es donde comienza el campo, mientras que el lado derecho limita los bits ocupados por el campo:
Luego puedes usarlos para abstraer la manipulación de los valores de registro, así:
Así es como se hace ahora. De hecho, así es como se utilizan la mayoría de los controladores en el kernel de Linux.
¿Existe una manera mejor? Si se pudiera derivar un nuevo sistema de tipos a partir de la investigación sobre lenguajes de programación modernos, sería posible cosechar los beneficios de la seguridad y la expresabilidad. Es decir, ¿cómo se puede utilizar un sistema tipográfico más rico y expresivo para hacer que este proceso sea más seguro y duradero?
Continuando con el ejemplo de registro anterior:
¿Cómo te gustaría representarlo como tipo Rust?
Comenzarás de manera similar, definiendo constantes para el desplazamiento de cada campo (es decir, qué tan lejos está del bit menos significativo) y su máscara. Una máscara es un valor cuya representación binaria se puede usar para actualizar o leer campos dentro de un registro:
Finalmente, usará un tipo de Registro que encapsule un número que coincida con el ancho de su tipo de registro. Register tiene una función de actualización que actualiza un registro con un campo determinado:
Con Rust, puede utilizar estructuras de datos para representar campos, asociarlos con registros específicos y brindar simplicidad al interactuar con el hardware. Ergonomía clara. Este ejemplo utiliza la funcionalidad más básica proporcionada por Rust. De todos modos, la estructura agregada alivia parte de la oscuridad en el ejemplo de C anterior. Ahora, los campos son cosas con nombres en lugar de números derivados de oscuros operadores bit a bit, y los registros son tipos con estado: una capa adicional de abstracción sobre el hardware.
La primera versión reescrita en Rust fue agradable, pero no ideal. Debe recordar incorporar máscaras y compensaciones, y realizar cálculos ad hoc a mano, lo cual es propenso a errores. Los humanos no somos buenos para tareas precisas y repetitivas: tendemos a cansarnos o perder la concentración, lo que nos lleva a cometer errores. Es casi seguro que grabar manualmente máscaras y compensaciones de un registro a la vez terminará mal. Esta es una tarea que es mejor dejar en manos de las máquinas.
En segundo lugar, piénselo estructuralmente: ¿qué pasa si hay una manera de hacer que el tipo de campo lleve información de máscara y compensación? ¿Qué pasaría si los errores en el código que implementa el acceso y la interacción del registro de hardware pudieran descubrirse en tiempo de compilación, en lugar de en tiempo de ejecución? Quizás pueda confiar en una estrategia común para resolver problemas en tiempo de compilación, como los tipos.
Puedes modificar el ejemplo anterior usando typenum, una biblioteca que proporciona números y aritmética a nivel de tipo. Aquí, parametrizarás el tipo de campo con una máscara y un desplazamiento, haciéndolo disponible para cualquier instancia de campo sin incluirlo en la llamada:
Ahora, al volver a visitar el campo Al construir la función, puedes ignorarlo los parámetros de máscara y desplazamiento, ya que esa información está incluida en el tipo:
Se ve bien, pero... si te equivocas acerca de si el valor dado cabe en el campo Error, ¿qué sucede? Considere un simple error tipográfico en el que puso 10 en lugar de 1:
En el código anterior, ¿cuál es el resultado esperado? Bueno, el código establecerá el bit de habilitación en 0 porque 10 y 1 = 0. Eso es desafortunado; es mejor saber si el valor que estás escribiendo en un campo cabe en ese campo antes de intentar escribirlo. De hecho, creo que truncar los bits altos del valor de campo incorrecto es un comportamiento indefinido (ja).
¿Cómo puedo comprobar de forma general si el valor de un campo se ajusta a la posición indicada? ¡Se necesitan más figuras a nivel de tipo!
Puedes agregar un parámetro Ancho al campo y usarlo para verificar que un valor determinado se ajuste al campo:
¡Ahora puedes construir un campo! De lo contrario, recibirá una señal Ninguno que indica que se produjo un error, en lugar de truncar los bits altos del valor y escribir silenciosamente el valor inesperado.
Sin embargo, tenga en cuenta que esto generará un error en el entorno de ejecución. Sin embargo, sabemos de antemano el valor que queremos escribir, ¿recuerdas? Teniendo esto en cuenta, podemos enseñarle al compilador a rechazar por completo programas con valores de campo no válidos: ¡no tenemos que esperar para ejecutarlo!
Esta vez, agregará un enlace de característica (cláusula donde) a la nueva implementación de new, new_checked, que requiere que el valor de entrada sea menor o igual al valor máximo posible que el campo dado puede mantenga usando Ancho:
Solo los números con esta propiedad implementan este rasgo, por lo que si usa un número inadecuado no se podrá compilar. ¡Echemos un vistazo!
new_checked no podrá generar un programa porque el valor de este campo tiene un bit alto incorrecto. Sus errores de entrada no explotarán simplemente en el entorno de ejecución, porque nunca obtendrá un artefacto que funcione.
Te estás acercando al extremo de Rust en términos de hacer que sea seguro interactuar con hardware mapeado en memoria. Pero lo que escribiste en tu primer ejemplo en C es más conciso que el desorden de parámetros de tipo con el que terminaste. ¿Es esto fácil de manejar cuando se habla de potencialmente cientos o incluso miles de registros?
Antes pensé que había algo mal en calcular la máscara a mano, pero volví a hacer lo mismo, aunque a nivel de tipo. Si bien usar este enfoque es excelente, llegar al punto de escribir cualquier código requiere mucha transcripción manual y repetitiva (aquí estoy hablando de sinónimos de tipo).
Nuestro equipo quería algo como los registros mmio de TockOS para producir una implementación de tipo seguro con una transcripción manual mínima. El resultado es una macro que genera el texto estándar necesario para obtener una API similar a Tock con verificación de límites basada en tipos. Para usarlo, escribe alguna información sobre el registro, sus campos, ancho y desplazamiento y opcionalmente los valores de la clase enum (debes darle un "significado" a los valores que puedan tener los campos):
A partir de esto, puede generar tipos de registros y campos, como se muestra en el ejemplo anterior, donde los índices: Ancho, Máscara y Desplazamiento se derivan de los valores de entrada de las partes ANCHO y DESPLAZAMIENTO de una definición de campo. Además, tenga en cuenta que todos estos números son "números de tipo" e irán directamente a la definición de su campo.
El código generado proporciona un espacio de nombres para los registros y sus campos asociados asignando nombres a los registros y campos. Esto es bastante complicado y se parece a esto:
La API generada contiene primitivas de lectura y escritura que nominalmente esperan obtener el valor de un registro sin formato, pero también tiene formas de obtener el valor de un único campo valor, métodos para realizar operaciones de configuración y determinar si alguno (o todos) del conjunto de bits está configurado. Puede leer la documentación completa sobre la API generada.
¿Cómo sería usar estas definiciones en un dispositivo real? ¿Se llenará el código con parámetros de tipo, oscureciendo la lógica real en la vista?
¡No! Al utilizar sinónimos de tipos e inferencia de tipos, en realidad no tiene que pensar en absoluto en las partes de nivel de tipo de su programa. Puede interactuar directamente con el hardware y obtener automáticamente garantías relacionadas con los límites.
Una vez implementados, usar estos registros es tan simple como leer() y modificar():
Cuando usamos valores de tiempo de ejecución, usamos opciones como se describió anteriormente. Aquí estoy usando unwrap, pero en un programa real donde se desconoce la entrada, es posible que desee verificar algo devuelto por la nueva llamada: 1 2
Dependiendo de su tolerancia personal al dolor, es posible que tenga Se ha observado que estos errores son casi incomprensibles. Eche un vistazo al recordatorio no tan sutil de lo que dije:
se esperaba struct typenum::B0, se encontró que struct typenum::B1 tiene sentido parcialmente, pero typenum::UIntlt; , typenum :: UInt... ¿Qué es exactamente? Bueno, ¡typenum representa números como unidades binarias contras! Errores como este dificultan las cosas, especialmente cuando tienes varios niveles de números de este tipo en un rango estrecho donde es difícil saber de qué número está hablando. A menos, por supuesto, que puedas convertir una representación binaria barroca en una representación decimal de un vistazo.
Después de intentar por centésima vez descifrar algún significado de este lío, uno de nuestros compañeros de equipo se volvió loco y no iba a soportarlo más)" e hizo una pequeña herramienta para liberarse de el dolor de esta unidad de desventajas binarias del espacio de nombres. tnfilt reemplaza la representación de los formatos de celdas contras con números decimales legibles por humanos. Pensamos que otros tendrían dificultades similares, así que compartimos la información.
Puedes usarlo así:
Convierte el resultado anterior para que se vea así:
¡Esto tiene sentido!
Los registros mapeados en memoria se utilizan comúnmente cuando el software interactúa con el hardware, y existen innumerables formas de describir estas interacciones, cada una con diferentes compensaciones en cuanto a facilidad de uso y seguridad. Descubrimos que el uso de programación a nivel de tipo para obtener comprobaciones en tiempo de compilación de las interacciones de registros mapeados en memoria puede proporcionarnos la información necesaria para crear un software más seguro. El código se puede encontrar en la caja de registros limitados (paquete Rust).
Nuestro equipo comenzó en el lado más seguro y luego intentó descubrir cómo acercar el control deslizante de accesibilidad al extremo de accesibilidad. De estas ambiciones surgió el "Registro de límites", que podemos usar cada vez que encontremos un dispositivo con mapas de memoria en nuestras aventuras en Auxon Corporation.
Este contenido se publicó originalmente en el blog de Auxon Engineering y ha sido editado y republicado con permiso.
vía: /article/20/1/c-vs-rust-abstractions
Autor: Dan Pittman Selección de tema: lujun9972 Traductor: wxy Corrección: wxy