22 noviembre 2024

Desafíos de la conversión de C# a C++ y cómo planeamos mejorar el traductor de código

Crear un traductor de código eficaz entre lenguajes como C# y C++ es una tarea compleja. El desarrollo de la herramienta CodePorting.Translator Cs2Cpp encontró muchos problemas debido a las diferencias en sintaxis, semántica y paradigmas de programación de estos dos lenguajes. Este artículo discutirá las principales dificultades que encontramos y las posibles formas de superarlas.

Problemas con la traducción de código y formas de superarlos

  1. La sintaxis de C# no tiene equivalentes directos en C++.

Esto concierne, por ejemplo, a los operadores using y yield:

using (var resource = new Resource())
{
    // Uso de un recurso
}
public IEnumerable<int> GetAllNumbers()
{
    for (int i = 0; i < int.MaxValue; i++)
    {
        yield return i;
    }
}

En tales casos, hay que escribir un código bastante complejo para emular el comportamiento del código fuente, tanto en el traductor como en la biblioteca, en el primer caso, o negarse a admitir tales construcciones, en el segundo.

  1. Las construcciones de C# no se traducen a C++ bajo nuestras reglas de traducción aceptadas.

Por ejemplo, el código fuente contiene métodos genéricos virtuales o constructores que utilizan funciones virtuales:

public class A
{
    public virtual T GenericMethod<T>(T param)
    {
        return param;
    }
}
public class A
{
    public A()
    {
        VirtualMethod();
    }

    public virtual void VirtualMethod()
    {
    }
}

public class B : A
{
    public override void VirtualMethod()
    {
    }
}

En tales casos, no tenemos más remedio que reescribir el código problemático en términos que puedan ser traducidos a C#. Afortunadamente, tales casos son raros y conciernen a pequeños fragmentos de código.

  1. El código C# depende de un entorno específico de .NET para ejecutarse.

Esto incluye recursos, reflexión, vinculación dinámica de ensamblajes e importación de funciones:

static void Main()
{
    var rm = new ResourceManager("MyApp.Resources", typeof(Program).Assembly);
    var value = rm.GetString("MyResource");
}
static void Main()
{
    var type = typeof(MyClass);
    var method = type.GetMethod("MyMethod");
    var result = method.Invoke(null, null);
    Console.WriteLine(result);
}

public class MyClass
{
    public static string MyMethod()
    {
        return "Hello, World!";
    }
}
static void Main()
{
    var assembly = Assembly.Load("MyDynamicAssembly");
    var type = assembly.GetType("MyDynamicAssembly.MyClass");
    var instance = Activator.CreateInstance(type);
    var method = type.GetMethod("MyMethod");
    method.Invoke(instance, null);
}

En tales casos, tenemos que emular los mecanismos correspondientes. Esto incluye soporte para recursos (incorporados en el ensamblaje como matrices estáticas y leídos a través de implementaciones de flujos especializados) y reflexión. Obviamente, no podemos conectar directamente ensamblajes .NET al código C++ ni importar funciones desde bibliotecas dinámicas de Windows cuando se ejecuta en otra plataforma, por lo que dicho código debe ser recortado o reescrito.

  1. El código depende de clases y métodos de .NET que no son compatibles con nuestra biblioteca.

En este caso, implementamos el comportamiento correspondiente, usualmente utilizando implementaciones de bibliotecas de terceros cuyas licencias no prohíban su uso en un producto comercial.

  1. El código de la biblioteca se comporta de manera diferente a las clases originales de .NET.

En algunos casos, se trata de errores de implementación simples que generalmente son fáciles de arreglar. Mucho peor, cuando la diferencia en el comportamiento radica en el nivel de los subsistemas utilizados por el código de la biblioteca.

Por ejemplo, muchas de nuestras bibliotecas utilizan extensivamente clases de la biblioteca System.Drawing, construida sobre GDI+. Las versiones de estas clases que desarrollamos para C++ utilizan Skia como motor gráfico. Skia a menudo se comporta de manera diferente a GDI+, especialmente en Linux, y tenemos que gastar recursos significativos para lograr la misma representación gráfica. De manera similar, libxml2, sobre la cual se construye nuestra implementación de System::Xml, se comporta de manera diferente en otros casos, y tenemos que parcharla o complicar nuestros envoltorios.

  1. El código traducido a veces se ejecuta más lento que el original.

