¿Por que Zig cuando ya existen C++, D y Rust?
Sin control de flujo oculto
Si un fragmento de código Zig no parece estar saltando a hacer una llamada a una función, es por que no lo está haciendo. Esto significa que podemos estar seguros de que el siguiente código llama solo a foo()
y después a bar()
y esto está garantizado sin tener que conocer tipo de dato alguno:
var a = b + c.d;
foo();
bar();
Ejemplos de control de flujo oculto:
- D tiene funciones
@property
, las cuales son métodos que puedes llamar con algo que se percibe como acceso a propiedades (con notación de punto), de tal forma que el ejemplo de código anterior en el lenguaje D,c.d
, podría estar llamando a una función. - C++, D y Rust tienen sobrecarga de operadores, de tal forma que el operador
+
podría llamar a una función. - C++, D y Go tienen manejo de excepciones con throw/catch, de tal manera que
foo()
podría arrojar una excepción y prevenir quebar()
sea llamada. (Por supuesto, incluso en Zig foo() puede tener un abrazo mortal-deadlock- y prevenir que bar() sea invocada, pero eso puede pasar en cualquier lenguaje Turing completo.)
El propósito de esta decisión de diseño es mejorar la legibilidad del código.
Sin asignaciones de memoria ocultas
Zig tiene un enfoque de no intervención cuando se trata de asignaciones de memoria. No existe palabra reservada new
u otra característica que haga uso de asignaciones de memoria (como operadores de concatenación de cadenas[1]). Todo el concepto de 'heap' está manejado por bibliotecas o el código de cada aplicación y no por el lenguaje.
Ejemplos de asignaciones ocultas:
- En Go, la palabra reservada
defer
asigna memoria en un stack (pila) local de funciones. Además de ser algo poco intuitiva esta forma de comportarse del flujo de ejecución, puede ocasionar fallas de falta de memoria si se usa undefer
dentro de bucles. - C++ asigna memoria en el 'heap' para poder llamar una corrutina.
- En Go, una llamada a función puede provocar asignación de memoria en el 'heap' ya que las gorutines asignan pequeñas pilas que cambian de tamaño cuando la cantidad de llamadas aumenta considerablemente.
- Las APIs de la biblioteca estándar de Rust, arrojan panic en condiciones de falta de memoria y las APIs alternativas que admiten parámetros de 'allocator' son una idea secundaria (ver rust-lang/rust#29802).
Casi todos los lenguajes con 'garbage collection' incurren en asignaciones de memoria ocultas, dado que el recolector de basura esconde la evidencia al momento de limpiar.
El problema principal con con las asignaciones de memoria ocultas es que previene la reusabilidad de porciones de código, limitando el número de ambientes en los que dicho código podría funcionar. Dicho de una forma simple, existen casos de uso en los cuales uno debería poder confiar en el control de flujo y los llamados de función no deberían tener efectos colaterales de asignación de memoria, por lo tanto un lenguaje de programación solamente puede servir a estos casos de uso si realmente aporta tal garantía.
En Zig, existen características de la biblioteca estándar que proveen y admiten 'heap allocators' (asignadores de memoria en el heap), pero estas son características opcionales de la biblioteca estándar y no construcciones internas del lenguaje mismo. Si nunca inicializas un heap allocator, puedes tener la certeza de que tu programa no hará asignaciones de memoria en el heap.
Cada funcionalidad de la biblioteca estándar que requiere asignación de memoria en heap acepta un parámetro Allocator
para poder operar. Esto quiere decir que la biblioteca estándar de Zig soporta arquitecturas no especificas. Por ejemplo, std.ArrayList
y std.AutoHashMap
puedes usarlas para programación de bajo nivel en procesadores/microcontroladores(bare metal programming).
Los allocators (asignadores de memoria) personalizados hacen que el manejo manual de memoria sea muy sencillo. Zig ofrece un allocator que mantiene seguridad de la memoria a primera vista frente a bugs de tipo doble liberación de objetos(double-free) y uso después de liberación de objetos(use-after-free). Detecta automáticamente fugas de memoria e imprime un stack trace (traza de pila). Otro allocator disponible es un "arena allocator" que permite asignar cualquier cantidad de espacios de memoria en una sola y da la facilidad de liberarlos todos al mismo tiempo en lugar de tener que manejar las liberaciones de cada asignación individualmente. Los allocators de propósito especial pueden usarse para mejorar el desempeño de manejo de memoria en casos particulares.
[1]: De hecho existe un operador de concatenación de strings (en general es un operador de concatenación de arreglos-arrays-), pero solo funciona en tiempo de compilación y por ello, no incurre en asignaciones de memoria en heap en tiempo de ejecución.
Soporte de primera clase para biblioteca no estándar
Como se sugiere arriba, Zig incluye una gran biblioteca estándar opcional. Cada API de la librería librería se compila solamente si la usas en tu programa. Zig soporta además efectuar linking opcional hacia libc. Zig es amigable para desarrollo de proyectos de muy bajo nivel(bare-metal) y proyectos orientados a alto rendimiento.
Es lo mejor de ambos mundos; por ejemplo, en Zig, los programas de WebAssembly pueden usar las características de la biblioteca estándar y aún así generar binarios muy pequeños en comparación con los generados por otros lenguajes que soportan compilar a WebAssembly.
Un lenguaje portable para bibliotecas
Uno de los santos griales de la programación es la reusabilidad del código. Desafortunadamente, en la práctica, nos encontramos con frecuencia reinventando la rueda. Algunas veces es justificable.
- Si una aplicación incluye requerimientos de cómputo en tiempo real, debemos descartar cualquier biblioteca que haga uso de garbage collection (limpieza de memoria manejada) o cualquier otro comportamiento no-determinístico.
- Si un lenguaje hace muy fácil ignorar errores y por ello vuelve difícil verificar que una biblioteca maneje errores correctamente, y los hace emerger hacia tu código(bubble up errors), puede ser tentador ignorar la biblioteca y reimplementarla, sabiendo que que hemos manejado correctamente todos los errores correctamente. Zig está diseñado de tal forma que lo más perezoso que puede hacer un programador es manejar errores correctamente, y uno puede confiar razonablemente que una librería podrá hacer emergentes los errores.
- Es cierto que actualmente C es el lenguaje mas versátil y portable. Cualquier lenguaje que carezca de la habilidad de interactuar con código C está en riesgo de quedar en la oscuridad. Zig intenta convertirse en el nuevo lenguaje portable para bibliotecas, haciendo fácil la compatibilidad con el ABI de C para funciones externas y al mismo tiempo incorporando seguridad y un diseño de lenguaje que previene errores comunes entre implementaciones.
Un manejador de paquetes y sistema de compilación para proyectos existentes
Zig es una cadena de herramientas(toolchain) además de ser un lenguaje de programación que incluye un sistema de compilación y un manejador de paquetes diseñados para ser útiles incluso en el contexto de proyectos C/C++ tradicionales.
No solo puedes escribir código Zig en lugar de código C o C++, sino también usar Zig como un reemplazo para autotools, cmake, make, scons, ninja, etc. Además de esto, provee un manejador de paquetes para dependencias nativas. Este sistema de compilación está diseñado para ser útil aún cuando la totalidad del código de un proyecto está escrito en C o C++. Por ejemplo, portar ffmpeg al sistema de compilación de Zig, se vuelve posible compilar ffmpeg en cualquier sistema soportado hacia cualquier sistema soportado usando tan solo una descarga de 50 MiB. Para proyectos de fuente abierta, esta habilidad ágil para armar desde las fuentes - e incluso con compilación cruzada(cross-compile) - puede ser la diferencia entre ganar o perder contribuciones valiosas.
Los manejadores de paquetes como apt, pacman, homebrew y otros, son fundamentales en la experiencia de usuario final, pero pueden ser insuficientes para las necesidades de los desarrolladores de software. Un manejador de paquetes de lenguajes específico puede ser la diferencia entre tener muchos o ningún contribuyente. Para proyectos de fuente abierta, la dificultad de hacer que un proyecto compile es una gran carga para potenciales contribuyentes. Para proyectos de C/C++ tener dependencias puede ser fatal, especialmente en Windows, donde no hay manejadores de paquetes. Incluso para compilar el lenguaje y herramientas Zig, la mayoría de contribuyentes potenciales encuentran difícil la dependencia con LLVM. Zig ofrece un mecanismo con el cual los proyectos dependen de bibliotecas nativas directamente - sin importar si el usuario tiene o no una versión correcta de su manejador de paquetes del sistema y de forma tal que prácticamente se garantiza que el proyecto compilará en el primer intento sin importar que plataforma se esté utilizando o para qué plataforma se esté armando.
Otros lenguajes tienen manejadores de paquetes pero no eliminan sistemas de dependencias problemáticos como lo hace Zig.
Zig puede reemplazar un sistema de compilación de un proyecto con un lenguaje razonable que aporta una API declarativa para compilar proyectos y que al mismo tiempo aporta un manejador de paquetes y, por ende, la habilidad de depender de otras bibliotecas de C. La habilidad de tener dependencias permite llegar a niveles de abstracción más altos y así la proliferación de código reusable de alto nivel.
Simplicidad
C++, Rust y D tienen una cantidad tan grande de características que pueden ser un distractor del verdadero significado de la aplicación en la que estás desarrollando. Es fácil verse depurando el propio conocimiento del lenguaje en lugar de estar depurando la aplicación en si.
Zig carece de macros pero es lo suficientemente poderoso para expresar programas complejos en una forma clara y no repetitiva. Incluso Rust, que cuenta con casos especiales de macros como format!
, implementado dentro del mismo compilador. Mientras tanto en Zig, la función equivalente está implementada en la biblioteca estándar sin casos especiales de código en el compilador.
Herramientas
Puedes descargar Zig desde la sección de descargas. Zig provee binarios para Linux, Windows, y MacOs. En seguida se describe qué obtienes con cada uno de estos archivos:
- se instala simplemente descargando el archivo y extrayendo el contenido, sin configuración adicional.
- compilado estáticamente y por ello, sin dependencias en tiempo de ejecución
- LLVM concede optimización al desplegar y gracias a backends propios de Zig hacer compilaciones más rápidas.
- adicionalmente soporta un backend para generar código C
- listo desde el inicio para compilación cruzada entre las principales plataformas
- viene con código fuente para libc, el cual será compilado en forma dinámica cuando sea necesario para cualquier plataforma soportada
- incluye un sistema de compilación con mecanismo de cache
- compila C y C++ con soporte de libc
- compatibilidad en línea de comandos de GCC/Clang con
zig cc
- Compilador de recursos Windows