Introducción al funcionamiento interno de IL2CPP

Hace ya casi un año que empezamos a hablar del futuro de los scripts en Unity. El nuevo backend de scripting IL2CPP prometía aportar a Unity una máquina virtual de alto rendimiento y gran portabilidad. En enero lanzamos nuestra primera plataforma con IL2CPP, iOS de 64 bits. El lanzamiento de Unity 5 trajo otra plataforma, WebGL. Gracias a las aportaciones de nuestra enorme comunidad de usuarios, hemos enviado muchos parches de actualización para IL2CPP, mejorando constantemente su compilador y su tiempo de ejecución. No tenemos previsto dejar de mejorar IL2CPP, pero hemos pensado que sería una buena idea dar un paso atrás y contarte un poco cómo funciona IL2CPP desde dentro. En los próximos meses, tenemos previsto escribir sobre los siguientes temas (y tal vez otros) en esta serie de entradas sobre IL2CPP Internals:
1. Conceptos básicos: cadena de herramientas y argumentos de línea de comandos (este post)
2. Recorrido por el código generado
3. Consejos para depurar el código generado
4. Llamadas a métodos (métodos normales, métodos virtuales, etc.)
5. Aplicación del reparto genérico
6. Envoltorios P/invoke para tipos y métodos
7. Integración del recolector de basura
8. Marcos de pruebas y uso
Para hacer posible esta serie de posts, vamos a discutir algunos detalles sobre la implementación de IL2CPP que seguramente cambiarán en el futuro. Esperamos poder seguir ofreciendo información útil e interesante.
La tecnología a la que nos referimos como IL2CPP tiene dos partes diferenciadas.
- Compilador por adelantado (AOT)
- Una biblioteca de ejecución para la máquina virtual
El compilador AOT traduce el lenguaje intermedio (IL), la salida de bajo nivel de los compiladores .NET, a código fuente C++. La biblioteca en tiempo de ejecución proporciona servicios y abstracciones como un recolector de basura, acceso independiente de la plataforma a hilos y archivos, e implementaciones de llamadas internas (código nativo que modifica directamente estructuras de datos gestionadas).
El compilador IL2CPP AOT se llama il2cpp.exe. En Windows se encuentra en el directorio Editor\Data\il2cpp. En OSX está en el directorio Contents/Frameworks/il2cpp/build en la instalación de Unity.
La utilidad il2cpp.exe es un ejecutable gestionado, escrito íntegramente en C#. Lo compilamos tanto con compiladores .NET como Mono durante nuestro desarrollo de IL2CPP. La utilidad il2cpp.exe acepta ensamblados administrados compilados con el compilador Mono que viene con Unity y genera código C++ que pasamos a un compilador C++ específico de la plataforma.
Puedes pensar en la cadena de herramientas IL2CPP así:

