Mike Slinn

Custom Ruby Bindings

Published 2025-09-05. Last modified 2025-09-18.
Time to read: 7 minutes.

This page is part of the ruby collection.

Ruby ERB expands Ruby expressions embedded in HTML or markup such that the encapsulated expression is replaced by its value. ERB ultimately calls eval, with some wrapping for safety and binding control.

Export Execution Contexts Via Ruby Bindings

I have been developing the next-generation Nugem code for a fair while yet. This command-line program was being constructed so that it would generate new working Ruby gems from command-line parameters.

When generating a new Ruby gem, Nugem needs to inflate ERB templates embedded within file system templates. The problem I encounted was that the values I wanted to inflate ERB templates with were defined in very different binding contexts.

To me, this sounded like there was a need for rudimentary management of Ruby binding context.

Easy, Simple, Flexible Ruby Binding Management

I was not happy with what I could see out there on the interwebs for assembling custom Ruby Bindings, so I created the CustomBinding Ruby gem. It provides a simple and flexible way of constructing a custom Ruby binding, and simplifies the rendering of ERB templates from a custom Ruby binding.

CustomBinding provides an easy, simple and flexible way of specifying variable and method references in ERB templates. The CustomBinding#render method resolves the embedded references against the CustomBinding internal state.

Nugem uses ERB templates to create every type of text file that a Ruby Gem needs. This includes Ruby code, RSpec tests, markdown files, .gitignore, and more.

ERB templates support expressions that contain:

  1. References to self
  2. Local, instance, and global variable names
  3. Local and instance public method names
  4. Computation using the above

An ERB template consists of a String expression enclosed in single or double quotes, or provides as a here doc.

object_id

Every Ruby object has a unique identifier.

IRB Session
$ x = Object.new
=> #<Object:0x00007fcd79d5c9a8>
irb(main):002>
x.object_id => 7160

If you find two variables with the same unique identifier, then they are aliases, or mirrors, for each other. This information could be helpful as you explore how Ruby Bindings work.

Bindings

For ERB to work, a mechanism must exist that can look up the value of a variable or obtain a method body from its name. This mechanism is called a binding. In Ruby, a Binding instance is an object that encapsulates the execution context at a specific point in the code. Each Binding instance maintains an execution environment, sometimes referred to as the execution state.

Each Binding instance in Ruby encapsulates the execution state at the point where it was created. This includes the current values of local variables, the value of self, method visibility, and the context for constant and method lookup. This execution environment can later be used to evaluate code with eval.

ERB is not the only thing that uses bindings; debuggers, the Ruby REPL, and code generators also use bindings.

The Top-Level Binding

Ruby provides a constant called TOPLEVEL_BINDING that returns the binding of the top-level scope. Use it to access the top-level scope from anywhere in the program. Because the top-level scope never contains local variables, TOPLEVEL_BINDING does not contain local variables either.

self and main

When Ruby starts, it sets self to an instance of Object. That instance is not bound to a constant or variable named main. The string "main" is just what you see if you print self:

IRB Session
$ irb
irb(main):001> TOPLEVEL_BINDING.eval 'self'
=> main
irb(main):002>
puts TOPLEVEL_BINDING.eval 'main' (eval):1:in `
': undefined local variable or method `main' for main:Object (NameError)
main ^^^^ from (irb):18:in `eval' from (irb):18:in `
' from /home/mslinn/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/irb-1.15.2/exe/irb:9:in `' from /home/mslinn/.rbenv/versions/3.2.2/lib/ruby/site_ruby/3.2.0/rubygems.rb:319:in `load' from /home/mslinn/.rbenv/versions/3.2.2/lib/ruby/site_ruby/3.2.0/rubygems.rb:319:in `activate_and_load_bin_path' from /home/mslinn/.rbenv/versions/3.2.2/bin/irb:25:in `
'

