Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use an array of Vector2f structs as an array of floats in-place

Tags:

c#

struct

I have an array Vector2f structs that each contain two floats, and I want to pass it to a function that takes an array of floats. These structs represent 2d coordinates, and I want the end result to be [x0, y0, x1, y1, ... xn, yn]. Some code to demonstrate:

using System;
using System.Runtime.InteropServices;

public class Test
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Vector2f
    {
        float x;
        float y;

        public Vector2f(float x, float y)
        {
            this.x = x;
            this.y = y;
        }
    }

    public static void Main()
    {
        Vector2f[] structs = new Vector2f[]
        {
            new Vector2f(1f, 2f),
            new Vector2f(3f, 4f)
        };

        // I want this to contain 1f, 2f, 3f, 4f
        // But Syntax error, cannot convert type!
        float[] floats = (float[])structs;
    }
}

This is easy by copying the contents into a new array of floats, but the data gets large and it would be nice not to duplicate it.

This may not be possible due to memory layout.

like image 503
31eee384 Avatar asked May 19 '15 18:05

31eee384


2 Answers

If you don't actually need to pass around a true array, but just something that can be accessed like an array, you can do something like this (untested):

public sealed class FloatArrayAdaptor : IReadOnlyList<float>
{
    private Vector2f[] _data;

    public FloatArrayAdaptor(Vector2f[] data)
    {
        _data = data;
    }

    public IEnumerator<float> GetEnumerator()
    {
        for (int i = 0; i < _data.Length; i++)
        {
            yield return _data[i].x;
            yield return _data[i].y;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int Count
    {
        get { return 2*_data.Length; }
    }

    public float this[int index]
    {
        get
        {
            //TODO: Add appropriate range checking and whatnot
            int i = index>>1;
            bool isX = (index & 0x1) == 0;
            return isX ? _data[i].x : _data[i].y;
        }
    }
}

You won't be able to 'recast' the type in C# like you can do in C. The closest you can get is to use unsafe code and grab an actual float*, but even then you can't treat that pointer like a safe array for passing to methods that take an array.


I did some experimenting and it's possible with unsafe code to 'convert' the type, but it's a terrible, terrible hack and you shouldn't actually do this. Nevertheless, it demonstrates some interesting things about the CLR object header data:

    public static unsafe void Main()
    {
        Vector2f[] data = new Vector2f[10];
        float[] dummy = new float[1];

        //NOTE: This is horrible and you should never actually do it
        //After this code, the original data array cannot be used safely anymore
        fixed (void* rawData = &data[0])
        fixed (void* rawDummy = &dummy[0])
        {
            int* intData = (int*)rawData;
            int* intDummy = (int*)rawDummy;

            //method table pointer is at X-4-sizeof(IntPtr)
            //This is what identifies the type via RTTI
            //We're going to forge our identity and change our size to change our type
            //This assumes x86
            intData[-2] = intDummy[-2];

            //Our length is doubled
            intData[-1] = 2*intData[-1];
        }

        if (data.GetType() == typeof(float[]))
            Console.WriteLine("Type is now float[]!");

        float[] floatData = (float[])(object)data;

        Console.ReadLine();
    }

Basically we replace the method table pointer so that the type now appears to be a float[], then we double the array's length field to compensate. This compiles, runs and reports the type is now float[]. That said, this might well blow up the GC later in some spectacular way and it's certainly quite implementation dependent, plus this doesn't deal with x64 vs. x86. Still, interesting... There's a reason this is called 'unsafe', though. Hopefully this helps demonstrates why it can't be supported in a safe way, as the RTTI (via the method table pointer) is baked into the memory where the data is stored.

like image 73
Dan Bryant Avatar answered Nov 14 '22 10:11

Dan Bryant


Now trying to answer why it is not possible.

C# is a type-safe language. This means that only certain conversion (cast) are allowed on compatible types. This explains why this code is not allowed:

Vector2f[] structs;
float[] floats = (float[])structs;

Indeed, C# use references instead of pointers. One of the difference is that a reference is not a static location in memory. Object can be moved during garbage collection.

However, C# allow some pointer arithmetic with unsafe code. To do so, the garbage collector must be notified that memory should not be moved (and that references must not be invalidated) for the considered objects. This is done with the fixed keyword.

In other word, to get a pointer to a reference object (same logic for a struct), you first need to freeze the object location in memory (this is also called pinned memory) :

fixed (Vector2f* pStructs = structs)
fixed (float* pFloats = floats)
{
    ...

Now that all is fixed you are not allowed to change the address of those pointers. This for example is not allowed:

pFloats = (float*)pStructs // this will change the address of pFloats which is fixed: illegal

Also you cannot convert a pointer back to a reference:

float[] floats = (float[])pFloats; // not allowed

In conclusion, once you get pointer you are able to move some bytes from one location to another, but you cannot change the location of the corresponding references (only the data can be moved).

Hope this answer your question.

As a side note, if you have a lot of performance-critical operations, you may consider implementing it in C++, expose some high-level functions and call some functions from C#.

like image 25
Kryptos Avatar answered Nov 14 '22 10:11

Kryptos