Emulador de GB: Port a Jai

C y C++ son dos de los lenguajes mas usados en aplicaciones que requieren alto rendimiento. Muchas veces el manejo de memoria manual, la flexibilidad para hacer casi cualquier cosa y la cercanía con el hardware son características necesarios en este tipo de aplicaciones.

Pero no son lenguajes perfectos (si es que existe algo así) y tienen muchas críticas. Muchos de las cosas que se les critican, creo, es porque son lenguajes viejos, que si bien tuvieron y siguen teniendo actualizaciones con features modernas, mantienen compatibilidad con todas las versiones anteriores y eso agrega mucha complejidad. Los lenguajes cambiaron tanto que hoy en día se habla de “Modern C++” como si fuese un lenguaje diferente, pero en la práctica la línea entre C y C++ y entre C++ y “modern” C++ es difusa. En software que lleva años de desarrollo o interactúa con APIs viejas se empieza a mezclar todo. Por ejemplo, para manejar strings existe la forma vieja con strings de C (char* y terminados en “\0”) y la forma nueva de C++ (clase std::string), y según que queramos hacer hay que usar una u otra en distintos lugares. E incluso es posible encontrar otras implementaciones de strings en un mismo proyecto, como dato curioso, trabajé en un proyecto en el que habían CINCO tipos de dato para representar strings (C, C++, Objective C, Qt y uno propietario).

Además de las complejidades de mezclar C, C++ y modern C++, los procesos para compilar estos lenguajes también varían un montón. Hay muchos compiladores para elegir y muchas herramientas para integrar todo, así que trabajar con C++ requiere tomar un montón de decisiones antes de empezar que después son dificiles de cambiar, y sumarse a un proyecto ya empezado requiere un montón de preparación del entorno.

Hay muchas mas críticas, como la separación en archivos header y source, el “boilerplate” que hay que escribir para muchas cosas, la cantidad de “undefined behaviour”, las macros, los mensajes de error poco claros, etc, etc, pero no quiero hacer un post entero sobre C++.

Alternativas

Desde hace muchos años existen otros lenguajes en desarrollo que, explícita o implícitamente, buscan ser una alternativa a C++. Rust es el mas popular, pero hay mas, como Zig o, en ciertos aspectos, Go.

Los juegos son de esas aplicaciones de alto rendimiento para las que C++ es muy útil, y en general es el lenguaje standard en la industria. Se pueden hacer juegos usando cualquier otro lenguaje, pero incluso en engines en los que se usan otros lenguajes, el engine en si suele estar escrito en C++ y los otros lenguajes se usan para lo que se suele llamar “scripting”. Mi idea no es minimizar esos lenguajes ni el trabajo de quienes programamos usando, por ejemplo, C# en Unity; es solo para explicar el contexto y a que me refiero con que C++ es el standard en la industria.

Entonces, si no se quiere o no se puede usar un engine existente, la alternativa es programar uno propio, y las opciones son pocas. Rust y Zig todavía no tienen mucho desarrollo relacionado con gaming, de Rust hay algunas bibliotecas, pero la transición no es tan sencilla por varias decisiones de diseño del lenguaje. Acá es donde aparece Jai.

Jai

Es un lenguaje que están desarrollando en Tekhla Inc. (Braid, The Witness) pensado en principio para hacer juegos sin usar C++, pero con las cosas de C y C++ que los hacen tan necesarios. Hay gente usándolo para otras cosas, pero algunas features que se ven en otros lenguajes capaz no están en Jai porque para los desarrolladores los beneficios al desarrollar juegos no justifican el costo de implementarlas.

Para reforzar la idea de que es una alternativa viable para juegos, en Tekhla no solo están haciendo el lenguaje, también están haciendo un engine y su próximo juego con Jai (sus juegos anteriores tienen su propio engine escrito en C++). El lenguaje está en desarrollo desde hace varios años y actualmente solo está disponible para participantes de una beta cerrada, a la que tengo acceso desde hace unos meses. Al final voy a dejar videos de Jonathan Blow donde explica sus motivos para crear Jai y los features que tiene.

