Как мне десериализовать классы в Psych?

#ruby #yaml #psych

#ruby #yaml #psych

Вопрос:

Как мне десериализовать в Psych, чтобы вернуть существующий объект, такой как объект класса?

Чтобы выполнить сериализацию класса, я могу сделать

 require "psych"

class Class
  yaml_tag 'class'
  def encode_with coder
    coder.represent_scalar 'class', name
  end
end

yaml_string = Psych.dump(String) # => "--- !<class> Stringn...n" 
  

но если я попытаюсь сделать Psych.load это, я получу анонимный класс, а не класс String.

Обычный метод десериализации — это Object#init_with(coder) , но это только изменяет состояние существующего анонимного класса, тогда как я хочу класс String.

Psych::Visitors::ToRuby#visit_Psych_Nodes_Scalar(o) есть случаи, когда вместо того, чтобы изменять существующие объекты с помощью init_with , они в первую очередь удостоверяются, что создан правильный объект (например, вызывая Complex(o.value) десериализацию комплексного числа), но я не думаю, что мне следует использовать этот метод monkeypatching.

Я обречен работать с низкоуровневым или среднеуровневым излучением, или я что-то упускаю?

Предыстория

Я опишу проект, зачем ему нужны классы и почему ему нужна (де)сериализация.

Проект

Малый собственный коллайдер предназначен для создания случайных задач для запуска Ruby. Первоначальной целью было посмотреть, возвращают ли разные реализации Ruby (например, Rubinius и JRuby) одинаковые результаты при выполнении одинаковых случайных задач, но я обнаружил, что это также полезно для обнаружения способов сегментирования Rubinius и YARV.

Каждая задача состоит из следующего:

 receiver.send(method_name, *parameters, amp;block)
  

где receiver — случайно выбранный объект, а method_name
имя случайно выбранного метода и *parameters представляет собой массив
случайно выбранные объекты. amp;block не очень случайный — это в основном
эквивалентно {|o| o.inspect} .

Например, если бы получатель был «a», имя_метода было:casecmp, а параметры были [«b»], тогда вы бы вызывали

 "a".send(:casecmp, "b") {|x| x.inspect}
  

что эквивалентно (поскольку блок не имеет значения)

 "a".casecmp("b")
  

небольшой собственный коллайдер запускает этот код и регистрирует эти входные данные и
также возвращаемое значение. В этом примере большинство реализаций Ruby
возвращает -1, но на одном этапе Rubinius вернул 1. (Я подал это как
ошибка https://github.com/evanphx/rubinius/issues/518 и Rubinius
разработчики исправили ошибку)

Зачем ему нужны классы

Я хочу иметь возможность использовать объекты класса в моем маленьком собственном коллайдере. Обычно они являются получателем, но они также могут быть одним из параметров.

Например, я обнаружил, что один из способов segfault YARV — это выполнить

 Thread.kill(nil)
  

В этом случае получателем является поток объектов класса, а параметрами являются
[ноль]. (Сообщение об ошибке:http://redmine.ruby-lang.org/issues/show/4367 )

Зачем ему нужна (де)сериализация

Небольшой собственный коллайдер нуждается в сериализации по нескольким причинам.

Во-первых, использование генератора случайных чисел для генерации серии случайных задач каждый раз непрактично. В JRuby есть другой встроенный генератор случайных чисел, поэтому даже при одинаковом начальном значении PRNG он будет давать разные задачи YARV. Вместо этого я создаю список случайных задач один раз (при первом запуске ruby bin / small_eigen_collider), при первоначальном запуске сериализую список задач в tasks.yml, а затем выполняю последующие запуски программы (с использованием разных реализаций Ruby) для чтения в этих задачах.yml-файл для получения списка задач.

Еще одна причина, по которой мне нужна сериализация, заключается в том, что я хочу иметь возможность редактировать список задач. Если у меня есть длинный список задач, которые приводят к ошибке сегментации, я хочу сократить список до минимума, необходимого для возникновения ошибки сегментации. Например, со следующей ошибкой https://github.com/evanphx/rubinius/issues/643 ,

 ObjectSpace.undefine_finalizer(:symbol)
  

само по себе не вызывает ошибки сегментации, и также

 Symbol.all_symbols.inspect
  

но если вы соедините их вместе, это сработает. Но я начал с
тысячи задач, и нужно было свести их обратно только к этим двум
задачи.

Имеет ли смысл в данном контексте десериализация, возвращающая существующие объекты класса, или вы думаете, что есть способ получше?

Комментарии:

1. Я не вижу в документах YAML (раздел yaml для ruby ), что (де) сериализация классов действительно возможна, только созданные экземпляры объектов и другие интересные вещи. Это означает: вы можете поместить объект в YAML, но не его определение класса, из которого был создан соответствующий объект. / КСТАТИ: я не могу понять идею, стоящую за вашей проблемой. Есть какие-нибудь примеры / кейсы, где это было бы полезно?

2. @asaaki: Когда я десериализуюstring, я просто хочу вернуть ссылку на существующий класс String. Мне не нужно сохранять и восстанавливать, какие методы есть в классе. Я отредактировал вопрос, чтобы дать (намного) больше информации.

3. Я не могу дать вам ответа, а только предложение: почему бы не использовать Marshal.dump / .load ? Поэтому попробуйте дополнительную упаковку, такую как: s = Psych.dump(Marshal.dump(String)) . Этап десериализации: Marshal.load(Psych.load(s)) — Я знаю, это выглядит дерьмово, но это работает!

Ответ №1:

Статус-кво моих текущих исследований:

Чтобы добиться желаемого поведения, вы можете использовать мой обходной путь, упомянутый выше.

Вот красиво оформленный пример кода:

 string_yaml  = Psych.dump(Marshal.dump(String))
  # => "--- ! "\x04\bc\vString"n"
string_class = Marshal.load(Psych.load(string_yaml))
  # => String
  

Ваш взлом с изменением класса, возможно, никогда не сработает, потому что реальная обработка классов не реализована в psych / yaml.

Вы можете воспользоваться этим репозиторием tenderlove / psych, который является автономной библиотекой.

(Gem: psych — чтобы загрузить его, используйте: gem 'psych'; require 'psych' и выполните проверку с помощью Psych::VERSION )

Как вы можете видеть в строке 249-251, обработка объектов с помощью анонимного класса Class не обрабатывается.

Вместо обезьяньего исправления класса class я рекомендую вам внести свой вклад в библиотеку Psych, расширив обработку этого класса.

Итак, на мой взгляд, конечный результат yaml должен быть чем-то вроде: "--- !ruby/class String"

После одной ночи размышлений об этом я могу сказать, что эта функция была бы действительно хороша!


Обновить

Найдено крошечное решение, которое, похоже, работает должным образом:

суть кода: gist.github.com/1012130 (с описательными комментариями)

Ответ №2:

Сопровождающий Psych реализовал сериализацию и десериализацию классов и модулей. Теперь это в Ruby!

Комментарии:

1. Приятно видеть — надеюсь, это был вопрос, который вызвал обновление. ; o)

2. К сожалению, нет, это не давало ему покоя в списке рассылки Seattle.rb. 😛