If a binding is created at the top-level, then binding’s self.to_s returns the string "main". self is an instance of Object. There is no separate main class or module in Ruby; this output just indicates that the binding currently provides a particular execution context. No standalone documentation page for main exists.

Note that IRB starts with TOPLEVEL_BINDING; the binding tends to accumulate things as code is executed.

Shell
$ irb
irb(main):001> self
=> main 
irb(main):002> self.class => Object

There is no global variable or constant called main. The String "main" is just the default output of to_s for that particular Object instance.

TOPLEVEL_BINDING Is Not Empty

The top-level binding is not empty:

IRB Session
$ irb
irb(main):001> puts TOPLEVEL_BINDING.eval 'self.public_methods'
to_s
inspect
conf
pretty_print_cycle
pretty_print
pretty_print_inspect
pretty_print_instance_variables
hash
singleton_class
dup
itself
methods
singleton_methods
protected_methods
private_methods
public_methods
instance_variables
instance_variable_get
instance_variable_set
instance_variable_defined?
remove_instance_variable
instance_of?
kind_of?
is_a?
display
pretty_inspect
public_send
extend
clone
<=>
class
===
!~
frozen?
then
tap
nil?
yield_self
eql?
respond_to?
method
public_method
singleton_method
define_singleton_method
freeze
object_id
send
to_enum
enum_for
!
equal?
__id__
__send__
==
!=
instance_eval
instance_exec
=> nil 

Note that the above incantation could have been written without self, since it is always implied if a receiver is not explicitly specified:

Equivalent IRB Session
$ irb
irb(main):001> puts TOPLEVEL_BINDING.eval 'public_methods'
to_s
inspect
conf
pretty_print_cycle
pretty_print
pretty_print_inspect
pretty_print_instance_variables
hash
singleton_class
dup
itself
methods
singleton_methods
protected_methods
private_methods
public_methods
instance_variables
instance_variable_get
instance_variable_set
instance_variable_defined?
remove_instance_variable
instance_of?
kind_of?
is_a?
display
pretty_inspect
public_send
extend
clone
<=>
class
===
!~
frozen?
then
tap
nil?
yield_self
eql?
respond_to?
method
public_method
singleton_method
define_singleton_method
freeze
object_id
send
to_enum
enum_for
!
equal?
__id__
__send__
==
!=
instance_eval
instance_exec
=> nil 

main Methods

When Ruby finishes initializing, main has the mixture of all the public methods of Object, Kernel, and BasicObject.

  • From Kernel: load, puts, print, p, gets, raise, rand, sleep, and require.
  • From Object: object_id, is_a?, class, tap, and public_send.
  • From BasicObject: ==, !=, !, __send__, and equal?.

The full list can be obtained like this:

Shell
$ ruby -e 'puts TOPLEVEL_BINDING.eval("methods.sort")'

binding and self

In Ruby, every Binding instance contains a self object.

A Binding object captures the execution context at a specific point in the code, including the current value of self, local variables, instance variables, class variables, global variables, and all methods.

The following code accesses the self of a Binding with Binding#receiver:

IRB session
$ irb
irb(main):001> x = Object.new
=> #<Object:0x00007fcd79d5c9a8>
irb(main):002>
b = x.instance_eval { binding } => #<Binding:0x00007f973f4d5138>
irb(main):003>
puts b.receiver.public_methods.sort ! != !~ <=> == === __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id pretty_inspect pretty_print pretty_print_cycle pretty_print_inspect pretty_print_instance_variables private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods tap then to_enum to_s yield_self

You can also evaluate self within the Binding:

IRB Session
irb(main):003> puts binding.eval 'self'
=> main 

You can interrogate self to discover the values of its properties:

