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

Los clientes valoran los productos de Aspose, que permiten manipular protocolos y archivos de formatos populares. La mayoría de ellos se desarrollaron inicialmente para .NET. Al mismo tiempo, las aplicaciones empresariales para formatos de archivo se ejecutan en diferentes entornos. Este artículo describirá cómo hemos tenido éxito en establecer las versiones de los productos de Aspose para C++, construyendo un marco para la traducción de código desde C#. Mantener la funcionalidad de las versiones de .NET para estos productos fue un desafío técnico.

Desarrollamos la infraestructura necesaria nosotros mismos, permitiendo la traducción de código entre lenguajes y la emulación de funciones de la biblioteca .NET. Al hacerlo, resolvimos un problema que generalmente se considera académico. Esto nos permitió comenzar a lanzar productos mensuales de .NET para el lenguaje C++, obteniendo el código para cada versión a partir de la versión correspondiente del código C#. Además, las pruebas que cubrían el código original de C# se traducen junto con él, asegurando que la funcionalidad de la solución resultante se monitoree, al igual que las pruebas especialmente escritas en C++.

Antecedentes

El éxito del traductor de código de C# a C++ se basa en la experiencia exitosa que tuvo el equipo de CodePorting al configurar la traducción automatizada de código de C# a Java. El marco creado transformaba las clases de C# en clases de Java, reemplazando adecuadamente las llamadas a la biblioteca del sistema.

Se consideraron diferentes enfoques para el marco. El desarrollo de versiones puras de Java desde cero requeriría demasiados recursos. Una opción era el enlace de llamadas desde el código Java al entorno .NET, pero esto limitaría el conjunto de plataformas de programación que podríamos admitir en el futuro. En ese momento, .NET estaba presente solo en Windows. El enlace de llamadas es conveniente con llamadas que rara vez ocurren y que llevan tipos de datos ampliamente utilizados. Sin embargo, se vuelve abrumador al trabajar con muchos objetos y tipos de datos personalizados.

En cambio, nos preguntamos cómo traducir completamente el código existente a una nueva plataforma. Esto era un problema relevante porque la migración de código debía realizarse mensualmente y para todos los productos, produciendo un flujo sincronizado de versiones con características similares.

La solución se dividió en dos partes:

  • Traductor — aplicación para transformar la sintaxis de C# en la de Java, reemplazando los tipos y métodos de .NET con sustituciones adecuadas de las bibliotecas del lenguaje objetivo.
  • Biblioteca — componente para emular las partes de la biblioteca .NET que no se podían asignar correctamente a Java. Para simplificar la tarea, se podrían utilizar componentes de terceros disponibles.

Los siguientes argumentos confirmaron que el plan era técnicamente viable:

  1. Los lenguajes C# y Java tienen una ideología similar. Al menos, en lo que respecta a la estructura de tipos y al modelo de gestión de memoria.
  2. Solo teníamos que traducir las bibliotecas, por lo que no era necesario trasladar las interfaces gráficas de usuario (GUI) a una plataforma diferente.
  3. Las bibliotecas traducidas contenían principalmente lógica empresarial y operaciones de archivos de bajo nivel, con las dependencias más complejas siendo System.Net y System.Drawing.
  4. Desde el principio, las bibliotecas se desarrollaron para funcionar en una amplia gama de versiones de .NET (incluyendo Framework, Standard e incluso Xamarin). Por lo tanto, las diferencias menores entre plataformas se podían ignorar.

No entraremos en más detalles sobre el traductor de C# a Java, ya que esto requeriría artículos dedicados. En resumen, la conversión de productos de C# a Java se había convertido en una práctica habitual de la empresa, gracias al traductor de código creado. El traductor había evolucionado desde un simple transformador de texto basado en reglas hasta un generador de código complicado que trabaja con la representación AST del código fuente.

El éxito del traductor de C# a Java nos ayudó a ingresar al mercado de Java, y se planteó la posibilidad de comenzar a lanzar productos para C++ utilizando el mismo escenario.

Requisitos

Para hacer posible el lanzamiento de la versión de nuestros productos en C++, era necesario crear un marco que nos permitiera traducir el código de C# a C++, compilarlo, probarlo y enviarlo al cliente. El código consistía en un conjunto de bibliotecas, cada una con hasta varios millones de líneas de código. El componente de biblioteca del traductor de código debía abordar lo siguiente:

  1. Emular el entorno .NET para el código traducido.
  2. Adaptar el código traducido para C++: estructura de tipos, gestión de memoria, etc.
  3. Cambiar el estilo del código de C# traducido al estilo de C++, para que los desarrolladores no familiarizados con los paradigmas de .NET puedan usarlo fácilmente.

Es probable que muchos lectores se pregunten por qué no consideramos el uso de soluciones existentes, como el proyecto Mono. Hubo varias razones para no hacerlo:

  1. Esto no cubriría los requisitos segundo y tercero.
  2. Mono está implementado en C# y depende de su tiempo de ejecución.
  3. Adaptar el código de terceros a nuestras necesidades (API, sistema de tipos, modelo de gestión de memoria, optimización, etc.) requeriría una cantidad de tiempo comparable a la creación de nuestra solución.
  4. Nuestros productos no requieren la implementación completa de .NET. Sin embargo, si tuviéramos una implementación completa, sería difícil distinguir qué métodos y clases necesitamos y cuáles no. Pasaríamos mucho tiempo corrigiendo características que nunca usaríamos.

En teoría, podríamos usar nuestro traductor para convertir una solución existente a C++. Sin embargo, esto requeriría tener un traductor completamente funcional desde el principio, ya que es imposible depurar cualquier código traducido sin una biblioteca del sistema. Además, los problemas de optimización serían aún más esenciales que para el código de los productos traducidos, ya que las llamadas a la biblioteca del sistema tienden a convertirse en cuellos de botella.

Volviendo a nuestros requisitos para el traductor de código. Debido a la incapacidad de mapear los tipos de .NET a los de STL, decidimos utilizar tipos de biblioteca personalizados como sustituciones. La biblioteca se desarrolló como un conjunto de adaptadores que permiten el uso de las características de bibliotecas de terceros a través de una API similar a la de .NET (similar a la de Java).

Al traducir las bibliotecas con la API existente, un requisito importante para el código traducido era que debería ejecutarse dentro de cualquier aplicación del cliente. Por lo tanto, no pudimos usar la recolección de basura para el código traducido, ya que cubriría toda la aplicación. En cambio, nuestro modelo de gestión de memoria debía ser claro para los desarrolladores de C++. Se eligieron los punteros inteligentes como un compromiso. Describiremos cómo logramos cambiar el modelo de memoria en un artículo separado.

CodePorting tiene una sólida cultura de cobertura de pruebas, y la capacidad de aplicar las pruebas escritas para el código C# a los productos C++ simplificaría significativamente la solución de problemas. El traductor de código también debía ser capaz de traducir las pruebas.

Inicialmente, la corrección manual del código Java traducido permitió acelerar el desarrollo y los lanzamientos de productos. Sin embargo, a largo plazo, esto aumentó significativamente los gastos necesarios para preparar cada versión para el lanzamiento, ya que cada error de traducción debía corregirse cada vez que aparecía. Esto podría manejarse alimentando el código Java resultante con los parches calculados como la diferencia entre las salidas del traductor generadas para dos revisiones consecutivas del código C# en lugar de convertirlo desde cero cada vez. Sin embargo, se decidió priorizar la corrección del marco de C++ sobre la corrección del código resultante, solucionando cada error de traducción solo una vez.

Artículos relacionados: