De C# a C++: Cómo hemos automatizado la conversión de proyectos – Parte 2

Desarrollo

El diseño y desarrollo del traductor de código de C# a C++ se realizó exclusivamente por CodePorting. Requirió muchas investigaciones, la aplicación de múltiples enfoques y pruebas, diferenciándose por el modelo de memoria y otros aspectos. Al final, se eligieron dos soluciones. Una de ellas se utiliza actualmente para las versiones de C++ de los productos Aspose.

Tecnologías

Ahora es el momento de explicar las tecnologías que utilizamos en el traductor de código. El traductor es una aplicación de consola escrita en C#, lo que facilita su integración en scripts que realizan secuencias típicas como traducir-compilar-probar. También hay un componente de GUI que te permite hacer lo mismo haciendo clic en los botones.

El análisis de sintaxis se realiza mediante la biblioteca NRefactory en la generación obsoleta del traductor y mediante Roslyn en la nueva.

El traductor utiliza varios recorridos de AST tree para recopilar información y generar código C++ de salida. Para el código C++, no se crea una representación AST; en su lugar, manejamos el código de salida en forma de texto puro.

En muchos casos, se requiere información adicional para ajustar el traductor. Esta información se pasa mediante opciones y atributos. Las opciones se aplican al proyecto completo. Por lo general, se utilizan para especificar el nombre de la macro de exportación de clases o los símbolos condicionales de C# utilizados al analizar el código. Los atributos se aplican a los tipos y entidades y proporcionan información específica para ellos, por ejemplo: marcar qué miembros de la clase requieren calificadores const o mutable en el código traducido o qué entidades deben excluirse de la traducción.

Las clases y estructuras de C# se convierten en clases de C++. Sus miembros y código fuente se traducen a análogos más cercanos. Los tipos y métodos genéricos se mapean a plantillas de C++. Las referencias de C# se traducen en punteros inteligentes (compartidos o débiles). Las clases de referencia se definen en la biblioteca. Otros detalles internos del traductor de código se describirán en un artículo aparte.

Por lo tanto, el proyecto traducido de C# a C++ depende de nuestra biblioteca en lugar de las bibliotecas .NET:

C# to C++

Para construir la biblioteca del traductor de código y los proyectos traducidos, utilizamos Cmake. Actualmente, ofrecemos soporte para los compiladores VS 2017 y 2019 (Windows), GCC y Clang (Linux).

Como ya se mencionó, la mayoría de nuestras implementaciones .NET son adaptadores delgados sobre bibliotecas de terceros, que incluyen:

  • Skia — soporte gráfico.
  • Botan — funciones de cifrado.
  • ICU — soporte para cadenas, páginas de códigos y culturas.
  • Libxml2 — operaciones XML.
  • PCRE2 — soporte para expresiones regulares.
  • zlib — funciones de compresión.
  • Boost — diversos propósitos.
  • Otras bibliotecas.

Tanto el Traductor como la Biblioteca están cubiertos con numerosas pruebas. Las pruebas de la Biblioteca utilizan el marco de trabajo GoogleTest. Las pruebas del Traductor están escritas principalmente en NUnit/xUnit y se dividen en varias categorías, lo que garantiza que:

  • La salida del traductor coincide con su objetivo en datos de entrada específicos.
  • La salida de los programas traducidos coincide con su objetivo.
  • Las pruebas NUnit/xUnit de los proyectos de entrada se traducen a pruebas de GoogleTest y pasan.
  • La API de los proyectos traducidos funciona correctamente en C++.
  • Las opciones y atributos del traductor funcionan según lo esperado.

Utilizamos GitLab como sistema de control de versiones. Para la integración continua, utilizamos Jenkins. Los productos traducidos están disponibles como paquetes NuGet y archivos descargables.

Desafíos