Port

Si bien Jai está siendo diseñado con la idea de hacer juegos, en el fondo sigue siendo un lenguaje compilado de bajo nivel que se puede usar para cualquier tipo de aplicación. Cómo quiero probar el lenguaje pero no tengo juegos propios ni planeo hacer un juego de cero, hice un port de la versión de C++ de mi emulador de GB.

Objetivo

Por el momento mi plan es hacer un port “1 a 1”, mas adelante voy a repensar y refactorizar las cosas para usar features de Jai y probablemente escriba sobre eso en otro post.

Por ahora quiero portar todo lo mas parecido posible a lo que hice en C++ para hacerlo funcionar, aunque no esté aprovechando ciertas cosas. En general lo que hice fue copiar el código y traducirlo, no estoy reimplementando el emulador de cero.

El resto del post va a ser una explicación de algunas cosas que tuve que tener en cuenta con este lenguaje nuevo.

Sintaxis

La sintaxis del lenguaje es diferente a los lenguajes “C-like”, y como dato no menor, todavía no está definida y hay detalles que pueden cambiar durante la beta.

Hay diferentes cosas que se pueden definir: funciones, variables, constantes, structs, enums y mas. Todo se define siguiendo reglas parecidas, así que no voy a explicar cada caso porque es algo muy repetitivo y aburrido. Lo importante es que se escriben diferente que en C++, así que todo el código que copio necesita algunos cambios. Por ejemplo, dos función en C++ y en Jai:

// C++
int Func1 (int arg) { return arg; };
void Func2 (string arg) {};

// Jai
Func1 :: (arg: int) -> int { return arg; };
Func2 :: (arg: string) {};

Tienen mas o menos las mismas cosas, pero en distinto orden. En Jai el nombre de lo que se define SIEMPRE va primero, y no se usa “void” cuando las funciones no devuelven nada.

Me parece que este orden de definir las cosas es mejor que el de C, porque el nombre de la función/variable/tipo es lo que buscamos en general y es lo primero que leemos.

Programación Orientada a Objetos

Jai no soporta POO, es algo que se definió desde el principio y no va a cambiar, así que cualquier código hecho con POO hay que adaptarlo. Los motivos por los que no se usa son varios, pero no vienen al caso. El emulador lo hice usando POO porque es lo que hago desde hace muchos años, pero usé pocas cosas y en pocos lugares así que la mayoría lo puedo portar sin problemas.

En POO una instancia tiene un conjunto de propiedades y métodos disponibles, por eso se pueden hacer cosas como “player.Health = 100” y “player.Attack()”. La asignación es así en casi cualquier paradigma y funciona como cualquiera esperaría en Jai. Para la llamada a la función en POO, internamente lo que está pasando es que se llama a la función “Attack” y se pasa como parámetro, de manera implícita, una referencia a la variable “player”, y dentro de la función todos los miembros de player están disponibles (con “this” o implícitos).

Es fácil traducir eso de POO a no-POO: La función se define con un primer parámetro del tipo del player, y en cualquier lugar donde sería válido “this” hay que usar esa referencia:

// C++
void GPU::Step(int emulatedClocks) {
clocks += emulatedClocks; // clocks es un miembro de GPU
UpdateMode(); // UpdateMode es una función de GPU
};

// Jai
Step :: (gpu: *GPU, emulatedClocks: int) {
gpu.clocks += emulatedClocks;
UpdateMode(gpu);
};

Using

En muchos lenguajes existe esta palabra reservada, pero su significado varía. En Jai se usa para agregar los miembros de un tipo al contexto actual. ¿Qué significa esto? Que en un bloque de código, después de usar “using unaVariable;” las siguientes líneas pueden referirse a miembros de unaVariable sin escribir “unaVariable.” antes. Esto simplifica un poco el código que mostré arriba:

