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] |
|
=>> #<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" |