Hooks, eval and DSLs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Ruby lets a program change itself while it runs: a class can be told when it gains a subclass or a method, a block can be
run with a different self, and a string can be compiled into code. Those three abilities, callbacks (hooks), evaluation
and the small languages built on top of them (DSLs), are what libraries such as test frameworks, command-line parsers and
build tools use to make their APIs read like prose. They are also where most metaprogramming bugs and security problems
come from, so this page spends as much time on what can go wrong as on what is possible.
The page starts with the hooks (the method that Ruby calls on a class or module when something happens to it), then the
evaluation family (instance_eval, instance_exec, class_eval, module_eval, class_exec, eval and Binding),
then why evaluating strings is dangerous, and finally how to put the pieces together into an internal DSL. Defining and
calling methods by name at run time (send, define_method, method_missing) is
Dynamic Dispatch and Method Definition,
asking an object what it is made of is
Reflection, Introspection and Tracing,
and the ancestor chain that include, prepend and extend build is
Modules and Mixins and
Method Lookup and super. Singleton classes, where
class methods live, are in
Singleton Classes and Class-Level Behavior.
Rails is out of scope for this reference.
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with
RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output
shown in # => comments, and in the output blocks, is the real output. The examples reuse the shelf library catalogue
used throughout this section (Shelf, Book, Isbn, Loan, Catalog, Shelf.configure) and the files that need it
start with require "shelf" (see the shelf gem source). The few RuboCop exceptions are deliberate illustrations and are marked where they occur.
Hooks
What a hook is
A hook (also called a callback) is an ordinary method that Ruby itself calls when something happens: a class is subclassed, a module is mixed in, a method or constant is defined. The default implementations do nothing, which is why a class that defines none of them behaves normally. To use a hook you define a method with the right name on the object that receives the event, and the whole mechanism is then just method lookup (see Method Lookup and super).
| Hook | Receiver and where it is defined | Called when | Argument |
|---|---|---|---|
|
The superclass, as a singleton method ( |
A subclass has been created, before its body runs. |
The new subclass. |
|
The module, as a singleton method. Documented on |
After |
The class or module that included it. |
|
The module, as a singleton method. |
After |
The object that was extended. |
|
The module, as a singleton method. |
After |
The class that prepended it. |
|
The module (private methods of |
Just before the matching |
The target class or object. |
|
The class or module whose instance methods change. Documented on |
A method is defined, removed ( |
The method name as a Symbol. |
|
Any object, as a singleton method. Documented on |
A singleton method of that object (for a class, a class method) changes. |
The method name as a Symbol. |
|
The class or module, as a singleton method. Documented on |
A constant is assigned, or a class or module is defined, inside it. |
The constant name as a Symbol. |
|
The class or module, as a singleton method. Documented on |
A constant lookup fails, before |
The constant name as a Symbol. |
Every hook in the table is looked up like any other method, so the usual rule for overriding applies: if a parent
class or an included module may also define the hook, the override must call super. Forgetting it is the most common
hook bug and the subject of the next section.
The diagram shows the order in which the events of class Sub < Parent reach their hooks: the creation hook before the
body, each definition inside the body as it happens, and for a mixin the insertion before the notification. The
examples below prove each step.
inherited
inherited is called on the superclass every time a class is created from it, whether with the class keyword,
Class.new(Parent) or Struct.new and Data.define (which also create subclasses of Struct and Data). It is how
plugin registries and per-subclass configuration are built. The rules that matter:
-
Always call
super. The defaultClass#inheritedis empty, so the hook appears to work without it. The problem is cooperation: the moment a module that isextend-ed into the class (or a parent class) also definesinherited, an override withoutsupercuts the chain and silently disables the other implementation. RuboCop reports it asLint/MissingSuper, which coversinherited,method_added,method_removed,method_undefined, the threesingleton_method_*hooks andinitializeof a subclass. -
The hook is called on the direct superclass, and runs before the body of the subclass. The subclass exists and has its superclass set but no methods, constants or macros yet, so the hook cannot inspect them.
-
The class may have no name yet. With
class Sqlite < Adapterthe name is already set, but withRemote = Class.new(Adapter)the hook runs while the class is still anonymous, because the constant is assigned afterClass.newreturns. -
It is inherited itself. A hook defined on
Storealso fires for grandchildren unless some subclass overrides it, as the next example shows.
The first example builds a registry of the adapters that Shelf::Store can have. Registry#inherited calls super
and then records the subclass in the class instance variable of the class being subclassed. The second subclass,
Careless, overrides inherited without super.
# frozen_string_literal: true
module Shelf
# Gives a class hierarchy a registry of its direct subclasses.
module Registry
def inherited(subclass)
super
(@registry ||= []) << subclass
end
attr_reader :registry
end
# Root of the store adapters.
class Store
extend Registry
end
class Memory < Store; end
# Overrides the hook but forgets to call super.
class Careless < Store
def self.inherited(subclass)
puts "Careless saw #{subclass}"
end
end
class Deep < Careless; end
end
Shelf::Store.registry # => [Shelf::Memory, Shelf::Careless]
Shelf::Careless.registry # => nil
Shelf::Store.registry.include?(Shelf::Deep) # => false
Shelf::Store.subclasses.sort_by(&:name) # => [Shelf::Careless, Shelf::Memory]
Shelf::Careless.singleton_class.ancestors.first(3) # => [#<Class:Shelf::Careless>, #<Class:Shelf::Store>, Shelf::Registry]
Output:
Careless saw Shelf::Deep
Source: Class#inherited
Deep was created from Careless, so Careless.inherited ran (that is the line printed) but, because it did not call
super, Registry#inherited never saw Deep: it is missing from Shelf::Store.registry. Shelf::Careless.registry is
nil for a second reason. Each class has its own @registry, and Careless never had one assigned. RuboCop flags the
override with Lint/MissingSuper; the example keeps that single offense on purpose, to show the bug.
Ruby 3.1 added Class#subclasses, which returns the direct subclasses that currently exist, excluding singleton
classes. Use it when you only need to ask, and a hook when you need to run code at the moment of subclassing. Its order is
not specified (the example sorts the result), anonymous subclasses that have been garbage collected disappear from it,
and it lists only direct subclasses. Before 3.1 the usual substitute was ObjectSpace.each_object(Class), which walks the
whole heap. A method that returns every descendant, Module#descendants, appears in the development NEWS for the
upcoming Ruby 4.1; it is not in 4.0.7 and is not used here.
The timing rules above are easy to confirm with a hook that prints what it can see.
# frozen_string_literal: true
module Shelf
# Prints what the inherited hook can see about each new subclass.
class Adapter
def self.inherited(subclass)
super
puts "inherited: name=#{subclass.name.inspect} superclass=#{subclass.superclass}"
end
end
class Sqlite < Adapter
puts "body of Sqlite runs after the hook"
end
Remote = Class.new(Adapter) { puts "block of Class.new runs after the hook" }
Class.new(Adapter)
end
Shelf::Remote.name # => "Shelf::Remote"
Output:
inherited: name="Shelf::Sqlite" superclass=Shelf::Adapter
body of Sqlite runs after the hook
inherited: name=nil superclass=Shelf::Adapter
block of Class.new runs after the hook
inherited: name=nil superclass=Shelf::Adapter
Source: Class#inherited
Both anonymous classes reported name=nil, and the block given to Class.new ran after the hook. After the assignment,
Shelf::Remote.name is "Shelf::Remote". Code that needs the name has to wait, for example by registering lazily the
first time the registry is read.
Class-level configuration copied to subclasses
Class instance variables belong to one class object and are not shared with subclasses, so a macro such as field
that stores into @fields would leave every subclass with an empty table, or with nil. inherited is the
natural place to give each subclass its own copy of the parent’s configuration, copying instead of sharing so that a
subclass can add entries without changing the parent. This is the pattern behind most class-level DSLs (declared
attributes, validations, routes).
# frozen_string_literal: true
module Shelf
# Class-level macros plus per-class settings that subclasses inherit as a copy.
module Fields
def self.extended(base)
base.instance_variable_set(:@fields, {})
end
def inherited(subclass)
super
subclass.instance_variable_set(:@fields, @fields.dup)
end
def field(name, default: nil)
@fields[name] = default
attr_accessor name
end
def fields = @fields.dup
end
# A mutable record described with the macros above.
class Record
extend Fields
field :title, default: "untitled"
def initialize(**values)
self.class.fields.each { |name, default| public_send(:"#{name}=", values.fetch(name, default)) }
end
end
# A subclass adds a field without touching the parent's table.
class Magazine < Record
field :issue, default: 1
end
end
Shelf::Record.fields # => {title: "untitled"}
Shelf::Magazine.fields # => {title: "untitled", issue: 1}
magazine = Shelf::Magazine.new(title: "Wired")
[magazine.title, magazine.issue] # => ["Wired", 1]
Shelf::Record.new.respond_to?(:issue) # => false
Source: Class#inherited
Magazine starts with a copy of the Record fields (title), adds issue, and Record is untouched. The copy is shallow,
which is enough for a Hash of Symbols to defaults. If the values are mutable (for example Arrays), duplicate them as well,
or freeze them. The same idea, with extended setting up the first table, is how the class-methods module of the next
section starts.
included, extended and prepended
The three module hooks are Module callbacks called on the module that was mixed in. included(base) receives the class
that ran include, extended(object) the object that ran extend (often another class or module), and
prepended(base) the class that ran prepend. A mixin that needs to add class methods as well as instance methods uses
the classic idiom of extending the including class from included: instance methods are the module’s own methods, and the
class methods go into a nested module (conventionally ClassMethods) that included passes to base.extend.
Behind each hook is a private method that does the work: include calls append_features(base), prepend calls
prepend_features(base) and extend calls extend_object(object), and only then the matching notification hook. They
are rarely overridden, and an override that does not call super means the module is not inserted at all, as the last
lines of the example show. Passing several modules to one call (Branch.include(First, Second)) processes them from
right to left, so the hooks run in the reverse of the written order while the ancestors keep the written order.
# frozen_string_literal: true
module Shelf
# A mixin that reacts to every way of being mixed in.
module Searchable
def self.included(base)
puts "included in #{base}"
base.extend(ClassMethods)
end
def self.extended(object) = puts("extended #{object.class}")
def self.prepended(base) = puts("prepended to #{base}")
def self.append_features(base)
puts "append_features #{base}"
super
end
def self.prepend_features(base)
puts "prepend_features #{base}"
super
end
def self.extend_object(object)
puts "extend_object #{object.class}"
super
end
# Class-level API installed by the included hook.
module ClassMethods
def searchable_fields = @searchable_fields ||= []
end
def search = :ok
end
class Wing; include Searchable; end
class Annex; prepend Searchable; end
# Two modules included in one call are processed right to left.
module First
def self.included(base) = puts("First included in #{base}")
end
module Second
def self.included(base) = puts("Second included in #{base}")
end
Branch = Class.new
Branch.include(First, Second)
# Skipping super in append_features means the module is never inserted.
module Skipped
def self.append_features(_base) = puts("skipping")
end
class Depot; include Skipped; end
end
Object.new.extend(Shelf::Searchable)
Shelf::Wing.ancestors.first(2) # => [Shelf::Wing, Shelf::Searchable]
Shelf::Annex.ancestors.first(2) # => [Shelf::Searchable, Shelf::Annex]
Shelf::Wing.searchable_fields # => []
Shelf::Branch.ancestors.first(3) # => [Shelf::Branch, Shelf::First, Shelf::Second]
Shelf::Depot.include?(Shelf::Skipped) # => false
Output:
append_features Shelf::Wing
included in Shelf::Wing
prepend_features Shelf::Annex
prepended to Shelf::Annex
Second included in Shelf::Branch
First included in Shelf::Branch
skipping
extend_object Object
extended Object
Source: Module#included; append_features; prepended; Object#extend
Things to remember about these three hooks:
-
includedruns after the module is already in the ancestors, sobase.ancestorsshows it.prependedlikewise. -
Mixing a module into a module (
module Reader; include Searchable; end) callsSearchable.included(Reader)with the module asbase, not with the classes that later includeReader. Chain the call (base.include(Other)inside your ownincluded) or useextendon the class if the second module must reach the final class. -
extendedis not called byinclude. To react to both, define both hooks, or put the shared setup in a method that each hook calls. -
Instead of
includedwithClassMethods, a module that only needs class-level methods can be used withextenddirectly (class Store; extend Registry; end, as in the first example), which needs no hook at all. -
prependplus a hook is the modern replacement for the old alias-the-original-method chain. The prepended module’s methods callsuperto reach the original, which keepsMethod#ownerandMethod#super_methodhonest (see Modules and Mixins). -
RuboCop’s
Style/MixinGroupingasks forincludewith several modules to be written as separate statements, andNaming/MethodParameterNameenforces parameter names of at least three characters, which is why the hook parameters are calledbaseandobjecthere.
method_added, method_removed, method_undefined and the singleton variants
method_added(name) is called on a class or module after an instance method has been defined with def,
define_method, alias_method or alias, or created by attr_reader, attr_writer or attr_accessor (an accessor pair
reports bar and bar=). Marking an inherited method private or public in a subclass can also report it, because Ruby
then records an entry for it in the subclass; changing the visibility of a method defined in the same class does not.
method_removed follows remove_method and method_undefined follows undef_method or the undef keyword (the
difference between removing and undefining is in
Dynamic Dispatch and Method Definition).
The singleton_method_* hooks are the same events for singleton methods and are defined on BasicObject, so they work
for any object. For a class they report class methods (def self.build, and the methods in a class << self block).
module_function generates them too, because it copies the method to the module’s singleton class (extend of a module with ordinary methods does not). One subtlety: defining
singleton_method_added is itself a singleton-method definition, so the hook is called with its own name as soon as it
exists, and the first line of the output below shows it.
const_added(name) (Ruby 3.2) is the constant counterpart. It is called for assignments such as VERSION = 1 and for the
definition of a nested class or module, and it is not called for constants that were defined before the hook. For lookups
that fail, const_missing(name) is called first and, if it calls super, ends in the usual NameError; autoloaders use it
for lazy loading.
# frozen_string_literal: true
module Shelf
# Records every definition event on itself.
class Audited
@log = []
class << self
attr_reader :log
def method_added(name)
super
@log << [:added, name]
end
def method_removed(name)
super
@log << [:removed, name]
end
def method_undefined(name)
super
@log << [:undefined, name]
end
def singleton_method_added(name)
super
@log << [:singleton_added, name]
end
def const_added(name)
super
@log << [:const_added, name]
end
def const_missing(name)
@log << [:const_missing, name]
super
end
end
VERSION = 1
def title = "t"
def author = "a"
def self.build = new
remove_method :title
undef_method :author
end
end
begin
Shelf::Audited::Nope
rescue NameError => e
Shelf::Audited.log << [:error, e.message]
end
Shelf::Audited.log.each { puts it.inspect }
Output:
[:singleton_added, :singleton_method_added]
[:singleton_added, :const_added]
[:singleton_added, :const_missing]
[:const_added, :VERSION]
[:added, :title]
[:added, :author]
[:singleton_added, :build]
[:removed, :title]
[:undefined, :author]
[:const_missing, :Nope]
[:error, "uninitialized constant Shelf::Audited::Nope"]
Only definitions that happen after a hook exists are reported, which is why attr_reader :log and the hook definitions
themselves are not in the log (apart from the self-report of singleton_method_added). Always call super first, for the
same cooperation reason as inherited.
Decorating methods from method_added
A frequent use of method_added is to wrap every method defined after some point, for logging, memoization or access
checks. The main trap is recursion: if the hook defines a method on the same class (with define_method, alias_method
and a new def), that definition triggers method_added again, and so on until Ruby raises SystemStackError ("stack
level too deep"). The classic way out is a flag that stops the re-entry. The better way is to put the wrapper in a
different module, usually one that is prepend-ed, so that its definitions notify that module, not the class. The example
does that.
# frozen_string_literal: true
module Shelf
# Wraps every public method defined after `extend Traced` in a logging layer.
module Traced
def self.extended(base)
base.prepend(base.const_set(:Wrappers, Module.new))
end
def method_added(name)
super
return unless public_method_defined?(name)
# The wrapper lives in another module, so defining it does not
# call method_added on this class again.
self::Wrappers.define_method(name) do |*args, **kwargs, &block|
puts "-> #{name}"
super(*args, **kwargs, &block).tap { puts "<- #{name}" }
end
end
end
# A desk that lends books.
class Desk
extend Traced
def lend(title) = "lent #{title}"
def back(title) = "got #{title}"
end
end
Shelf::Desk.new.lend("Dune") # => "lent Dune"
Shelf::Desk.ancestors.first(2) # => [Shelf::Desk::Wrappers, Shelf::Desk]
Output:
-> lend
<- lend
Source: Module#method_added; Module#define_method
Several details of this example are worth keeping. The wrapper takes *args, **kwargs and &block and passes all
three on with an explicit-arguments super(...), so that keyword arguments stay keyword arguments (see
Calling Methods and Argument Forwarding).
The hook only wraps public methods, because a wrapper defined in a module would otherwise make a private method public.
Desk.ancestors shows the generated Wrappers module in front of the class, and a Method#owner check on any wrapped
method returns that module. A hook that needs to wrap methods that already exist does it once, at the end of the class
body, by walking public_instance_methods(false).
Evaluating code with a different self
instance_eval, instance_exec, class_eval, module_eval and class_exec
All five methods run code in a context chosen by the caller. They differ in two settings that Ruby tracks for every piece
of code: self, the receiver of calls without an explicit receiver, and the default definee (also called the cref or the
default target of def), the class that a bare def adds a method to.
| Method | self inside |
Default definee for def |
Notes |
|---|---|---|---|
|
|
The singleton class of |
Passes |
|
|
The singleton class of |
Like |
|
|
|
Aliases of each other. Also accept a string. Defined on |
|
|
|
Like |
The pairing to memorise is that instance_eval makes self the receiver and defines singleton methods, while
class_eval makes self the receiver and defines instance methods of the receiver. On a class, instance_eval
therefore defines class methods and class_eval defines instance methods. instance_exec and class_exec exist to get
arguments into the block: the block runs with a new self, so it cannot see the caller’s self or instance variables,
but it can receive values as block parameters, and class_exec(:badge) { |name| define_method(name) { name } } is the
clean way to generate a method whose name is known only at run time.
# frozen_string_literal: true
module Shelf
# A desk with one private detail.
class Desk
def initialize = @secret = 42
end
end
desk = Shelf::Desk.new
# instance_eval and instance_exec: self is the receiver.
desk.instance_eval { @secret } # => 42
desk.instance_eval { |object| object.equal?(self) } # => true
desk.instance_exec(5) { |extra| @secret + extra } # => 47
# def inside instance_eval defines a singleton method of the receiver.
desk.instance_eval { def greet = "hi" }
desk.singleton_methods # => [:greet]
Shelf::Desk.instance_methods(false) # => []
# def inside class_eval defines an instance method of the class.
Shelf::Desk.class_eval { def wave = "o/" }
Shelf::Desk.instance_methods(false) # => [:wave]
# instance_eval on a class defines a class method.
Shelf::Desk.instance_eval { def open = new }
Shelf::Desk.singleton_methods # => [:open]
# class_exec passes arguments, which makes define_method with a runtime name easy.
Shelf::Desk.class_exec(:badge) { |name| define_method(name) { name } }
desk.badge # => :badge
Since Ruby 3.1 instance_eval and instance_exec allocate the singleton class of the receiver only when something actually
needs it (a def or similar), so using them just to read instance variables or to run a block as the receiver is cheap.
Blocks versus strings
All of the methods except the _exec pair also accept a string. The two forms are not equivalent:
-
A block is a closure. Constant lookup and local variables are those of the place where the block was written, not of the receiver. A constant referenced in the block resolves lexically; in the example below,
LIMITin aclass_evalblock is the top-level constant, not the one insideShelf::Desk. -
A string is compiled when it runs, with the receiver as
selfand as the lexical class, so the constantLIMITresolves to the class’s own. It still sees the local variables of the code that calledclass_eval, but local variables that it creates stay inside it. -
A block gets the file and line of its source for free. For a string, pass
__FILE__and__LINE__ + 1as the second and third arguments (with a heredoc, the body starts on the next line), or errors andMethod#source_locationwill point at(eval at ...), which is useless in a stack trace. -
Blocks are checked when the file is parsed (a typo is a syntax error at load time) and can be seen by RuboCop and by coverage tools. Strings are only checked when they run.
# frozen_string_literal: true
LIMIT = :outer
module Shelf
# A desk with its own LIMIT constant.
class Desk
LIMIT = :desk
end
end
desk = Shelf::Desk.new
local = 10
# A block keeps the constant lookup and locals of where it was written.
Shelf::Desk.class_eval { LIMIT } # => :outer
desk.instance_eval { LIMIT } # => :outer
Shelf::Desk.class_eval { local } # => 10
# A string is compiled with the receiver as self and as the lexical class, so constants
# come from the class. It still sees the local variables of the caller.
Shelf::Desk.class_eval("LIMIT", __FILE__, __LINE__) # => :desk
Shelf::Desk.class_eval("local", __FILE__, __LINE__) # => 10
# But a variable first assigned inside the string does not leak back out.
Shelf::Desk.class_eval("created = 1", __FILE__, __LINE__)
binding.local_variable_defined?(:created) # => false
# A lambda is strict about arguments, and instance_eval passes the receiver as one.
strict = -> { :zero }
begin
desk.instance_eval(&strict)
rescue ArgumentError => e
puts e.message
end
desk.instance_exec(&strict) # => :zero
# Immediate values have no singleton class, frozen objects cannot change.
begin
5.instance_eval { def double = self * 2 }
rescue TypeError => e
puts "#{e.class}: #{e.message}"
end
begin
Shelf::Desk.new.freeze.instance_eval { @secret = 1 }
rescue FrozenError => e
puts e.class
end
Output:
wrong number of arguments (given 1, expected 0)
TypeError: can't define singleton
FrozenError
Three rules are visible here. A lambda passed with & keeps its strict arity, and instance_eval calls it with one argument
(the receiver), so a lambda with no parameters raises ArgumentError where a proc would not; instance_exec passes no
arguments unless asked. Integers, Floats and Symbols have no singleton class, so defining a method on one raises
TypeError (nil, true and false accept it, the method then belongs to NilClass, TrueClass or FalseClass). A frozen object raises FrozenError as soon as the block assigns an instance variable.
The common error inside a block that was meant for a DSL, a NameError for a name that exists in the caller but not in
the new self, is described under Internal DSLs below.
Generating methods from a string
Generating many similar methods is the legitimate use of string class_eval: when every method is the same except for a
name, a string compiles into plain, fast methods that read like hand-written ones, and it supports syntax that
define_method cannot, such as a method with its own default arguments, a begin block or yield. The cost is that the
string is code you must trust and test. When a closure is enough, prefer define_method, which is also shown below so the
two can be compared. The convention taken from large codebases is to pass the file and line, and to add a comment that shows
what the generated code looks like; RuboCop enforces both with Style/EvalWithLocation and
Style/DocumentDynamicEvalDefinition.
# frozen_string_literal: true
require "shelf"
module Shelf
class Loan
STATES.each do |state|
# The string is evaluated with the file and line of the heredoc,
# so a stack trace points at this source. The comment shows the result.
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{state}? = @state == :#{state} # def lent? = @state == :lent
RUBY
# The same method with a closure needs no string at all.
define_method(:"#{state}_again?") { @state == state }
end
end
end
book = Shelf::Book.new(Shelf::Isbn.new("978-0-596-51774-8"), "Dune", Shelf::Author.new("Frank Herbert"))
loan = Shelf::Loan.new(book, state: :lent)
[loan.lent?, loan.available?, loan.lent_again?] # => [true, false, true]
puts Shelf::Loan.instance_method(:lent?).source_location.inspect
# Without a file and line, errors in generated code point at the eval itself.
Shelf::Loan.class_eval("def broken = raise('boom')") # rubocop:disable Style/EvalWithLocation
begin
loan.broken
rescue RuntimeError => e
puts e.backtrace.first
end
Shelf::Loan.class_eval("def fixed = raise('boom')", __FILE__, __LINE__)
begin
loan.fixed
rescue RuntimeError => e
puts e.backtrace.first
end
Output:
["predicates.rb", 11]
(eval at predicates.rb:26):1:in 'Shelf::Loan#broken'
predicates.rb:33:in 'Shelf::Loan#fixed'
The generated predicates report the line of the heredoc as their source location. The backtrace of broken names only
(eval at ...), while the one of fixed points at the file and line that were passed in. The one RuboCop exception, the
rubocop:disable Style/EvalWithLocation comment, exists to show the failing form on purpose.
eval and Binding
Kernel#eval
Kernel#eval(string, binding = nil, file = nil, line = nil) compiles and runs a string. Without a binding it uses the
self and the local variables of the caller. With a binding argument it uses those of that binding, and with file and line
it labels the code for error messages, backtraces and __FILE__. Since Ruby 3.3 the default file name is
(eval at path:line), which names the call site (earlier releases printed just (eval)). Syntax errors in the string
raise SyntaxError, with the position inside the string.
eval returns the value of the last expression. A local variable first assigned by the string exists only inside that
evaluation and is not visible to the code that called eval; to keep it, evaluate against a Binding object that you
hold on to, as the next section shows. Under ruby -w the evaluated code is also checked: a statement whose value is discarded
produces "possibly useless use" warnings, and a variable used only inside strings is reported as "assigned but unused",
because the parser cannot see inside the string.
Binding
A Binding is an object that captures a point in a program: the local variables in scope, self and the current method
and block context. Kernel#binding returns the one for the current position, Proc#binding returns the one captured by a
proc, and TOPLEVEL_BINDING is the one of the main script. The useful Binding methods are local_variables,
local_variable_get, local_variable_set (which can add a variable that exists only in that binding),
local_variable_defined?, eval, receiver (the self of the binding) and source_location. Ruby 4.0 adds
implicit_parameters, implicit_parameter_get and implicit_parameter_defined? for the numbered parameters (_1)
and the it parameter, which local_variables no longer includes. binding.irb opens an IRB session at that point and is
described in IRB and Debugging.
Bindings keep every local variable they can see alive for as long as the Binding is alive, so holding on to one (or to a
proc that is later asked for binding) can keep large objects from being collected. Kernel#binding raises RuntimeError when
called from a method implemented in C (Ruby 3.2).
# frozen_string_literal: true
# This file shows eval without its location on purpose, so the cop that asks for it is off.
# rubocop:disable Style/EvalWithLocation
# Returns the binding of a method call, with its parameters inside.
def loan_binding(title, days) = binding
copy = loan_binding("Dune", 14)
copy.local_variables # => [:title, :days]
copy.local_variable_get(:days) # => 14
copy.local_variable_defined?(:fee) # => false
copy.local_variable_set(:fee, 5)
copy.eval("days + fee") # => 19
copy.receiver # => main
copy.source_location.last # => 7
# Kernel#eval without a binding uses the caller's own locals.
def double(limit) = limit.then { eval("limit * 2") }
double(3) # => 6
begin
eval("limit * 2", copy)
rescue NameError => e
puts e.message
end
# A proc remembers the binding where it was created.
def counter
count = 0
-> { count += 1 }
end
tick = counter
tick.call # => 1
tick.binding.local_variable_get(:count) # => 1
tick.binding.local_variable_set(:count, 10)
tick.call # => 11
# File and line shape the names and errors of evaluated code.
puts eval("__FILE__")
puts eval("__FILE__", nil, "shelf/virtual.rb", 7)
eval("__LINE__", nil, "shelf/virtual.rb", 7) # => 7
begin
eval("end", nil, "shelf/virtual.rb", 7)
rescue SyntaxError => e
puts e.message
end
# rubocop:enable Style/EvalWithLocation
Output:
undefined local variable or method 'limit' for main
(eval at bindings.rb:41)
shelf/virtual.rb
shelf/virtual.rb:7: syntax error found
> 7 | end
| ^~~ unexpected 'end', ignoring it
Source: Binding; Kernel#eval; Proc#binding
The limit lookup fails because limit is a parameter of double, not a local of loan_binding. The closure of
counter stays reachable through tick.binding, which is why a Proc can be used to read and even change the variables it
captured. The file-wide rubocop:disable Style/EvalWithLocation at the top and bottom of the file marks the second
deliberate exception, because this file is about eval without a location.
ERB is the standard library’s best-known user of bindings. ERB#result(binding) evaluates the template with the locals
of that binding, and ERB#result_with_hash builds a fresh binding from a Hash and is usually the safer choice.
The last lines of the next example use both.
Why string eval is dangerous
eval, instance_eval with a string, class_eval with a string and Binding#eval all run whatever text they get with
the full privileges of the Ruby process: they can read files, run commands, change global state and exit. If any part of
the string comes from outside the program (a request, a file, a database field, a configuration value, an environment
variable) the program has a code-injection hole, and the injected code runs with the same rights as the application. It
does not help to filter the input: the Ruby grammar is far too big to describe what is "safe". Older Rubies had $SAFE levels and
object tainting for this purpose; both were removed (the $SAFE variable lost its special meaning in Ruby 3.0 and
Object#taint and its family were removed in Ruby 3.2), and in any case they were never a reliable sandbox. There is no
in-process sandbox in Ruby 4.0: untrusted code needs a separate process, container or Wasm runtime (see
Security).
The same applies to the other ways of turning data into code: Kernel#send or public_send with a method name from the
user (send("exit") runs it, public_send refuses private methods), Module#const_get and Object#instance_variable_get
with a user-controlled name, Kernel#system and backticks with string interpolation, and Marshal.load and unsafe YAML
loading on untrusted input. The alternatives, in order of preference:
| Instead of | Use |
|---|---|
|
|
|
A data format (JSON, or YAML with |
|
|
String |
|
A template engine fed untrusted templates |
A logic-less template (no embedded Ruby) or a sandboxed service. ERB templates are code and must be trusted. |
Running untrusted Ruby |
A separate operating system process with limited rights; not |
The example shows the injected assignment, the safe parsing, the allow-list and the ERB lines. In real code the first call is the one that would be removed.
# frozen_string_literal: true
require "erb"
require "shelf"
# Imagine this text arrived in a form field or a query string.
loan_days = "Shelf.config.loan_days = 9999"
# Dangerous: the text is run as Ruby with the full power of the process.
eval(loan_days) # rubocop:disable Security/Eval
Shelf.config.loan_days # => 9999
# Safe: parse the value, never execute it.
Shelf.configure { |config| config.loan_days = 14 }
Integer("21", exception: false) # => 21
Integer(loan_days, exception: false) # => nil
# Safe dispatch: an allow-list in front of public_send. Kernel methods such as exit are private.
ALLOWED = %i[title author].freeze
book = Shelf::Book.new(Shelf::Isbn.new("978-0-596-51774-8"), "Dune", Shelf::Author.new("Frank Herbert"))
field = "title"
book.public_send(field) if ALLOWED.include?(field.to_sym) # => "Dune"
command = "exit"
begin
book.public_send(command)
rescue NoMethodError => e
puts e.message
end
# ERB evaluates trusted templates against a Binding or a hash of locals.
template = ERB.new("<%= book.title %> by <%= book.author.name %>")
template.result_with_hash(book:) # => "Dune by Frank Herbert"
template.result(binding) # => "Dune by Frank Herbert"
Output:
private method 'exit' called for an instance of Shelf::Book
Source: Kernel#eval; Kernel#Integer; Object#public_send; Ruby 4.0 documentation: Security; ERB#result_with_hash
The injected string changed Shelf.config.loan_days to 9999 without any validation; the same text could have read a file or
run a program. The single RuboCop exception is the rubocop:disable Security/Eval comment on that line, which is the
cop that exists to forbid exactly this call. The exit check shows why public_send is the right default: exit is a
private method of Kernel, so it is rejected, while send("exit") would have called it.
The cases where string evaluation is fine are the ones where the string is part of the program: a template that ships with
the code, a generator (as in the predicates example) that interpolates only names the program itself controls, a debugger or
console (IRB is an interactive eval by design), and tests that exercise code from fixtures.
Internal DSLs
What a DSL is in Ruby
A domain-specific language (DSL) is a small language for one task. An internal DSL, the kind this page is about, is
just Ruby code written so that it reads like declarations: Shelf.configure do |config| config.loan_days = 21 end,
a test framework’s describe blocks, a build file’s task definitions. Everything is ordinary method calls and blocks, so
the DSL author’s tools are exactly the ones above: yield, instance_eval, instance_exec, hooks and class-level
macros. The goal is readability for the person who writes the DSL code, and the cost is that the implementation uses the
language’s dynamic features, which makes tracebacks, editor completion and static analysis weaker. Use a DSL when many
people write the same kind of declaration; use plain objects, keyword arguments and Data when a single caller builds the
object once.
| Style | Strengths | Weaknesses |
|---|---|---|
Yield an object: |
Explicit receiver; the block still sees the caller’s |
A little more typing in every line. |
|
No receiver needed; reads like a declaration. |
|
Both, chosen by the block arity |
Callers can use either style. |
Two behaviours to document and test. |
Class-level macros: |
Declarations sit in the class body and generate methods; one |
The hooks and the copy-on-inherit rules are part of the contract (see |
Clean room: a |
The block can call only what the DSL allows; no accidental |
More code, and |
The shelf configuration DSL
Shelf.configure (in the gem since the first page of this section) uses the first style: it yields its Config struct.
The next file keeps that and adds a Settings builder that evaluates the same kind of block three ways, so the
differences can be compared side by side: build with instance_eval, build_with with instance_exec to pass
arguments in, and configure, which looks at the block’s arity and picks the style. The Librarian class shows the price
of instance_eval: inside the block the librarian’s own helper method and instance variable are not reachable.
# frozen_string_literal: true
require "shelf"
# Style 1 is already in the gem: yield an explicit receiver.
Shelf.configure do |config|
config.loan_days = 21
end
Shelf.config.loan_days # => 21
module Shelf
# Collects settings with blocks evaluated in three different ways.
class Settings
# Style 2: instance_eval, so the block calls the DSL methods without a receiver.
def self.build(&)
new.tap { |settings| settings.instance_eval(&) }.to_h
end
# Style 2 with arguments: instance_exec.
def self.build_with(*, &)
new.tap { |settings| settings.instance_exec(*, &) }.to_h
end
# Style 3: support both by looking at the block arity.
def self.configure(&block)
settings = new
block.arity.zero? ? settings.instance_eval(&block) : block.call(settings)
settings.to_h
end
def initialize = @values = {}
def loan_days(days) = @values[:loan_days] = days
def max_loans(count) = @values[:max_loans] = count
def to_h = @values.dup
end
# Its settings block cannot call the librarian's own helper.
class Librarian
attr_reader :name
def initialize = @name = "Ana"
def settings
Settings.build do
loan_days 7
@name = "Bo"
max_loans helper
end
end
def helper = 5
end
end
limit = 3
settings = Shelf::Settings.build do
loan_days 28
max_loans limit
end
settings.to_h # => {loan_days: 28, max_loans: 3}
# The cost of instance_eval: self changes, so the caller's methods and instance variables disappear.
librarian = Shelf::Librarian.new
begin
librarian.settings
rescue NameError => e
puts e.message
end
librarian.name # => "Ana"
Shelf::Settings.build_with(10) { |days| loan_days days } # => {loan_days: 10}
Shelf::Settings.configure { loan_days 1 } # => {loan_days: 1}
Shelf::Settings.configure { |settings| settings.loan_days 2 } # => {loan_days: 2}
Output:
undefined local variable or method 'helper' for an instance of Shelf::Settings
Notice that max_loans limit worked: the block still closes over the caller’s local variable limit, because a block is a
closure and instance_eval changes only self, not local variables. What disappears is anything that needs the caller’s
self: instance variables, private helper methods and self.class. In the Librarian example the assignment
@name = "Bo" ran, but on the Settings object, which is why librarian.name is still "Ana", and the call to helper
failed with NameError mentioning Shelf::Settings. A DSL that supports the instance_eval style should say so in its
documentation, and offer the yield style for blocks that need the caller’s context.
Class-level macros
The class-level DSLs of libraries (attribute :title, has_many, validates) are a module with singleton-style methods
plus hooks. The recipe is: define the vocabulary in a module, extend it into the base class, let extended or the first
macro initialise the storage, let inherited copy the storage into subclasses, and generate the methods with
define_method, attr_accessor or class_eval. The Fields module in the inherited section above is a complete,
small example of it. Two details are easy to miss: macros run at class-definition time, so they should be cheap and must not
touch the database or the network; and a macro that stores state in class instance variables is shared by every thread, so
if it can be called at run time, guard it with a mutex (see
Threads and the GVL).
Clean rooms
A clean room is an object that is deliberately almost empty, used as the self of the block. Subclassing BasicObject
gives an object with only eight public instance methods (the example prints them), so puts, raise, require and the rest of
Kernel are not callable by accident; the DSL decides in method_missing what to forward and what to reject. When doing
this, call Kernel methods with an explicit ::Kernel and constants with a leading ::, because inside a BasicObject
subclass the constant lookup does not reach Object (that is what the leading :: does). Forward arguments with
anonymous * and & so that keyword arguments and blocks pass through unchanged.
# frozen_string_literal: true
require "shelf"
module Shelf
# A BasicObject has almost no methods, so a DSL block cannot call Kernel by accident.
class CleanRoom < BasicObject
def initialize(target) = @target = target
def method_missing(name, *, &)
if @target.respond_to?(name)
@target.public_send(name, *, &)
else
::Kernel.raise ::NoMethodError, "the DSL has no #{name}"
end
end
def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
end
end
room = Shelf::CleanRoom.new([3, 1, 2])
room.instance_eval { sort } # => [1, 2, 3]
begin
room.instance_eval { puts "hello" }
rescue NoMethodError => e
puts e.message
end
BasicObject.instance_methods.sort # => [:!, :!=, :==, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
Output:
the DSL has no puts
Source: BasicObject; BasicObject#instance_eval
Designing a DSL that stays maintainable
-
Prefer the yield style for configuration and use
instance_evalonly for declarative blocks whose lines are all calls to the DSL. Do not mix: a block that needs the caller’s methods and the DSL’s methods will fail at the first collision. -
Validate in the DSL methods and raise
ArgumentErrorwith a message that names the keyword and the offending value. A DSL is read by people who did not write it. -
Return
selfor a frozen result from the end of the block so callers cannot keep editing it. -
Keep the vocabulary small and documented, with RDoc or YARD (including
@!methodfor generated methods; see Documentation with RDoc and YARD), and test the DSL through its public entry point (see Testing with Minitest). Generated code needs tests more than hand-written code does, because nothing else reads it. -
Keep stack traces readable: pass
__FILE__and__LINE__to string evaluation, avoid deepmethod_missingchains, and respond torespond_to_missing?when you do usemethod_missing. -
Do not evaluate DSL files that users upload. A file in a Ruby DSL is a program (see Why string eval is dangerous).
-
When a plain Hash, a
Datavalue or keyword arguments would express the same thing, use those.
Where older books and tutorials are outdated
Hooks and evaluation are the part of Ruby where many printed examples still show the pre-2.0 or pre-3.0 style. The table lists what changed, so that an old example can be recognised and modernised.
| Older advice | Ruby 4.0 practice | Since |
|---|---|---|
Override |
Always call |
Always true, now linted |
List subclasses with |
|
Ruby 3.1 |
Detect a new constant by polling |
|
Ruby 3.2 |
Wrap a method with |
|
Long available |
Use |
Neither exists any more; do not evaluate untrusted input, parse it as data |
Ruby 3.0 ( |
|
The singleton class is allocated only when a definition needs it |
Ruby 3.1 |
Evaluated code is named |
|
Ruby 3.0 for |
|
Use |
Ruby 4.0 |
|
A small class, |
Current practice |
Common exceptions and warnings
| Error or warning | Typical cause | What to do |
|---|---|---|
|
The block of an |
Yield an object instead, or capture the value in a local variable before the block. |
|
A lambda used where |
Use a proc or a block, give the lambda a parameter, or call |
|
|
Use |
|
|
Do the evaluation before freezing, or work on a |
|
A string passed to |
Pass |
|
The string was compiled with a different |
Check the self and constant rules in the evaluation table; use an explicit receiver. |
|
|
Define wrappers in another module, or guard with a flag. |
|
A hook or an |
Remove the old definition first or avoid redefining it. |
|
An evaluated string contains a statement whose value is dropped. |
Return the value or remove the statement. |
|
A local variable is read only from inside an |
Pass the value another way, for example |
|
|
Keep the allow-list; do not switch to |
RuboCop cops for hooks and evaluation
| Cop | What it checks |
|---|---|
|
The callbacks |
|
Forbids |
|
String |
|
An interpolated string given to |
|
|
|
The hook examples above: one |
The cop catalogue is at Lint, Security and Style. Configuration and the other tools are in Style, Linting and Static Analysis.
Pitfalls
-
A hook without
superworks until another module in the chain defines the same hook. Makesuperthe first line. -
inheritedruns before the subclass body, so it cannot see the subclass’s methods or constants and, forClass.new, not even its name. Defer anything that needs them. -
A registry filled by
inheritedorincludedis global state. Reloading code (or defining classes in tests) fills it with stale entries, and a registry that is written after boot from several threads needs a mutex. -
method_addedthat defines a method on its own class recurses without end; wrap in another module or guard with a flag. -
instance_evalandclass_evalhave opposite effects on a baredef. When a method lands in the wrong place, the first thing to check is which one was used. -
Instance variables set inside an
instance_evalDSL block land on the DSL object, not on the caller. The program raises no error, which makes it a hard bug to see. -
instance_evalwith a lambda fails because it passes one argument; withinstance_execit works. -
String
evalis slower than a block because it is compiled on every call, hides errors until run time, and is the main source of injection bugs. Never build the string from external data. -
A
Bindingholds on to every local variable it can see, so objects stay alive as long as the binding does. -
const_missingandconst_addedrun for every lookup or definition in that class; keep them fast and callsuper. -
Module#includedis not called when a module is mixed into another module and then into a class; the hook sees the intermediate module. -
A class-level DSL reads class-instance state. Subclasses do not share it unless
inheritedcopies it.
Version notes
| Version | Change relevant to hooks, eval and DSLs |
|---|---|
3.0 |
|
3.1 |
|
3.2 |
|
3.3 |
Evaluated code without a given file reports |
4.0 |
|
4.1 (preview) |
|
References
-
Ruby 4.0 documentation: Class (
inherited,subclasses,attached_object) and Module (included,extendedthroughObject#extend,prepended,append_features,method_added,const_added,const_missing,class_eval,class_exec) -
BasicObject (
instance_eval,instance_exec,singleton_method_added), Kernel (eval,binding) and Binding -
Ruby 3.0.0 NEWS, 3.1.0, 3.2.0 and 4.0.0
-
RuboCop Lint cops (
Lint/MissingSuper), Security cops (Security/Eval) and Style cops (Style/EvalWithLocation,Style/DocumentDynamicEvalDefinition)