Формат сериализации Marshal в Ruby
В Ruby любой объект можно превратить (сериализовать) в последовательность байт, а
потом без каких-либо потерь обратно восстановить в объект. Для этого нужен
модуль Marshal
и методы dump
и load
, которые идут из коробки.
Это очень любопытный механизм, потому что он универсальный (может
сериализовать любой объект любого класса) и совместимый (как backward так
и forward) с предыдущими версиями Ruby. Кроме того одна из целей -
это маленький размер дампа. Экономят буквально на байтах. Учитывая
динамическую природу Ruby, богатство функционала и большую стандартную
библиотеку разработать такой формат было нетривиальной задачей.
Давайте разберем в деталях этого формата на примере Ruby 3.2.
Содержание
- Структура дампа
- true
- false
- nil
- Integer в примерах
- Объекты
- Объект с подмешанным модулем
- Ссылка на объект
- Пользовательский формат
- Built-in классы из core library
- String
- Symbol
- Array
- Hash
- Class
- Module
- Range
- Regexp
- Time
- Struct
- Encoding
- Float
- Rational
- Complex
- Integers
- Решения в этом формате
- Итоги
- PS
- Ссылки
Структура дампа
В начале дампа сохраняется версия формата. Она кодируется двумя байтами. Текущая версия - 4.8. Далее идут данные самого объекта.
Есть несколько форматов данных, которые описывают:
- как универсальное представление для объекта,
- так и built-in классы из core library
- и дополнительные свойства объекта - его instance variables, подмешанные в объект
(используя метод
#extend
) модули итд.
В начале каждого формата идет 1 байт - префикс формата. Обычно это ASCII символ, который указывает на название формата. И затем уже идут данные. Форматы могут быть вложенными друг в друга.
Для объектов пользовательских классов применяется универсальный формат объекта - сохраняется имя класса и instance variables. C многочисленными built-in классами из core library все сложнее - большая их часть реализована на С и данные хранятся во внутренних структурах, а не instance variables.
Такие built-in классы могут использовать свои специализированные форматы, например классы String или Symbol. Или как Range использовать универсальный формат объекта и сохранять данные как instance variables, но при десериализации читать данные из instance variables и инициализировать свои внутренние структуры.
true
Singleton объект класса TrueClass
представлен одним байтом - ASCII код символа “T”:
\x04\bT
false
Singleton объект класса FalseClass
представлен одним байтом - ASCII код символа “F”:
\x04\bF
nil
Singleton объект класса NilClass
представлен одним байтом - ASCII код цифры “0”:
\x04\b0
Integer в примерах
Числа представлены довольно сложным образом - это описано в отдельном разделе. Сейчас нам хватит только формата для положительных маленьких чисел (до 127).
Такие числа представлены одним байтом, а к значению прибавляется 5 (так
как значения 1-4 зарезервированы для представления бОльших чисел). Таким
образом число 1 кодируется как 6 - 0x06 в 16-й системе и "\x06"
в виде
строки.
Приведем значения, которые будут использоваться ниже:
число | +5 | в 16-чной системе | в виде строки |
2 | 7 | 0x07 | “\a” |
3 | 8 | 0x08 | “\b” |
4 | 9 | 0x09 | “\t” |
5 | 10 | 0x0A | “\n” |
6 | 11 | 0x0B | “\v” |
7 | 12 | 0x0C | “\f” |
8 | 13 | 0x0D | “\r” |
9 | 14 | 0x0E | “\0E” |
10 | 15 | 0x0F | “\0F” |
0
кодируется особым способом - это просто 0x00.
Префикс для Integer - ASCII код символа “i”.
И в итоге число 10, например, выглядит следующим образом:
\x04\bi\x0F
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“i” | префикс формата Integer |
“\x0F” | Integer 10 |
Объекты
Все объекты за редким исключением используют универсальный формат объектов. Сохраняется имя класса и instance variables. Как следствие нельзя сериализовать объект анонимного класса.
Префикс для объектов - ASCII код символа “o”.
Формат состоит из
- префикса объекта (“o”),
- затем имя класса в формате Symbol (“:” + длина + строка)
- затем количество instance variables
- затем instance variables как пары имя-значение, где имя в формате Symbol
Дамп объекта класса User и instance variables @foo = 1
и @bar = 2
:
\x04\bo:\tUser\a:\t@fooi\x06:\t@bari\a
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“o” | префикс объекта |
”:” | префикс Symbol |
“\t” | Integer 4 |
“User” | строка |
“\a” | Integer 2 |
”:” | префикс Symbol |
“\t” | Integer 4 |
“@foo” | строка |
“i” | префикс Integer |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\t” | Integer 4 |
“@bar” | строка |
“i” | префикс Integer |
“\a” | Integer 2 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
o
: "\t" User
"\a"
: "\t" @foo
i "\x06"
: "\t" @bar
i "\a"
где:
o
- префикс объекта: "\t" User
- имя класса"\a"
- количество instance variables - 2: "\t" @foo
- имя переменной@foo
i "\x06"
- ее значение 1: "\t" @bar
- имя переменной@bar
i "\a"
- ее значение 2
Объект с подмешанным модулем
В объект можно подмешать модуль используя метод #extend
. В таком случае
имя этого модуля тоже попадает в дамп. Как следствие нельзя сделать дамп
если модуль анонимный.
Префикс для подмешанного модуля - ASCII код символа “e”.
Формат состоит из:
- префикс “e”
- имя модуля в формате Symbol
- дамп самого объекта
Дамп объекта класса User с подмешанным модулем Comparable
:
\x04\be:\x0FComparableo:\tUser\x00
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“e” | префикс подмешанного модуля |
”:” | префикс Symbol |
“\x0F” | Integer 10 |
“Comparable” | строка |
“o” | префикс объекта |
”:” | префикс Symbol |
“\t” | Integer 4 |
“User” | строка |
“\x00” | Integer 0 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
e
: "\x0F" Comparable
o
: "\t" User
"\x00"
где:
e
- префикс подмешанного модуля: "\x0F" Comparable
- имя модуляo
- префикс объекта: "\t" User
- имя класса"\x00"
- количество instance variables - 0
Ссылка на объект
Вместо объекта в дампе может встретиться ссылка на него. Нередко объект повторяется и в первый раз он заносится в таблицу объектов, а во второй раз в дамп попадает его порядковый номер в таблице. Для обратной совместимости очень важно сохранить порядок обхода вложенных объектов - элементов коллекции (таких как Array или Hash) или instance variables.
Не все объекты попадают в таблицу объектов и заменяются ссылками.
Singleton объекты, такие как nil
, true
, false
или Symbol’ы, не
заменяются ссылками (для Symbol’ов ведется отдельная таблица, но об этом
позже). Целые singleton числа (которые помещаются в нативный int
) тоже не заменяются ссылками.
Кстати, таблица объектов решает и проблему циклов.
Префикс для ссылки на объект - ASCII код символа @
.
Формат состоит из:
- префикс “@”
- номер объекта в таблице объектов
Дамп массива с двумя элементами [a = Object.new, a]
:
\x04\b[\ao:\vObject\x00@\x06
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”[” | префикс Array |
“\a” | Integer 2 |
“o” | префикс объекта |
”:” | префикс Symbol |
“\v” | Integer 6 |
“Object” | строка |
“\x00” | Integer 0 |
”@” | префикс ссылки на объект |
“\x06” | Integer 1 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
[
"\a"
o
: "\v" Object
"\x00"
@ "\x06"
где:
[
- префикс Array\a
- количество элементов в Array - 2o : "\v" Object "\x00"
- 1й элемент - объект@ "\x06"
- 2й элемент - ссылка на объект #1 (#0 - это сам Array)
Пользовательский формат
Формат Marshal расширяем и можно задавать способ, как сериализовать объект конкретного пользовательского класса. Поддерживается два способа задать новый формат.
Методы #_dump и ._load
Можно реализовать свой механизм сериализации объекта строку и десериализации из нее.
Для этого надо реализовать в пользовательском классе методы #_dump
и
._load
. Метод #_dump
возвращает строку, а метод ._load
создает и
возвращает объект десериализованный из строки.
Префикс для объекта - ASCII код символа u
.
Формат состоит из:
- префикс “u”
- имя класса
- строка с сериализованными данными
Возьмем пример из документации
class MyObj
def initialize name, version, data
@name = name
@version = version
@data = data
end
def _dump level
[@name, @version].join ':'
end
def self._load args
new(*args.split(':'))
end
end
Дамп объекта MyObj.new('Apollo', 11, 'July 20, 1969')
:
\x04\bIu:\nMyObj\x0EApollo:11\x06:\x06ET
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
“u” | префикс объекта сериализованного используя механизм #_dump и ._load |
”:” | префикс Symbol |
“\n” | Integer 5 |
“MyObj” | строка |
“\x0E” | Integer 9 |
“Apollo:11” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“T” | true |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
I
u
: "\n" MyObj
"\x0E"
Apollo:11
"\x06"
: "\x06" E
T
где:
I
- префикс объекта built-in класса с instance variablesu
- префикс объекта сериализованного используя механизм#_dump
и._load
: "\n" MyObj
- имя класса"\x0E" Apollo:11
- сериализованные данные"\x06"
- количество instance variables - 1: "\x06" E
- имя переменнойE
T
- ее значение - true
Формат объекта built-in класса с instance variables описан ниже.
Instance variable E
и значение true означают кодировку строки
UTF-8.
Методы #marshal_dump и #marshal_load
Есть и более простой вариант. Можно не реализовывать полностью свой
формат, а положиться на уже существующий. Метод #marshal_dump
должен
вернуть объект, который будет сериализован вызовом Marshal.dump
. А
метод #marshal_load
вызывается на созданном в Marshal.load
но не инициализированном
объекте (используя метод Class#allocate
) и должен выполнить инициализацию используя
десериализованный объект. Удобно использовать контейнеры из core
library, такие как Array или Hash.
Префикс для объекта - ASCII код символа U
.
Формат состоит из:
- префикс “U”
- имя класса
- дамп объекта, который вернул метод
#marshal_dump
Возьмем пример из документации
class MyObj
def initialize name, version, data
@name = name
@version = version
@data = data
end
def marshal_dump
[@name, @version]
end
def marshal_load array
@name, @version = array
end
end
Дамп объекта MyObj.new('Apollo', 11, 'July 20, 1969')
:
\x04\bU:\nMyObj[\aI"\vApollo\x06:\x06ETi\x10
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“U” | префикс объекта сериализованного используя механизм #marshal_dump и #marshal_load |
”:” | префикс Symbol |
“\n” | Integer 5 |
“MyObj” | строка |
”[” | префикс Array |
“\a” | Integer 2 |
“I” | префикс объекта built-in класса с instance variables |
”"” | префикс String |
“\v” | Integer 6 |
“Apollo” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“T” | true |
“i” | префикс Integer |
“\x10” | Integer 11 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
U
: "\n" MyObj
[
"\a"
I
" "\v" Apollo
"\x06"
: "\x06" E
T
i "\x10"
где:
U
- префикс объекта сериализованного используя механизм#marshal_dump
и#marshal_load
: "\n" MyObj
- имя класса в формате Symbol[
- префикс Array"\a"
- количество элементов в ArrayI " "\v" Apollo "\x06" : "\x06" E T
- 1й элемент - строка “Apollo”i "\x10"
- 2й элемент - Integer 11
Built-in классы из core library
Как уже отметили выше часть классов из core library, которые реализованы на C, не используют универсальный формат объектов. Большая их часть использует свой собственный формат, например String, Array и Hash.
Некоторые классы используют формат пользовательской сериализации с методами
#_dump
и ._load
, например Time и Encoding. Но они реализовывают
методы #_dump
и ._load
как приватные, то есть нельзя полагаться на их
наличие и вызывать в пользовательском коде (если, например, мы решим
реализовать формат Marshal самостоятельно)
Для Range используют универсальный формат объектов. Данные сериализуются в виде Range source строки и instance variables. Вот только у объекта Range нет таких instance variables, а значит сериализация и десериализация Range реализована иначе чем для универсального формата объектов.
По этим причинам и пользовательская сериализация и универсальная сериализация объектов для built-in классов это тоже часть формата Marshal и будут описаны ниже.
Instance variables
В дамп можно сохранить instance variables для любого объекта. Для этого есть специальный формат и префикс “I”:
- префикс “I”
- дамп объекта
- количество instance variables
- пары имя переменной и ее значение
По факту этот формат используется только для форматов built-in
классов и для пользовательской сериализации используя методы #_dump
и
._load
(данные сериализуются в строку, а для строки надо сохранить ее
кодировку - для этого сохраняют instance variable E
или encoding
).
Часто у объекта built-in класса есть какие-то атрибуты, например кодировка строки, или название часового пояса. Такие атрибуты могут быть опциональными. Они сериализуются как псевдо instance variables используя такой механизм.
Подклассы built-in классов
Built-in классы можно наследовать. Для подклассов Array, Hash, Regexp и String введен дополнительный префикс “C”, который означает, что надо использовать формат родительского класса, но создавать объект дочернего.
Формат состоит из:
- префикс “C”
- имя дочернего класса
- дамп объекта (в формате родительского built-in класса)
Рассмотрим пример для подкласса Array:
class MyArray < Array
end
Дамп объекта MyArray.new([0])
:
\x04\bC:\fMyArray[\x06i\x00
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“C” | префикс подкласса built-in класса |
”: \f MyArray” | имя класса |
”[\x06i\x00” | дамп массива [0] |
Кодировка
Некоторые классы, такие как String, Symbol и Regexp, имеют кодировку,
которая тоже должна сохраняться в дамп. Кодировка сохраняется как
instance variable encoding
, а ее значение - название кодировки. В
некоторых случаях используют instance variable E
.
Есть несколько вариантов представления кодировки:
кодировка объекта | представление |
---|---|
ASCII-8BIT | кодировка не сохраняется в дампе |
US-ASCII | instance variable E и значение “F” (false) |
UTF-8 | instance variable E и значение “T” (true) |
остальные кодировки | instance variable encoding , значение - название кодировки |
Строка “foobar” с кодировкой ASCII-8BIT:
\x04\b"\vfoobar
Как видим, здесь нет instance variable и как следствие префикса “I” тоже нет.
Строка с кодировкой US-ASCII:
\x04\bI"\vfoobar\x06:\x06EF
Строка с кодировкой UTF-8:
\x04\bI"\vfoobar\x06:\x06ET
Строка с кодировкой отличной от приведенных выше, например UTF-16LE:
\x04\bI"\vfoobar\x06:\rencoding"\rUTF-16LE
String
Префикс для объекта класса String - ASCII код символа "
.
Формат состоит из:
- префикс объекта built-in класса с instance variables
- префикс объекта класса String
- длина строки (в байтах)
- символы строки
- количество instance variables
- instance variables
Дамп строки "foobar"
:
\x04\bI"\vfoobar\x06:\x06ET
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
”"” | префикс String |
“\v” | Integer 6 |
“foobar” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“T” | true |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
I
" "\v" foobar
"\x06"
: "\x06" E
T
где:
I
- префикс объекта built-in класса с instance variables" "\v" foobar
- строка “foobar”"\x06"
- количество instance variables - 1: "\x06" E
- имя переменной -E
T
- ее значение - true
Symbol
Префикс для объекта класса Symbol - ASCII код символа :
.
Формат состоит из:
- префикс объекта класса Symbol
- длина в байтах
- символы
Дамп :foobar
:
\x04\b:\vfoobar
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”:” | префикс объекта класса Symbol |
“\v” | длина - 6 |
“foobar” | символы |
Кодировка
Для Symbol кодировка сохраняется немного иначе, чем для String.
Если Symbol имеет кодировку US-ASCII или ASCII-8BIT - тогда кодировка не сохраняется в дампе вообще. При чтении объекта из дампа кодировка по-умолчанию - ASCII-8BIT. Если нет явно сохраненной кодировки и все символы ASCII-совместимые (т.е. codepoint в диапазоне 0-127), тогда выставляется кодировка US-ASCII.
Дамп для Symbol с ASCII-8BIT ("\xFF".b.to_sym
):
\x04\b:\x06\xFF
Дамп для Symbol с US-ASCII ("a".to_sym
):
\x04\b:\x06a
Ссылка на Symbol
Подобно таблице объектов ведется и таблица символов. Вместо повторяющегося объекта класса Symbol в дамп попадает ссылка на него.
Префикс для ссылки на Symbol - ASCII код символа ;
.
Формат состоит из:
- префикс “;”
- номер Symbol в таблице символов
Дамп массива из двух элементов [:symbol, :symbol]
:
\x04\b[\a:\vsymbol;\x00
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”[” | префикс Array |
“\a” | Integer 2 |
”:” | префикс Symbol |
“\v” | Integer 6 |
“symbol” | строка |
”;” | префикс ссылки на Symbol |
“\x00” | Integer 0 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
[
"\a"
: "\v" symbol
; "\x00"
где:
[
- префикс Array"\a"
- количество элементов в Array - 2: "\v" symbol
- 1й элемент -:symbol
; "\x00"
- 2й элемент - ссылка на Symbol #0
Array
Префикс для объекта класса Array - ASCII код символа [
.
Формат состоит из:
- префикс объекта класса Array
- количество элементов
- элементы массива
Дамп массива [1, 2, 3]
:
\x04\b[\bi\x06i\ai\b
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”[” | префикс объекта класса Array |
“\b” | Integer 3 |
“i” | префикс Integer |
“\x06” | Integer 1 |
“i” | префикс Integer |
“\a” | Integer 2 |
“i” | префикс Integer |
“\b” | Integer 3 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
[
"\b"
i "\x06"
i "\a"
i "\b"
где:
[
- префикс массива"\b"
- длина массива, 3i "\x06"
- 1й элемент - число 1i "\a"
- 2й элемент - число 2i "\b"
- 3й элемент - число 3
Hash
Префикс для объекта класса Hash - ASCII код символа {
.
Формат состоит из:
- префикс объекта класса Hash
- количество пар ключ-значение
- последовательность пар
Дамп {a: 9}
:
\x04\b{\x06:\x06ai\x0E
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”{“ | префикс объекта класса Hash |
“\x06” | Integer 1 |
”:” | префикс объекта класса Symbol |
“\x06” | Integer 1 |
“a” | символы Symbol |
“i” | префикс объекта класса Integer |
“\x0E” | Integer 9 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
{
"\x06"
: "\x06" a
i "\x0E"
где:
{
- префикс объекта класса Hash"\x06"
- размер - 1 пара ключ-значение: "\x06" a
- ключ -:a
i "\x0E"
- значение - число 9
Значение по-умолчанию
Hash может иметь значение по-умолчанию и это тоже должно сохраняться в дамп.
Префикс для объекта класса Hash со значением по-умолчанию - ASCII код символа }
.
Формат состоит из:
- префикс
}
- количество пар ключ-значение
- последовательность пар
- значение по-умолчанию
Дамп объекта класса Hash с парой a => 9
и значением по-умолчанию :foo
:
\x04\b}\x06:\x06ai\x0E:\bfoo
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
”}” | префикс для объекта класса Hash со значением по-умолчанию |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“a” | символы Symbol |
“i” | префикс Integer |
“\x0E” | Integer 9 |
”:” | префикс Symbol |
“\b” | Integer 3 |
“foo” | символы Symbol |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
}
"\x06"
: "\x06" a
i "\x0E"
: "\b" foo
где:
}
- префикс объекта класса Hash со значением по-умолчанию"\x06"
- размер - 1 пара ключ-значение: "\x06" a
- ключ -:a
i "\x0E"
- значение - число 9: "\b" foo
- значение по-умолчанию -:foo
Флаг compare_by_identity
У Hash’а есть флаг compare_by_identity, который влияет на поведение:
Sets self to consider only identity in comparing keys; two keys are considered the same only if they are the same object; returns self.
Поэтому он тоже должен сохраняться в дампе.
Формат состоит из:
- префикс
C:\tHash
- префикс объекта класса Hash
- количество пар ключ-значение
- последовательность пар
Дамп {a: 9}
с выставленным флагом compare_by_identity:
\x04\bC:\tHash{\x06:\x06ai\x0E
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“C” | префикс экземпляра класса, который наследуют built-in класс |
”:” | префикс Symbol |
“\t” | Integer 4 |
“Hash” | строка |
”{“ | префикс объекта класса Hash |
“\x06” | Integer 1 |
”:” | префикс объекта класса Symbol |
“\x06” | Integer 1 |
“a” | символы Symbol |
“i” | префикс объекта класса Integer |
“\x0E” | Integer 9 |
Если сгруппировать отдельные фрагменты, то выйдет следующая иерархия:
C
: "\t" Hash
{
"\x06"
: "\x06" a
i "\x0E"
где:
C
- префикс экземпляра класса, который наследуют built-in класс: "\t" Hash
- имя built-in класса -:Hash
{
- префикс объекта класса Hash"\x06"
- размер - 1 пара ключ-значение: "\x06" a
- ключ -:a
i "\x0E"
- значение - число 9
Флаг ruby2_keywords
Hash можно выставить также еще один флаг - ruby2_keywords
(документация).
Он сохраняется в дампе как instance variable K
.
Рассмотрим дамп для Hash.ruby2_keywords_hash({a: 1})
:
\x04\bI{\x06:\x06ai\x06\x06:\x06KT
Как видим добавилась переменная K
и значение T
(true).
Class
Префикс для объекта класса Class - ASCII код символа c
.
Формат состоит из:
- префикс класса
- длина имени класса
- имя класса
Дамп класса String
:
\x04\bc\vString
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“c” | префикс класса |
“\v” | Integer 6 |
“String” | строка |
Module
Префикс для объекта класса Module - ASCII код символа m
.
Формат состоит из:
- префикс модуля
- длина имени модуля
- имя модуля
Дамп модуля Enumerable
:
\x04\bm\x0FEnumerable
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“m” | префикс модуля |
“\x0F” | Integer 10 |
“Enumerable” | строка |
Range
Объект класса представлен в стандартном формате объекта.
Формат состоит из:
- префикс объекта
- имя класса
- количество instance variables (3)
- instance variables -
excl
,begin
,end
Обратите внимание, что имена переменных без символа @
в начале, как у
обычного объекта. Методы #instance_variable_get
и
#instance_variable_set
требуют, чтобы имя переменной было корректным и
начиналось с “@”.
Дамп Range 1..2
:
\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“o” | префикс объекта |
”:” | префикс Symbol |
“\n” | Integer 5 |
“Range” | строка |
“\b” | Integer 3 |
”:” | префикс Symbol |
“\t” | Integer 4 |
“excl” | строка |
“F” | false |
”:” | префикс Symbol |
“\n” | Integer 5 |
“begin” | строка |
“i” | префикс Integer |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\b” | Integer 3 |
“end” | строка |
“i” | префикс Integer |
“\a” | Integer 2 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
o
: "\n" Range
"\b"
: "\t" excl
F
: "\n" begin
i "\x06"
: "\b" end
i "\a"
где:
o
- префикс объекта: "\n" Range
- имя класса объекта - Range"\b"
- количество instance variables - 3: "\t" excl
- имя instance variable:excl
F
- ее значение -false
: "\n" begin
- имя instance variablebegin
i "\x06"
- ее значение - 1: "\b" end
- имя instance variableend
i "\a"
- ее значение - 2
Beginless и endless Range
Если границу Range не задать - то в дампе сохраняется значение nil
(символ '0'
) в соответствующей instance variable - begin
или end
.
Beginless Range (..2
):
\x04\bo:\nRange\b:\texclF:\nbegin0:\bendi\a
Instance variable begin
выставлена в nil
- :\nbegin0
.
Endless Range (1..
):
\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bend0
Instance variable end
выставлена в nil
- :\bend0
.
.. и …
Признак включения правой границы (который возвращает метод
#exclude_end?
) сохраняется в instance variable excl
как булевое значение
true или false, которые сериализуются в символы T и F
соответственно.
Дамп для 1..2
:
\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a
В переменной :excl
сохранили false: :\texclF
.
Дамп для 1...2
:
\x04\bo:\nRange\b:\texclT:\nbegini\x06:\bendi\
В переменной :excl
сохранили true: :\texclT
.
Regexp
Префикс для объекта класса Regexp - ASCII код символа /
.
Формат состоит из:
- префикс объект built-in класса с instance variables
- префикс объекта класса Regexp
- длина строки Regexp source (в байтах)
- символы строки Regexp source
- Regexp options (число)
- количество instance variables
- instance variables
Для Regexp сохраняется только одна instance variable - E
(или
encoding
), в которой сохраняется данные о кодировке.
Дамп Regexp /abc/
:
\x04\bI/\babc\x00\x06:\x06EF
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объект built-in класса с instance variables |
”/” | префикс Regexp |
“\b” | Integer 3 |
“abc” | строка |
“\x00” | байт 0 |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“F” | false |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
I
/
"\b" abc
"\x00"
"\x06"
: "\x06" E
F
где:
I
- префикс объект built-in класса с instance variables/
- префикс Regexp"\b" abc
- Regexp source - “abc”"\x00"
- Regexp options - 0"\x06"
- количество instance variables - 1: "\x06" E
- переменнаяE
F
- ее значение false
Timeout
В Ruby 3.2 у Regexp появился параметр timeout
, но в дамп он не
сохраняется:
Дамп Regexp.new("abc", timeout: 5)
:
\x04\bI/\babc\x00\x06:\x06EF
Time
Для класса Time используют формат пользовательской сериализации
используя методы #_dump
и ._load
.
Формат состоит из:
- префикс объекта built-in класса с instance variables
- префикс объекта сериализованного используя механизм
#_dump
и._load
- имя класса - Time
- бинарное представление даты/времени
- количество instance variables
- instance variables - обязательные
offset
иzone
и опциональныеnano_num
,nano_den
иsubmicro
В offset
сохраняется UTC offset в секундах, а в zone
- название
часового пояса (например EET).
Важная для реализации деталь - instance variables Time заносятся в таблицу объектов до того как в таблицу попадает сам объект Time, в отличии от всех остальных форматов со вложенными объектами.
Дамп для Time.new('2023-12-03 18:30:59 +0300')
:
\x04\bIu:\tTime\ro\xEC\x1E\x80\x00\x00\xB0{\a:\voffseti\x020*:\tzone0
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
“u” | префикс объекта сериализованного используя механизм #_dump и ._load |
”:” | префикс Symbol |
“\t” | Integer 4 |
“Time” | строка |
“\r” | Integer 8 |
“o\xEC\x1E\x80\x00\x00\xB0{“ | строка |
“\a” | Integer 2 |
”:” | префикс Symbol |
“\v” | Integer 6 |
“offset” | строка |
“i” | префикс Integer |
“\x020*” | Integer 10800 |
”:” | префикс Symbol |
“\t” | Integer 4 |
“zone” | строка |
“0” | nil |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
I
u
: "\t" Time
"\r"
"o\xEC\x1E\x80\x00\x00\xB0{"
"\a"
: "\v" offset
i "\x020*"
: "\t" zone
0
где:
I
- префикс объекта built-in класса с instance variablesu
- префикс объекта сериализованного используя механизм#_dump
и._load
: "\t" Time
- имя класса Time"\r"
- длина строки в байтах"o\xEC\x1E\x80\x00\x00\xB0{"
- бинарное представление даты/времени"\a"
- количество instance variables - 2: "\v" offset
- имя переменнойoffset
i "\x020*"
- ее значение - Integer 10800 - число секунд в 3-х часах: "\t" zone
- имя переменнойzone
0
- ее значение -nil
Бинарное представление
Время/дата занимают всего 8 байт. В них упакованы все составляющие Time - часы, минуты, секунды итд:
диапазон бит | длина | компонент |
---|---|---|
0-4 | 5 | часы |
5-9 | 5 | день месяца |
10-13 | 4 | месяц - 1 |
14-29 | 16 | год - 1900 |
30 | 1 | время в UTC? |
31 | 1 | константа 0x1UL |
32-51 | 20 | миллисекунды |
52-57 | 6 | секунды |
58-63 | 6 | минуты |
Возьмем пример выше - время 2023-12-03 18:30:59 +0300 кодируется следующей строкой:
o\xEC\x1E\x80\x00\x00\xB0{
В бинарном виде это:
01101111 11101100 0001111010 000000 00000000 00000000 10110000 01111011
Разобьем его на компоненты:
диапазон бит | компонент | биты | значение |
---|---|---|---|
0-4 | часы | 0 1111 | 15, учитывая utc offset +0300 часы равны 18 |
5-9 | день месяца | 0 0011 | 3 |
10-13 | месяц - 1 | 1011 | 11, 11 + 1 = 12 |
14-29 | год - 1900 | 0000 0000 0111 1011 | 123, 123 + 1900 = 2023 |
30 | время в UTC? | 0 | 0, нет |
31 | константа 0x1UL | 1 | 1 |
32-51 | миллисекунды | 0000 0000 0000 0000 0000 | 0 |
52-57 | секунды | 11 1011 | 59 |
58-63 | минуты | 01 1110 | 30 |
zone
В переменную zone
сохраняется значение возвращаемое методом Time#zone
- это название локального часового пояса или nil
.
В дампе объекта в локальном часовом поясе Time.local(2023, 12, 3, 18,
30, 59)
:
\x04\bIu:\tTime\rp\xEC\x1E\x80\x00\x00\xB0{\a:\voffseti\x02 \x1C:\tzoneI"\bEET\x06:\x06EF
в переменную zone
сохраняется строка “EET” - :\tzoneI"\bEET\x06:\x06EF
.
В случае с UTC:
\x04\bIu:\tTime\rr\xEC\x1E\xC0\x00\x00\xB0{\x06:\tzoneI"\x00\x06:\x06EF
в zone
сохраняется пустая строка - :\tzoneI\"\x00\x06:\x06EF
.
nano_num, nano_den и submicro
Time хранит время с точностью до наносекунд, то есть до .
В бинарном представлении
хранятся только микросекунды (точность до ),
а оставшиеся 3 разряда для наносекунд представляются дополнительными
instance variables nano_num
, nano_den
и submicro
.
Если верить комментарию в исходном коде, то переменная submicro
нужна
только для совместимости с Ruby 1.9.1. Это строка из двух символов,
байтовое представление которых представляет 4 десятичные цифры. Первые 3
из них представляют 3 разряда для наносекунд, а 4-я отбрасывается.
Рассмотрим пример с 123456789 наносекундами.
Дамп для Time.new(2000, 12, 31, 23, 59, 59.1234567891)
:
\x04\bIu:\tTime\r\xF5/\x19\x80@\xE2\xB1\xEF\n:\rnano_numl+\bwqYfF\xC5:\rnano_denl+\b\x00\x00\x00\x00@\x00:\rsubmicro"\ax\x90:\voffseti\x02 \x1C:\tzoneI"\bEET\x06:\x06EF
Значение переменной submicro
- строка “x\x90”, или в 16й системе 78 90
.
Первые 3 цифры соответствуют 7-й, 8-й и 9-й цифре наносекунд 123456789.
Одновременно эту же информацию кодируют переменные nano_num
и
nano_den
. Число 789 представлено в виде дроби, числитель и знаменатель
которой сохранены в этих переменных.
В примере выше nano_num
равна 216906155520375, а nano_den
- 274877906944.
Целая часть от деления nano_num
на nano_den
равна 789.
Struct
Префикс для объекта подкласса Struct - ASCII код символа S
.
Формат состоит из:
- префикс Struct
- имя класса
- количество атрибутов (members)
- последовательность пар имя атрибута и его значение
Дамп структуры Struct.new("Person", :name).new("Alex")
:
\x04\bS:\x13Struct::Person\x06:\tnameI"\tAlex\x06:\x06ET
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“S” | префикс Struct |
”:” | префикс Symbol |
“\x13” | Integer 14 |
“Struct::Person” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\t” | Integer 4 |
“name” | строка |
“I” | префикс объекта built-in класса с instance variables |
”"” | префикс String |
“\t” | Integer 4 |
“Alex” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“T” | true |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
S
: "\x13" Struct::Person
"\x06"
: "\t" name
I
" "\t" Alex
"\x06"
: "\x06" E
T
где:
S
- префикс Struct: "\x13" Struct::Person
- имя класса “Struct::Person”"\x06"
- количество атрибутов - 1: "\t" name
- имя атрибута - “name”I " "\t" Alex "\x06" : "\x06" E T
- значение атрибута - строка “Alex”
Encoding
Для класса Encoding используют формат пользовательской сериализации
используя методы #_dump
и ._load
.
Формат состоит из:
- префикс объекта built-in класса с instance variables
- префикс объекта сериализованного используя механизм
#_dump
и._load
- имя класса - Encoding
- название кодировки
- количество instance variables
- instance variables
Дамп Encoding::UTF_8
:
\x04\bIu:\rEncoding\nUTF-8\x06:\x06EF
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
“u” | префикс объекта сериализованного используя механизм #_dump и ._load |
”:” | префикс Symbol |
“\r” | Integer 8 |
“Encoding” | строка |
“\n” | Integer 5 |
“UTF-8” | строка |
“\x06” | Integer 1 |
”:” | префикс Symbol |
“\x06” | Integer 1 |
“E” | строка |
“F” | false |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
I
u
: "\r" Encoding
"\n"
UTF-8
"\x06"
: "\x06" E
F
где:
I
- префикс объекта built-in класса с instance variablesu
- префикс объекта сериализованного используя механизм#_dump
и._load
: "\r" Encoding
- имя класса"\n"
- длина строки в байтахUTF-8
- имя encoding в виде последовательности байтов"\x06"
- количество instance variables - 1: "\x06" E
- имя переменной -E
F
- ее значение false
Float
Префикс для объекта класса Float - ASCII код символа f
.
Формат состоит из:
- префикс Float
- длина строки
- строковое представление в 10-чной системе
Дамп числа 3.14:
\x04\bf\t3.14
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“f” | префикс Float |
“\t” | Integer 4 |
“3.14” | строка |
Экспоненциальная запись
Иногда число может быть в экспоненциальной записи (scientific notation):
Дамп литерала 1e10
:
\x04\bf\t1e10
Видим, что число сохранили в исходной нотации - 1e10
.
Специальные значения (NaN, Infinity)
Специальные значения, такие как Float::INFINITY
и Float::NAN
,
сохраняются особым образом - в обычном формате Float, но строковое
представление - “inf” и “nan” соответственно:
Marshal.dump(Float::INFINITY) # => "\x04\bf\binf"
Marshal.dump(Float::NAN) # => "\x04\bf\bnan"
Отрицательные числа
Знак сохраняется в строковом представлении:
- -3.14 -
\x04\bf\n-3.14
-Float::INFINITY
-\x04\bf\t-inf
Rational
Для класса Rational используют формат пользовательской сериализации
используя методы #marshal_dump
и #marshal_load
. Сериализуется
массив из двух элементов - числитель и знаменатель (numerator и denominator).
Формат состоит из:
- префикс объекта built-in класса с instance variables
- префикс объекта сериализованного используя механизм
#marshal_dump
и#marshal_load
- имя класса - Rational
- числитель и знаменатель
- количество instance variables
- instance variables
Дамп Rational(5, 6)
:
\x04\bU:\rRational[\ai\ni\v
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
“U” | префикс объекта сериализованного используя механизм #marshal_dump и #marshal_load |
”:” | префикс Symbol |
“\r” | Integer 8 |
“Rational” | строка |
”[” | префикс Array |
“\a” | Integer 2 |
“i” | префикс Integer |
“\n” | Integer 5 |
“i” | префикс Integer |
“\v” | Integer 6 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
U
: "\r" Rational
[
"\a"
i "\n"
i "\v"
где:
U
- префикс объекта сериализованного используя механизм#marshal_dump
и#marshal_load
: "\r" Rational
- имя класса - Rational[
- префикс Array"\a"
- количество элементов - 2i "\n"
- 1й элемент - 5i "\v"
- 2й элемент - 6
Complex
Для класса Complex используют формат пользовательской сериализации
используя методы #marshal_dump
и #marshal_load
. Аналогично Rational
для Complex сериализуется массив из двух элементов - действительное
число (real) и мнимое (imagine).
Формат состоит из:
- префикс объекта built-in класса с instance variables
- префикс объекта сериализованного используя механизм
#marshal_dump
и#marshal_load
- имя класса - Complex
- действительное и мнимое числа
- количество instance variables
- instance variables
Дамп литерала 5 + 6i
:
\x04\bU:\fComplex[\ai\ni\v
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“I” | префикс объекта built-in класса с instance variables |
“U” | префикс объекта сериализованного используя механизм #marshal_dump и #marshal_load |
”:” | префикс Symbol |
“\f” | Integer 8 |
“Complex” | строка |
”[” | префикс Array |
“\a” | Integer 2 |
“i” | префикс Integer |
“\n” | Integer 5 |
“i” | префикс Integer |
“\v” | Integer 6 |
Если сгруппировать отдельные фрагменты, то выйдет следующая структура:
U
: "\f" Complex
[
"\a"
i "\n"
i "\v"
где:
U
- префикс объекта сериализованного используя механизм#marshal_dump
и#marshal_load
: "\f" Complex
- имя класса - Complex[
- префикс Array"\a"
- количество элементов - 2i "\n"
- 1й элемент - 5i "\v"
- 2й элемент - 6
Integers
Префикс для объекта класса Integer - ASCII код символа i
.
Для представления целых чисел используют несколько форматов:
0
представлено значением 0 (0x00
)- небольшие значения (до 122 (0x7A)) занимают 1 байт, и хранятся как n + 5
- значения до (0x3FFFFFFF) хранятся в формате
<количество байт><байты>
с little-endian порядком байт - значения больше хранятся в формате Bignum (разберем его немного позже)
значения | формат | пример |
---|---|---|
0 | 0x00 |
0x00 - \x04\bi\x00 |
0x01..0x7A | n + 5 |
0x01 - \x04\bi\x06 |
0x7B..0xFF | 0x01 + 1 байт |
0xF1 - \x04\bi\x01\xF1 |
0x0100..0xFFFF | 0x02 + 2 байта |
0xABCD - \x04\bi\x02\xCD\xAB |
0x010000..0xFFFFFF | 0x03 + 3 байта |
0xABCDEF - \x04\bi\x03\xEF\xCD\xAB |
0x01000000..0x3FFFFFFF | 0x04 + 4 байта |
0x03ABCDEF - \x04\bi\x04\xEF\xCD\xAB\x03 |
0x40000000.. | Bignum | 0xABCDEF98 - \x04\bl+\a\x98\xEF\xCD\xAB |
Отрицательные числа
Отрицательные числа кодируется аналогично, только используется two’s complement представление.
Для представления целых чисел используют несколько форматов:
- небольшие значения (до -123 (-0x7B)) занимают 1 байт, и хранятся как n - 5
- значения до (-0x40000000) хранятся в формате
<количество байт><байты>
с little-endian порядком байт и количество байт тоже кодируется в two’s complement формате - значения меньше хранятся в формате Bignum (разберем его немного позже)
значения | формат | пример |
---|---|---|
-0x7B..-0x01 | n - 5 |
-0x01 - \x04\bi\x06 |
-0x100..-0x7C | 0xFF + 1 байт |
-0x100 - \x04\bi\xFF\x00 |
-0x10000..-0x0101 | 0xFE + 2 байта |
-0x10000 - \x04\bi\xFE\x00\x00 |
-0x1000000..-0x10001 | 0xFD + 3 байта |
-0x1000000 - \x04\bi\xFD\x00\x00\x00 |
-0x40000000..-0x1000001 | 0xFC + 4 байта |
-0x40000000 - \x04\bi\xFC\x00\x00\x00\xC0 |
..-0x40000001 | Bignum | -0x40000001 - \x04\bl-\a\x01\x00\x00@ |
Формат Bignum
Префикс для больших значений Integer - ASCII код символа l
.
Диапазон значений, которые представляются в этом формате, это или .
Формат состоит из:
- префикс “l”
- знак “+” или “-“
- количество байт деленное на 2
- бинарное представление числа
Дамп числа :
\x04\bl+\a\x00\x00\x00@
Разберем этот дамп:
фрагмент | значение |
---|---|
“\x04\b” | версия формата (4.8) |
“l” | префикс Bignum Integer |
”+” | знак числа - ‘+’ |
“\a” | длина в байтах деленная на 2 равна 2, следовательно длина равна 4 байтам |
“\x00\x00\x00@” | бинарное представление числа (0x40000000) |
Bignum и большие Fixnum
Исторически сложилось, что числа могут трактоваться как объекты с точки зрения таблицы объектов, а могут и нет:
значения | формат | входят в таблицу объектов |
---|---|---|
префикс "i" (Fixnum) | нет | |
, | префикс "l" (Bignum) | нет |
, | префикс "l" (Bignum) | да |
Подробнее о больших Fixnum расскажу ниже в “Переносимость между платформами”.
Решения в этом формате
Дизайн формата Marshal преследуют в первую очередь следующие цели - компактность дампа, его переносимость между разными платформами, обратную совместимость и расширяемость.
Внимание также уделили удобству разработчиков - несмотря на бинарный
формат дампа его можно читать как есть благодаря символьным префиксам.
Например, дамп String начинается с символа/байта "
, Array - с [
, а
Hash - с {
.
Компактность
Компактность достигается разными способами.
Самый яркий пример - представление целых чисел. Формат чисел имеет переменную длину. Малые значения (и, вероятно, самые часто используемые) занимают всего один байт. БОльшие значения занимают 2, 3 и 4 байта плюс 1 дополнительный байт для длины. Совсем большИе числа (и, вероятно, самые редкие) требуют уже от двух и больше дополнительных байт.
Singleton объекты true, false и nil, которые часто встречаются в коде, занимают также всего один байт.
Для повсеместно используемых String сериализуются еще и кодировка
символов. Часто используемые кодировки US-ASCII и UTF-8 сохраняются в
компактном виде - как “T” (true) или “F” (false) в сумме занимая
всего 4 байта. Для остальных кодировок сохраняется полное название плюс
имя переменной encoding
.
Еще один способ сэкономить место - избежать дублирования с помощью таблиц объектов и Symbol’ов. Вместо всего объекта-дубликата в дамп попадает ссылка - его номер в таблице.
Переносимость между платформами
Ruby можно использовать на совсем разных аппаратных платформах и дамп сделанный на одно платформе должен успешно прочитаться на любой другой.
Основная проблема возникает в представлении чисел - может отличаться порядок байт - big-endian или little-endian, разрядность чисел - 32 или 64 бита, нативное представление чисел с плавающей точкой. Поэтому формат изолируют от особенностей реальной платформы.
Примеры:
- бинарное представление целых чисел строго в little-endian порядке
- Float представлены в строковой форме полностью избегая бинарного представления (интересное обсуждение потери точности)
- большие Fixnum (которым нужно от 32 до 64 бит) не попадают в таблицу объектов, хотя и сохраняются в формате Bignum
Проблема больших Fixnum
Проблема больших Fixnum любопытна. CRuby по возможности хранит целые
числа в нативном int
и называет это Fixnum. Значения, которые не
помещаются в int
, реализованы как объекты и называются Bignum. Эта деталь
реализации скрыта за общим Ruby классом Integer. CRuby поддерживает как
x86 так и amd64 платформы и дамп созданный на amd64 должен прочитаться
на x86 и наоборот. На amd64 Fixnum использует 62 бита, а на x86
соответственно 30 бит. Поэтому Fixnum на amd64, которые занимает от 30 до
62 бит (большой Fixnum), с точки зрения CRuby на x86 уже Bignum.
Эта проблема решена следующим способом - на amd64 большие Fixnum сериализуются в формате Bignum, а не Fixnum. Поэтому такой дамп успешно прочтется на x86. Но теперь усложнится чтение формата Bignum на amd64 - результат может быть как Fixnum так и Bignum в зависимости от значения числа.
Совместимость
Из версии в версию core library Ruby расширяется - добавляется новый функционал, фиксятся баги, делают breaking changes. Меняется и формат Marshal.
В дампе сохраняется версия формата (текущая 4.8), но уже много лет она не меняется. Это означает, что все изменения в формате должны быть одновременно forward и backward совместимы. То есть дамп сделанные в старой версии Ruby должен корректно читаться в новой версии. И наоборот - дамп сделанный в новой версии должен прочитаться в старой. Давайте посмотрим как это получается.
Новый атрибут built-in класса можно попросту не сериализовывать. Например
Regexp#timeout
в Ruby 3.2 есть, а в дамп его не сохраняют.
Новый атрибут может сохраниться как instance variable. Так сохраняют
флаг Hash ruby2_keywords - в виде переменной K
.
Еще один добавленный атрибут Hash compare_by_identity сохранился совершенно
неожиданным способом - используя формат “подкласс built-in класса”. К
дампу Hash просто добавился префикс C:\tHash
, что должно означать, что
это объект класса Hash унаследованного от built-in класса. Но так как
built-in класса это все тот же Hash то получилось странно и длинно.
А вот с изменением дела сложнее. Например, наносекунды Time раньше
сохранялись в instance variable submicro
как строка, а сейчас в
instance variables nano_num
и nano_den
как числа. Чтобы добиться
forward и backward совместимости сейчас сохраняются и читаются и
submicro
и nano_num
с nano_den
.
Гибкость и расширяемость
Гибкость и динамичность Ruby потребовало гибкости и от формата сериализации. Возможность комбинировать свойства объектов (instance variables, подмешанные модули) и разные форматы самого объекта (универсальный формат объекта, объект разных built-in классов, класс, модуль, пользовательские форматы) позволило просто и красиво выразить многообразие свойств объекта в Ruby.
Возможность задавать пользовательский формат сериализации оказалась такой удачной, что ее используют для сериализации некоторых built-in классов, например, Time, Complex, Rational и Encoding.
Итоги
Формат Marshal получился и компактный и гибкий и расширяемый. Еще и невооруженным глазом можно читать.
С другой стороны он не совсем бинарный и, как следствие, компактный (в строковом виде сохраняются Float и Bignum, а сами строки не сжаты). Можно и компактнее ужать. Но формат и не совсем текстовый - с числами и Time надо возиться на уровне битов и байтов.
Текущая версия формата 4.8 с нами уже много-много лет. Она появилась не то в Ruby 1.8.0 не то в какой-то из версий 1.7.x. За годы накопился груз изменений и компромиссов, чтобы сохранить совместимость. Накопились не оптимальные решения. И с этим придется жить до следующей версии.
PS
Для удобства чтения дампов, декодирования чисел и описания фрагментов дампа я сделал небольшой gem marshal-parser.
Ссылки
- https://shopify.engineering/caching-without-marshal-part-one
- https://iliabylich.github.io/2016/01/25/ruby-marshalling-from-a-to-z.html
- http://jakegoulding.com/blog/2013/01/15/a-little-dip-into-rubys-marshal-format/
- http://jakegoulding.com/blog/2013/01/16/another-dip-into-rubys-marshal-format/
- http://jakegoulding.com/blog/2013/01/20/a-final-dip-into-rubys-marshal-format/
- https://docs.ruby-lang.org/en/3.2/marshal_rdoc.html
- https://docs.ruby-lang.org/en/3.2/Marshal.html