How to programmatically remove “singleton informat

2019-03-29 15:47发布

问题:

I have created an object that failed to marshal due to a "singleton metaclass definition executed on runtime" (Is this description of what the code does correct?).

This is performed by following code:

# define class X that my use singleton class metaprogramming features
# through call of method :break_marshalling!
class X
   def break_marshalling!
     meta_class = class << self
       self 
     end
     meta_class.send(:define_method, :method_y) do 
      return 
    end
  end
end

# prepare my instance of X now
instance_of_x = X.new

# marshalling fine here
Marshal.dump instance_of_x

# break marshalling with metprogramming features
instance_of_x.break_marshalling!

Marshal.dump instance_of_x
# fails with TypeError: singleton can't be dumped 

What can I do to make the object marshall correct? Is it possible to "remove" the singleton components from class X of object instance_of_x?

I really need an advise on that because of some of our objects needed to be cached through Marshal.dump serialization mechanism. This code is executed in ruby-1.9.3 but I expect it to behave similar in ruby-2.0 or ruby-2.1

回答1:

You can define custom marshal_dump and marshal_load methods:

class X
  def break_marshalling!
    meta_class = class << self
      self 
    end
    meta_class.send(:define_method, :method_y) do 
      return 
    end
  end

  # This should return an array of instance variables
  # needed to correctly restore any X instance. Assuming none is needed
  def marshal_dump
    []
  end

  # This should define instance variables
  # needed to correctly restore any X instance. Assuming none is needed
  def marshal_load(_)
    []
  end
end

# Works fine
restored_instance_of_x = 
  Marshal.load Marshal.dump(X.new.tap { |x| x.break_marshalling! })

# Does not work
restored_instance_of_x.method_y

If you want you can manage dynamic methods definitions via method_missing:

class X
  def method_missing(name, *args)
    if name == :method_y
      break_marshalling!
      public_send name, *args
    else
      super
    end
  end
end

# Works fine
Marshal.load(Marshal.dump(X.new)).method_y


回答2:

The one-liner way to (usually) remove singleton information from instance_of_x:

instance_of_x = instance_of_x.dup


回答3:

I don't think there is such a thing is remove_singleton_class_information!, but you can try to programatically undefine things in the singleton class.

According to the documentation Marshal doesn't like objects with singleton methods. With some experimentation, it seems it also doesn't like objects with a singleton class that has instance variables.

class Foo
  def break_marshal
    singleton_class.class_eval do
      @@x = true
      @x = false
      define_method :bar do
        puts @@x
        puts singleton_class.instance_variable_get(:@x)
        puts singleton_class.send(:baz)
        puts singleton_class.const_get(:X)
      end
      define_singleton_method :baz do
        "Singleton method in a singleton"
      end
    end
  end

  def fix_marshal
    singleton_methods.each { |m| singleton_class.send(:remove_method, m) }
    singleton_class.class_eval do
      instance_variables.each { |v| remove_instance_variable v }
    end
  end
end

f = Foo.new
f.break_marshal
f.singleton_class.const_set(:X, 1)
f.bar
f.fix_marshal

Marshal.dump f

Note how hard I tried to break Marshal. I gave the singleton a class variable, an instance variable, a method, its own singleton method, and a constant. But Marshal works fine if you just remove the singleton method and the instance variable.

However, I don't think this is the right way to marshal such an object. @mdesantis's answer of defining your own marshal_dump and marshal_load is more proper.



回答4:

I came up with this method, which recursively removes singleton_methods from an object, and from objects assigned to instance variables:

def self.strip_singleton(obj)
  obj = obj.dup unless (obj.nil? || obj.singleton_methods.empty?)
  obj.instance_variables.each do |var|
    obj.instance_variable_set(var, strip_singleton(obj.instance_variable_get(var)))
  end
  obj
end