Re: Review requested: Binding.call_stack(), Binding.of_caller()-style



Update. So I learn that using an exception as the unrolling
mechanism will trip any ensure() blocks it passes through: each will
get executed twice, once during and once after, and the client won't
know why. Included is the patch--the entire file without the docs, for
simplicity's sake. A row of asterisks marks the only addition.

#<call_stack.rb>

# Pop open for a bit:
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.1.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.
#aCallers.slice!(0, nSkipFrames) FIXME: Don't let this creep back
in!

# 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|

# Capturing frames:
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
elsif( event == 'line' )
# *** This breaks free of inadvertent ensure() or rescue()
blocks:
throw( Binding::CallStack::tracing_exception ) #
***************
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>

If the unrolling goes uninterrupted, set_trace_func() gets only
'return' events. If it reports a 'line' event, we bully our way back
out with another exception. This also takes care of the related
potential injudicious use of rescue(). It does not, however, address
the fact that use of call_stack() will cause else() blocks to *never*
be executed, and this one may really have me stumped. It was my
understanding, my impression, at least, that recovering from an
exception is a traditional and accepted application for a continuation,
and the use of an else() block presupposes that every exception, or
whatever you want to call it, is an error--which is inconsistent with
the language's inclusion of throw() and catch(). In all, this looks
like a good reason for avoiding else() blocks--and I'm not just being
sore (I hope).

-J

.



Relevant Pages

  • Function names for managed callstack under SOS debugging of .NET 2.0 app
    ... wrote that sets the unhandled exception filter ... unhandled exception filter, and we generate a mini-dump. ... The callstack above happens ...
    (microsoft.public.vsnet.debugging)
  • Review requested: Binding.call_stack(), Binding.of_caller()-style
    ... The technique of using set_trace_functo capture a Binding one ... subclass--containing one StackFrame object per calling frame. ... to array for output ... class CallStack < Array ...
    (comp.lang.ruby)
  • Re: Review requested: Binding.call_stack(), Binding.of_caller()-style
    ... The technique of using set_trace_functo capture a Binding one ... subclass--containing one StackFrame object per calling frame. ... to array for output ... class CallStack < Array ...
    (comp.lang.ruby)
  • Re: NullReferenceExeption in DataTable
    ... where the exception is being thrown there are only 33 rows in the table. ... query string is non null and valid (it is filtering on four of the seven PK ... >> CallStack: at System.Data.RecordManager.NewRecordBase ...
    (microsoft.public.dotnet.framework.adonet)
  • StackWalk behaviour
    ... I am adding an exception handler to certain of our build configurations ... actually dump the callstack for each thread. ... the second and third seemed to result in a valid callstack. ...
    (microsoft.public.win32.programmer.kernel)