// Jai
Step :: (using gpu: *GPU, clocks: int) {
clocks += emulatedClocks; // equivalente a gpu.clocks
UpdateMode(gpu); // en funciones hay que escribir todo igual
};

Polimorfismo, herencia, métodos virtuales

El lenguaje tiene algo similar a polimorfismo pero no tiene nada parecido a herencia de POO. Yo usé herencia para los MBCs y métodos virtuales, así que eso lo tengo que cambiar.

Para polimorfismo hay que usar la misma palabra reservada “using” que nombré antes, capaz en otro post explico mejor como funciona en este contexto.

Herencia implica que una clase derivada de otra tiene todas las propiedades y funciones de la clase padre. En Jai no hay herencia, los tipos están definidos por si solos, pero de nuevo, con la palabra “using” se puede hacer algo similar para las propiedades.

Para reemplazar métodos virtuales por el momento simplemente hice un switch según el tipo de la variable y llamo a distintas funciones que se llaman igual, pero reciben distintos tipos de MBC.

Casteo

Jai tiene muy pocos casteos automáticos, así que muchas cosas que en C++ son implícitas hay que hacerlas explícitas. Por ejemplo, en muchos lenguajes se puede asignar un valor float a una variable int sin problemas. Capaz algunos lenguajes tienen la opción de mostrar un warning, pero es raro que sea un error. En Jai es un error de compilación.

La idea del autocast tan restrictivo es evitar algunos errores difíciles de encontrar. Sólo se hace autocast cuando el tipo de la variable destino es del mismo tipo primitivo e incluye todos los valores del tipo de origen. Por ejemplo:

a : u8 = 156; // entero de 8 bits, sin signo
b : u16 = a; // entero de 16 bits, sin signo, funciona
c : s8 = a; // entero de 8 bits, con signo, no funciona
d : s16 = a; // entero de 16 bits, con signo, funciona
e : float = a; // flotante de 64 bits, con signo, no funciona

“c” no funciona porque la variable “a” puede tener el valor 255, pero “c” al ser de 8 bits con signo solo permite valores entre -128 y 127.

“e” no funciona porque es de tipo con punto flotante y “a” es de tipo entero, aunque un float puede representar los valores del 0 al 255 sin problemas.

En el emulador usé tipos similares a esos en todos lados, así que tuve que revisar muchos casos de estos.

En otro post voy a hablar mas en detalle sobre casteo porque hay muchas cosas a tener en cuenta y es lo que me trajo mas problemas.

Arrays

Jai tiene varios tipos de array:

  • Tamaño fijo (ej: a : [3]int;). El temaño se define con una constante durante la compilación.
  • Vistas (ej: b : []int = a;). Se crean a partir de otros arrays en tiempo de ejecución.
  • Tamaño variable (ej: c : [..]int;)

Los 3 tipos tienen siempre un miembro “count” con la cantidad de elementos del array, y un miembro “data” que es un puntero al primer elemento.

En C++ definir arrays de esta forma (int[]) es la forma “vieja” de C, que es bastante limitada: no tiene información de la cantidad de elementos y solo existen de tamaño fijo. La alternativa de C++ es usar std::array o std::vector que son clases, pero como dije antes, C y C++ se mezclan. En el emulador usé muchos arrays de C porque es mucho más cómodo escribir “u8[256]” que “std::array<u8,256>”.

Cómo el emulador tiene mas que nada arrays de tamaño fijo no tuve que hacer muchos cambios, solo editarlo para que tenga la sintaxis correcta.

Una cosa importantísima de los arrays de Jai es que por defecto hacen un “bounds check”, es decir, al momento de acceder a un elemento del array en cierto índice, si el índice no es válido (mayor o igual a 0 y menor a .count) el programa crashea con un mensaje de error muy descriptivo. En los arrays de C hay que hacer este chequeo a mano, porque el acceso a índices inválidos “funciona” y termina modificando memoria fuera del array. Estos problemas son muy difíciles de detectar, porque no traen consecuencias inmediatas, sólo trae problemas al querer acceder a esa otra variable que se modificó sin saberlo.