IRB Session
irb(main):004> binding.eval 'self.singleton_methods'
=> [:to_s, :inspect, :conf] 
irb(main):005> binding.eval 'self.local_variables' => [:b, :x, :_]
irb(main):006> binding.eval 'self.instance_variables' => [] irb(main):007> puts binding.eval 'self.global_variables' $-p $-l $-a $@ $; $-F $-I $: $" $LOAD_PATH $LOADED_FEATURES $-v $VERBOSE $-W $-w $-d $DEBUG $PROGRAM_NAME $0 $& $` $' $+ $= $? $$ $stdin $stdout $> $stderr $DEBUG_RDOC $_ $~ $! $/ $, $\ $-0 $< $. $FILENAME $-i $* => nil %}

The implied caller (called a receiver in Ruby) is self, so the above can be written without the self prefix.

IRB Session
irb(main):004> binding.eval 'singleton_methods'
=> [:to_s, :inspect, :conf] 
irb(main):005> binding.eval 'local_variables' => [:b, :x, :_]
irb(main):006> binding.eval 'instance_variables' => [] irb(main):007> puts binding.eval 'global_variables' $-p $-l $-a $@ $; $-F $-I $: $" $LOAD_PATH $LOADED_FEATURES $-v $VERBOSE $-W $-w $-d $DEBUG $PROGRAM_NAME $0 $& $` $' $+ $= $? $$ $stdin $stdout $> $stderr $DEBUG_RDOC $_ $~ $! $/ $, $\ $-0 $< $. $FILENAME $-i $* => nil %}

my_binding.local_variables is not equivalent to my_binding.receiver.local_variables or my_binding.self.local_variables.

my_binding.local_variables returns an array of the names of local variables available in the context captured by the binding.

my_binding.receiver.local_variables calls the local_variables method on the object that is self in the binding. This usually returns the local variables in the current scope of that object, which is typically not the same as those in the binding unless you are in the same context.

For example:

IRB Session
$ irb
irb(main):001> x = 42
=> 42
irb(main):002>
b = binding => #<Binding:0x000073a8689ec918>
irb(main):003>
b.local_variables => [:b, :x, :_]
irb(main):004>
b.receiver.send 'local_variables' => [:b, :x, :_]

Leaky Top-Level Definitions

The top-level binding context is the outermost scope, and is not encapsulated within any class or module.

The hello method shown below is defined at the top-level binding context / outermost scope. Its method definition exists separately in the top-level binding context, distinct from all other entries.

Ruby code
def hello
  'hi'
end

p main.method(:hello) # => #<s;Method: Object#hello>

As you can see, hello is not shown as a method of main. This is because it is an instance method of Object, available everywhere because every object inherits from Object. Promiscuous top-level definitions that leak from the main instance into the Object definition are said to leak globally.

All top-level definitions leak, including methods, constants, local variables, and instance variables. The precise mechanism varies for each type of definition, but the result is the same: definitions propagating through Object to everything inheriting from Object. To prevent accidental leakage, which would pollute the global namespace, wrap your definitions within a module or class instead of defining them at the top level.

Variable and Method Resolution

Here’s how variable and method resolution works:

  1. If a name matches a local variable in the binding, the value is returned.
  2. Ruby next tries to resolve instance variables and method references on the current self object. Instance variables are only accessible if the binding was created inside an object where they exist. Methods are accessible if the object in the binding responds to them.
  3. Ruby then looks up constants through the normal constant lookup rules.
  4. If no suitable binding is found, ERB falls back to TOPLEVEL_BINDING.

Interrogating a Binding

To list methods and variables in a Ruby binding you can use two different but equivalent syntaxes:

IRB Session
$ irb
irb(main):001> my_binding.local_variables # Preferred
=> [:my_binding, :_]
irb(main):003>
my_binding.eval 'local_variables' => [:my_binding, :_]
Ruby code to list instance variables
my_binding.instance_variables

The binding and the object it is attached to are distinct. This means they have separate methods and variables. You can examine the public methods of a Binding instance like this:

Ruby code to list public methods of a binding
my_binding.public_methods

To see the methods of the object the binding is attached to, first access binding.receiver (this is self within the Binding instance):

