jgbailey
3/4/2006 6:56:00 PM
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 <Dymaio@gmail.com> 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.
>
> #<call_stack.rb>
> =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:
>
> # <stacktest.rb>
> require 'call_stack'
>
> puts Binding::call_stack.first.to_a # Retrieve StackFrame and convert
> to array for output
> # </stacktest.rb>
>
> produces:
>
> main
> nil
> #<Binding:0xb16fa4f00l>
> stacktest.rb
> 4
>
> Not interesting. But given this
>
> # <stacktest.rb>
> 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
> # </stacktest.rb>
>
> 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
>
> # </call_stack.rb>
>
> 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?
>
>
>