0

I want to read an binary file which has an unknown numbers of structs lined up one after another.

Some Pseudo Code:

struct:
   int64 timestamp,
   byte dataBlock1[600][16]
   byte dataBlock2[600][16]

open = FileOpen(...)
while (!EOF)
   FileRead(open,&struct, sizeof(struct));
   parseStruct(struct)

How do I read a file into a struct in C#? My Struct also has an 2D Array inside, but unsafe struct/fixed arrays are only one dimensional. I could make a [X * Y] on dimensional array, but this feels rubbish.

Max R.
  • 811
  • 1
  • 13
  • 31
  • You use a `FileStream` (`File.OpenRead()`) and on top of that a `BinaryReader` – xanatos Apr 04 '17 at 11:56
  • It would help you get a sample of your file and the struct it should be parsed into. I recommend using File.ReadAllLines and iterate the lines (a list of strings) because you only have to acces your file once at it's a cleaner approach. – Mighty Badaboom Apr 04 '17 at 11:58
  • 1
    @MightyBadaboom OP says "I want to read a binary file" – Tim Rogers Apr 04 '17 at 11:59
  • `StructLayout` lets you specify the packing, or even explicit layout if you need it. And then you could read directly into a struct, but not with the usual API (you can P/Invoke the native API). But you shouldn't do any of this, just use BinaryReader and manually assign the fields – harold Apr 04 '17 at 12:00
  • 1
    Possible duplicate of [C# array within a struct](http://stackoverflow.com/questions/8704161/c-sharp-array-within-a-struct) – Tim Rogers Apr 04 '17 at 12:02
  • Okay, initially I start programming C# in the hope that a lot of work isn't needed to do anymore.. The struct is much more bigger then shown in the pseudo code (about 1200 bytes and 150 variables (I know, it's gross and was definitely now my Idea..)) so I have to read the file byte per byte and assign it to the variables needed? – Max R. Apr 04 '17 at 12:04

2 Answers2

1

It should be:

public class Block
{
    public long Timestamp { get; set; }
    public byte[][] DataBlock1 { get; set; } = new byte[600][];
    public byte[][] DataBlock2 { get; set; } = new byte[600][];
}

and then

var lst = new List<Block>();

var enc = Encoding.GetEncoding("iso-8859-1");

using (var fs = File.OpenRead("somefile.bin"))
using (var br = new BinaryReader(fs, enc))
{
    while (br.PeekChar() != -1)
    {
        var block = new Block();

        block.Timestamp = br.ReadInt64();

        for (int i = 0; i < block.DataBlock1.Length; i++)
        {
            block.DataBlock1[i] = br.ReadBytes(16);

            if (block.DataBlock1[i].Length != 16)
            {
                throw new Exception();
            }
        }

        for (int i = 0; i < block.DataBlock2.Length; i++)
        {
            block.DataBlock2[i] = br.ReadBytes(16);

            if (block.DataBlock2[i].Length != 16)
            {
                throw new Exception();
            }
        }

        lst.Add(block);
    }
}

In the end you use a FileStream (returned by File.OpenRead()) and on top of that you put a BinaryReader. I read 16 bytes at a time with BinaryReader, but would be perhaps faster to read multiple "rows" at a time and then split them (a DataBlockX is 9600 bytes, so it isn't too much big). For the EOF handling, I've used BinaryReader.PeekChar, as suggested by Marc Gravell. But note that I concur with the problems presented by some persons, so I'm using the iso-8859-1 encoding, that guarantees that any byte is ok for the method (the default solution suggested by Gravell will break for some combination of bytes)

Something like:

public static void ReadDataBlock(BinaryReader br, byte[][] dataBlock, int size)
{
    int totalSize = dataBlock.Length * size;
    byte[] bytes = br.ReadBytes(totalSize);

    if (bytes.Length != totalSize)
    {
        throw new Exception();
    }

    for (int i = 0; i < dataBlock.Length; i++)
    {
        var block = new byte[size];
        dataBlock[i] = block;
        Buffer.BlockCopy(bytes, i * size, block, 0, size);
    }
}

and then:

block.Timestamp = br.ReadInt64();
ReadDataBlock(br, block.DataBlock1, 16);
ReadDataBlock(br, block.DataBlock2, 16);
Community
  • 1
  • 1
xanatos
  • 109,618
  • 12
  • 197
  • 280
  • This means a lot of work to do since the structure is about 1200 bytes long and has 150 variables in it I have to assign by hand.. But thank you, I am able to understand your code very well. – Max R. Apr 04 '17 at 12:58
  • @MaxR. Sadly your struct isn't blittable (you can't simply copy it from a binary file, because there are arrays). In C it is blittable (because the arrays are "contained" in the struct). If it was blittable then other solutions would be possible: http://stackoverflow.com/q/2384/613130 – xanatos Apr 04 '17 at 13:00
1

Use Marshal Techniques

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Runtime.InteropServices;
using System.IO;


namespace ConsoleApplication49
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Data
    {
        [MarshalAs(UnmanagedType.I8)]
        public Int64 timestamp;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 600 * 16)]
        public byte[,] dataBlock1;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 600 * 16)]
        public byte[,] dataBlock2;
    }
    class Program
    {

        const string FILENAME = @"c:\temp\test.bin";
        static void Main(string[] args)
        {
            Stream oStream = File.OpenWrite(FILENAME);
            BinaryWriter writer = new BinaryWriter(oStream);

            Data writeData = new Data();
            writeData.timestamp = DateTime.Now.ToBinary();

            int size = Marshal.SizeOf(typeof(Data));

            IntPtr wPtr = Marshal.AllocHGlobal(size);
            Marshal.StructureToPtr(writeData,wPtr,true);
            byte[] oBuffer = new byte[size];
            Marshal.Copy(wPtr, oBuffer, 0, size);
            writer.Write(oBuffer);

            writer.Flush();
            writer.Close();

            Stream iStream = File.OpenRead(FILENAME);
            BinaryReader reader = new BinaryReader(iStream);

            while (!((iStream.Position + size) < iStream.Length))
            {

                IntPtr ptr = Marshal.AllocHGlobal(size);

                byte[] buffer = reader.ReadBytes(size);

                Marshal.Copy(buffer, 0, ptr, size);

                Data data = (Data)Marshal.PtrToStructure(ptr, typeof(Data));
                DateTime now = DateTime.FromBinary(data.timestamp);

            }

        }
    }


}
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • Does it really works? Where does the marshaller takes the two dimensions of the arrays? – xanatos Apr 04 '17 at 13:44
  • Yes it works. I inserted a time stamp to show it works. It takes the two dimensions of the array by declaring the array as byte[,]. A two dimension array in memory is just continuous bytes and the compiler handles the conversion from bytes to a two dimensional object. – jdweng Apr 04 '17 at 13:56
  • Try doing a `data.dataBlock1[0, 0]` after the `DateTime now = `. 2d arrays in .NET "save" internally both of their sizes (and even in C, one size must be a constant known at compile time to use a 2d array) – xanatos Apr 04 '17 at 14:00
  • this does actually **not** work, how should c# now that the first dimension is 600 and the second is 16? This is nowehere determined. I fixed it by using just one dimension and then calculate via: `for (int nAmplData = 0; nAmplData < 600; nAmplData++) for (int nChannel = 0; nChannel < 7; nChannel++) UssWriter.Write(SGAfs.ucAmplData[nAmplData * 16 + nChannel]);` – Max R. Apr 12 '17 at 16:04
  • Don't forget to free the `ptr` with `Marshal.FreeHGlobal(ptr)` after you're done with it (at the end of the while loop) – EagleBirdman Oct 06 '22 at 14:36