Ruby code
my_binding
  .receiver
  .public_methods
  .sort

Example ERB

The following simple example shows a template String being interpreted according to the local binding.

Ruby code
 1: require 'date'
 2: require 'erb'
 3: 
 4: name = 'Mike'
 5: age = ((Date.today - Date.civil(1956, 10, 15)) / 365).to_i
 6: template = '<%= name %> is <%= age %> years old.'
 7: 
 8: erb = ERB.new template
 9: puts erb.result binding

The above code contains two ERB expressions in the template on line 6. Both ERB expressions implicitly use the binding to look up the value of the given variable name or method name. When I ran the above, the output was:

Output
Mike is 68 years old.

Purpose of custom_binding

It is a good practice to evaluate ERBs in a different portion of a code base than where the computation might be performed. For example, Model-View-Controller architectures are based on this style of programming. However, this means that the variables and methods that the ERB needs to evaluate are defined in different binding contexts than where they are used.

custom_binding to the rescue! It allows you to define a custom binding either by creating new binding or mirroring an existing binding. You can add mirrors of objects in your code to the custom binding. The result is a completely normal binding that can be accessed from anywhere in your program.

Objects can contain variables and methods. When mirrored in the custom binding, variables and functions stored within objects are always in sync with the objects they mirror.

Custom­Binding#render invokes ERB#render with the custom binding.

The custom_binding RSpec tests show how to call Custom­Binding#result, which in turn invokes ERB#result with the appropriate binding object. Here is the GitHub’s online editor for the custom_binding project.

Usage

Some of the RSpec tests are shown below as regular code to simplify the examples. This code is provided in the playground directory of the custom_binding Git project.

playground/examples.rb defines a custom binding with specific objects mirrored from other binding contexts that the ERB will reference.

Test data from playground/examples.rb
# Test data
class Foo
  def initialize
    @foo = 42
  end
end

# Another class to demonstrate adding an instance to the mirrored binding
class Bar
  attr_accessor :bar

  def initialize(bar)
    @bar = bar
  end

  def tell_me_a_story = 'A man was born. He lived, then died.'
end

The following method shows how Nugem uses CustomBinding with the above test data.

nugem_test method from playground/examples.rb
# Just exercise those CustomBinding methods required by Nugem:
#  - CustomBinding.new
#  - CustomBinding#add_object_to_binding_as
#  - CustomBinding#eval
def nugem_test
  puts 'nugem_test'
  custom_binding = CustomBinding::CustomBinding.new binding # the current scope include the hello method

  bar = Bar.new 'value of bar.bar'
  custom_binding.add_object_to_binding_as('local_bar', bar)
  custom_binding.add_object_to_binding_as('@test_bar', bar)

  another_bar = Bar.new 'value of another_bar.bar'
  custom_binding.add_object_to_binding_as('@another_test_bar', another_bar)

  puts <<-END_MSG
  local_bar.eval '1+2' = #{custom_binding.eval '1+2'}
  local_bar.result '<%= 1+2 %>' = #{custom_binding.result '<%= 1+2 %>'}

  hello = #{custom_binding.eval('hello')}

  local_bar.tell_me_a_story         = #{custom_binding.eval('local_bar.tell_me_a_story')}
  @test_bar.tell_me_a_story         = #{custom_binding.eval('@test_bar.tell_me_a_story')}
  @another_test_bar.tell_me_a_story = #{custom_binding.eval('@another_test_bar.tell_me_a_story')}

  local_bar.bar         = #{custom_binding.eval('local_bar.bar')}
  @test_bar.bar         = #{custom_binding.eval('@test_bar.bar')}
  @another_test_bar.bar = #{custom_binding.eval('@another_test_bar.bar')}
  END_MSG
end

Output is:

Output of nugem_test method
nugem_test
  hello = Hello from Mars

  local_bar.tell_me_a_story         = A man was born. He lived, then died.
  @test_bar.tell_me_a_story         = A man was born. He lived, then died.
  @another_test_bar.tell_me_a_story = A man was born. He lived, then died.

  local_bar.bar         = value of bar.bar
  @test_bar.bar         = value of bar.bar
  @another_test_bar.bar = value of another_bar.bar

The following method shows how to uses all CustomBinding methods, again with the same test data.

full_test method from playground/examples.rb
# Exercise all CustomBinding methods
def full_test
  puts 'full_test'
  custom_binding = CustomBinding::CustomBinding.new Foo.new

  # Create a new (internal) binding for the same object
  mirrored_binding = custom_binding.mirror_binding
  # Both original_binding and mirrored_binding are for the same object (obj).
  # Any changes to @foo via either binding are reflected in the other,
  # because they reference the same object’s instance variable.

  # Set or update the instance variable via the original binding
  custom_binding.eval '@foo = 100'
  puts '  custom_binding:   @foo = ' + custom_binding.eval('@foo').to_s # => 100
  puts '  mirrored_binding: @foo = ' + mirrored_binding.eval('@foo').to_s # => 100

  # Update the reference in the original binding via the new binding
  mirrored_binding.eval '@foo = 200'
  puts '  custom_binding:   @foo = ' + custom_binding.eval('@foo').to_s # => 200
  puts '  mirrored_binding: @foo = ' + mirrored_binding.eval('@foo').to_s # => 200

  # Add a new instance of Bar to the mirrored binding
  bar = Bar.new 'value of bar.bar'
  custom_binding.add_object_to_binding_as('@another', bar)
  puts '  custom_binding:   @another.bar = ' + custom_binding.eval('@another.bar') # => 'value of bar.bar'
  puts '  mirrored_binding: @another.bar = ' + mirrored_binding.eval('@another.bar') # => 'value of bar.bar'

  # Change the value via the original binding
  custom_binding.eval '@another.bar = "value of bar.bar changed"'
  puts '  mirrored_binding: @another.bar = ' + mirrored_binding.eval('@another.bar') # => 'value of bar.bar changed'

  # Ensure this still works:
  puts '  custom_binding:   @foo = ' + custom_binding.eval('@foo').to_s # => 200
  puts '  mirrored_binding: @foo = ' + mirrored_binding.eval('@foo').to_s # => 200
end

Output is:

Output of full_test method
full_test
  custom_binding:   @foo = 100
  mirrored_binding: @foo = 100
  custom_binding:   @foo = 200
  mirrored_binding: @foo = 200
  custom_binding:   @another.bar = value of bar.bar
  mirrored_binding: @another.bar = value of bar.bar
  mirrored_binding: @another.bar = value of bar.bar changed
  custom_binding:   @foo = 200
  mirrored_binding: @foo = 200

Ruby Quirks

Ruby methods defined at the top level, also known as global methods, are associated with the Kernel module and are generally accessible from anywhere. However, they are conceptually tied to Object as a private method due to encapsulation rules.

In other languages, like Java or C#, top-level methods would be defined within a specific class or would be static, not private methods of a general Object instance.

As an example, lets define a top-level method called hi and see how Ruby classifies it. The following code displays the result of every type of inquiry possible for Ruby methods. Note that some functionality overlaps; the methods method returns both public and protected methods of the receiver, while the public_methods only returns public methods.

IRB session
$ irb
irb(main):001> def hi = 'Hello from Mars'
=> :hi
irb(main):002>
methods.select{ |x| x == :hi } => []
irb(main):003>
private_methods.select{ |x| x == :hi } => [:hi]
irb(main):004>
protected_methods.select{ |x| x == :hi } => []
irb(main):005>
public_methods.select{ |x| x == :hi } => []
irb(main):006>
singleton_methods.select{ |x| x == :hi } => []

Thus, if you define a top-level Ruby method and then do not see it defined the way you expected, you know why. Nothing is broken, Ruby just has a few oddities.

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.