La otra parte de la tecnología IL2CPP es una biblioteca en tiempo de ejecución para dar soporte a la máquina virtual. Hemos implementado esta biblioteca utilizando casi en su totalidad código C++ (tiene un poco de código ensamblador específico de la plataforma, pero que quede entre nosotros). Llamamos a la biblioteca en tiempo de ejecución libil2cpp, y se envía como una biblioteca estática vinculada al ejecutable del reproductor. Una de las principales ventajas de la tecnología IL2CPP es esta biblioteca de ejecución sencilla y portátil.
Puede encontrar algunas pistas sobre cómo está organizado el código de libil2cpp mirando los archivos de cabecera de libil2cpp que enviamos con Unity (los encontrará en el directorio Editor\Data\PlaybackEngines\webglsupport\BuildTools\Libraries\libil2cpp\include en Windows, o en el directorio Contents/Frameworks/il2cpp/libil2cpp en OSX). Por ejemplo, la interfaz entre el código C++ generado por il2cpp.exe y el tiempo de ejecución de libil2cpp se encuentra en el archivo de cabecera codegen/il2cpp-codegen.h.
Una parte clave del tiempo de ejecución es el recolector de basura. Estamos distribuyendo Unity 5 con libgc, el recolector de basura Boehm-Demers-Weiser. Sin embargo, libil2cpp ha sido diseñada para permitirnos utilizar otros recolectores de basura. Por ejemplo, estamos investigando una integración de la GC de Microsoft, que fue de código abierto como parte de CoreCLR. Tendremos más que decir sobre esto en nuestro post sobre la integración del recolector de basura más adelante en la serie.
Veamos un ejemplo. Usaré Unity 5.0.1 en Windows, y empezaré con un proyecto nuevo y vacío. Para que tengamos al menos un script de usuario que convertir, añadiré este simple componente MonoBehaviour al objeto de juego Main Camera:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.
Cuando construyo para la plataforma WebGL, puedo utilizar Process Explorer para ver la línea de comandos Unity utilizada para ejecutar il2cpp.exe:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.
Esa línea de comandos es bastante larga y horrible, así que vamos a descomprimirla. En primer lugar, Unity está ejecutando este ejecutable:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.
El siguiente argumento en la línea de comandos es la propia utilidad il2cpp.exe.
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.
El resto de argumentos de la línea de comandos se pasan a il2cpp.exe, no a mono.exe. Veámoslos. En primer lugar, Unity pasa cinco banderas a il2cpp.exe:
- --copy-level=None
- Especifica que il2cpp.exe no debe realizar una copia especial de archivos del código C++ generado.
- --enable-generic-sharing
- Se trata de una función de reducción del tamaño del código y de los binarios. IL2CPP compartirá la implementación de métodos genéricos cuando pueda.
- --enable-unity-event-support
- Soporte especial para garantizar que el código de los eventos de Unity, a los que se accede mediante reflexión, se genera correctamente.
- --output-format=Compact
- Genera código C++ en un formato que requiere menos caracteres para los nombres de tipos y métodos. Este código es difícil de depurar, ya que los nombres en el código IL no se conservan, pero a menudo se compila más rápido, ya que hay menos código para que el compilador C++ lo analice.
- --extra-types.file="C:\Program Files\Unity\Editor\Data\il2cpp\il2cpp_default_extra_types.txt"
- Utiliza el archivo de tipos extra por defecto (y vacío). Este archivo puede ser añadido en un proyecto Unity para permitir que il2cpp.exe sepa que tipos genéricos o de array serán creados en tiempo de ejecución, pero no están presentes en el código IL.
Es importante tener en cuenta que estos argumentos de línea de comandos pueden y serán modificados en versiones posteriores. Aún no disponemos de un conjunto estable y compatible de argumentos de línea de comandos para il2cpp.exe. Por último, tenemos una lista de dos archivos y un directorio en la línea de comandos:
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\Assembly-CSharp.dll"
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\Managed\UnityEngine.UI.dll"
- "C:\Users\Josh Peterson\Documents\IL2CPP Blog Example\Temp\StagingArea\Data\il2cppOutput"
La utilidad il2cpp.exe acepta una lista de todos los ensamblados IL que debe convertir. En este caso son el ensamblado que contiene mi MonoBehaviour simple, Assembly-CSharp.dll, y el ensamblado GUI, UnityEngine.UI.dll. Tenga en cuenta que aquí faltan algunos montajes. Claramente, mi script hace referencia a UnityEngine.dll, y eso hace referencia al menos a mscorlib.dll, y tal vez a otros ensamblados. ¿Dónde están? En realidad, il2cpp.exe resuelve esos ensamblados internamente. Se pueden mencionar en la línea de comandos, pero no son necesarios. Unity sólo necesita mencionar explícitamente los ensamblados raíz (aquellos a los que no hace referencia ningún otro ensamblado).
El último argumento de la línea de comandos il2cpp.exe es el directorio en el que deben crearse los archivos C++ de salida. Si tienes curiosidad, echa un vistazo a los archivos generados en ese directorio, serán el tema del próximo post de esta serie. Antes de hacerlo, es posible que desee elegir la opción "Reproductor de desarrollo" en la configuración de compilación de WebGL. Esto eliminará el argumento de línea de comandos --output-format=Compact y le proporcionará mejores nombres de tipos y métodos en el código C++ generado.
Prueba a cambiar varias opciones en los ajustes de WebGL o del reproductor iOS. Deberías poder ver diferentes opciones de línea de comandos pasadas a il2cpp.exe para habilitar diferentes pasos de generación de código. Por ejemplo, si se cambia la opción "Activar excepciones" de la configuración del reproductor WebGL a un valor "Completo", se añaden los argumentos --emit-null-checks, --enable-stacktrace y --enable-array-bounds-check a la línea de comandos de il2cpp.exe.
Me gustaría señalar uno de los retos que no asumimos con IL2CPP, y no podríamos estar más contentos de haberlo ignorado. No hemos intentado reescribir la biblioteca estándar de C# con IL2CPP. Cuando construyes un proyecto Unity que utiliza el backend de scripting IL2CPP, todo el código de la librería estándar C# en mscorlib.dll, System.dll, etc. es exactamente el mismo código utilizado para el backend de scripting Mono.
Nos basamos en código de la biblioteca estándar de C# que ya es bien conocido por los usuarios y está bien probado en proyectos de Unity. Así, cuando investigamos un fallo relacionado con IL2CPP, podemos estar bastante seguros de que el fallo está en el compilador AOT o en la biblioteca de ejecución, y en ningún otro sitio.
Desde el lanzamiento público inicial de IL2CPP en la versión 4.6.1p5 en enero, hemos enviado 6 versiones completas y 7 versiones de parches (a través de las versiones 4.6 y 5.0 de Unity). Hemos corregido más de 100 errores mencionados en las notas de la versión.
Para que esta mejora continua se produzca, desarrollamos internamente contra una sola versión del código IL2CPP, que se encuentra en el borde de la rama troncal de Unity utilizada para enviar versiones alfa y beta. Justo antes de cada lanzamiento, portamos los cambios de IL2CPP a la rama de lanzamiento específica, ejecutamos nuestras pruebas y verificamos que todos los errores que hemos corregido estén corregidos en esa versión. Nuestros equipos de control de calidad e ingeniería sostenida han realizado un trabajo increíble para hacer posible una entrega a este ritmo. Esto significa que nuestros usuarios nunca están a más de una semana de las últimas correcciones de errores de IL2CPP.
Nuestra comunidad de usuarios ha demostrado ser inestimable al enviar muchos informes de errores de gran calidad. Agradecemos todos los comentarios de nuestros usuarios para ayudar a mejorar continuamente IL2CPP, y esperamos recibir más de ellos.
El equipo de desarrollo que trabaja en IL2CPP tiene una fuerte mentalidad que da prioridad a las pruebas. A menudo empleamos prácticas de Test Driven Design y rara vez fusionamos un pull request sin buenas pruebas. Esta estrategia funciona bien para una tecnología como la IL2CPP, en la que tenemos entradas y salidas claras. Significa que la gran mayoría de los fallos que vemos no son comportamientos inesperados, sino casos inesperados (por ejemplo, es posible usar una IntPtr de 64 bits como índice de un array de 32 bits, haciendo que clang falle con un error del compilador de C++, ¡y el código real realmente lo hace!) Esa diferencia nos permite corregir errores rápidamente con un alto grado de confianza.
Con la ayuda de nuestra comunidad, estamos trabajando duro para que IL2CPP sea lo más estable y rápido posible. Por cierto, si algo de esto te entusiasma, estamos contratando (por decir algo).
Me temo que he dedicado aquí demasiado tiempo a bromear con futuras entradas del blog. Tenemos mucho que decir y no cabe todo en un solo post. La próxima vez, profundizaremos en el código generado por il2cpp.exe para ver qué aspecto tiene tu proyecto para el compilador de C++.