0

I have an array full of instances of this class

public class TransformData {
    public float pos_x, pos_y, pos_z;
    public float rot_x, rot_y, rot_z;
    public float scale_x, scale_y, scale_z;
}

I'm trying to convert this array to bytes using:

byte[] byteArray = new byte[transformArrayByteSizeHere];
uffer.BlockCopy(transformArrayHere, 0, byteArray, 0, transformArrayByteSizeHere);

So that I can write a binary file. However I'm getting: "Object must be an array of primitives." I gather this means that I can only convert to bytes from types that C# already knows about, such a float? But why? My class is full of primitive types... I feel as though it should know how to do this. What am I missing?

Stradigos
  • 814
  • 1
  • 13
  • 29
  • TransformData is not a primitive. If you are trying to write out an array of TransformData, that is not an array of primitives – Joe Oct 17 '15 at 02:11

3 Answers3

4

One thing you should be aware of is that an array of reference type (in C# the class keyword denotes a reference type) is not even contiguous. There's a buffer holding all the pointers contiguously, but the content itself is scattered everywhichway in the GC heap.

Changing to a structure (value type) would fix that, and indeed, you could then blit it using p/invoke or C# raw pointers. But Buffer.BlockCopy is not smart enough to see that a user-defined structure is made up of primitives and therefore copyable, it just rejects all compound types. So you must make up your mind between bulk copy using unsafe code, or doing it element-by-element field-by-field by hand.

The fast way, requiring the unsafe keyword, is roughly:

// assumes TransformData is struct, not class
fixed( TransformData* p = &transformArray[0] )
    Marshal.Copy(new IntPtr(p), byteArray, 0, byteArray.Length);
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
1

Unlike basic array of int/byte/float there is no binary standard on how other types are present in memory. So low level operations are not practical on general types (in addition to the fact that in some cases bitwise copy may cause issues with consistency - i.e. clone FileStream or Drawing.Pen object without updating internal counters will lead to very bad issues).

It is generally better to use well defined serialization format (XML/JSON if text is fine) to store data. If you need more compact binary format for many objects Binary Serialization would work.If you need compatibility with non .Net systems something like proto-buff may be better option (see When should I use XML Serialization vs. Binary Serialization in the .NET framework? for discussion).

In all cases of serializtion you need to be aware that there is large set of types that can't be serialized/deserialized due to presence of run-time data (i.e. connection/service types like SqlConnection or OS level types - files, drawing object).

If you really want to copy bytes - limiting your types to some struct may work with Marshl.Copy - see How to convert a structure to a byte array in C#?.

Community
  • 1
  • 1
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • Well that's disappointing. I mean, it already knows the size of my TransformData class because it can instantiate it with "new", and to know the size you have to know the data types within it... I just don't see why it cares what types are in it since they're still just binary data. I guess I've spent too much time in C++? I'm pretty sure this is something I've done before simply by using a reference like myFunc(&myArray[0], size). – Stradigos Oct 17 '15 at 02:20
  • @Stradigos C++? `class MyString {public char* text;}` is almost as pointless to copy as bytes... or save VMT pointer to file - what are you going to do with that later? – Alexei Levenkov Oct 17 '15 at 02:22
  • @AlexeiLevenkov: He doesn't have any pointers in his UDT.... and probably didn't realize he had them in the array. – Ben Voigt Oct 17 '15 at 02:27
1

I think the best way would have been to change your class to a struct and use Array.Copy, if possible.

But be aware that "Array.Copy" while used for copying in into the same array is badly implemented and very slow (more than twice as slow as in C++). (as of 2017-06-01)

This is part of code where I do it calling extern C code:

[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false), SuppressUnmanagedCodeSecurity]
        public static unsafe extern void* CopyMemory(void* dest, void* src, ulong count);


        // ************************************************************************
        /// <summary>
        /// Insert an "item" at "index" position into an "array" which could be bigger 
        /// than the real count of items ("countOfValidItems") in it.
        /// </summary>
        /// <param name="array"></param>
        /// <param name="item"></param>
        /// <param name="index"></param>
        /// <param name="countOfValidItems"></param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        unsafe public static void InsertItemPinvokeC(Point[] array, Point item, int index, ref int countOfValidItems)
        {
            if (countOfValidItems >= array.Length)
            {
                Point[] dest = new Point[array.Length * 2];

                // The next 2 calls are replaced by a call to CopyMemory (memcopy from defined in C library "msvcrt.dll").
                // Array.Copy(array, 0, dest, 0, index);
                // Array.Copy(array, index, dest, index + 1, countOfValidItems - index);

                //Buffer.BlockCopy(array, 0, dest, 0, (countOfValidItems - index) * sizeof(Point));
                //Buffer.BlockCopy(array, index * sizeof(Point), dest, (index + 1) * sizeof(Point), (countOfValidItems - index) * sizeof(Point));

                fixed (Point* s = array)
                {
                    fixed (Point* d = dest)
                    {
                        CopyMemory(d, s, (ulong)(index * sizeof(Point)));
                        CopyMemory(d + ((index + 1) * sizeof(Point)), s + (index * sizeof(Point)), (ulong)((countOfValidItems - index) * sizeof(Point)));
                    }
                }

                array = dest;
            }
            else
            {
                // Array.Copy(array, index, array, index + 1, countOfValidItems - index);

                // Buffer.BlockCopy(array, index * sizeof(Point), array, (index + 1) * sizeof(Point), (countOfValidItems - index) * sizeof(Point));

                fixed (Point* p = array)
                {
                    CopyMemory(p + ((index + 1) * sizeof(Point)), p + (index * sizeof(Point)), (ulong)((countOfValidItems - index) * sizeof(Point)));
                }
            }

            array[index] = item;
            countOfValidItems++;
        }
Eric Ouellet
  • 10,996
  • 11
  • 84
  • 119
  • I think you may have made an error with the pointer arithmetic. When you add the index offset to a pointer, you don't need to scale the indices by `sizeof(Point)` as the pointer is a `Point*` type. – Stephen Smolley Jan 17 '22 at 05:35
  • @StephenSmolley In C# a Point is a "struct" which is considered as a simple type like an "int", "float" or "double". A struct is passed on the stack, not its pointer. An array of struct really contains struct, not a pointer. Then an array of Point, in C#, really contains Point and not Point*. Each element is a real struct of Point, ex: dest[0] = Point where its size is sizeof(Point). – Eric Ouellet Jan 18 '22 at 15:44