#include #include #include #include #include #define DEBUG_VERSION "0.1.2" typedef struct { int thnum; int stop_next; int dest_frame; int src_line; int stop_line; int stop_frame; int suspend; int tracing; VALUE frames; VALUE thread; } debug_context_t; typedef struct { VALUE file; VALUE line; VALUE binding; ID id; } debug_frame_t; typedef struct { VALUE source; VALUE pos; VALUE disabled; } debug_breakpoint_t; static VALUE threads_tbl = Qnil; static VALUE breakpoints = Qnil; static VALUE catchpoint = Qnil; static VALUE waiting = Qnil; static VALUE tracing = Qfalse; static VALUE mDebugger; static VALUE cContext; static VALUE cFrame; static VALUE cBreakpoint; static ID idDebugCommand; static ID idAtBreakpoint; static ID idAtCatchpoint; static ID idAtTracing; static ID idBinding; static int thnum_max = 0; static int last_debugged_thnum = 0; static VALUE debug_suspend(VALUE); static VALUE create_binding(VALUE); static VALUE debug_stop(VALUE); static VALUE debug_is_started(VALUE self) { return threads_tbl != Qnil ? Qtrue : Qfalse; } static VALUE debug_check_started() { if(threads_tbl == Qnil) { rb_raise(rb_eRuntimeError, "Debugger.start is not called yet."); } } static void debug_context_mark(void* data) { debug_context_t *debug_context = (debug_context_t *)data; rb_gc_mark(debug_context->frames); rb_gc_mark(debug_context->thread); } static VALUE debug_context_create(VALUE thread) { VALUE result; debug_context_t *debug_context; debug_context = ALLOC(debug_context_t); debug_context-> thnum = ++thnum_max; debug_context->stop_next = -1; debug_context->dest_frame = -1; debug_context->src_line = -1; debug_context->stop_line = -1; debug_context->stop_frame = -1; debug_context->suspend = 0; debug_context->tracing = 0; debug_context->frames = rb_ary_new(); debug_context->thread = thread; result = Data_Wrap_Struct(cContext, debug_context_mark, xfree, debug_context); return result; } static VALUE thread_context_lookup(VALUE thread) { VALUE context; debug_check_started(); context = rb_hash_aref(threads_tbl, thread); if(context == Qnil) { context = debug_context_create(thread); rb_hash_aset(threads_tbl, thread, context); } return context; } static void debug_frame_mark(void *data) { debug_frame_t *debug_frame = (debug_frame_t *)data; rb_gc_mark(debug_frame->binding); rb_gc_mark(debug_frame->file); rb_gc_mark(debug_frame->line); } static VALUE debug_frame_create(VALUE file, VALUE line, VALUE binding, ID id) { VALUE result; debug_frame_t *debug_frame; debug_frame = ALLOC(debug_frame_t); debug_frame->file = file; debug_frame->line = line; debug_frame->binding = binding; debug_frame->id = id; result = Data_Wrap_Struct(cFrame, debug_frame_mark, xfree, debug_frame); return result; } static VALUE call_debug_command(VALUE context, int thnum, VALUE self, ID mid, VALUE file, VALUE line) { VALUE id, binding; id = mid ? ID2SYM(mid) : Qnil; binding = self? create_binding(self) : Qnil; last_debugged_thnum = thnum; debug_suspend(mDebugger); return rb_funcall(context, idDebugCommand, 4, file, line, id, binding); } static void set_frame_source(debug_context_t *debug_context, VALUE file, VALUE line) { VALUE frame; debug_frame_t *top_frame; if(RARRAY(debug_context->frames)->len > 0) { frame = *RARRAY(debug_context->frames)->ptr; Data_Get_Struct(frame, debug_frame_t, top_frame); top_frame->file = file; top_frame->line = line; } } static void save_call_frame(VALUE self, VALUE file, VALUE line, ID mid, debug_context_t *debug_context) { VALUE frame, binding; binding = self? create_binding(self) : Qnil; frame = debug_frame_create(file, line, binding, mid); rb_ary_unshift(debug_context->frames, frame); } static int check_breakpoints(VALUE context, VALUE file, VALUE klass, VALUE pos, ID id) { VALUE breakpoint; debug_breakpoint_t *debug_breakpoint; int i; int result = 0; if(RARRAY(breakpoints)->len == 0) return 0; for(i = 0; i < RARRAY(breakpoints)->len; i++) { breakpoint = rb_ary_entry(breakpoints, i); Data_Get_Struct(breakpoint, debug_breakpoint_t, debug_breakpoint); if(debug_breakpoint->pos != pos && !(TYPE(pos) == T_STRING && TYPE(debug_breakpoint->pos) == T_STRING && rb_str_cmp(debug_breakpoint->pos, pos) == 0)) continue; if(TYPE(debug_breakpoint->source) == T_STRING && rb_str_cmp(debug_breakpoint->source, file) == 0 || klass != Qnil && debug_breakpoint->source == klass) { result = 1; rb_funcall(context, idAtBreakpoint, 2, breakpoint, id ? rb_str_new2(rb_id2name(id)) : Qnil); continue; } } return result; } /* * This is a NASTY HACK. For some reasons rb_f_binding is decalred * static in eval.c */ static VALUE create_binding(VALUE self) { typedef VALUE (*bind_func_t)(VALUE); static bind_func_t f_binding = NULL; if(f_binding == NULL) { ID idBinding = rb_intern("binding"); NODE *body, *method; st_lookup(RCLASS(rb_mKernel)->m_tbl, idBinding, (st_data_t *)&body); method = (NODE *)body->u2.value; f_binding = (bind_func_t)method->u1.value; } return f_binding(self); } static void check_suspend(debug_context_t *debug_context) { if(rb_thread_critical == Qtrue) return; while(1) { rb_thread_critical = Qtrue; if(!debug_context->suspend) break; rb_ary_push(waiting, rb_thread_current()); debug_context->suspend = 0; rb_thread_stop(); } rb_thread_critical = Qfalse; } static void debug_event_hook(rb_event_t event, NODE *node, VALUE self, ID mid, VALUE klass) { VALUE thread; VALUE context; debug_context_t *debug_context; VALUE file = Qnil, line = Qnil; static int debugging = 0; if(debugging) return; debugging++; thread = rb_thread_current(); context = thread_context_lookup(thread); Data_Get_Struct(context, debug_context_t, debug_context); check_suspend(debug_context); if(node) { file = rb_str_new2(node->nd_file); line = INT2FIX(nd_line(node)); } switch(event) { case RUBY_EVENT_LINE: { set_frame_source(debug_context, file, line); if(RTEST(tracing) || debug_context->tracing ) { rb_funcall(context, idAtTracing, 2, file, line); } if(debug_context->dest_frame == -1 || RARRAY(debug_context->frames)->len == debug_context->dest_frame) { debug_context->stop_next--; if(debug_context->stop_next < 0) debug_context->stop_next = -1; /* we check that we actualy moved to another line */ if(line != Qnil && debug_context->src_line != FIX2INT(line)) { debug_context->stop_line--; } } else if(RARRAY(debug_context->frames)->len < debug_context->dest_frame) { debug_context->stop_next = 0; } if(RARRAY(debug_context->frames)->len == 0) { save_call_frame(self, file, line, mid, debug_context); } if(debug_context->stop_next == 0 || debug_context->stop_line == 0 || check_breakpoints(context, file, klass, line, mid)) { /* reset all pointers */ debug_context->dest_frame = -1; debug_context->src_line = -1; debug_context->stop_line = -1; debug_context->stop_next = -1; call_debug_command(context, debug_context->thnum, self, mid, file, line); } break; } case RUBY_EVENT_C_CALL: { if(node) { set_frame_source(debug_context, file, line); } break; } case RUBY_EVENT_CALL: { save_call_frame(self, file, line, mid, debug_context); if(check_breakpoints(context, file, klass, rb_str_new2(rb_id2name(mid)), mid)) { call_debug_command(context, debug_context->thnum, self, mid, file, line); } break; } case RUBY_EVENT_RETURN: case RUBY_EVENT_END: { if(RARRAY(debug_context->frames)->len == debug_context->stop_frame) { debug_context->stop_next = 1; debug_context->stop_frame = 0; } rb_ary_shift(debug_context->frames); break; } case RUBY_EVENT_CLASS: { save_call_frame(self, file, line, mid, debug_context); break; } case RUBY_EVENT_RAISE: { VALUE ancestors; VALUE expn_class, aclass; int i, found = 0; expn_class = rb_obj_class(ruby_errinfo); if( !NIL_P(rb_class_inherited_p(expn_class, rb_eSystemExit)) ) { debug_stop(mDebugger); rb_exit(0); } if(catchpoint == Qnil) break; ancestors = rb_mod_ancestors(expn_class); for(i = 0; i < RARRAY(ancestors)->len; i++) { aclass = rb_ary_entry(ancestors, i); if(rb_str_cmp(rb_mod_name(aclass), catchpoint) == 0) { rb_funcall(context, idAtCatchpoint, 0); call_debug_command(context, debug_context->thnum, self, mid, file, line); break; } } break; } case RUBY_EVENT_C_RETURN: { break; } } debugging--; } static VALUE debug_thnum(VALUE self) { VALUE thread, context; debug_context_t *debug_context; thread = rb_thread_current(); context = thread_context_lookup(thread); Data_Get_Struct(context, debug_context_t, debug_context); return INT2FIX(debug_context->thnum); } static VALUE debug_start(VALUE self) { threads_tbl = rb_hash_new(); breakpoints = rb_ary_new(); waiting = rb_ary_new(); rb_add_event_hook(debug_event_hook, RUBY_EVENT_LINE | RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN | RUBY_EVENT_CALL | RUBY_EVENT_RETURN | RUBY_EVENT_CLASS | RUBY_EVENT_END | RUBY_EVENT_RAISE ); return Qnil; } static VALUE debug_stop(VALUE self) { debug_check_started(); rb_remove_event_hook(debug_event_hook); waiting = Qnil; breakpoints = Qnil; threads_tbl = Qnil; return Qnil; } static void breakpoint_mark(void *data) { debug_breakpoint_t *breakpoint; breakpoint = (debug_breakpoint_t *)data; rb_gc_mark(breakpoint->source); rb_gc_mark(breakpoint->pos); } static VALUE debug_add_breakpoint(VALUE self, VALUE source, VALUE pos) { VALUE result; debug_breakpoint_t *breakpoint; debug_check_started(); breakpoint = ALLOC(debug_breakpoint_t); breakpoint->source = source; breakpoint->pos = pos; breakpoint->disabled = Qfalse; result = Data_Wrap_Struct(cBreakpoint, breakpoint_mark, xfree, breakpoint); rb_ary_push(breakpoints, result); return result; } static VALUE debug_breakpoints(VALUE self) { debug_check_started(); return breakpoints; } static VALUE debug_catchpoint(VALUE self) { debug_check_started(); return catchpoint; } static VALUE debug_set_catchpoint(VALUE self, VALUE value) { debug_check_started(); if (!NIL_P(value) && TYPE(value) != T_STRING) { rb_raise(rb_eTypeError, "value of checkpoint must be String"); } if(NIL_P(value)) catchpoint = Qnil; else catchpoint = rb_str_dup(value); return value; } static VALUE debug_interrupt(VALUE self) { VALUE thread, context; debug_context_t *debug_context; debug_check_started(); thread = rb_thread_current(); context = thread_context_lookup(thread); Data_Get_Struct(context, debug_context_t, debug_context); debug_context->stop_next = 1; return Qnil; } static int find_last_context_func(VALUE key, VALUE value, VALUE *result) { debug_context_t *debug_context; Data_Get_Struct(value, debug_context_t, debug_context); if(debug_context->thnum == last_debugged_thnum) { *result = value; return ST_STOP; } return ST_CONTINUE; } static VALUE find_last_context() { VALUE result = Qnil; rb_hash_foreach(threads_tbl, find_last_context_func, (st_data_t)&result); return result; } static VALUE debug_interrupt_last(VALUE self) { VALUE thread, context = Qnil; debug_context_t *debug_context; debug_check_started(); context = find_last_context(); if(context != Qnil) { Data_Get_Struct(context, debug_context_t, debug_context); debug_context->stop_next = 1; } return Qnil; } static VALUE debug_current_context(VALUE self) { VALUE thread, context; debug_check_started(); thread = rb_thread_current(); context = thread_context_lookup(thread); return context; } static VALUE debug_contexts(VALUE self) { volatile VALUE list; volatile VALUE new_list; VALUE thread, context; debug_context_t *debug_context; int i; debug_check_started(); new_list = rb_ary_new(); list = rb_funcall(rb_cThread, rb_intern("list"), 0); for(i = 0; i < RARRAY(list)->len; i++) { thread = rb_ary_entry(list, i); context = thread_context_lookup(thread); rb_ary_push(new_list, context); } /* * I wonder why rb_hash_clear is declared static? */ rb_funcall(threads_tbl, rb_intern("clear"), 0); for(i = 0; i < RARRAY(new_list)->len; i++) { context = rb_ary_entry(new_list, i); Data_Get_Struct(context, debug_context_t, debug_context); rb_hash_aset(threads_tbl, debug_context->thread, context); } return new_list; } static VALUE debug_suspend(VALUE self) { VALUE current, context; VALUE saved_crit; VALUE context_list; debug_context_t *debug_context; int i; debug_check_started(); saved_crit = rb_thread_critical; rb_thread_critical = Qtrue; context_list = debug_contexts(self); current = thread_context_lookup(rb_thread_current()); for(i = 0; i < RARRAY(context_list)->len; i++) { context = rb_ary_entry(context_list, i); if(current == context) continue; Data_Get_Struct(context, debug_context_t, debug_context); debug_context->suspend = 1; } rb_thread_critical = saved_crit; if(rb_thread_critical == Qfalse) rb_thread_schedule(); return context; } static VALUE debug_resume(VALUE self) { VALUE current, context; VALUE saved_crit; VALUE thread; VALUE context_list; debug_context_t *debug_context; int i; debug_check_started(); saved_crit = rb_thread_critical; rb_thread_critical = Qtrue; context_list = debug_contexts(self); current = thread_context_lookup(rb_thread_current()); for(i = 0; i < RARRAY(context_list)->len; i++) { context = rb_ary_entry(context_list, i); if(current == context) continue; Data_Get_Struct(context, debug_context_t, debug_context); debug_context->suspend = 0; } for(i = 0; i < RARRAY(waiting)->len; i++) { thread = rb_ary_entry(waiting, i); rb_thread_run(thread); } rb_ary_clear(waiting); rb_thread_critical = saved_crit; rb_thread_schedule(); return context; } static VALUE debug_tracing(VALUE self) { return tracing; } static VALUE debug_set_tracing(VALUE self, VALUE value) { tracing = RTEST(value) ? Qtrue : Qfalse; return value; } static VALUE context_stop_next(VALUE self, VALUE steps) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); if(FIX2INT(steps) < 0) rb_raise(rb_eRuntimeError, "Steps argument can't be negative."); debug_context->stop_next = FIX2INT(steps); return steps; } static VALUE context_step_over(int argc, VALUE *argv, VALUE self) { VALUE lines, frame, cur_frame; debug_context_t *debug_context; debug_frame_t *debug_frame; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); if(RARRAY(debug_context->frames)->len == 0) rb_raise(rb_eRuntimeError, "No frames collected."); rb_scan_args(argc, argv, "11", &lines, &frame); debug_context->stop_line = FIX2INT(lines); if(argc == 1) { debug_context->dest_frame = RARRAY(debug_context->frames)->len; } else { if(FIX2INT(frame) < 0 && FIX2INT(frame) >= RARRAY(debug_context->frames)->len) rb_raise(rb_eRuntimeError, "Destination frame is out of range."); debug_context->dest_frame = FIX2INT(frame); } cur_frame = *RARRAY(debug_context->frames)->ptr; Data_Get_Struct(cur_frame, debug_frame_t, debug_frame); debug_context->src_line = FIX2INT(debug_frame->line); return Qnil; } static VALUE context_stop_frame(VALUE self, VALUE frame) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); if(FIX2INT(frame) < 0 && FIX2INT(frame) >= RARRAY(debug_context->frames)->len) rb_raise(rb_eRuntimeError, "Stop frame is out of range."); debug_context->stop_frame = FIX2INT(frame); return frame; } static VALUE context_frames(VALUE self) { debug_context_t *debug_context; Data_Get_Struct(self, debug_context_t, debug_context); return debug_context->frames; } static VALUE context_thread(VALUE self) { debug_context_t *debug_context; Data_Get_Struct(self, debug_context_t, debug_context); return debug_context->thread; } static VALUE context_thnum(VALUE self) { debug_context_t *debug_context; Data_Get_Struct(self, debug_context_t, debug_context); return INT2FIX(debug_context->thnum); } static VALUE context_set_suspend(VALUE self) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); debug_context->suspend = 1; return Qnil; } static VALUE context_clear_suspend(VALUE self) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); debug_context->suspend = 0; return Qnil; } static VALUE context_tracing(VALUE self) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); return debug_context->tracing ? Qtrue : Qfalse; } static VALUE context_set_tracing(VALUE self, VALUE value) { debug_context_t *debug_context; debug_check_started(); Data_Get_Struct(self, debug_context_t, debug_context); debug_context->tracing = RTEST(value) ? 1 : 0; return value; } static VALUE frame_file(VALUE self) { debug_frame_t *debug_frame; Data_Get_Struct(self, debug_frame_t, debug_frame); return debug_frame->file; } static VALUE frame_line(VALUE self) { debug_frame_t *debug_frame; Data_Get_Struct(self, debug_frame_t, debug_frame); return debug_frame->line; } static VALUE frame_binding(VALUE self) { debug_frame_t *debug_frame; Data_Get_Struct(self, debug_frame_t, debug_frame); return debug_frame->binding; } static VALUE frame_id(VALUE self) { debug_frame_t *debug_frame; Data_Get_Struct(self, debug_frame_t, debug_frame); return debug_frame->id ? ID2SYM(debug_frame->id): Qnil; } static VALUE breakpoint_source(VALUE self) { debug_breakpoint_t *breakpoint; Data_Get_Struct(self, debug_breakpoint_t, breakpoint); return breakpoint->source; } static VALUE breakpoint_pos(VALUE self) { debug_breakpoint_t *breakpoint; Data_Get_Struct(self, debug_breakpoint_t, breakpoint); return breakpoint->pos; } #if defined(_WIN32) __declspec(dllexport) #endif void Init_ruby_debug() { mDebugger = rb_define_module("Debugger"); rb_define_const(mDebugger, "VERSION", rb_str_new2(DEBUG_VERSION)); rb_define_module_function(mDebugger, "start", debug_start, 0); rb_define_module_function(mDebugger, "stop", debug_stop, 0); rb_define_module_function(mDebugger, "started?", debug_is_started, 0); rb_define_module_function(mDebugger, "thnum", debug_thnum, 0); rb_define_module_function(mDebugger, "breakpoints", debug_breakpoints, 0); rb_define_module_function(mDebugger, "add_breakpoint", debug_add_breakpoint, 2); rb_define_module_function(mDebugger, "catchpoint", debug_catchpoint, 0); rb_define_module_function(mDebugger, "catchpoint=", debug_set_catchpoint, 1); rb_define_module_function(mDebugger, "interrupt", debug_interrupt, 0); rb_define_module_function(mDebugger, "interrupt_last", debug_interrupt_last, 0); rb_define_module_function(mDebugger, "contexts", debug_contexts, 0); rb_define_module_function(mDebugger, "current_context", debug_current_context, 0); rb_define_module_function(mDebugger, "suspend", debug_suspend, 0); rb_define_module_function(mDebugger, "resume", debug_resume, 0); rb_define_module_function(mDebugger, "tracing", debug_tracing, 0); rb_define_module_function(mDebugger, "tracing=", debug_set_tracing, 1); cContext = rb_define_class_under(mDebugger, "Context", rb_cObject); rb_define_method(cContext, "stop_next=", context_stop_next, 1); rb_define_method(cContext, "step_over", context_step_over, -1); rb_define_method(cContext, "stop_frame=", context_stop_frame, 1); rb_define_method(cContext, "frames", context_frames, 0); rb_define_method(cContext, "thread", context_thread, 0); rb_define_method(cContext, "thnum", context_thnum, 0); rb_define_method(cContext, "set_suspend", context_set_suspend, 0); rb_define_method(cContext, "clear_suspend", context_clear_suspend, 0); rb_define_method(cContext, "tracing", context_tracing, 0); rb_define_method(cContext, "tracing=", context_set_tracing, 1); cFrame = rb_define_class_under(cContext, "Frame", rb_cObject); rb_define_method(cFrame, "file", frame_file, 0); rb_define_method(cFrame, "line", frame_line, 0); rb_define_method(cFrame, "binding", frame_binding, 0); rb_define_method(cFrame, "id", frame_id, 0); cBreakpoint = rb_define_class_under(mDebugger, "Breakpoint", rb_cObject); rb_define_method(cBreakpoint, "source", breakpoint_source, 0); rb_define_method(cBreakpoint, "pos", breakpoint_pos, 0); idDebugCommand = rb_intern("debug_command"); idAtBreakpoint = rb_intern("at_breakpoint"); idAtCatchpoint = rb_intern("at_catchpoint"); idAtTracing = rb_intern("at_tracing"); idBinding = rb_intern("binding"); rb_global_variable(&threads_tbl); rb_global_variable(&breakpoints); rb_global_variable(&catchpoint); rb_global_variable(&waiting); }