Los programadores de C# optimizan su código para las condiciones en las que se ejecuta. Sin embargo, muchas estructuras comienzan a funcionar más lento en un entorno desconocido.

Por ejemplo, crear una gran cantidad de objetos pequeños en C# generalmente funciona más rápido que en C++ debido a diferentes esquemas de gestión de memoria (incluso considerando la recolección de basura). La conversión de tipos dinámica en C++ también es algo más lenta. La contabilidad de referencias al copiar punteros es otra fuente de sobrecarga ausente en C#. Finalmente, usar conceptos traducidos de C# (enumeradores) en lugar de los incorporados y optimizados de C++ (iteradores) también ralentiza el rendimiento del código.

La forma de eliminar los cuellos de botella depende en gran medida de la situación. Si el código de la biblioteca se puede optimizar relativamente fácil, mantener el comportamiento de los conceptos traducidos mientras se optimiza su rendimiento en un entorno desconocido puede ser bastante desafiante.

  1. El código traducido no se alinea con el espíritu de C++.

Por ejemplo, las API públicas podrían tener métodos que acepten SharedPtr<Object>, los contenedores carecen de iteradores y los métodos de manejo de flujos aceptan System::IO::Stream en lugar de istream, ostream o iostream, y así sucesivamente.

Ampliamos continuamente el traductor y la biblioteca para hacer nuestro código conveniente para los programadores de C++. Por ejemplo, el traductor ya puede generar métodos begin-end y sobrecargas que funcionan con flujos estándar.

  1. El código traducido expone nuestros algoritmos.

Los archivos de encabezado de C++ contienen tipos y nombres de campos privados, así como el código completo de los métodos plantilla. Esta información generalmente se ofusca al liberar ensamblajes .NET.

Nos esforzamos por excluir información innecesaria utilizando herramientas de terceros y modos especiales del traductor, pero esto no siempre es posible. Por ejemplo, eliminar campos estáticos privados y métodos no virtuales no afecta la operación del código cliente; sin embargo, es imposible eliminar o renombrar métodos virtuales sin perder funcionalidad. Los campos pueden ser renombrados, y sus tipos pueden ser reemplazados con stubs del mismo tamaño, siempre que los constructores y destructores sean exportados desde el código compilado con archivos de encabezado completos. Al mismo tiempo, es imposible ocultar el código de los métodos plantilla públicos.

Planes de desarrollo del proyecto

Los lanzamientos de productos para el lenguaje C++, creados utilizando nuestro framework, se han lanzado con éxito durante muchos años. Inicialmente, lanzamos versiones reducidas de los productos, pero ahora logramos mantener una funcionalidad mucho más completa.

Al mismo tiempo, todavía hay mucho espacio para mejoras y correcciones. Esto incluye apoyar construcciones sintácticas y partes de bibliotecas previamente omitidas, así como mejorar la facilidad de uso del traductor.

Además de resolver problemas actuales y mejoras planificadas, estamos trabajando en migrar el traductor al analizador de sintaxis moderno Roslyn. Hasta hace poco, usábamos el analizador NRefactory, que estaba limitado a soportar versiones de C# hasta la 5.0. La transición a Roslyn nos permitirá admitir construcciones modernas del lenguaje C#, como:

  • Coincidencia de patrones
  • Miembros de expresión-lambda
  • Tipos de referencia anulables
  • Y muchos otros

Finalmente, planeamos expandir el número de idiomas soportados, tanto de destino como de origen. Adaptar soluciones basadas en Roslyn para leer código VB será relativamente fácil, especialmente considerando que las bibliotecas para C++ y Java ya están listas. Por otro lado, el enfoque que utilizamos para soportar Python es mucho más simple, y de manera similar, otros lenguajes de scripting como PHP pueden ser soportados.

Noticias relacionadas

Videos relacionados

Artículos relacionados