initialize_clone, initialize_dup and initialize_copy in Ruby

Ruby has two methods for creating shallow copies of objects:

  • Object#clone copies the object, including its frozen state
  • Object#dup copies the object, not including its frozen state

In general clone is meant to be a more “exact” mechanism for copying objects, whereas dup is often implemented by simply creating a new instance of the relevant class with the appropriate parameters. Both clone and dup copy the tainted state of the object. clone copies the singleton class (if any), whereas dup does not.

Initialising copies

Sometimes it is useful to be able to specify some initialisation code that should be run when an object is copied. For example, suppose an object tracks its own internal state in some way, you may wish to reset this state when the object is copied.

Ruby 1.9 has 3 methods to help you do this: initialize_clone, initialize_dup and initialize_copy. At present, there is no documentation (that I can find), so I had to do a bit of digging through C code to work out the exact behaviour.

The implementation is expressed by the following psuedo-code:

class Object
  def clone
    clone = self.class.allocate

    clone.copy_instance_variables(self)
    clone.copy_singleton_class(self)

    clone.initialize_clone(self)
    clone.freeze if frozen?

    clone
  end

  def dup
    dup = self.class.allocate
    dup.copy_instance_variables(self)
    dup.initialize_dup(self)
    dup
  end

  def initialize_clone(other)
    initialize_copy(other)
  end

  def initialize_dup(other)
    initialize_copy(other)
  end

  def initialize_copy(other)
    # some internal stuff (don't worry)
  end
end

initialize_copy runs for both clone and dup, but it is called by initialize_clone and initialize_dup. Therefore, if you implement your own version of initialize_clone or initialize_dup, it is advisable to call super to make sure that initialize_copy is also called.

Ruby 1.8

Ruby 1.8 behaves in roughly the same way, but it does not have initialize_dup or initialize_clone built-in.

It would be possible to implement some sort of backport in pure Ruby, but harder to get the semantics to be identical:

  • In Object#clone, the clone is frozen after initialize_clone is called
  • The backport would probably be implemented by calling super and then calling initialize_clone or initialize_dup (which would be defined in pure Ruby on Object). But note that the super call will result in initialize_copy being called already, which is inherently different from the 1.9 implementation where initialize_clone and initialize_dup are in charge of calling initialize_copy.

Ouch, head hurts

Yeah, never mind really. I was just curious and thought I’d share my findings.

Comments

I'd love to hear from you here instead of on corporate social media platforms! You can also contact me privately.

MarkDBlackwell's avatar

MarkDBlackwell

Thanks a lot!

tombeynon's avatar

tombeynon

Thanks, this was very helpful. I'm surprised there's still such little documentation on this, especially since it's mentioned in the Ruby docs under #dup and #clone.

Dorian's avatar

Dorian

Thanks, got lead here because it was used in a few vulnerabilities against mruby: https://hackerone.com/repor...

Avdi Grimm's avatar

Avdi Grimm

This is still such a great breakdown. I'm using it to refresh my memory without digging into the source today. Thanks!

Add your comment