#include <stdio.h>
#include <ruby.h>
#include <node.h>
#include <rubysig.h>
#include <st.h>

#define DEBUG_VERSION "0.1.3"

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 expr;
} 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 binding, ID mid, VALUE file, VALUE line)
{
    VALUE id;

    id = mid ? ID2SYM(mid) : 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)
{
    VALUE breakpoint; 
    debug_breakpoint_t *debug_breakpoint;
    int i;

    if(RARRAY(breakpoints)->len == 0)
        return -1;
    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(rb_str_cmp(debug_breakpoint->source, file) == 0 ||
            klass != Qnil && rb_str_cmp(debug_breakpoint->source, rb_mod_name(klass)) == 0)
            return i;
    }
    return -1;
}

/*
 * 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 VALUE
get_breakpoint_at(int index) 
{
    return rb_ary_entry(breakpoints, index);
}

static VALUE
eval_expression(VALUE args)
{
    return rb_funcall(rb_mKernel, rb_intern("eval"), 2, RARRAY(args)->ptr[0], RARRAY(args)->ptr[1]);
}

static int
check_breakpoint_expression(VALUE breakpoint, VALUE binding)
{
    debug_breakpoint_t *debug_breakpoint;
    VALUE args, expr_result;
    
    Data_Get_Struct(breakpoint, debug_breakpoint_t, debug_breakpoint);
    if(NIL_P(debug_breakpoint->expr))
        return 1;
    
    args = rb_ary_new3(2, debug_breakpoint->expr, binding);
    expr_result = rb_protect(eval_expression, args, 0);
    return RTEST(expr_result);
}

static void
debug_event_hook(rb_event_t event, NODE *node, VALUE self, ID mid, VALUE klass)
{
    VALUE thread, context, binding, breakpoint;
    debug_context_t *debug_context;
    debug_breakpoint_t *debug_breakpoint;
    VALUE file = Qnil, line = Qnil;
    int breakpoint_index = -1;

    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 ||
            (breakpoint_index = check_breakpoints(context, file, klass, line)) != -1)
        {
            binding = self? create_binding(self) : Qnil;
            /* check breakpoint expression */
            if(breakpoint_index != -1)
            {
                breakpoint = get_breakpoint_at(breakpoint_index);
                if(check_breakpoint_expression(breakpoint, binding))
                    rb_funcall(context, idAtBreakpoint, 2, breakpoint, mid ? rb_str_new2(rb_id2name(mid)) : Qnil);
                else
                    break;
            }

            /* 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, binding, 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);
        breakpoint_index = check_breakpoints(context, file, klass, rb_str_new2(rb_id2name(mid)));
        if(breakpoint_index != -1)
        {
            binding = self? create_binding(self) : Qnil;
            breakpoint = get_breakpoint_at(breakpoint_index);
            if(check_breakpoint_expression(breakpoint, binding))
            {
                rb_funcall(context, idAtBreakpoint, 2, breakpoint, mid ? rb_str_new2(rb_id2name(mid)) : Qnil);
                call_debug_command(context, debug_context->thnum, binding, 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);
                binding = self? create_binding(self) : Qnil;
                call_debug_command(context, debug_context->thnum, binding, 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);
    rb_gc_mark(breakpoint->expr);
}

static VALUE
debug_add_breakpoint(int argc, VALUE *argv, VALUE self)
{
    VALUE source, pos, expr;
    VALUE result;
    debug_breakpoint_t *breakpoint;

    debug_check_started();
    
    if(rb_scan_args(argc, argv, "21", &source, &pos, &expr) == 2)
    {
        expr = Qnil;
    }

    breakpoint = ALLOC(debug_breakpoint_t);
    breakpoint->source = StringValue(source);
    breakpoint->pos = pos;
    breakpoint->expr = NIL_P(expr) ? expr: StringValue(expr);
    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;
}

static VALUE
breakpoint_expr(VALUE self)
{
    debug_breakpoint_t *breakpoint;

    Data_Get_Struct(self, debug_breakpoint_t, breakpoint);
    return breakpoint->expr;
}


#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, -1);
    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);
    rb_define_method(cBreakpoint, "expr", breakpoint_expr, 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);
}