Skip to content
Nenad Ocelic edited this page Jun 4, 2015 · 17 revisions

MessagePack is an efficient binary serialization format.

This is a fork of Ruby MessagePack library.

It aims to add the support for extended types from the MessagePack specification to the MessagePack-Ruby library.

  • Create serializers and deserializers for types not explicitly supported by the MessagePack specification, like Time or Symbol.

  • Create SerDes mechanism for user-defined classes.

By default, when a MessagePack::Packer instance is requested to pack an object of an unknown class, it will delegate the packing to the object’s to_msgpack method.

To be packable, the object needs to respond to the to_msgpack method.

For example:

class MyClass
  def initialize( data )
      @data = data.to_s
  end
  def to_msgpack( packer )
      MessagePack::ExtType.pack( packer, 22, @data )
  end
end

will use extended type number 22 for packing instances of MyClass class. In this particular case, the extended type data (payload) will be whatever is supplied to the initializer of MyClass.

MessagePack::ExtType.pack hides the low level details of the MessagePack::Packer and MessagePack::Buffer from the user.

It is possible to use Packer#write_extended_header and Buffer#write directly to achieve the same effect as ExtType.pack. However, this requires slightly more caution because it may result in an incorrect output if the length argument supplied to Packer#write_extended_header does not match the actual byte length of the string fed to Buffer#write.

The default low-level to_msgpack mechanism gives the control of the extended type number to the packed object, not to the packer.

If the opposite is desired, it is possible to register classes with a packer and this way give the control of the extended type number to the packer. In that case, the object’s serialization method only needs to return the payload as a string, everything else is handled by the packer. For that reason, this approach is called “high-level” packing.

When a class is registered with a packer, the method called on that class instances is to_exttype:

class MyClass  # (reusing the initializer from above)
  def to_exttype
    return @data
  end
end
packer = MessagePack::Packer.new
packer.register_exttype 33, MyClass

By default, registered classes will be handled by the described high-level mechanism, and for unregistered classes the packer will fall back to the low-level to_msgpack mechanism.

To prevent the packer from falling back to to_msgpack in case of unregistered classes, the packer can be created with the :unknown_class => false option:

packer = MessagePack::Packer.new :unknown_class => false

A packer created this way will refuse objects of unregistered classes and will raise TypeError if such objects are attempted to be packed.

If some classes need to be handled by a packer via the low-level mechanism, but

  • the packer is set up to refuse unknown classes, and/or

  • a method different from to_msgpack should be used,

the class can be registered with the packer using register_lowlevel:

packer.register_lowlevel MyClass

or, equivalently, the register_exttype method with typenr = nil:

packer.register_exttype nil, MyClass

Instead of predefined methods on the registered class instances, it is possible to register any callable object as the packing handler. Callable objects include bound methods and procs. If a block is given to register_exttype / register_lowlevel, it will be automatically converted to a proc:

packer.register_exttype( Time, 0x7f ) do |time|
  [time.to_i].pack("N")
end
packer.exttype Time  #=>  [127, #<Proc:.......>]

Or, if a low-level packing is desired:

packer.register_lowlevel( Time ) do |time, packer|
  MessagePack::ExtType.pack( packer, 0x7f, [time.to_i].pack('N') )
end
packer.exttype Time  #=>  [nil, #<Proc:.......>]
packer.pack( Time.now).to_s  #=>  "\xD6\x7FUo\x9B\xEC"

The above MessagePack::ExtType.pack call is equivalent to this even lower-level code:

packer.register_lowlevel( Time ) do |time, packer|
  buff = packer.buffer
  buff.write_extended_header( 0x7f, 4 )
  buffer.write( [time.to_i].pack('N') )
end

By default, when a MessagePack::Unpacker instance finds extended types in its input stream, it creates a MessagePack::ExtType instance holding the type number and raw data found in the input.

unpacker = MessagePack::Unpacker.new
unpacker.feed( "\xD6@abcd" ).unpack  #=>  #<MessagePack::ExtType:.... @type=64, @data="abcd">

This default behavior can be modified by assigning to MessagePack::Unpacker.default_exttype. For example:

MessagePack::Unpacker.default_exttype = false

changes the default behavior to raising a MessagePack::UnpackerError exception for unknown extended types.

To change the default behavior of just a single MessagePack::Unpacker instance, default_exttype= can be invoked on that instance:

unpacker = MessagePack::Unpacker.new
unpacker.default_exttype = false

or it can be created with the :default_exttype option:

unpacker = MessagePack::Unpacker.new :default_exttype => false

To alter the behavior of MessagePack::Unpacker instances for specific extended types, the corresponding handler can be registered with register_exttype:

class MyClass
  def initialize( data )
      @data = data.to_s
  end
  def self.from_exttype( type, data )
      self.new data
  end
end
unpacker = MessagePack::Unpacker.new
unpacker.register_exttype 22, MyClass

The above code will instruct the unpacker to call from_exttype method of the MyClass class when it needs to unpack extended type nr 22.

The data argument passed to the from_exttype method contains already unpacked extended type payload as a string.

To register a type on the global level, MessagePack::Unpacker.register_exttype:

MessagePack::Unpacker.register_exttype 22, MyClass

Such a registration will be available to all MessagePack::Unpacker instances, unless they override it on per-instance basis.

So far we have seen per-instance and global extended type registration, as well as the per-instance and global defaults.

The exact lookup chain is as follows:

1.  self.exttype( typenr )
2.  self.default_exttype
3.  Unpacker.exttype( typenr )
4.  Unpacker.default_exttype

where self is a MessagePack::Unpacker instance and typenr is the extended type number found in the input stream.

The search proceeds down the lookup chain until a non-nil value is found.

To find out how an extended type typenr would be unpacked, resolve_exttype method can be used:

unpacker.resolve_exttype 22  #=>  MyClass

If resolve_exttype returns nil for an extended type typenr, an attempt to unpack such an extended type will result in an UnpackerError exception.

Instead of predefined class methods, it is possible to register any callable object (e,g, Method or Proc) as the unpacking handler.

If a block is given to <tt>register_exttype<tt>, it will be automatically converted to a proc:

unpacker.register_exttype( 0x7f ) do |type, data|
  if data.length != 4
    raise MessagePack::MalformedFormatError, "Expected 4 bytes for Extended type 0x7f but got #{data.size} bytes"
  end
  Time.at( *data.unpack('N') )
end
unpacker.feed( "\xD6\x7FUo\x90\xA6" ).unpack  #=>  2015-06-04 01:41:26 0200