Чтение/запись двоичных файлов на Ruby.

Данная мини-статья родилась в связи с тем, что мне понадобилось читать/писать двоичный файл. Соглашусь, задача для Руби несколько нетипичная, но надо - значит надо.

В сети информации по данной теме практически нет. Что-то есть на google groups, но толкового описания я не нашёл. В результате, начал выдумывать сам.

Итак была задача научиться читать и писать двоичный файл, созданный с помощью MFC метода Serialize. В файле присутствуют CString, int, float, double, BOOL и COLORREF.

Приведу кусочек кода на C++:

// создание файла
... ::Serialize(CArchive& archive)
{
    int A = 44;
    COLORREF Color = RGB(255,10,128);
    float F = 0.345678;
    BOOL B = FALSE;
    CString str = _T("This is a sample string.");
    archive << A << Color << F << B << str;
}

Небольшое отступление про сериализацию в MFC:
данные в файле хранятся "как есть", за исключением CString. Сей объект хранится в паскалевской нотации вида <1 байт - длина строки><строка>. В случае строк длиннее 255 символов - в виде <0xFF><2 байта - длина строки><строка>. В случае нулевой строки - <0>.

Речь идёт про ANSI строки, как хранятся UNICODE строки я не выяснял, в мои задачи это не входило. Предлагаю вам самим исследовать этот вопрос и поделиться с народом :)

Далее я применил слегка "нечестный" метод - подсчитал сколько байт занимает тот или иной тип. "Нечестный" - потому что на разных платформах данные будут разными. Меня интересовал Windows XP и 32-х битный компилятор Visual Studio 7.1.

#include ...
void main(void)
{
    printf("float:%d, double:%d, int:%d, COLORREF:%d, BOOL:%d\n",
        sizeof(float), sizeof(double), sizeof(int), sizeof(COLORREF), sizeof(BOOL));
}

Получаем для Windows XP:

float:4, double:8, int:4, COLORREF:4, BOOL:4

Собственно и всё. Далее - дописываем нужные методы к классу "File":

class File
    def read_float
          f = read(4).unpack("f")
    end

    def read_int
        f = read(4).unpack("i")
    end

    def read_cstring
        f = read(1).unpack("c")
        if (f == 0)
            return ""
        end
        if (f[0] == -1)
            f = read(2).unpack('s')
            str = read(f[0])
            return str
        end
        str = read(f[0].to_i)
    end

    def read_colorref
          f = read(4).unpack("i")
    end
   
    def read_double
        f = read(8).unpack("d")
    end

    def write_float(float_var)
        write(float_var.pack("4f"))
    end

    def write_double(double_var)
        write(double_var.pack("8d"))
    end

    def write_int(int_var)
        write(int_var.pack("4i"))
    end

    def write_colorref(colorref_var)
        write(colorref_var.pack("4i"))
    end

    # TODO Поддержать длинные строки
    def write_cstring(str_var)
        len = str_var.length
        write([len].pack("1c"))
        write(str_var) if len > 0
    end
end

Читаем:

def ReadBinary
    f = File.open ("our.binary.file", "rb")
    @A      = f.read_int
    @Color  = f.read_colorref
    @F      = f.read_float
    @B      = f.read_int
    @str    = f.read_cstring
    f.close
end

Пишем:

def WriteBinary
    f = File.open("our.binary.file", "wb")
    f.write_int(@A)
    f.write_colorref(@Color)
    f.write_float(@F)
    f.write_int (@B)
    f.write_cstring(@str)
    f.close
end

Разумеется, порядок чтения/записи должен соответствовать порядку в функции сериализации.

Обсуждение

Re: Чтение/запись двоичных файлов на Ruby.

Ооо, замечательная работа, автору зачот!

Re: Чтение/запись двоичных файлов на Ruby.

File#open как правило употребляется с блоком. В таком случае по завершении блока даже при возникновении исключения будет вызван #close.

class File
  def self.open filename, mode
    f = new filename, mode
    return f unless block_given?
   
    result = nil
    begin
      result = yield f
    ensure
      f.close
    end
    result
  end
end

def WriteBinary
    File.open("our.binary.file", "wb") {|f|
      f.write_int(@A)
      f.write_colorref(@Color)
      f.write_float(@F)
      f.write_int (@B)
      f.write_cstring(@str)
    }
end

Re: Чтение/запись двоичных файлов на Ruby.

Вот тут можно более компактно написать, вместо:

    result = nil
    begin
      result = yield f
    ensure
      f.close
    end
    result

я бы написал так:

    begin
      return yield f
    ensure
      f.close
    end

Вход для пользователей