From: Justin Bailey Date: 2006-03-05T03:55:35+09:00 Subject: Re: Review requested: Binding.call_stack(), Binding.of_caller()-style Wow, impressive. I can't comment very much on the implementation, I'm still trying to wrap my head around what you've done. Would you mind writing a little bit about how you are using continuations to walk the stack in the Binding::call_stack method? How does that interact with set_trace_func and the throw at the bottom of the callcc block? (e.g. throw( Binding::CallStack::tracing_exception )) Looks pretty cool to me! On 3/4/06, Dumaiu wrote: > 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? > > >