La belleza de MIPS

Todos los ordenadores, móviles y en general, cualquier dispositivo que lleva software necesita un procesador. Los procesadores se agrupan por familias, familias de procesadores que se programan igual, en un lenguaje llamado ensamblador. La más popular es Intel x86, presente en cualquier PC y en algunos móviles, tablets y servidores. Pero no voy a hablaros hoy de x86, ni de ARM, sino de MIPS. El ensamblador hecho bello. Adentrémonos en este mundo. Si nunca has visto ensamblador, este es tu momento. Si ya lo has visto, quizá te apetezca recordar algunas cosas.

MIPS, reinventemos la rueda… pero bien

Los orígenes de la arquitectura MIPS se remontan a 1981 cuando John L. Hennessy y su equipo de la Universidad de Stanford buscan implementar un procesador lo más simple posible. Al contrario que en las arquitecturas de tipo CISC lo que se busca en una de tipo de RISC como MIPS es definir las instrucciones más simples posibles y optimizar estas. ARM es otro ejemplo de arquitectura RISC. Para optimizar aún más, MIPS hace un uso intensivo de la segmentación.

Los procesadores MIPS ganaron popularidad rápidamente. Fueron usados por Silicon Graphics (SGI) para sus workstations con sistema operativo IRIX. Allí fueron usadas en estudios de Hollywood, centros científicos, etc En su máximo esplendor estas máquinas acogieron grandes avances, como la invención de OpenGL y del sistema de archivos XFS.

Esta relación de MIPS con el mundo multimedia le hizo popular entre las consolas. La Nintendo 64, la PlayStation 2 o la PSP llevan procesador MIPS.

Finalmente MIPS ha entrado en cierta decadencia. SGI cerró y las consolas decidieron apostar por otro tipo de procesadores. Sin embargo MIPS sigue vivo. Es usado en routers y algunos móviles. Existe Loongson, una variación de la aquitectura MIPS usada en China. Incluso ha habido placas estilo Raspberry Pi con MIPS, como por ejemplo, la Creator CI20.

Cuatro principios del diseño

Estos principios se aplican en MIPS.

  • La simplicidad favorece la regularidad
    • La regularidad facilita la implementación
    • La simplicidad mejora el rendimiento a menor coste
  • Cuanto más pequeño más rápido
  • Mejorar en lo posible los casos más frecuentes
  • Un buen diseño requiere buenas soluciones de compromiso

Tres tipos de instrucciones

La belleza de MIPS se basa en su diseño, claro, conciso y con pocas excepciones. Empecemos por lo básico, en MIPS todas las instrucciones son de 32 bits y son de una de estas tres categorías:

  • R, operan con tres registros
  • I, operan con dos registros y una constante
  • J, operan con una dirección de memoria

32 registros de 32 bits

En MIPS existen 32 registros. Bastantes si los comparamos con x86 de Intel. ¿Qué es un registro? Es donde se guardan los datos que se van a necesitar en las operaciones. Como si fueran cajas donde podemos guardar cosas, las cajas de la CPU. Salvo un par de ellos especiales, en realidad todos son iguales, no obstante, por convenio ciertos registros se usan para unas cosas y otros para otras.

Tomado de Wikipedia

Operaciones básicas

Las operaciones básicas son la suma, la resta, la suma inmediata, los desplazamientos, las operaciones lógicas and, or y nor, las operaciones de carga y guardado en memoria, el comparador menor que, los branch y los saltos. Y ya esta. Todo lo demás se implementa mediante pseudoinstrucciones, es decir, instrucciones que el ensamblador convierte en varias instrucciones básicas. Muchos se habrán sorprendido que no haya operación de multiplicar. O que no haya move. O que solo se pueda comparar con menor que. Todas esas cosas las podemos hacer pero debemos ser conscientes de que son pseudoinstrucciones y MIPS en realidad no tiene esas operaciones registradas. Veamos un ejemplo de código MIPS. Primero tenemos el código en C equivalente, y después el código MIPS.

a =  b + c + 5
# a en $s0, b en $s1, c en $s2
add $s0, $s1, $s2 # Sumamos $s1 + $s2 y ponemos el resultado en $s0
addi $s0, $s0, 5 # Sumamos a $s0 la constante 5 y dejamos el resultado en $s0

Por norma general en MIPS cuando queramos usar una constante en vez de un registro bastará con poner una i (de inmmediato) al final. Así en vez de usar una instrucción de tipo R usaremos una de tipo I.

Branch y jump

La magia de los computadores reside en que son capaces de tomar decisiones. Pueden ejecutar bucles y evaluar condiciones. En ensamblador este tipo de cosas se hacen con saltos. Sin embargo MIPS incluye dos tipos distintos de saltos.

Los branch evalúan condiciones y saltan en caso de que se cumpla a una posición del código N bytes por arriba o por debajo de la dirección de memoria actualmente almacenada en $ra.

Los jump saltan a una dirección de memoria de forma absoluta. Como MIPS es de 32 bits y las instrucciones también son de 32 bits resulta evidente que no es posible saltar a cualquier dirección de memoria, no al menos en una simple instrucción. Los bits que faltan, los más significativos se dejan a los mismos que había en donde se hizo el jump.

Esto parece muy complicado pero veamos que no lo es tanto en la práctica. Como recomendación, usa jumps para saltar a subrutinas y usa branch para hacer ifs y bucles.

int x = 5;
do{
    x--;
}while(x!=0);
# x en $s0
.text
main:
    addi $s0, $zero, 5 # Cargamos 5 a $s0
    # es equivalente a la pseudoinstrucción li $s0, 5
bucle:
    addi $s0, $s0, -1 # las constantes también pueden ser negativas
    bne $s0, $zero, bucle # branch on not equal. Si $s0 != $zero, se salta a bucle. Si no, se sigue para abajo
