Tuesday, July 29, 2014

Or, how to get the anonymous module from Kernel#load with wrap=true?

With the same technique than in the previous post it is also possible to capture the anonymous wrapper module that Kernel#load method with wrap=true uses to build the sandbox. This way you can load configuration or other data without polluting your namespace.

File b.rb:

class Foo
  def bar
    puts "bar"
  end
end

throw :wrapper, Module.nesting.last

File main2.rb:

mod = catch :wrapper do
  load File.expand_path('b.rb'), true
end
print "Anonymous module is '#{mod}' (#{mod.class})\n"
mod::Foo.new.bar
print "This will not work due the sandbox:\n"
Foo.new.bar

Output:

Anonymous module is '#<Module:0x007fbccc2ace08>' (Module)
bar
This will not work due the sandbox:
main2.rb:7:in `<main>': uninitialized constant Foo (NameError)

This can be packaged to a module for easier usage. Unfortunately the call of Module.nesting needs to be lexically in the loaded file for this to work so it cannot be embedded to Sandbox module. One implementation could be the following.

File c.rb:

class Foo
  def bar
    puts "bar"
  end
end

Sandbox.exit Module.nesting.last

File main3.rb:

module Sandbox
  def self.load(file)
    catch :sandbox do
      Kernel.load file, true
    end
  end

  def self.exit(value)
    throw :sandbox, value
  end
end

mod = Sandbox.load('c.rb')
print "Anonymous module is '#{mod}' (#{mod.class})\n"
mod::Foo.new.bar
print "This will not work due the sandbox:\n"
Foo.new.bar

Output:

Anonymous module is '#<Module:0x007f4d8b4f8500>' (Module)
bar
This will not work due the sandbox:
main3.rb:16:in `<main>': uninitialized constant Foo (NameError)

Simple and elegant :)

How to return value from loaded file?

Have you ever wanted to get return value from file which is read with Kernel#load without resorting to global variables or constants. It is actually quite simple when you realize that you can use non-local exit (continuations) to do that.

File a.rb:

class Foo
  def bar
    puts "bar"
  end
end

throw :value, "hello"

File main.rb:

value = catch :value do
  load File.expand_path('a.rb')
end
print "a.rb returned value '#{value}'\n"
Foo.new.bar

Output:

a.rb returned value 'hello'
bar