Memoria compartida entre C# struct

 

Problemática:

Imaginemos que estamos desarrollando un graphics ó physics engine en C#, el cual queremos que sea multiplataformas por lo que luego lo usaremos contra diferentes APIs como XNA, OpenTK y SharpDX. Trabajaremos con nuestras propias estructuras para Matrix4x4 ó Vector3 por ejemplo y luego las tendremos que traducir a las structs concretas de cada API.

 

La struct para Vector3 de nuestro motor puede ser tan simple como:

  1. public struct Vector
  2. {
  3.     public float X;
  4.     public float Y;
  5.     public float Z;
  6.  
  7.     public Vector(float x, float y, float z)
  8.     {
  9.         this.X = x;
  10.         this.Y = y;
  11.         this.Z = z;
  12.     }
  13.  
  14.     public override string ToString()
  15.     {
  16.         return string.Format("(X:{0} Y:{1} Z:{2})", X, Y, Z);
  17.     }
  18. }

 

Primera idea:

Por ejemplo para usarlo con XNA, una vez hayamos hecho todos nuestros cálculos para tener la posición de nuestro player necesitaríamos pasar el resultado a un método XNA del estilo:

  1. public void XNAMethod(ref Microsoft.Xna.Framework.Vector3 vector)

 

Como se puede observar el parámetro que se le pasa es un Vector3, y esta estructura a pesar de tener los mismos campos que nuestra estructura no puede “castearse” directamente:

 

  1. Vector myVector = new Vector(1,2,3);
  2. Vector3 xnaVector = (Vector3)myVector;

Error    1    Cannot convert type ‘WindowsGame1.Vector’ to ‘Microsoft.Xna.Framework.Vector3’   

 

(El error nos recuerda que no se puede hacer el casting explícito).

 

Tendremos que crear una struct nueva y copiar todos los valores, por ejemplo:

  1. Vector myVector = new Vector(1,2,3);
  2. Vector3 xnaVector = new Vector3() { X = myVector.X, Y = myVector.Y, Z = myVector.Z };

Pros:

Nos permite intercambiar datos entre las struct de XNA y las de nuestro motor.

 

Contra:

Esta solución no mola mucho ya que hay que hacer un new y duplicar las mismas struct en memoria.

 

 

Segunda idea:

Una segunda idea pasa por intentar algo similar a los Union de C++, lo cual nos permite tener dos tipos distintos de estructuras compartiendo la misma zona de memoria. Esto en C# se hace con los Attributes StructLayout y FieldOffset que encontramos dentro del namespace InteropServices:

  1. using System.Runtime.InteropServices;
  2. [StructLayout(LayoutKind.Explicit)]
  3. public struct UnionVector
  4. {
  5.     [FieldOffset(0)]
  6.     public Microsoft.Xna.Framework.Vector3 XnaVector;
  7.     [FieldOffset(0)]
  8.     public float X;
  9.     [FieldOffset(4)]
  10.     public float Y;
  11.     [FieldOffset(8)]
  12.     public float Z;
  13. }

 

Con esto le estamos indicando cómo hacer el matching a nivel de bytes, ya que especificamos el offset de cada uno de los campos (un float son 4 bytes). Podemos indicar la conversión con varias struct, no solo con una:

  1. [StructLayout(LayoutKind.Explicit)]
  2. public struct UnionVector
  3. {
  4.     [FieldOffset(0)]
  5.     public Microsoft.Xna.Framework.Vector3 XnaVector;
  6.     [FieldOffset(0)]
  7.     public OpenTK.Vector3 OpenTkVector;
  8.     [FieldOffset(0)]
  9.     public float X;
  10.     [FieldOffset(4)]
  11.     public float Y;
  12.     [FieldOffset(8)]
  13.     public float Z;
  14. }

 

Luego lo único que tenemos que hacer es inicializar la estructura y leer los datos con el tiempo concreto que necesitemos en cada caso, es importante destacar que están trabajando sobre la misma zona de memoria:

  1. UnionVector myVector = new UnionVector() { X = 1, Y = 2, Z = 3};
  2. Vector3 xnaVector = myVector.XnaVector;
  3. OpenTK.Vector3 oTKVector3 = myVector.OpenTkVector;

 

Pros:

Trabajamos con la misma zona de memoria, por lo que nos evitamos el duplicado de struct y el coste de los new.

 

Contra:

En la dll en la que definamos nuestra struct Union tenemos que referenciar a las dlls de XNA, OpenTK, por lo que el código de nuestras struct no es independiente, y si en alguna plataforma no podemos enlazar las dlls necesarias no funcionará.

 

 

Tercera idea:

Si la plataforma soporta código unsafe y las struct son idénticas a nivel de bytes podemos intentar referenciar con punteros del tipo necesario a las zonas de memoria donde están nuestras struct:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     // Creamos un puntero de nuestro tipo de struct que apunta a la dirección de memoria de nuestro vector.
  5.     Vector* pVector = &vector;
  6.    
  7.     // Creamos un puntero del tipo de struct de XNA y hacemos un casting entre punteros.
  8.     Vector3* pVector3 = (Vector3 *)pVector;
  9.     // Los métodos de XNA no permiten pasar como parámetro un puntero, por lo que necesitamos la última conversión.
  10.     Vector3 vector3 = *pVector3;
  11.     XNAMethod(vector3);
  12. }

 

Todo esto que hemos hecho por pasos se puede simplicar en una sola linea de la siguiente forma:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     XNAMethod(*(Vector3*)&vector);
  5. }

 

Estas llamadas pasan los parámetros de tipo struct por valor haciendo una copia de la struct en la llamada, muchos de los métodos de XNA tienen las sobrecargas para evitar esto usando ref, out.

La llamada a un método de este tipo quedaría:

  1. Vector vector = new Vector(1, 2, 3);
  2. unsafe
  3. {
  4.     XNAMethod(ref *(Vector3*)&vector);
  5. }

 

Pros:

Evitamos los new para convertir los tipos, evitamos el duplicado en memoria y no es necesario referenciar en la dll donde creamos nuestra struct a las dlls donde se encuentra las otras structs a las que queremos convertir.

 

Contra:

No todas las plataformas soportan código unsafe, por ejemplo en Windows Phone 7 no podríamos hacer esta optimización.

 

 

Espero que todo esto os mole tanto como a mi XD.

 

Saludos