int double(int x){
    return x*2;
}
...
int y = 5;
y = double(y);
.text
main:
    addi $s0, $zero, 5
    add $a0, $s0, $zero #podriamos usar la pseudoinstruccion move
    jal double
    move $s0, $v0 #pseudoinstruccion para copiar de $v0 a $s0
    li $v0, 10
    syscall # ejecuta una syscall, con código el que hay en $v0 (10 vamos a suponer que es salir del programa)
double:
    sll $v0, $a0, 1
    jr $ra

¿Qué hace jal? Llama a la subrutina double ejecutando un salto y poniendo $ra a la siguiente dirección (para retomar el ciclo de ejecución normal cuando salgamos de la subrutina con jr $ra).

Cargando datos

Hasta ahora hemos trabajado siempre con datos que ya estaban en los registros. ¿Qué pasa si queremos recorrer un array por ejemplo? Un array es un tipo de dato más complejo, que no entra en un registro. Este ejemplo suma los números del array.

int* x = [1,2,3,4,5];
int suma = 0;
for(int i =0;i<4;i++){
    suma = suma + x[i]
}
.data
x: .word 1,2,3,4,5
.text
main:
	li $s0, 0
	li $t9, 5
	la $t0, x
bucle:
	lw $t1, 0($t0)
	add $s0, $s0, $t1
	addi $t0, $t0, 4
	addi $t9, $t9, -1
	bne $zero, $t9, bucle
	li $v0, 10
	syscall

Usamos la cabecera data para registrar datos en el stack que el sistema operativo repartirá como pueda. Le llamamos x y contiene WORDS, que en MIPS son 4 bytes, es decir, 32 bits.

la es una pseudoinstrucción (usa lui y ori internamente) que sirve para cargar direcciones de 32 bits en un registro. En este caso cargamos la posición de donde empieza el vector x en $t0. $t0 es un puntero ahora.

lw es la instrucción importante aquí, significa load word y permite cargar palabras de la memoria a un registro. Le indicamos donde queremos que se guarde ($t1), y donde está el dato en memoria ($t0). Adicionalmente le indicamos el desplazamiento, que digamos es una constante que podemos aplicar para cargar unos bytes antes o después de lo que indique el registro. Esto es muy útil, pero en este ejemplo no tiene sentido usarse y lo he dejado a 0.

¿Por qué sumamos 4 a $t0 en cada pasada? Hemos dicho que los WORD en MIPS ocupan 32 bits, 4 bytes. Pues en esa operación estamos moviendo el puntero 4 bytes más en la memoria, para pasar al siguiente elemento del vector. Esto es aritmética de punteros. ¡Chachi pistachi!

El emulador Mars

¿Ya has visto por qué MIPS es tan bonito? A diferencia de otros ensambladores, la sintaxis de MIPS es muy regular, con comportamientos predecibles aunque no conozcamos exactamente la instrucción. Si quieres probar tus destrezas en MIPS existen varios simuladores. El que uso ahora mismo se llama Mars, está hecho en Java y es bastante completo. Es posible inspeccionar la memoria al completo, ir paso a paso en cada instrucción, insertar breakpoints y observar el valor de los registros en cada momento.

Descargar Mars

También si nos vemos con fuerzas podremos pasar a hardware real con Linux. OpenWrt, presente en routers, o Debian, en la Creator CI20 pueden ser buenas opciones.

Conclusión

Seguro que si habías programado con anterioridad en otro ensamblador esto te ha parecido muy sencillo. Y es que MIPS no es especialmente complejo.

loading...

Novedades de C++17

Después de tres años de trabajo, C++17 ha sido finalmente estandarizado. Esta nueva versión de C++ incorpora y elimina elementos del lenguaje, con el fin de ponerlo al día y convertirlo en un lenguaje moderno y eficaz. El comité ISO de C++ se ha tomado muy en serio su labor, C++11 supuso este cambio de mentalidad, que se ha mantenido en C++14 y ahora en C++17, la última versión de C++.

Repasemos las noveades que incorpora C++17 respecto a C++14

if-init

Ahora podemos incluir una sentencia de inicialización antes de la condición en sentencias if y switch. Esto es particularmente útil si queremos operar con un objeto y desconocemos su validez.

// ANTES
Device dev = get_device();
if(dev.isOk()){
    dev.hacerCosas();
}

// AHORA
if(Device dev = get_device(); dev.isOk()){
    dev.hacerCosas();
}

Declaraciones de descomposición

Azúcar sintántico que permite mejorar la legibiliad en ciertas situaciones. Por ejemplo, en el caso de las tuplas, su uso se vuelve trivial.

// FUNCIÓN QUE DEVUELVE TUPLA
std::tuple<int, std::string> funcion();

// C++14
auto tup = funcion();
int i = std::get<0>(tup);
std::string s = std::get<1>(tup);

// C++17
auto [i,s] = funcion();

Esto funciona para multitud de estructuras de datos, como estructuras, arrays, std::array, std::map,…

std::map m = ...;
for(auto && [key, value] : m){

}

Deduction Guides

Ahora es menos necesario que nunca indicar los tipos en ciertas expresiones. Por ejemplo, al crear pares y tuplas:

// ANTES
auto p = std::pair<int,std::string>(42,"Adrianistan");

// AHORA
auto p = std::pair(42,"Adrianistan");

Esto por supuesto también sirve para estructuras y otras construcciones:

template<typename T>
struct Thingy
{
  T t;
};

// Observa
Thingy(const char *) -> Thingy<std::string>;

Thingy thing{"A String"}; // thing.t es de tipo std::string

template auto

// ANTES
template <typename T, T v>
struct integral_constant
{
   static constexpr T value = v;
};
integral_constant<int, 2048>::value
integral_constant<char, 'a'>::value

// AHORA
template <auto v>
struct integral_constant
{
   static constexpr auto value = v;
};
integral_constant<2048>::value
integral_constant<'a'>::value

Fold expressions

Imagina que quieres hacer una función suma, que admita un número ilimitado de parámetros. En C++17 no se necesita apenas código.

template <typename... Args>
auto sum(Args&&... args) {
   return (args + ... + 0);
}

Namespaces anidados

Bastante autoexplicativo

// ANTES

namespace A{
    namespace B {
        bool check();
    }
}

// AHORA

namespace A::B {
    bool check();
}

Algunos [[atributos]] nuevos

[[maybe_unused]]

Se usa para suprimir la advertencia del compilador de que no estamos usando una determinada variable.

int x = 5;
[[maybe_unused]] bool azar = true;
x = x + 10

[[fallthrough]]

Permite usar los switch en cascada sin advertencias del compilador.

switch (device.status())
{
case sleep:
   device.wake();
   [[fallthrough]];
case ready:
   device.run();
   break;
case bad:
   handle_error();
   break;
}

Variables inline

Ahora es posible definir variables en múltiples sitios con el mismo nombre y que compartan una misma instancia. Es recomendable definirlas en un fichero de cabecera para luego reutilizarlas en ficheros fuente.

// ANTES
// en una cabecera para que la usasen los demás
extern int x;

// solo en un fichero fuente, para inicializarla
int x = 42;
// AHORA

// en la cabecera
inline int x = 42;

if constexpr

Ahora es posible introducir condicionales en tiempo de compilación (similar a las macros #IFDEF pero mejor hecho). Estas expresiones con constexpr, lo que quiere decir que son código C++ que se evalúa en tiempo de compilación, no de ejecución.

template<class T>
void f (T x)
{
    if  constexpr(std:: is_integral <T>::value)  {
        implA(x);
    }
    else  if  constexpr(std:: floating_point <T>::value)  {
        implB(x);
    }
    else
    {
        implC(x);
    }
}

std::optional

Tomado de la programación funcional, se incorpora el tipo optional, que representa un valor que puede existir o no. Este tipo ya existe en Rust bajo el nombre de Option y en Haskell como Maybe.

std::optional opt = f();
if(opt)
    g(*opt);

// otra opción de uso si queremos proveer de un reemplazo
std::optional opt = f();
std::cout << opt.value_or(0) << std::endl;

std::variant

Descritas como las unions pero bien hechas. Pueden contener variables de los tipos que nosotros indiquemos.

std::variant<int, double, std::vector> precio; // precio puede ser un int, un double o un std::vector

// comprobar si el valor en un variant es de un determinado tipo
if(std::holds_alternative<double>(precio))
    double x = std::get<double>(precio);

std::any

Si con std::variant restringimos los posibles tipos de la variable a los indicados, con std::any admitimos cualquier cosa.

std::any v = ...;
if (v.type() == typeid(int)) {
   int i = any_cast<int>(v);
}

std::filesystem

Se añade a la librería estándar este namespace con el tipo path y métodos para iterar y operar con directorios. Dile adiós a las funciones POSIX o Win32 equivalentes.

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

void main(){
  fs::path dir = "/";
  dir /= "sandbox";
  fs::path p = dir / "foobar.txt";
  std::cout << p.filename() << "\n";
  fs::copy(dir, "/copy", fs::copy_options::recursive);
}

Algoritmos en paralelo

Muchos de los algoritmos de STL ahora pueden ejecutarse en paralelo bajo demanda. Con std::execution::par indicamos que queremos que el algoritmo se ejecute en paralelo.

std::sort(std::execution::par, first, last);

¿Qué novedades se esperan en C++20?

Ya hemos visto lo que trae C++17. Ahora veremos que se espera que traiga C++20 en 2020.

  • Módulos. Reemplazar el sistema de includes
  • Corrutinas. Mejorar la programación asíncrona
  • Contratos. Mejorar la calidad del código
  • Conceptos. Mejorar la programación genérica
  • Redes. Estandarizar la parte de red en C++ tal y como se ha hecho con std::filesystem
  • Rangos. Nuevos contenedores

Referencias:

 

Un nuevo lenguaje de programación para juegos

En la inocentada sobre Rust puse un vídeo de Jonathan Blow titulado Ideas about a new programming language for games. En el vídeo, Blow analiza los problemas que presenta C++ para el desarrollo de juegos y por qué según él ni Go ni D ni Rust consiguen mejorar la situación. El lenguaje de programación perfecto para juegos debería tener las siguientes características:

  • Poca fricción
  • Placer por programar
  • Rendimiento
  • Simplicidad
  • Diseñado para buenos programadores

Con poca fricción se refiere a que la tarea de programar no debe añadir mucha complejidad para solucionar problemas que tendríamos si programásemos de la forma más simple posible. Fricción es para él RAII en C++. Fricción es la gestión de errores en Rust. Fricción se entiende como código que no añade significativamente nada pero que es necesario para un correcto funcionamiento. Fricción es rellenar papeleo de Hacienda. Muchos defensores de estas posturas argumentan que en realidad esa productividad perdida se recupera con el tiempo al reducir el número de bugs que pueden ocurrir. Blow dice que según su experiencia en juegos AAA realmente no compensa. Tardas más tiempo solventado bugs potenciales que bugs reales. Su solución no es evitar al 100% este tipo de bugs (como hace Rust) sino habilitar unas herramientas potentes que ayuden a solucionar estos bugs si alguna vez suceden.

Esto se relaciona con el placer por programar. Un lenguaje que te invite a programar, a experimentar, donde te sientas a gusto. Muchos lenguajes han perdido esa esencia. Con el tiempo muchos lenguajes se han ido complicando de forma innecesaria y se han ido volviendo pesadillas. Ruby sería el caso contrario, un lenguaje que conserva ese placer. Pero Ruby no entra en juego por razones obvias de rendimiento.

Con rendimiento básicamente dice que cualquier lenguaje que disponga de GC (recolector de basura) no es válido. Incluso Go, que tiene un GC mucho mejor que Java o la plataforma .NET no lo considera correcto.

Con simplicidad se busca legibilidad y potencia. El lenguaje debe ser uniforme, con cohesión en todos sus elementos.

Y con diseñado para buenos programadores se refiere a que el lenguaje no debe suponer que el programador es idiota e intentar corregir sus errores. Debe permitir hacer virguerías si así lo desea el programador. Rust está bien pues permite tener código unsafe. Justo lo que se necesita para hacer virguerías. Pero hace falta más trabajo en este aspecto pues supone un cambio de mentalidad.

La idea detrás de RAII es incorrecta

Mucha gente opina que RAII es una de las mejores cosas que han pasado en la programación. Muchos lenguajes presuponen RAII. D por ejemplo considera que RAII es la manera correcta de programar. Resource Acquisition Is Initialization consiste en que cada vez que vamos a acceder a un recurso tenemos que codificarlo en una clase, inicializar el recurso en un constructor y liberar el recurso en un destructor. Añades operadores para permitir copia, … Este sistema presenta una elevada fricción. Y además no funciona bien, en el sentido de que todo se acaba sobrecomplicando. Alejándose de esa simplicidad que estamos buscando.

Uno de los principales problemas de este patrón de diseño es que no existe un recurso. Es una generalización errónea de varios conceptos. Un recurso puede ser memoria, otro recurso puede ser un archivo, una entrada de teclado, etc El problema es que estos recursos son demasiado diferentes como para ser tratados con un mismo patrón de forma óptima. Mientras RAII puede ser interesante hablando de archivos, es una muy mala opción si hablamos de memoria. Porque la memoria es el recurso más importante para un programador. Se podría simplificar diciendo que un programador lo único que hace es modificar la memoria constantemente.

Pero muchos de los usos de RAII tienen que ver con las excepciones. Y a Blow tampoco le gustan las excepciones. La gestión de errores en C es pésima pero las excepciones son muy complejas. Una de las cosas más complejas que implementan los lenguajes de programación que disponen de ellas. Y la implementación de C++ más todavía. Blow se lamenta de que haya gente que siga pensando que es una buena idea. Reduce la claridad del código, complica el flujo del programa. RAII en C++ ayuda a que en caso de que se de una excepción los recursos puedan ser liberados.

No solo lo dice él, sino que enlaza el siguiente vídeo: Systematic Error Handling in C++ por Andrei Alexandrescu.


Un mejor sistema que las excepciones

Go implementa múltiples valores de retorno (al contrario que la mayoría de lenguajes derivados de C donde solo de devuelve una cosa). Go lo soporta de forma nativa. Pero Matt Newport le responde como podría hacer eso en C++11 con std::tie.

#include <iostream>;
#include <tuple>;
#include <functional>;
 
std::tuple<int, int> f()
{
    int x = 5;
    return std::make_tuple(x, 7); // return {x,7}; en C++17
}
 
int main()
{
    int a, b;
    std::tie(a, b) = f();
    std::cout << a << " " << b << "\n";
}

Rust, como Go, soporta esto de forma nativa:

fn f() -> (i32,i32) {
	(4,7)
}

fn main() -> () {
	let (a,b) = f();
	println!("A es {}, B es {}",a,b);
}

Aunque no es la manera en la que Rust maneja los errores. En su lugar posee Option y Result que en C++17 también van a ser implementados en std::optional y que es en realidad un concepto presente en Haskell.

Sintaxis exasperante

En la charla Blow sigue hablando y comenta que desde un punto de visto purista y correcto la sintaxis de punteros de C++11 es incorrecta. Que std::unique_ptr<Vector3[]> implica que quieres un Unique Ptr basado en Vector3 pero en realidad la idea correcta sería quiero un Vector3 con características de Unique Ptr. Lo mismo es aplicable para std::shared_ptr. Este tipo de punteros no deberían estar expresados de esta forma sino que deberían entrar en la sintaxis del lenguaje, por su utilidad práctica.

En Rust, el equivalente a std::unique_ptr sería Box que es el puntero más usado. El equivalente a std::shared_ptr sería Rc, no tan usado pero disponible.

Blow sigue hablando en este vídeo y en el siguiente de más errores que tiene C++, aunque de menor importancia. En todo caso, Blow sigue en el desarrollo de su compilador de Jai. C++ ha avanzado un montón y me ha llegado a sorprender que existiesen cosas como constexpr y los módulos de C++, una solución a los archivos de cabecera que son tan tediosos de escribir.

Si tenéis tiempo y sabéis inglés miráos el vídeo original. Creo que esta mucho mejor explicado que esto. Y también la respuesta de Matt Newport en la que dice que C++ SÍ es válido para todo lo que dice Blow.

Programando para Haiku – File panel – Parte II

Continuamos los tutoriales de programación en Haiku. Hoy veremos como usar el File Panel. ¿Qué es el File Panel? El File Panel es el diálogo que nos aparece cuando queremos abrir o guardar un archivo o carpeta. En todos los sistemas operativos gráficos es similar.

File Panel de Windows

BFilePanel

La clase básica es BFilePanel. Esta clase se encarga de mostrar esa ventanita que nos deja elegir el archivo o carpeta para abrir o para guardar. Lo primero que tenemos que saber si queremos hacer un File Panel es si va a ser para abrir un archivo existente o para guardar un nuevo archivo. Así, distinguimos entre B_OPEN_PANEL y B_SAVE_PANEL. Si estamos dentro de un B_OPEN_PANEL además indicaremos si aceptamos archivos, carpetas, enlaces simbólicos o alguna combinación de estas cosas. Por último, ¿cómo recibimos la información del panel? Pues usando BMessage, como es habitual en Haiku/BeOS. Pero hay que indicar quién va procesar el mensaje, el conocido como BMessenger. Veamos código:

const uint32 OPEN_FILE = 42;
BFilePanel* filepanel = new BFilePanel(B_OPEN_PANEL,new BMessenger(this),NULL,B_FILE_NODE,new BMessage(OPEN_FILE));
filepanel->Show();

En este código creamos un file panel para abrir un archivo. El BMessenger encargado de procesarlo será el del mismo contexto en el que se ejecute este código. Hay que tener en cuenta que tanto BApplication como BWindow heredan de BMessenger y por tanto cualquier objeto de estas clases es apto. El siguiente parámetro es la carpeta por defecto, que con NULL la dejamos a elección de Haiku. Luego indicamos que queremos abrir archivos, no carpetas ni enlaces simbólicos. Por último especificamos el ID del BMessage que enviará el panel. Esto nos servirá para después saber que ID tenemos que leer dentro de la función MessageReceived del BMessenger. Por último mostramos el panel para que el usuario decida el archivo a abrir. Si la acción es cancelada también será disparado el mensaje, tendremos que comprobar si el usuario eligió el archivo o cerró el diálogo.

Haiku File Panel

Leer la respuesta

Dentro de la función MessageReceived del BMessenger tenemos que accionar un caso especial si el ID del BMessage es el que hemos especificado en el panel.

void MiVentana::MessageReceived(BMessage* msg)
{
	switch(msg->what){
    	case READ_FILE: {
			if (msg->HasRef("refs")) {
	  			entry_ref ref;
	  			if (msg->FindRef("refs", 0, &ref) == B_OK) {
					BEntry entry(&ref, true);
					BPath path;
					entry.GetPath(&path);
					std::cout << "El archivo es " << path.Path() << std::endl; 
			 	}
			}
			break;
  		}
    }

}

Tenemos que comprobar si el mensaje tiene la propiedad “refs”. La propiedad “refs” la ajusta el File Panel cuando se ha seleccionado un archivo. Si la propiedad existe entonces lo leemos. Leeremos una entryref. Un entryref es una entrada dentro del sistema de archivos. Sin embargo esta estructura es de bajo nivel y no sabe exactamente donde se ubica. BEntry representa localizaciones dentro del sistema de archivos. Se construye con un entry_ref y esta clase ya sabe donde se ubica de forma legible por un humano (o un programador perezoso). Si queremos saber la ruta del archivo antes tendremos que crear un objeto vacío BPath que llenaremos con contenido. Finalmente la ruta, como string, la podremos leer llamando a la función Path dentro del objeto BPath.

Ya hemos visto como se usan los file panel en Haiku. Los file panel de guardar archivo se programan exactamente igual cambiando esa pequeña flag al principio.

¿Cómo programar en C (en 2016)?

Este artículo es una traducción del artículo How to C in 2016. Todo el contenido aparece originalmente en aquel artículo, yo solo me he limitado a traducirlo.

c

La primera regla de C es no escribir en C si puedes evitarlo.

Si te ves obligado a escribir en C, deberías seguir las reglas modernas.

C ha estado con nosotros desde principios de los 70. La gente a “aprendido C” en numerosos puntos de su evolución, pero el conocimiento normalmente se para después de aprender. Así pues todo el mundo piensa diferente sobre C según el año en que empezaron a aprenderlo.

Es importante no quedarse paralizado en las “cosas que aprendí en los 80/90” cuando programas en C.

Esta página asume que estás en una plataforma moderna, con estándares modernos y no tienes que mantener una compatibilidad con sistemas antiguos muy elevada. No debemos estar atados a estándares anticuados solo porque algunas compañías rechacen actualizar sistemas con más de 20 años de antigüedad.

Preliminar

Standard C99 (C99 significa “Estándar C de 1999”; C11 significa “Estándar C de 2011”, así que C11 > C99)

  • clang, por defecto
    • C99 es la implementación de C por defecto en clang, no necesita opciones extra
    • Sin embargo esta implementación no es realmente estándar. Si quieres forzar el estándar, usa -std=c99
    • Si quieres usar C11, debes especificar -std=c11
    • clang compila el código fuente más rápidamente que gcc
  • gcc necesita que especifiques -std=c99 o -std=c11
    • gcc compila más lentamente pero a veces genera ejecutables más rápidos
    • gcc-5 establece por defecto -std=gnu11, así que debes seguir especificando una versión estándar c99 o c11.

Optimizaciones

  • -O2, -O3
    • generalmente querrás -O2, pero algunas veces querrás -O3. Prueba tu código con ambos niveles (y entre distintos compiladores) y mantente con los ejecutables más eficientes y rápidos.
  • -Os
    • -Os ayuda si te preocupa la eficiencia de la caché (que debería)

Advertencias

  • -Wall -Wextra -pedantic
    • las nuevas versiones de los compiladores tienen -Wpedantic, pero todavía aceptan el antiguo -pedantic por cuestiones de compatibilidad.
    • durante las pruebas deberías añadir -Werror y -Wshadow en todas tus plataformas
    • puede ser peliagudo enviar a producción con -Werror porque cada plataforma y cada compilador y cada librería pueden emitir distintas advertencias. Probablemente no querrás terminar la compilación entera de un usuario porque su versión de GCC en una plataforma que nunca habías visto se queja de manera nueva y sorprendente.
    • algunas opciones más sofisticadas son -Wstrict-overflow -fno-strict-aliasing
    • especifica -fno-strict-aliasing o estate seguro de que solo accedes a los objetos con el tipo que tuvieron en su definición. Como mucho código en C ya existente se salta lo último es mucho más seguro usar -fno-strict-aliasing particularmente si no controlas todo el código que debes compilar.
    • ahora mismo, clang reporta alguna sintaxis válida como advertencia, así que debes añadir -Wno-missing-field-initializers
    • GCC resolvió este problema después de GCC 4.7

Compilando

  • Unidades de compilación
    • La manera más común de compilar proyectos en C es generar un fichero objeto de cada fichero fuente y unirlo todos al final. Este procedimiento es muy bueno para el desarrollo incremental, pero no lo es para el rendimiento y la optimización. El compilador no puede detectar optimizaciones entre archivos con este método.
  • LTO – Link Time Optimization
    • LTO arregla el problema de las unidades de compilación generando además una representación intermedia que puede ser sujeta de optimizaciones entre archivos. Este sistema ralentiza el tiempo de enlazado significativamente pero make -j puede ayudar.
    • clang LTO (guía)
    • gcc LTO
    • Ahora mismo, 2016, clang y gcc soportan LTO simplemente añadiendo -flto en las opciones tanto de compilación como de enlazado.
    • LTO todavía necesita asentarse. A veces, si tu programa tiene código que no usas directamente pero alguna librería sí, LTO puede borrarlo, porque detecta que en tu código no se hace nunca una llamada a esa función.

Arquitectura

  • -march=native
    • Le da al compilador permiso para usar todas las características de tu CPU
    • otra vez, compara el funcionamiento con los distintos tipos de optimización y que no tengan efectos secundarios.
  • msse2 y -msse4.2 pueden ser útiles si necesitas características que no están disponibles en el sistema desde el que compilas.

Escribiendo código

Tipos

Si te encuentras escribiendo char o int o short o long o unsigned, lo estás haciendo mal.

Para los programas modernos deberías incluir #include <stdint.h> y usar los tipos estándar.

Los tipos estándar comunes son:

  • int8_t, int16_t, int32_t, int64_t – enteros con signo
  • uint8_t, uint16_t, uint32_t, uint64_t – enteros sin signo
  • float – coma flotante de 32 bits
  • double – coma flotante de 64 bits

Te darás cuenta que ya no tenemos char. char está malinterpretado en C.

Los desarrolladores han abusado de char para representar un byte incluso cuando hacen operaciones sin signo. Es mucho más limpio usar uint8_t para representar un único byte sin signo y uint8_t * para representar una secuencia de bytes sin signo.

Una excepción a nunca-char

El único uso aceptable de char en 2016 es si una API ya existente necesita char (por ejemplo, strncat, printf,…) o si estás inicializando una cadena de texto de solo lectura (const char *hello = "hello";) porque el tipo de C para cadenas de texto sigue siendo char *

Además, en C11 tenemos soporte Unicode nativo y el tipo para cadenas UTF-8 sigue siendo char * incluso para secuencias multibyte como const char *abcgrr = u8"abc?";.

El signo

A estas alturas de la película no deberías escribir unsigned nunca en tu código. Podemos escribir sin usar la fea convención de C para tipos multi-palabra que restan legibilidad. ¿Quién quiere escribir unsigned long long int cuando puede escribir uint64_t? Los tipos de <stdint.h> son más explícitos, más exactos en su significado y son más compactos en su escritura y su legibilidad.

Pero podrías decir, “¡Necesito hacer cast a punteros a long para realizar aritmética de punteros sucia!”

Podrías decirlo. Pero estás equivocado.

El tipo correcto para aritmética de punteros es uintptr_t definido en <stddef.h>.

En vez de:

long diff = (long)ptrOld - (long)ptrNew;

Usa:

ptrdiff<em>t diff = (uintptr</em>t)ptrOld - (uintptr_t)ptrNew;

Además:

printf("%p is unaligned by %" PRIuPTR " bytes\",(void *)p, ((uintptr_t)somePtr &amp; (sizeof(void *) - 1)));

Tipos dependientes del sistema

Sigues argumentando, “¡en una plataforma de 32 bits quiero longs de 32 bits y en una de 64 bits quiero longs de 64 bits!”

Si nos saltamos la idea de que estás introduciendo deliberadamente código que dificulta la comprensión del código al tener tamaños distintos dependiendo de la plataforma, aún no tendrías necesidad de usar long.

En estas situaciones debes usar intptr_t que se define según la plataforma en que te encuentres.

En plataformas de 32 bits, intptr_t es int32_t.

En plataformas de 64 bits, intptr_t es int64_t.

intprt_t también tiene una versión sin signo uintptr_t.

Para almacenar diferencias entre punteros, tenemos el ptrdiff_t.

Máxima capacidad

¿Quieres tener el entero con mayor capacidad de tu sistema?

La gente tiene a usar el más grande que conozca, en este caso uint64_t nos podrá almacenar el número más grande. Pero hay una manera más correcta de garantizar que podrá contener cualquier otro valor que se esté utilizando en el programa.

El contenedor más seguro para cualquier entero es intmax_t (también uintmax_t). Puedes asignar cualquier entero con signo a intmax_t sin pérdida de precisión. Puedes asignar cualquier entero sin signo a uintmax_t sin pérdida de precisión.

Ese otro tipo

Otro tipo que depende del sistema y es usado comúnmente es size_t.

size_t se define como “un entero capaz de contener el mayor tamaño de memoria disponible”.

En el lado práctico, size_t es el tipo que devuelve el operador sizeof.

En cualquier caso, la definición de size_t es prácticamente la misma que la de uintptr_t en todas las plataformas modernas.

También existe ssize_t que es size_t con signo y que devuelve -1 en caso de error. (Nota: ssize_t es POSIX así que no se aplica esto en Windows).

Así que, ¿debería usar sisze_t para aceptar tamaños dependientes del sistema en mis funciones? Sí, cualquier función que acepte un número de bytes puede usar size_t.

También lo puedes usar en malloc, y ssize_t es usado en read() y write() (solo en sistemas POSIX).

Mostrando tipos

Nunca debes hacer cast para mostrar el valor de los tipos. Usa siempre los especificadores adecuados.

Estos incluyen, pero no están limitados a:

  • size_t%zu
  • ssize_t%zd
  • ptrdiff_t%td
  • valor del puntero – %p (muestra el valor en hexadecimanl, haz cast a (void *) primero)
  • los tipos de 64 bits deben usar las macros PRIu64 (sin signo) y PRId64 (con signo)
    • es imposible especificar un valor correcto multiplataforma sin la ayuda de estas macros
  • intptr_t"%" PRIdPTR
  • uintptr_t"%" PRIuPTR
  • intmax_t"%" PRIdMAX
  • uintmax_t"%" PRIuMAX

Recordar que PRI* son macros, y las macros se tienen que expandir, no pueden estar dentro de una cadena de texto. No puedes hacer:

printf("Local number: %PRIdPTR\n\n", someIntPtr);

deberías usar:

printf("Local number: %" PRIdPTR "\n\n", someIntPtr);

Tienes que poner el símbolo ‘%’ dentro de la cadena de texto, pero el especificador fuera.

C99 permite declaraciones de variables en cualquier sitio

Así que no hagas esto:

void test(uint8_t input) {
    uint32_t b;

    if (input > 3) {
        return;
    }

    b = input;
}
[/cpp+

haz esto


void test(uint8_t input) {
    if (input > 3) {
        return;
    }

    uint32_t b = input;
}

Aunque si tienes un bucle muy exigente (un tight loop) las declaraciones a mitad de camino pueden ralentizar el bucle.

C99 permite a los bucles for declarar los contadores en la misma línea

Así que no hagas esto

    uint32_t i;

    for (i = 0; i < 10; i++)

Haz esto:

for (uint32_t i = 0; i < 10; i++)

La mayoría de compiladores soportan #pragma once

Así que no hagas esto:


#ifndef PROJECT_HEADERNAME
#define PROJECT_HEADERNAME
.
.
.
#endif /* PROJECT_HEADERNAME */

haz esto:

#pragma once

#pragma once le dice al compilador que solo incluya el archivo de cabecera una vez y no necesitas escribir esas tres líneas para evitarlo manualmente. Este pragma esta soportado por todos los compiladores modernos en todas las plataformas y está recomendado por encima de nombrar manualmente las cláusulas.

Para más detalles, observa la lista de compiladores que lo soportan en pragma once

C permite la inicialización estática de arrays ya asignados memoria

Así que no hagas:

    uint32_t numbers[64];
    memset(numbers, 0, sizeof(numbers));

Haz esto:

uint32_t numbers[64] = {0};

C permite la inicialización estática de structs ya asignados en memoria

Así que no hagas esto:

    struct thing {
        uint64_t index;
        uint32_t counter;
    };

    struct thing localThing;

    void initThing(void) {
        memset(&localThing, 0, sizeof(localThing));
    }

Haz esto:

    struct thing {
        uint64_t index;
        uint32_t counter;
    };

    struct thing localThing = {0};

NOTA IMPORTANTE: Si tu estructura tiene padding (relleno extra para coincidir con el alineamiento del procesador, todas por defecto en GCC, __attribute__((__packed__)) para desactivar este comportamiento), el método de {0} no llenará de ceros los bits de padding. Si necesitases rellenar todo de ceros, incluido los bits de padding, deberás seguir usando memset(&localThing, 0, sizeof(localThing)).

Si necesitas reinicializar un struct puedes declarar un struct global a cero para asignar posteriormente.

    struct thing {
        uint64_t index;
        uint32_t counter;
    };

    static const struct thing localThingNull = {0};
    .
    .
    .
    struct thing localThing = {.counter = 3};
    .
    .
    .
    localThing = localThingNull;

C99 permite arrays de longitud variable (VLA)

Así que no hagas esto:

    uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
    void *array[];

    array = malloc(sizeof(*array) * arrayLength);

    /* remember to free(array) when you're done using it */

Haz esto:

    uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
    void *array[arrayLength];

    /* no need to free array */

NOTA IMPORTANTE: Los VLA suelen situarse en el stack, junto a los arrays normales. Así que si no haces arrays de 3 millones de elementos normalmente, tampoco los hagas con esta sintaxis. Estos no son listas escalables tipo Python o Ruby. Si especificas una longitud muy grande en tiempo de ejecución tu programa podría empezar a hacer cosas raras. Los VLA están bien para situaciones pequeñas, de un solo uso y no se puede confiar en que escalen correctamente.

Hay gente que considera la sintaxis de VLA un antipatrón puesto que puede cerrar tu programa fácilmente.

NOTA: Debes estar seguro que arrayLength tiene un tamaño adecuado (menos de un par de KB se te darán para VLA). No puede asignar arrays enormes pero en casos concretos, es mucho más sencillo usar las capacidades de C99 VLA en vez de pringarse con malloc/free.

NOTA DOBLE: como puedes ver no hay ninguna verificación de entrada al usar VLA, así que cuida mucho el uso de las VLA.

C99 permite indicar parámetros de punteros que no se solapan

Mira la palabra reservada restrict (a veces, __restrict)

Tipos de los parámetros

Si una función acepta arbitrariamente datos y longitud, no restrinjas el tipo del parámetro.

Así que no hagas:

void processAddBytesOverflow(uint8_t *bytes, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        bytes[0] += bytes[i];
    }
}

Haz esto:

void processAddBytesOverflow(void *input, uint32_t len) {
    uint8_t *bytes = input;

    for (uint32_t i = 0; i < len; i++) {
        bytes[0] += bytes[i];
    }
}

Los tipos de entrada definen la interfaz de tu código, no lo que tu código hace con esos parámetros. La interfaz del código dice “aceptar un array de bytes y una longitud”, así que no quieres restringirles usar solo uint8_t. Quizá tus usuarios quieran pasar char * o algo más inesperado.

Al declarar el tipo de entrada como void * y haciendo cast dentro de tu función, los usuarios ya no tienen que pensar en abstracciones dentro de tu librería.

Algunos lectores afirman que podría haber problemas de alineamiento con este ejemplo, pero como estamos accediendo a los bytes uno por uno no hay problema en realidad. Si por el contrario tuviéramos tipos más grandes tendríamos que vigilar posibles problemas de alineamiento, mirar Unaligned Memory Access

Parámetros de devolución

C99 nos da el poder de usar <stdbool.h> que define true como 1 y false como 0.

Para valores de éxito/error, las funciones deben devolver true o false, nunca un entero especificando manualmente 1 y 0 (o peor, 1 y -1 (¿o era 0 éxito y 1 error? ¿o era 0 éxito y -1 error?))

Si una función modifica el valor de un parámetro de entrada, no lo devuelvas, usa dobles punteros.

Así que no hagas:

void *growthOptional(void *grow, size_t currentLen, size_t newLen) {
    if (newLen > currentLen) {
        void *newGrow = realloc(grow, newLen);
        if (newGrow) {
            /* resize success */
            grow = newGrow;
        } else {
            /* resize failed, free existing and signal failure through NULL */
            free(grow);
            grow = NULL;
        }
    }

    return grow;
}

Haz esto:

/* Return value:
 *  - 'true' if newLen > currentLen and attempted to grow
 *    - 'true' does not signify success here, the success is still in '*_grow'
 *  - 'false' if newLen <= currentLen */
bool growthOptional(void **_grow, size_t currentLen, size_t newLen) {
    void *grow = *_grow;
    if (newLen > currentLen) {
        void *newGrow = realloc(grow, newLen);
        if (newGrow) {
            /* resize success */
            *_grow = newGrow;
            return true;
        }

        /* resize failure */
        free(grow);
        *_grow = NULL;

        /* for this function,
         * 'true' doesn't mean success, it means 'attempted grow' */
        return true;
    }

    return false;
}

O incluso mejor:

typedef enum growthResult {
    GROWTH_RESULT_SUCCESS = 1,
    GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY,
    GROWTH_RESULT_FAILURE_ALLOCATION_FAILED
} growthResult;

growthResult growthOptional(void **_grow, size_t currentLen, size_t newLen) {
    void *grow = *_grow;
    if (newLen > currentLen) {
        void *newGrow = realloc(grow, newLen);
        if (newGrow) {
            /* resize success */
            *_grow = newGrow;
            return GROWTH_RESULT_SUCCESS;
        }

        /* resize failure, don't remove data because we can signal error */
        return GROWTH_RESULT_FAILURE_ALLOCATION_FAILED;
    }

    return GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY;
}

Formato

El estilo del código es muy importante.

Si tu proyecto tiene una guía de formato de 50 páginas, nadie te ayudará, pero si tu código tampoco se puede leer, nadie querrá ayudarte.

La solución es usar siempre un programa para formatear el código.

El único formateador de código usable en el 2016 es clang-format. clang-format tiene los mejores ajustes por defecto y sigue en desarrollo activo.

Aquí está el script que uso para formatear mi código:

#!/usr/bin/env bash

clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, AllowShortFunctionsOnASingleLine: None, KeepEmptyLinesAtTheStartOfBlocks: false}" "$@"

Luego lo llamo

./script.sh -i *.{c,h,cc,cpp,hpp,cxx} 

La opción -i indica que sobrescriba los archivos con los cambios que realice, en vez de generar nuevos archivos o crear copias.

Si tienes muchos archivos, puedes hacer la operación en paralelo

#!/usr/bin/env bash

# note: clang-tidy only accepts one file at a time, but we can run it
#       parallel against disjoint collections at once.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc \) |xargs -n1 -P4 cleanup-tidy

# clang-format accepts multiple files during one run, but let's limit it to 12
# here so we (hopefully) avoid excessive memory usage.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc -or -name \*.h \) |xargs -n12 -P4 cleanup-format -i

Y ahora el contenido del script cleanup-tidy aquí.

#!/usr/bin/env bash

clang-tidy \
    -fix \
    -fix-errors \
    -header-filter=.* \
    --checks=readability-braces-around-statements,misc-macro-parentheses \
    $1 \
    -- -I.

clang-tidy es una herramienta de refactorización basada en reglas. Las opciones de arriba activan dos arreglos:

  • readability-braces-around-statements – fuerza a que todos los if/while/for tengan el cuerpo rodeado por llaves.
    • ha sido un error que C permitiese las llaves opcionales. Son causa de muchos errores, sobre todo al mantener el código con el tiempo, así que aunque el compilador te lo acepte, no dejes que ocurra.
  • misc-macro-parentheses – añade automáticamente paréntesis alrededor de los parámetros usados en una macro.

clang-tidy es genial cuando funciona, pero para código complejo puede trabarse. Además, clang-tidy no formatea, así que necesitarás llamar a clang-format para formatear y alinear las nuevas llaves y demás cosas.

Legibilidad

Comentarios

Comentarios con sentido, dentro del código, no muy extensos.

Estructura de archivos

Intenta no tener archivos de más de 1000 líneas (1500 como mucho)

Otros detalles

Nunca uses malloc

Usa siempre calloc. No hay penalización de rendimiento por tener la memoria limpia, llena de ceros.

Los lectores han informado de un par de cosas:

  • calloc sí tiene un impacto en el rendimiento en asignaciones enormes
  • calloc sí tiene un impacto en el rendimiento en plataformas extrañas (sistemas empotrados, videoconsolas, hardware de 30 años de antigüedad, …)
  • una buena razón para no usar malloc() es que no puede comprobar si hay un desbordamiento y es un potencial fallo de seguridad

Todos son buenos puntos, razón por la que siempre debes probar el funcionamiento en todos los sistemas que puedas.

Una ventaja de usar calloc() directamente es que, al contrario que malloc(), calloc() puede comprobar un desbordamiento porque suma todo el tamaño necesario antes de que lo pida.

Algunas referencias al uso de calloc() se pueden encontrar aquí:

Sigo recomendando usar siempre calloc() para la mayoría de escenarios en 2016.

Nunca uses memset (si puedes evitarlo)

Nunca hagas memset(ptr, 0, len) cuando puedes inicializar una estructura (o un array) con {0}.

Generics en C11

C11 ha añadido los Generics. Funcionan como un switch, que distingue entre los tipos y dependiendo del valor que se le de devuelve una u otra cosa. Por ejemplo:

#define probarGenerics(X) _Generic((X), char: 1, int32_t: 2, float: 3, default: 0)

probarGenerics('a') // devolverá 1
probarGenerics(2) // devolverá 2