22 marzo 2024
En este artículo, exploraremos cómo nuestro traductor convierte miembros de clase, variables, campos, operadores y estructuras de control de C#. También abordaremos el uso de la biblioteca de soporte del traductor para la conversión correcta de tipos del .NET Framework a C++.
Los métodos de clase se mapean directamente en C++. Esto también se aplica a los métodos estáticos y constructores. En algunos casos, puede aparecer código adicional – por ejemplo, para emular llamadas a constructores estáticos. Los métodos de extensión y operadores se traducen en métodos estáticos y se llaman explícitamente. Los finalizadores se convierten en destructores.
Los campos de instancia de C# se convierten en campos de instancia de C++. Los campos estáticos también permanecen sin cambios, excepto en casos donde el orden de inicialización es importante – esto se implementa traduciendo dichos campos como singletons.
Las propiedades se dividen en un método getter y un método setter, o solo uno si falta el segundo método. Para las auto-propiedades, también se agrega un campo de valor privado. Las propiedades estáticas se dividen en un getter estático y un setter. Los indexadores se procesan utilizando la misma lógica.
Los eventos se traducen en campos, cuyo tipo corresponde a la especialización requerida de System::Event
. La traducción en forma de tres métodos (add
, remove
e invoke
) sería más correcta y, además, permitiría soportar eventos abstractos y virtuales. Posiblemente, en el futuro, llegaremos a tal modelo, pero en este momento la opción de la clase Event
cubre completamente nuestras necesidades.
El siguiente ejemplo ilustra las reglas anteriores:
public abstract class Generic<T>
{
private T m_value;
public Generic(T value)
{
m_value = value;
}
~Generic()
{
m_value = default(T);
}
public string Property { get; set; }
public abstract int Property2 { get; }
public T this[int index]
{
get
{
return index == 0 ? m_value : default(T);
}
set
{
if (index == 0)
m_value = value;
else
throw new ArgumentException();
}
}
public event Action<int, int> IntIntEvent;
}
Resultado de la traducción de C++ (código insignificante eliminado):
template<typename T>
class Generic : public System::Object
{
public:
System::String get_Property()
{
return pr_Property;
}
void set_Property(System::String value)
{
pr_Property = value;
}
virtual int32_t get_Property2() = 0;
Generic(T value) : m_value(T())
{
m_value = value;
}
T idx_get(int32_t index)
{
return index == 0 ? m_value : System::Default<T>();
}
void idx_set(int32_t index, T value)
{
if (index == 0)
{
m_value = value;
}
else
{
throw System::ArgumentException();
}
}
System::Event<void(int32_t, int32_t)> IntIntEvent;
virtual ~Generic()
{
m_value = System::Default<T>();
}
private:
T m_value;
System::String pr_Property;
};
Los campos constantes y estáticos se traducen en campos estáticos, constantes estáticas (en algunos casos – constexpr
), o en métodos estáticos que proporcionan acceso a un singleton. Los campos de instancia de C# se convierten en campos de instancia de C++. Cualquier inicializador complejo se traslada a los constructores, y a veces es necesario agregar explícitamente constructores predeterminados donde no existían en C#. Las variables de pila se pasan tal cual. Los argumentos del método también se pasan tal cual, excepto que tanto los argumentos ref
como out
se convierten en referencias (afortunadamente, la sobrecarga en ellos está prohibida).
Los tipos de campos y variables se reemplazan por sus equivalentes en C++. En la mayoría de los casos, dichos equivalentes son generados por el propio traductor a partir del código fuente de C#. Los tipos de bibliotecas, incluidos los tipos de .NET Framework y algunos otros, están escritos por nosotros en C++ y forman parte de la biblioteca de soporte del traductor, que se suministra junto con los productos convertidos. var
se traduce en auto
, excepto en casos donde se necesita una indicación de tipo explícita para suavizar las diferencias en el comportamiento.
Además, los tipos de referencia se envuelven en SmartPtr
. Los tipos de valor se sustituyen tal cual. Dado que los argumentos de tipo pueden ser tipos de valor o de referencia, también se sustituyen tal cual, pero cuando se instancian, los argumentos de referencia se envuelven en SharedPtr
. Así, List<int>
se traduce como List<int32_t>
, pero List<Object>
se convierte en List<SmartPtr<Object>>
. En algunos casos excepcionales, los tipos de referencia se traducen como tipos de valor. Por ejemplo, nuestra implementación de System::String
se basa en el tipo UnicodeString
de ICU y está optimizada para el almacenamiento en pila.
Para ilustrar, traduzcamos la siguiente clase:
public class Variables
{
public int m_int;
private string m_string = new StringBuilder().Append("foobazz").ToString();
private Regex m_regex = new Regex("foo|bar");
public object Foo(int a, out int b)
{
b = a + m_int;
return m_regex.Match(m_string);
}
}
Después de la traducción, toma la siguiente forma (se elimina el código insignificante):
class Variables : public System::Object
{
public:
int32_t m_int;
System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);
Variables();
private:
System::String m_string;
System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;
};
System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b)
{
b = a + m_int;
return m_regex->Match(m_string);
}
Variables::Variables()
: m_int(0)
, m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar"))
{
this->m_string = System::MakeObject<System::Text::StringBuilder>()->
Append(u"foobazz")->ToString();
}
La similitud de las principales estructuras de control jugó a nuestro favor. Operadores como if
, else
, switch
, while
, do
-while
, for
, try
-catch
, return
, break
y continue
se transfieren mayormente tal cual. La excepción en esta lista es quizás solo el switch
, que requiere un par de tratamientos especiales. En primer lugar, C# permite su uso con el tipo de cadena – en C++ generamos una secuencia de if
-else if
en este caso. En segundo lugar, la adición relativamente reciente de la capacidad de hacer coincidir la expresión comprobada con una plantilla de tipo – que, sin embargo, también se despliega fácilmente en una secuencia de if
.
Las construcciones que no están presentes en C++ son de interés. Así, el operador using
garantiza la llamada del método Dispose()
al salir del contexto. En C++, emulamos este comportamiento creando un objeto guardián en la pila, que llama al método requerido en su destructor. Sin embargo, antes de eso, es necesario capturar la excepción que lanzó el código que era el cuerpo de using
, y almacenar el exception_ptr
en el campo del guardián – si Dispose()
no lanza su excepción, la que almacenamos será relanzada. Este es justo ese caso raro cuando el lanzamiento de una excepción desde un destructor está justificado y no es un error. El bloque finally
se traduce según un esquema similar, solo que en lugar del método Dispose()
, se llama a una función lambda, en la que el traductor envolvió su cuerpo.
Otro operador que no está presente en C# y que nos vemos obligados a emular es foreach
. Inicialmente, lo tradujimos en un while
equivalente, llamando al método MoveNext()
del enumerador, que es universal pero bastante lento. Dado que la mayoría de las implementaciones en C++ de contenedores .NET utilizan estructuras de datos STL, hemos llegado a usar sus iteradores originales donde sea posible, convirtiendo foreach
en for
basado en rango. En casos donde los iteradores originales no están disponibles (por ejemplo, el contenedor está implementado en C# puro), se utilizan iteradores envoltorios, que trabajan internamente con enumeradores. Anteriormente, la elección del método de iteración correcto era responsabilidad de una función externa, escrita usando la técnica SFINAE, ahora estamos cerca de tener las versiones correctas de los métodos begin
-end
en todos los contenedores, incluidos los traducidos.
Al igual que con las estructuras de control, la mayoría de los operadores (al menos aritméticos, lógicos y de asignación) no requieren un procesamiento especial. Sin embargo, hay un punto sutil: en C#, el orden de evaluación de las partes de una expresión es determinista, mientras que en C++ puede haber un comportamiento indefinido en algunos casos. Por ejemplo, el siguiente código traducido se comporta de manera diferente después de la compilación por diferentes herramientas:
auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +
block[i++] * 256 * 256 * 256;
Afortunadamente, tales problemas son bastante raros. Tenemos planes de enseñar al traductor a lidiar con tales momentos, pero debido a la complejidad del análisis que identifica expresiones con efectos secundarios, esto aún no se ha implementado.
Sin embargo, incluso los operadores más simples requieren un procesamiento especial cuando se aplican a propiedades. Como se mostró anteriormente, las propiedades se dividen en métodos getter y setter, y el traductor tiene que insertar las llamadas necesarias dependiendo del contexto:
obj1.Property = obj2.Property;
string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());
System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),
&MyClass::get_Property, &MyClass::set_Property, u"suffix")
En la primera línea, el reemplazo resultó ser trivial. En la segunda, fue necesario usar el envoltorio setter_add_wrap
, asegurando que la función GetObj()
se llame solo una vez, y el resultado de concatenar la llamada a get_Property()
y el literal de cadena se pasa no solo al método set_Property()
(que devuelve void
), sino también más adelante para su uso en la expresión. El mismo enfoque se aplica al acceder a indexadores.
Los operadores de C# que no están en C++: as
, is
, typeof
, default
, ??
, ?.
, y así sucesivamente, se emulan utilizando funciones de la biblioteca de soporte del traductor. En casos donde es necesario evitar la doble evaluación de argumentos, por ejemplo, para no desplegar GetObj()?.Invoke()
en GetObj() ? GetObj().Invoke() : nullptr
, se utiliza un enfoque similar al mostrado anteriormente.
El operador de acceso a miembros (.
) puede ser reemplazado por un equivalente de C++ dependiendo del contexto: el operador de resolución de ámbito (::
) o la “flecha” (->
). Tal reemplazo no es requerido al acceder a miembros de estructuras.