From: Dumaiu Date: 2006-03-04T18:48:40+09:00 Subject: Review requested: Binding.call_stack(), Binding.of_caller()-style Hi, all-- I'd like to submit for evaluation an elaboration I've made on the famous 'Binding.of_caller()' of Florian Gro�. I have tried to follow the the description of the interface for Binding.call_stack() suggested in ruby-talk 12097, "RCR: replacing 'caller'", although in standard Ruby instead of an extension. The technique of using set_trace_func() to capture a Binding one stack frame up has been around at least since 2001 [1]. Here I raise an exception and ride it out to main scope, catching Bindings as the stack unrolls. File and line info is taken from caller() as usual, and the input from the two traces is knitted together. # =begin Binding::call_stack() returns a CallStack object--an Array subclass--containing one StackFrame object per calling frame. Unless specified otherwise, the last StackFrame will contain information from the outermost scope, the 'main' quasi-object. A StackFrame, like a Struct, is a bundle of attributes, as follows: 'object': Name of module|class in which the calling function is defined. Will be 'main' if call_stack() is invoked at global scope. 'method': Name of method whose calling scope we currently inhabit. Will be nil at global scope. 'binding': Binding within calling method's scope. This is the really useful one, carrying on Binding.of_caller()'s performance. 'file': Current file. 'line': Number of line at which current method was called, as returned by Kernel::caller(). As far as I have understood the logic behind caller(), I have tried to integrate the Binding retrieval smoothly thereinto. Here are some examples: # require 'call_stack' puts Binding::call_stack.first.to_a # Retrieve StackFrame and convert to array for output # produces: main nil # stacktest.rb 4 Not interesting. But given this # require 'call_stack' main_local = 'main scope' class C def first() first_local = 'in C.first()' return second end def second() second_local = 'in C.second()' return Binding::call_stack # <---- Nested call end end# C # Print CallStack array: C.new.first().each { |frame| # Dump current frame: puts "Class: '%s'" % [frame.object] puts "Method name: '%s'" % [frame.method] puts "Filename: '%s'" % [frame.file] puts "Line no.: %d" % [frame.line] puts "Locals in binding: #{eval 'local_variables()', frame.binding}\n\n" }# each # we see Class: 'C' Method name: 'second' Filename: 'stacktest.rb' Line no.: 18 Locals in binding: second_local Class: 'C' Method name: 'first' Filename: 'stacktest.rb' Line no.: 13 Locals in binding: first_local Class: 'main' Method name: '' Filename: 'stacktest.rb' Line no.: 22 Locals in binding: main_local The first frame holds the environment in which call_stack() was itself called, that is, the method C#second(). The second frame holds *it's* calling environment, C#first(), and the third frame, global binding. The eval()'d calls to Kernel::local_variables() suggest how these captured bindings can be used to mess with other people's stuff. Three configuration attributes control the classes used internally: * Binding::CallStack::frame_class : The class held here is instantiated into containers for the data returned by call_stack(). Default value is Binding::StackFrame. * Binding::CallStack::tracing_exception : The exception thrown to unroll the stack. Because it arrests itself, this is one exception we *don't* want to be caught, to which end its type is generated from the system clock when the file is loaded. But you can change it. * Binding::stack_class : The class instantiated for return by call_stack(). In this case, it must quack like an Array with two extra methods--call(), invoked each time a frame is added; and ret(), invoked just before the object is returned. =end # BINDING class Binding require 'test/unit/assertions' class << self include Test::Unit::Assertions # for Binding::call_stack end # *************** STACKFRAME *************** class StackFrame include Test::Unit::Assertions # TODO: Make read-only: attr_accessor :object attr_accessor :method attr_accessor :binding attr_accessor :file attr_accessor :line # to_a(): Return data as new array. def to_a() return [ self.object, self.method, self.binding, self.file, self.line ] end# to_a() # to_s(): Return join()ed string. def to_s() return to_a().to_s() end# to_s() # inspect(): Return as array of values. def inspect() return to_a().inspect() end# inspect() # to_h(): Return in key/value form. Note: A pair will only exist if accessor assignment has been used. def to_h() h = Hash::new var = nil # temp instance_variables().each { |var| var =~ /^@ (\w+) $/x assert( $1 ) h[$1] = instance_variable_get(var) }# each return h end# to_h() end# StackFrame # *************** CALLSTACK *************** # Actually an Array, not a Stack proper. class CallStack < Array include Test::Unit::Assertions require 'date' @version = '0.0.1' class << self attr_reader :version end # Class instance vars: @frame_class = Binding::StackFrame @tracing_exception = "xCallStack: #{DateTime.now.to_s}".intern() class << self attr_accessor :frame_class attr_accessor :tracing_exception end # Ctor: call_stack() passes its binding hither, in case we need to read locals therefrom (a utility device). Read-only! # Note: A null value for call_stack_binding will yield an empty object. def initialize( call_stack_binding = nil ) if( call_stack_binding ) @call_stack_binding = call_stack_binding # Store for use in call(). push StackFrame::new() # Disposable: eliminated in ret(). end# if # else empty end# ctor() # call(): Repeatedly used by call_stack() to add frames to the array (hence the name). # [ class, method, binding, file, line ] def call( trace_event, trace_file, trace_line, trace_method, trace_bind, trace_object ) assert( eval( "defined? aCallers", @call_stack_binding ) == 'local-variable' ) # Ensure we have the proper name for locvar in call_stack(). aCallers = eval( "aCallers", @call_stack_binding ) # temp to avoid repeat eval() calls push StackFrame::new() # The nature of set_trace_func() is to return the class and method name for the *previous* stack frame. at(-2).object = trace_object at(-2).method = trace_method last.binding = trace_bind aCallers.first =~ /^ ( [\w\.]+ ) : ( \d+ ) \D*/x assert( $1 && $2 ) last.file = $1 last.line = $2 return self end# call() # ret(): Called after all iterations are finished, just before the CallStack is passed back to the client. def ret() shift() # Remove placeholder first element. assert( length > 0 ) assert( eval( "defined? nCount", @call_stack_binding ) == 'local-variable' ) # Ensure we have the proper name for locvar in call_stack(). # If the final frame isn't at main scope, depending on the arguments to call_stack(), it's also a placeholder and must be removed: if( eval( "nCount", @call_stack_binding ) ) pop() else # Flesh out final element: last.object = 'main' last.method = nil end# if return self end# ret() end# CallStack # *************** CALL_STACK *************** # call_stack(): Retrieve CallStack object (array of StackFrame objects). def Binding::call_stack( nSkipFrames = 0, nCount = nil ) # Validate vars: [nSkipFrames, nCount].each { |var| if( var && (!var.is_a?(Integer) || (var < 0 )) ) then raise( ArgumentError, "Optional argument to call_stack() should be a nonnegative Integer." ); end }# each # Store temporarily to obviate reinvocation. Needed below, and also by the current implementation of class CallStack, so I opted to store it here and pass the CallStack ctor this, call_stack()'s, binding, to allow for greater flexibility if CallStack is extended. aCallers = caller() # If at toplevel or nSkipFrames is greater than the number of frames available, /or/ the client explicitly requests zero frames, exit early: if( (aCallers.length-nSkipFrames <= 0) || (nCount == 0) ) then return Binding::stack_class::new(); end # Trim the first entry, the client function, which we don't want to include; then omit the next nSkipFrames entries. # If nCount sufficiently large, no reason to use it at all: if( nCount && ( nSkipFrames+nCount >= aCallers.length) ) then nCount = nil; end # Trim end if nCount provided: if( nCount ) assert( nCount < aCallers.length ) nCount += nSkipFrames+1 # If nCount must be used in its "proper" function, we have to pad out the number of trace calls to one beyond the specified, in order to receive object and method info (see CallStack::call(), above). aCallers.slice!(nCount .. -1) end# end aCallStack = Binding::stack_class::new( binding() ) # Pass current Binding, giving CallStack access to locals. callcc { |cc| # Hope springs eternal(!): #old_tracefunc = get_trace_func() set_trace_func( proc { # event, file, line, id, bind, classname |event, *remaining_args| if( event == 'return' ) # If frame capture yet enabled: if( nSkipFrames == 0 ) # capture current: aCallStack.call( event, *remaining_args ) else # skip to next nSkipFrames -= 1 end# if aCallers.shift() # If at toplevel, end ride: if( aCallers.length == 0 ) #set_trace_func( get_trace_func() ) set_trace_func( nil ) # -----> Stack unrolling finishes here: cc.call() end# if end# if }# proc )# set_trace_func # Stack unrolling begins here: -----> throw( Binding::CallStack::tracing_exception ) }# callcc assert(nSkipFrames) # Resume execution in client func: aCallStack.ret() return( aCallStack ) end# call_stack() # Class instance var: @stack_class = Binding::CallStack class << self attr_accessor :stack_class end end# Binding # I would appreciate your input with regard to the following: * The three class attributes mentioned above (and the version member) allow noninvasive tweaking, but I'm not sure whether I should be using class variables or class instance vars for them. * I noticed that Herr Gro� renders his script thread critical(). I know not enough about thread safety to incorporate this without dumbly parroting him. * Use of set_trace_func() breaks compatibility with the debugger, and I haven't had much luck with irb, either. Under what other circumstances do Binding.of_caller() and relatives not work well? Also, my rdoc skills are rudimentary and there's a sort of half-assed Hungarian notation going on, because I haven't really worked out a style for myself. (:-P Finally: I've only been in Ruby for a little over a month, so if it looks somewhere like I don't know what I'm doing, that's probably the case. :-) If I have wasted your time, please don't look back. Yours, Jonathan J-S Off to bed. * [1]: I've started thinking of this as the 'red carpet idiom.' Any takers?