Mientras trabajábamos en este proyecto, nos encontramos con varios problemas diferentes. Algunos de ellos eran esperados, mientras que otros surgieron en el camino:

  1. Diferencias en el sistema de tipos entre .NET y C++.
    En C++, no existe una sustitución directa para el tipo Object, y la mayoría de las clases de la biblioteca estándar no tienen RTTI (información de tipo en tiempo de ejecución). Esto hace imposible mapear los tipos de .NET a los de la STL (Standard Template Library) de C++.
  2. Los algoritmos de traducción son complicados.
    Se requiere descubrir muchas sutilezas no triviales en el código traducido. Por ejemplo, en C#, existe un orden definido para calcular los argumentos de un método, mientras que en C++ esto puede dar lugar a comportamientos indefinidos.
  3. La depuración es difícil.
    Depurar código traducido requiere habilidades específicas. Las sutilezas mencionadas anteriormente pueden afectar el funcionamiento del programa de manera crucial, produciendo errores difíciles de explicar. Por otro lado, también pueden convertirse fácilmente en errores ocultos que persisten durante mucho tiempo.
  4. Diferencias en los sistemas de gestión de memoria.
    C++ no tiene recolección de basura (garbage collection). Debido a esto, se requieren más recursos para que el código traducido se comporte como el original.
  5. Se requiere disciplina por parte de los desarrolladores de C#.
    Los desarrolladores de C# deben acostumbrarse a las limitaciones impuestas por el proceso de traducción de código. Algunas de las razones para estas limitaciones son:
    • La versión del lenguaje debe ser compatible con el analizador de sintaxis del traductor.
    • Se prohíben las construcciones de código no admitidas por el traductor (por ejemplo, yield).
    • El estilo de código está limitado por la estructura del código traducido (por ejemplo, cada campo de referencia debe ser inequívocamente una referencia débil o compartida, mientras que en el código C# arbitrario esto no es necesariamente cierto).
    • El lenguaje C++ impone sus propias restricciones (por ejemplo, en C#, las variables estáticas no se eliminan antes de que todos los hilos principales finalicen, mientras que en C++ esto no es el caso).
  6. Gran cantidad de trabajo.
    El subconjunto de la biblioteca .NET utilizado en nuestros productos es lo suficientemente grande, y llevará mucho tiempo implementar todas las clases y métodos.
  7. Requisitos especiales para los desarrolladores.
    La necesidad de adentrarse en los detalles internos de la plataforma y trabajar con dos o más lenguajes de programación limita el número de candidatos disponibles. Por otro lado, los desarrolladores interesados en la teoría de compiladores u otras disciplinas exóticas encuentran su lugar en el proyecto con facilidad.
  8. Fragilidad del sistema.
    Aunque tenemos miles de pruebas y millones de líneas de código para probar el traductor, a veces nos encontramos con problemas cuando los cambios realizados para solucionar la compilación de un proyecto afectan a otro. Por ejemplo, esto puede ocurrir con construcciones de sintaxis raras y estilos de código específicos en proyectos.
  9. Altas barreras de entrada.
    La mayoría de las tareas en el proyecto de traducción de código requieren un análisis profundo. Debido al gran número de subsistemas y escenarios, cada nueva tarea requiere familiarizarse con nuevos aspectos del proyecto durante mucho tiempo.
  10. Problemas de protección de propiedad intelectual.
    Aunque existen muchas soluciones listas para ofuscar eficazmente el código C#, en C++ se preserva mucha información en los encabezados de clase. Además, algunas definiciones no se pueden eliminar de los encabezados públicos sin consecuencias. Mapear clases y métodos genéricos a plantillas crea otra vulnerabilidad, ya que revela los algoritmos.

A pesar de todo esto, el proyecto de traducción de código es muy interesante desde un punto de vista técnico, y su complejidad académica nos obliga a aprender algo nuevo constantemente.

Conclusión

Mientras trabajábamos en el proyecto de traducción de código, logramos implementar un sistema que resuelve una interesante tarea académica de traducción de código. Hemos organizado lanzamientos mensuales de las bibliotecas Aspose para el lenguaje con el que no se suponía que debían funcionar.

Está previsto publicar más artículos sobre el traductor de código. El próximo artículo explicará el proceso de conversión en detalle, incluyendo cómo se mapean las construcciones concretas de C# a C++. Otro artículo tratará sobre el modelo de gestión de memoria.

Haremos nuestro mejor esfuerzo para responder a las preguntas que se nos hagan. Si los lectores están interesados en otros aspectos del desarrollo del traductor de código, podríamos considerar escribir más artículos al respecto.

Artículos relacionados: