The dRuby Book

4.2 Passing by Reference Automatically

It would be very inconvenient if you had to use DRbObject.new() every time you needed to pass by reference. No worries, because dRuby has an automatic mechanism to find out which way is suitable and selects the correct mechanism for you. If dRuby can serialize an object using Marshal.dump, then you should pass by value. If not, pass by reference.

Simple, right? Let’s check the behavior by doing a few experiments.

Can’t Dump

When you use Marshal, there are certain objects that you can’t dump, such as IO, Thread, and Proc. Let’s try with ($stdout).

  ​% irb --prompt simple​
  ​>> Marshal.dump($stdout)​
  ​TypeError: can't dump IO​
  ​...​

A TypeError exception is raised. This is because $stdout is an instance of IO, so you can’t dump. The error happens even when it’s possible to dump the object you’re going to dump, but the object contains a reference to an object that can’t dump. Here is the example of an Array with $stdout:

  ​>> ary = []​
  ​=> []​
  ​>> Marshal.dump(ary)​
  ​=> "?004 ...."​
  ​>> ary[0] = $stdout​
  ​=> #<IO:0x ..... >​
  ​>> Marshal.dump(ary)​
  ​TypeError: can't dump IO​
  ​...​

Let’s see what happens if we pass $stdout to a different process via dRuby. Let’s start two terminals.

  ​#[Terminal 1]​
  ​% irb --prompt simple -r drb/drb​
  ​>> front = {}​
  ​>> DRb.start_service('druby://localhost:12345', front)​
  ​=> #<DRb::DRbServer:0x ..... >​
  ​>> DRb.uri​
  ​=> "druby://localhost:12345"​

As we tried earlier, we pass Hash to a front object and start a server from terminal 1.

  ​# [Terminal 2]​
  ​% irb --prompt simple -r drb/drb​
  ​>> DRb.start_service​
  ​=> #<DRb::DRbServer:0x ..... >​
  ​>> DRb.uri​
  ​=> "druby://localhost:1121"​
  ​>> there = DRbObject.new_with_uri('druby://localhost:12345')​
  ​=> #<DRb::DRbObject:0x ..... >​

OK, a client is ready at terminal 2. Let’s pass $stdout to terminal 1.

  ​# [Terminal 2]​
  ​>> there[:stdout] = $stdout​
  ​=> #<IO:<STDOUT>>​

You can’t do Marshal.dump, but no error is raised. Let’s check what happened to $stdout, which is passed to terminal 1.

  ​# [Terminal 1]​
  ​>> front[:stdout]​
  ​=&gt;&gt; #<IO:0x007ffe7206fd10>​
  ​>> front[:stdout].class​
  ​=> DRb::DRbObject​

front[:stdout] becomes a DRbObject instead of a IO. The @uri of DRbObject points to DRb.uri of terminal 2. You can see that front[:stdout] at DRbObject is a reference that refers to an object at terminal 2.

So, what just happened? When $stdout is passed from terminal 2 to terminal 1, dRuby captures the Marshal.dump failure and passes by reference instead of by value.

Let’s check if the reference of $stdout at terminal 2 is really passed to terminal 1. Try printing out a string to front[:stdout]. Did it display the string in terminal 2?

  ​# [Terminal 1]​
  ​>> front[:stdout].puts("Hello, DRbObject")​
  ​# [Terminal 2]​
  ​>> Hello, DRbObject​

As expected, "Hello, DRbObject" appears at terminal 2. front[:stdout] is actually $stdout of terminal 2. Phew.

This is how dRuby behaves when it sends an object that cannot do Marshal.dump. dRuby automatically chooses to pass by reference without specifying it.

Also, if you try to access a dRuby object that contains a reference to your own process (rather than the target), then dRuby returns the real object rather than the DRbObject object. Let’s check out the reference to $stdout.

  ​# [Terminal 2]​
  ​>> there[:stdout]​
  ​=> #<IO:<STDOUT>>​
  ​>> there[:stdout].class​
  ​=> IO​

This is an IO instance, not DRbObject, which has the same ID as $stdout.

  ​# [Terminal 2]​
  ​>> there[:stdout] == $stdout​
  ​=> true​

DRbUndumped

In the previous section, we learned how dRuby passes by reference if you can’t do Marshal.dump to an object, such as IO, Thread, and Proc.

However, you may sometimes want to pass your own class by reference even though the object can be dumped. DRbUndumped is a helper module to tell dRuby to pass by value. You can either include a class or extend an object you want to pass by reference; then the object can’t be dumped. Let’s try it.

First we prepare the Foo class (foo.rb).

foo.rb
  class Foo​
  def initialize(name)​
  ​ @name = name​
  end
  ​ attr_accessor :name​
  end

We require foo.rb and then start the dRuby service.

  ​# [Terminal 1]​
  ​% irb --prompt simple -r drb/drb -r ./foo.rb​
  ​>> front = {}​
  ​>> DRb.start_service('druby://localhost:12345', front)​
  ​# [Terminal 2]​
  ​% irb --prompt simple -r drb/drb -r ./foo.rb​
  ​>> DRb.start_service​
  ​>> there = DRbObject.new_with_uri('druby://localhost:12345')​

Make sure that the instance of Foo can be dumped via Marshal.dump.

  ​# [Terminal 1]​
  ​>> foo = Foo.new('Foo1')​
  ​>> Marshal.dump(foo)​
  ​=> "?004?006 .... "​
  ​>> front[:foo] = foo​
  ​=> #<Foo:0x .... >​
  ​# [Terminal 2]​
  ​>> there[:foo]​
  ​=> #<Foo:0x .... >​
  ​>> there[:foo].name​
  ​=> "Foo1"​

When you look at there[:foo], it’s an actual instance of Foo, rather than the reference.

  ​# [Terminal 2]​
  ​>> there[:foo].name = 'Foo2'​
  ​# [Terminal 1]​
  ​>> foo.name​
  ​'Foo1'​

Because foo is a copy of the object in terminal 1, changing the value at terminal 2 doesn’t affect terminal 1.

Next let’s extend DRbUndumped. foo should be passed by reference.

  ​# [Terminal 1]​
  ​>> foo.extend(DRbUndumped)​
  ​>> Marshal.dump(foo)​
  ​TypeError: can't dump​
  ​ ....​

Once we mix DRbUndumped into the foo instance and then do Marshal.dump, then foo raises a TypeError error. Pass foo from terminal 2 to terminal 1, and it should send by reference.

  ​# [Terminal 2]​
  ​>> there[:foo]​
  ​=> #<DRb::DRbObject:0x .... >​
  ​>> there[:foo].name​
  ​=> "Foo1"​
  ​>> there[:foo].name = 'Foo2'​
  ​=> "Foo2"​
  ​>> there[:foo].name​
  ​=> "Foo2"​
  ​# [Terminal 1]​
  ​>> foo.name​
  ​=> "Foo2"​

there[:foo] returned DRbObject instead of Foo. If you use there[:foo].name= to change @name, then it will impact foo. This means that it is passing by reference as we expect.

When you want to include it in a instance, you use extend. When you want to include it in a class, you use include.

Let’s try the include way as well. First, make sure that an instance of Foo can do Marshal.dump.

  ​# [Terminal 1]​
  ​>> bar = Foo.new('Bar1')​
  ​>> Marshal.dump(bar)​
  ​=> "?004?006 .... "​

Mix DRbUndumped with include.

  ​# [Terminal 1]​
  ​>> class Foo​
  ​>> include DRbUndumped​
  ​>> end​
  ​>> Marshal.dump(bar)​
  ​TypeError: can't dump​
  ​ ....​
  ​>> Marshal.dump(Foo.new('Foo'))​
  ​TypeError: can't dump​
  ​ ....​

Now any instances made out of Foo fail to Marshal.dump. Because they fail to Marshal.dump, the instances of Foo are always passed by reference.

  ​# [Terminal 1]​
  ​>> front[:bar] = bar​
  ​# [Terminal 2]​
  ​>> there[:bar]​
  ​=> #<DRb::DRbObject:0x .... >​
  ​>> there[:bar].name​
  ​=> "Bar1"​
  ​>> there[:bar].name = 'Bar2'​
  ​=> "Bar2"​
  ​>> there[:bar].name​
  ​=> "Bar2"​
  ​# [Terminal 1]​
  ​>> front[:bar].name​
  ​=> "Bar2"​