BinaryWriter Floats

So for some reason, serializing a float using binarywriter allocates memory. This does not happen when writing an integer. Is there any way to go about doing this without allocating memory?

1 Like

Perhaps I could remove the decimal from the float, send that “integer” over the wire, decode it as a integer on the client, convert it to a float, and just shift the decimal places?

It is doing that because inside the Mono implementation it resolves to this:

unsafe private static byte[] GetUIntBytes (byte *bytes)
{
    if (BitConverter.IsLittleEndian)
        return new byte [] { bytes [0], bytes [1], bytes [2], bytes [3] };
    else
        return new byte [] { bytes [3], bytes [2], bytes [1], bytes [0] };
}

https://github.com/mono/mono/blob/def4f3be68c44c3fb353fe46f00d540f4ed9bd15/mcs/class/Mono.Security/Mono.Security/BitConverterLE.cs#L48

You can pack your floats yourself if you want.
One trick you can use is a preallocated fixed layout struct.

[StructLayout(LayoutKind.Explicit)]
public struct ConverterStruct
{
    [FieldOffset(0)] public int integerValue;
    [FieldOffset(0)] public float floatValue;
}
var converter = new ConverterStruct();
converter.floatValue = 10f;

binarywriter.Write(converter.integerValue);

But be careful, do this only with types the same sizes in bytes. And you should always interpret the values you read back on the same way, but backwards, obviously.

1 Like

I appreciate the response, but I’m not really clear as to what the struct is doing. Can you provide a little context? Sorry I’m exhausted just trying to get this working

If you’re familiar with the C-style Unions, this is the same concept.
The members of the struct aligned in memory in the relative position the FieldOffset tells them to. Since I put 0 in both, they both lay out in memory over the same bytes. When you put a float into it the float’s bytes will be filled into those 4 bytes. When you read as an integer, you read the same 4 bytes as an integer. The value itself doesn’t get converted, every single bit will be at the same position, we just assume that those bits now an integer.
You basically have a 4 bytes window in memory to write an int or a float into it and get the same 4 bytes as int or as float as you wish.

So can I deserialize this normally using binaryreader’s ReadSingle()?

Yes, you can read it with ReadSingle(). The bytes are the same. Unless of course the ReadSingle also generates garbage. :smile:

Test script:

using System.IO;
using System.Runtime.InteropServices;
using UnityEngine;

namespace LurkingNinja.GameWizards.Runtime
{
    public class Test : MonoBehaviour
    {
        private string _filepath;
        private BinaryWriter _bwrite;
        private BinaryReader _bread;

        [StructLayout(LayoutKind.Explicit)]
        public struct ConverterStruct
        {
            [FieldOffset(0)] public int integerValue;
            [FieldOffset(0)] public float floatValue;
        }
        private void Awake()
        {
            var converter = new ConverterStruct {floatValue = 13131.13f};

            _filepath = Application.dataPath + "/test.sav";
            _bwrite = new BinaryWriter(File.Open(_filepath, FileMode.Create));
            _bwrite.Write(converter.integerValue);
            _bwrite.Close();

            _bread = new BinaryReader(File.Open(_filepath, FileMode.Open));
            var f = _bread.ReadSingle();
            //  converter.integerValue = _bread.ReadInt32();
            _bread.Close();
            // var f = converter.floatValue;
            Debug.Log(f);
        }
    }
}

Awesome, appreciate it a ton!

1 Like

Quick question though, can I use a single struct to perform this operation on multiple floats? Just increase the field offset right?

Yes, you can, and you can convert any type, you just need to calculate the sizes in bytes and adjust the difference between numbers.
If you only use this for int ↔ float, then you just increment in fours. You also can define a byte[ ] for the 0 position with a capacity of the size of your other members combined in bytes. :wink: Basically you can map it into a byte array. Or whatever.
Just always test the outcome before you put it into production. And if you have unit tests, this piece must be in it. It’s easy to miscalculate byte sizes.

So I was reading up on union structs and apparently when you do byte stuff, it won’t always be the same on all processors? Does that apply here?

I’m not too familiar with how endianness works

Well, you know your use case, is it expected the file to be read on different type of devices? Is it a save file or a network communication? It depends.

It’s quite comprehensive (I know, wikipedia but this time it’s fairly good): https://en.wikipedia.org/wiki/Endianness

If you aren’t sure, you can always define a byte array over the float and write out individual bytes. If you want to be sure, I sent you above the Mono implementation, you can copy the method how to shuffle the bytes. The difference is that you don’t need to store them in a new array, you just forward it to the BinaryWriter.

But don’t forget: always test on the target device.

Can you explain how I would define a byte array over the float and write out individual bytes? Is it by just doing bitwise operations?

      [System.Security.SecuritySafeCritical]  // auto-generated
        public unsafe virtual void Write(float value)
        {
            uint TmpValue = *(uint *)&value;
            _buffer[0] = (byte) TmpValue;
            _buffer[1] = (byte) (TmpValue >> 8);
            _buffer[2] = (byte) (TmpValue >> 16);
            _buffer[3] = (byte) (TmpValue >> 24);
            OutStream.Write(_buffer, 0, 4);
        }

I was told that the above wouldn’t work when deserializing on differing architecture. BinaryPrimitives is seeming like my last resort. And yes, I am sending data over a network to different clients.