Open-Source Wikis

/

Ruby

/

Core classes

/

Proc, Method, and Binding

ruby/ruby

Proc, Method, and Binding

Ruby's first-class callable objects: Proc, Lambda, Method, UnboundMethod, and Binding. They wrap iseqs and frame state in objects that can be passed around and re-invoked.

Files

File Purpose
proc.c Proc, Lambda, Method, UnboundMethod, Binding. ~138 KB.
vm_method.c The method-table machinery Method/UnboundMethod reflect over.
vm_eval.c rb_funcall* family, Method#call.
eval.c Top-level entry points.
vm.c Frame creation when invoking a captured callable.

Proc

A Proc wraps:

  • An iseq (the bytecode body).
  • An environment — the captured local variables and self at creation time.
  • A flag indicating proc-like vs lambda-like semantics.
struct RObject {
    /* ... */
    rb_proc_t proc;
};

typedef struct {
    const struct rb_block block;     /* iseq + environment */
    unsigned int is_from_method: 1;
    unsigned int is_lambda: 1;
    unsigned int is_isolated: 1;     /* Ractor-isolated */
} rb_proc_t;

The block field references the captured rb_iseq_t and an rb_env_t (the environment of the surrounding method when the Proc was created). The environment keeps the parent method's locals alive — closures.

Proc vs Lambda

Trait Proc Lambda
Created by Proc.new, proc { }, &block lambda { }, ->{}, Method#to_proc
Argument count check Lenient (extra args dropped, missing args nil) Strict (raises ArgumentError)
return keyword Returns from the surrounding method Returns from the lambda only
Kernel#lambda? false true

The differences live in proc.c::proc_call_* and the iseq's flag bits set at compile time.

Block conversion

An ampersand argument captures or passes a block:

def takes_block(&blk)   # captures block as a Proc
  blk.call(1)
end

[1, 2, 3].each(&proc { |x| puts x })   # passes a Proc as a block

vm_method.c and proc.c together handle both directions. A Symbol#to_proc exists too: [1, 2, 3].map(&:to_s) works because Symbol defines to_proc returning a Proc that calls the method named by the symbol.

Method and UnboundMethod

A Method is a Proc-like wrapper bound to a specific receiver:

m = "hello".method(:upcase)
m.call          # → "HELLO"
m.receiver      # → "hello"
m.unbind        # → UnboundMethod, receiver-less

UnboundMethod is the same callable without a receiver. You can rebind:

um = String.instance_method(:upcase)
um.bind("world").call   # → "WORLD"

Internally:

  • Method stores (receiver, method entry, defined class).
  • UnboundMethod stores (method entry, defined class), no receiver.

method entry is rb_callable_method_entry_t — the same struct the VM uses for method dispatch. So Method#call is essentially a wrapped invocation through rb_vm_call_cfunc or vm_call_iseq_setup.

Binding

A Binding captures the local variables and self at the point of capture:

def some_method
  x = 1
  binding
end

b = some_method
b.local_variables       # [:x]
b.eval("x")             # 1
b.eval("x = 2")
b.local_variable_get(:x) # 2

Internally a Binding is an rb_env_t (the environment chain) plus the iseq of the calling method. eval against a Binding compiles new code in that environment — variables defined are added to the binding.

This is how IRB and Pry implement context: capture binding in the user's call site, then drop into a REPL that evaluates input against it.

Closures and the env

Every Proc (and Lambda, and inner method-defined-as-proc) carries an rb_env_t chain. The chain points at the surrounding method's local-variable storage — this is what makes Ruby closures actually closed over their lexical environment.

struct rb_env_struct {
    const VALUE *env_obj;        /* iseq's env array */
    long env_size;
    const VALUE *ep;             /* environment pointer in the parent frame */
};

When a method returns but a Proc still holds its environment, the GC keeps the locals alive on the heap (the original frame's stack region was promoted to a heap-allocated env).

The escape-analysis variant of this — when the compiler detects that no Proc escapes a method, it can keep locals stack-allocated. Otherwise it allocates them in a heap env.

Method dispatch internals

Method#call(args):

  1. Reach the rb_callable_method_entry_t.
  2. Allocate a new VM frame (rb_control_frame_t) on the calling thread's stack.
  3. Bind self to Method#receiver.
  4. Push args.
  5. Either jump into the iseq (Ruby method) or call the C function (C method).
  6. Pop the frame on return.

This is essentially the same path as a normal method call — the difference is just that self and the method entry come from the Method object instead of the call-site cache.

Refinements

Module#refine creates a refinement — a sandboxed override of methods on a class. Refinements are activated per-file with using:

module ShoutyString
  refine String do
    def upcase
      super + "!"
    end
  end
end

using ShoutyString
"hi".upcase   # => "HI!"

Refinements are tracked per iseq. The dispatcher in vm_method.c consults the iseq's refinement table when looking up methods. This makes refinements lexically scoped — they affect only code within using's scope.

Method visibility

Ruby has public, protected, private, plus module_function. Visibility is stored in the method entry's visi field (vm_method.c). The dispatcher honours it on every call:

  • private — only callable as a "function-style" call (no explicit receiver).
  • protected — callable from any instance of the same class hierarchy.
  • public — always callable.

Common pitfalls

  • return in a Proc can LocalJumpError if the enclosing method has returned.
  • Proc captures self: capturing a method's local proc captures the method's self too. Re-binding it via instance_exec changes self for the call.
  • Method keeps the receiver alive: as long as you hold a Method, its receiver doesn't get GC'd.
  • Binding is heavy: it pins the local variables, so leaking a Binding can prevent stack frames from being collected.

Entry points for modification

  • New Proc behaviour: proc.c::proc_call_*. The block invocation entry point is rb_vm_invoke_proc.
  • Method dispatch tweak: vm_method.c::vm_call_method — central dispatcher.
  • Visibility rules: vm_method.c::rb_method_entry_complement_defined_class.
  • Refinements: vm_method.c::vm_search_method_with_refinements.

See systems/vm.md for the dispatch loop and systems/compiler.md for how blocks are compiled.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Proc, Method, and Binding – Ruby wiki | Factory