La estructura intraducible, I (de Interop y otras yerbas)

A veces resulta curioso cómo algo tan sencillo como mapear una estructura de C en .NET se puede convertir en un algo muy cercano a una pesadilla dependiendo del lenguaje que hayamos elegido para realizarlo. Partamos de la siguiente estructura de C:

typedef struct
{
    BYTE byOut[4]; // Outputs [0..3]
    BYTE byAux; // Aux output
} usbOutput;

A simple vista es algo muy elemental: un array de cuatro bytes y uno más después. Esto en una máquina Intel en realidad termina ocupando 8 bytes, 4 para el primer elemento y otros cuatro para el segundo si no se cambia la alineación.

La traducción a C# requiere de ciertos trucos un tanto oscuros y no muy bien documentados:

[StructLayoutAttribute(LayoutKind.Sequential)]
public struct usbOutput
{
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] byOut;
    public byte byAux;
}

Todo el secreto está en la declaración de los atributos que preceden al array. Básicamente le estamos diciendo al runtime que lo que viene a continuación es un array embebido directamente y no un array normal; es decir, que en lugar de guardar un puntero en la estructura y coloque el array en otro lugar, lo que está haciendo es colocar el array de cuatro elementos directamente en la estructura.

El autor no sabe si esa construcción funciona o no porque no lo ha probado, pero supone que debe hacerlo ya que si no difícilmente se iba a poder trabajar con Win32 desde C#.

Pero resulta que uno está haciendo el ensamblado en C++/CLI para aprovechar las facilidades del código mixto, por lo que piensa que la traducción directa servirá:

[StructLayout(LayoutKind::Sequential)]
public value struct usbOutput
{
    [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 4)]
    Array<byte>^ byOut;
    byte byAux;
};

Pero tras las primeras pruebas descubre que no, que .NET te ignora olímpicamente y el array byOut se queda como un puntero en la estructura, con el consiguiente desbarajuste cuando dicha estructura es bloqueada (mediante un pin_ptr) y pasada a la función nativa. Con la experiencia del autor, en general o bien se te cuelga el IDE o bien el aplicativo empieza a hacer cosas bastante extrañas…

Tras recorrerse Internet completo, de hecho el autor llegó hasta aquí, no descubre nada que le sea útil excepto una construcción de Nishant Sivakumar que no le funciona bien. Pero como el autor de esta entrada tiene el libro de Nish, busca el original y descubre que la versión encontrada en Internet no funciona, pero la del libro sí lo hace, pero funciona sólo en C++/CLI, ya que si se llama a byIn desde C#, el compilador falla debido a que la sobrecarga de operadores es mucho más versátil en C++ y C++/CLI que en C# y éste no sabe cómo actuar.

La solución de Nish se puede encontrar en la página 185 de su libro titulado C++/CLI in Action, que recomiendo encarecidamente a todo aquel que quiera aprender este lenguaje.

Mi solución parte de la de Nish con una ligera modificación, que es la de sustituir la sobrecarga del operador de acceso indexado, [], por el indexador por defecto.

La solución

La potencia expresiva de C++ es prácticamente infinita, ya que si existe alguna construcción en cualquier otro lenguaje que no exista en C++, resulta poco menos que seguro que se pueda simular y casi integrar en el lenguaje a través una biblioteca que simule el tipo o el constructo necesario, y eso es lo que aquí hemos hecho.

Necesitamos algo que nos permita insertar una secuencia repetida de un mismo elemento y que tenga semántica de array… Pues nada, elemental, querido Watson:

template<typename T,int size>
[StructLayout(LayoutKind::Sequential,Size=sizeof(T)*size)]
public value class FixedSizeArray
{
    T FixedElementField;
    public:
    property T default[int]
    {
        T get(int i)
        {
            return *(&FixedElementField+i);
        }
        void set(int i,T t)
        {
            *(&FixedElementField+i)=t;
        }
    }
};

Creamos un nuevo tipo que recibe un tipo llamado T y un entero que va a indicar el tamaño estático del array. Luego le decimos a .NET que nuestra clase es una estructura secuencial y que su tamaño es de tantas veces como elementos a repetir en relación al tipo base.

Y declaramos el tipo en cuestión, que hemos llamado FixedSizeArray y que contiene un elemento del tipo T. Asimismo posee un indexador por defecto que recibirá un entero que será el índice del elemento a leer o escribir. Lo único que le falta al ejemplo es controlar el tamaño del índice, pero como en el caso del autor es algo interno a él mismo, lo obvia.

La línea más curiosa de todas es el acceso al elemento interno:

*(&FixedElementField+i)

Expresándolo paso a paso, en primer lugar obtenemos la dirección de memoria de FixedElementField a través del operador &. A esa dirección añadimos el índice como si de un puntero de C++ se tratara, y luego desreferenciamos para obtener (o asignar) el valor.

A primera vista no debería funcionar, pero sí que lo hace ya que debemos tener conocimiento de cómo C++ y C++/CLI (y en general casi cualquier lenguaje orientado a objetos funciona). Cuando uno crea un objeto de una clase, sólo está creando el bloque de datos del mismo, ya que la clase ha sido traducida a una lista de funciones más o menos globales que residen en el segmento de código, y cuando se invoca un método cualquiera, realmente estamos haciendo una llamada de función global pasando en primer lugar un puntero a la dirección donde están los datos…

El uso de esta construcción es bien sencilla:

[StructLayout(LayoutKind::Sequential)]
public value struct usbOutput
{
    FixedSizeArray<byte,4>byOut;
    byte byAux;
};

Simplemente indicamos un objeto del tipo FixedSizeArray del tipo y tamaños adecuado. Y el acceso desde, por ejemplo, C#, todavía lo es más:

usbInput input=new usbInput();
input. byOut[2]=55;
Console.WriteLine(input. byOut[2]);
GetInputs(ref input);
//etc.

La única limitación de esta construcción es que sólo permite simular arrays de una dimensión. En una próxima entrada pondré cómo hacerlo para elementos de varias.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *