Update float array from C++ native plugin

I am seeing a pretty bizarre issue while trying to pass an array from C++ to C#. I am using Marshal.Copy (specifically: https://msdn.microsoft.com/en-us/library/a53bd6cz(v=vs.110).aspx).

Problem: float array from C++ to C# is yielding a few NaN's in the resulting array. (Note: I am working in the context of the Unity game engine)


Code

Example C++ code:

extern "C" bool UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API getSomeFloats(float** points, int* count) {
        std::vector<float> results;
        std::vector<SOME_TYPE> key_points = <SOME_POINTS>

        for (auto iter = key_points.begin(); iter < key_points.end(); iter++) {
            results.push_back(static_cast<float>(iter->pt.x));
            results.push_back(static_cast<float>(iter->pt.y));
        }

        *points = results.data();
        *count = results.size();

        //<Print results to csv here>

        return true;
}

Example C# code:

[DllImport("NativePlugin")]
private static extern bool getSomeFloats (ref IntPtr ptrResultItems, ref int resultItemsLength);

private static float[] getFloatArrayFromNative() {
        IntPtr ptrResultItems = IntPtr.Zero;
        int resultItemsLength = 0;
        bool success = getSomeFloats (ref ptrResultItems, ref resultItemsLength);
        float[] resultItems = null;
        if (success) {
            // Load the results into a managed array.
            resultItems = new float[resultItemsLength];
            Marshal.Copy (ptrResultItems
                , resultItems
                , 0
                , resultItemsLength);

            // <PRINT out resultItems to csv here>

            return resultItems;
        } else {
            Debug.Log ("Something went wrong getting some floats");
            return new float[] { -1, -2 };
        }
    }

Example Ouput: Take the following example: C++ output (print_out.csv):

123, 456, 789

C# output (print_out_cs.csv):

123, NaN, 789


I'm completely stumped on this one. I just don't understand why only some (roughly 7/100) floats are returning NaN. Does anyone have any advice/insight that might help?

Thanks!


Found few problems in your code:

1. std::vector<float> results; is declared on the stack. It will be gone by the time the function has returned. Declare it as a pointer

std::vector<float> *results = new std::vector<float>(10);

but make sure to also declare a function that will free it on the C++ side.

2.The function parameter do not match.

Your C++:

getSomeFloats(float** points, int* count, CameraPose* pose)

Your C#:

getSomeFloats (ref IntPtr ptrResultItems, ref int resultItemsLength);

You either have to remove CameraPose* pose from the C++ side or add IntPtr pose to the C# side.

3. The use of UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API.

You don't need that. This is used when you want to use Unity's built in functions such as GL.IssuePluginEvent. You are not using it in this case.

This should do it:

#define DLLExport __declspec(dllexport)

extern "C"
{
    DLLExport void fillArrayNative(float* data, int count, int* outValue) 
    {
    }
}

4.C# has a garbage collector that moves variables around in the memory. You must pin C# array if you want to modify it from C++ side. You only need to pin C# array. Another option is to allocate the array on C++ side, return it to C# copy it to a temporary variable on the C# side then delete it on the C++ side.

5.Copy the result back to the array instead of assigning it.



Recommended method:

There are just many ways to do this and some of them are extremely slow. If you want to use Marshal.Copy, you have to allocate the array on the C++ side or else you will run into some undefined behavior.

The fastest and the most efficient way to do this is to allocate the array on the C# side as a global variable. Pass the array and its length to the native side. Also pass a third parameter which C++ can use to tell C# the amount of index that has been updated or written to.

This is much more better than creating new array, copying it to C# variable then destroying it each time the function is called.

This is what you should be using:

C++:

#define DLLExport __declspec(dllexport)

extern "C"
{
    DLLExport void fillArrayNative(float* data, int count, int* outValue) 
    {
        std::vector<float> results;
        for (int i = 0; i < count; i++)
        {
            //Fill the array
            data[i] = results[i];
        }
        *outValue = results.size();
    }
}

You can also use: std::copy ( data, data+count, results.begin() ); instead of loop to copy the data too.

C#:

[DllImport("NativePlugin", CallingConvention = CallingConvention.Cdecl)]
private static extern void fillArrayNative(IntPtr data, int count, out int outValue);

public unsafe void getFillArrayNative(float[] outArray, int count, out int outValue)
{
    //Pin Memory
    fixed (float* p = outArray)
    {
        fillArrayNative((IntPtr)p, count, out outValue);
    }
}

Usage:

const int arraySize = 44500;
float[] arrayToFill = new float[arraySize];

void Start()
{
    int length = arrayToFill.Length;
    int filledAmount = 0;
    getFillArrayNative(arrayToFill, length, out filledAmount);

    //You can then loop through it with with the returned filledAmount
    for (int i = 0; i < filledAmount; i++)
    {
        //Do something with arrayToFill[i]
    }
}

This is just an example and it is faster than all other methods I've used before. Avoid doing it the way you are currently doing it with Marshal.Copy. If you still want to do it your way or use Marshal.Copy then here is the appropriate way to do it which requires allocation, copying data and de-allocating memory in each call.


The pointer you return in getSomeFloats is owned by results. Before getSomeFloats returns, the vector destructor for results will free that memory. When the C# code tries to use the pointer, you are accessing unallocated memory, which results in Undefined Behavior. In your case most of the data hasn't been changed yet, but some of it has. Potentially any or all of the data could have been changed (if the memory has been reused), or even a program crash (if the freed memory has been returned to the OS).