El chequeo se puede deshabilitar para mejorar la performance, pero en las pruebas que hice no hizo mucha diferencia así que lo dejé activado.

Valor inicial

Una diferencia chica pero importante es qué valor tienen las variables si se las define sin asignarles nada. En C++ es “basura”, lo que sea que haya en la memoria donde se creó esa variable. Esto trae muchos problemas, sobre todo cuando recién se empieza a programar en C++, porque punteros sin inicializar apuntan a cualquier lado. Estos errores son difíciles de encontrar sin alguna herramienta que haga análisis estático de código, y además no siempre traen problemas evidentes.

En Jai todas la variables se inicializan por defecto en 0 o el equivalente de 0 (ej: un string está vacío, un puntero es null, un booleano es false), similar a otros lenguajes como C#. Es posible crear una variable sin inicializar para evitar esa operación cuando se sabe que el valor se va a asignar mas tarde y no se va a acceder hasta ese momento.

a : int = 0; // es lo mismo que a : int;
b : float = 2.0; // si no se pone 2.0 se inicializa en 0.0, los floats van sin "f"!!
c : s16 = ---; // se crea la variable sin valor inicial, puede tener cualquier cosa

Es algo chico, pero que permite borrar un montón de ” = 0″, “= false”, “= null”, etc, cosas que uno se acostumbra a poner en C++.

Documentación

Por ahora el lenguaje y el compilador están en beta, y tiene poca documentación. Hay varios “how to” incluidos en el proyecto, pero varias cosas requieren mirar la implementación para entenderlas mejor y para saber que funciones están disponibles.

Por ejemplo, en una parte del emulador tengo que separar el path al rom en carpeta, nombre del archivo y extensión. Mi implementación en C++ era hacerlo a mano y así empecé a hacerlo en Jai, hasta que revisando el módulo de strings encontré que ya existen funciones para esto.

Bindings

Bindings se le dice a el código que hay que escribir en un lenguaje para poder usar dlls o funciones del sistema.

Junto con el compilador y la documentación que se nos envío ya hay bindings y módulos con un montón de funcionalidad, e incluso juegos simples de ejemplo. Hay cosas como bindings para distintas APIs gráficas, modulos para abrir archivos y de encriptación. En la versión de C++ usé SFML, pero en lo que trajo lo mas parecido son bindings con SDL2, así que la versión de Jai la adapté para usar SDL2. Esto requiere reescribir algunas cosas, pero me pareció mas simple que aprender a configurar bindings para SFML.

IDE

Por ahora estoy usando una mezcla, hay un plugin para Visual Studio Code para tener colores en el código, para compilar uso la consola/terminal (desde VSCode se puede abrir una), y para debuggear uso Visual Studio. Abriendo el proyecto con el .exe ya compilado al darle play se carga el PDB automáticamente, y se pueden poner breakpoints o inspeccionar variables sin problemas.

Beta

Es posible encontrar bugs en el compilador o inconsistencias en el lenguaje, y hay cosas que pueden cambiar de una versión a otra, así que hay que prestar atención a los logs en cada release. Lo bueno es que hay muchos ejemplos incluidos y hay un Discord para la beta donde otros usuarios están compartiendo cosas útiles.

Código

No quiero escribir sobre cada cosa que hice porque es muy repetitivo, pero dejo acá el link al repositorio con el código: https://github.com/DiegoSLTS/gb-jai

Conclusión

El proceso del port es similar a un port entre cualquier par de lenguajes, aunque es interesante pasar de un lenguaje a otro que se plantea explícitamente como una alternativa/reemplazo. Está bueno ver que herramientas tiene cada lenguaje para solucionar un problema, incluso pude arreglar varios crashes que en C++ me costaba mucho encontrar gracias a cosas que Jai hace diferente.

Links

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Crea tu sitio web con WordPress.com
Empieza ahora
A %d blogueros les gusta esto: