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 blockvm_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-lessUnboundMethod is the same callable without a receiver. You can rebind:
um = String.instance_method(:upcase)
um.bind("world").call # → "WORLD"Internally:
Methodstores(receiver, method entry, defined class).UnboundMethodstores(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) # 2Internally 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):
- Reach the
rb_callable_method_entry_t. - Allocate a new VM frame (
rb_control_frame_t) on the calling thread's stack. - Bind
selftoMethod#receiver. - Push args.
- Either jump into the iseq (Ruby method) or call the C function (C method).
- 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
returnin a Proc canLocalJumpErrorif the enclosing method has returned.- Proc captures
self: capturing a method's local proc captures the method'sselftoo. Re-binding it viainstance_execchangesselffor the call. Methodkeeps the receiver alive: as long as you hold aMethod, its receiver doesn't get GC'd.Bindingis 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 isrb_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.