rutie 0.12.1

The tie between Ruby and Rust.
Documentation
use std::convert::TryFrom;

use crate::{
    binding::vm,
    rubysys::debug::{self, DebugInspector},
    types::{c_int, c_long, c_void, CallbackMutPtr, Value},
};

// Fills a buffer with the `start + limit` innermost frames with `fill`, and
// returns the frames from the `start`th, with their line numbers. Ruby 3.1 to
// 3.3 ignore the `start` argument of `rb_profile_frames`, so Rutie always
// asks from the top and skips frames itself.
fn collect_frames<F>(start: usize, limit: usize, fill: F) -> Vec<(Value, c_int)>
where
    F: FnOnce(c_int, *mut Value, *mut c_int) -> c_int,
{
    let total = c_int::try_from(start.saturating_add(limit)).unwrap_or(c_int::MAX);
    let mut frames = vec![Value::from(0); total as usize];
    let mut lines: Vec<c_int> = vec![0; total as usize];

    let filled = fill(total, frames.as_mut_ptr(), lines.as_mut_ptr()).max(0) as usize;

    frames
        .into_iter()
        .zip(lines)
        .take(filled)
        .skip(start)
        .collect()
}

pub fn profile_frames(start: usize, limit: usize) -> Vec<(Value, c_int)> {
    collect_frames(start, limit, |total, frames, lines| unsafe {
        debug::rb_profile_frames(0, total, frames, lines)
    })
}

#[cfg(ruby_gte_3_3)]
pub fn profile_thread_frames(thread: Value, start: usize, limit: usize) -> Vec<(Value, c_int)> {
    collect_frames(start, limit, |total, frames, lines| unsafe {
        debug::rb_profile_thread_frames(thread, 0, total, frames, lines)
    })
}

// The `profile_frame_*` functions take a frame from `profile_frames`.
pub fn profile_frame_path(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_path(frame) }
}

pub fn profile_frame_absolute_path(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_absolute_path(frame) }
}

pub fn profile_frame_label(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_label(frame) }
}

pub fn profile_frame_base_label(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_base_label(frame) }
}

pub fn profile_frame_full_label(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_full_label(frame) }
}

pub fn profile_frame_first_lineno(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_first_lineno(frame) }
}

pub fn profile_frame_classpath(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_classpath(frame) }
}

pub fn profile_frame_is_singleton_method(frame: Value) -> bool {
    unsafe { debug::rb_profile_frame_singleton_method_p(frame) }.is_true()
}

pub fn profile_frame_method_name(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_method_name(frame) }
}

pub fn profile_frame_qualified_method_name(frame: Value) -> Value {
    unsafe { debug::rb_profile_frame_qualified_method_name(frame) }
}

// `data` points to an `Option<F>` on the caller's stack, taken exactly once.
rutie_callback! {
    fn debug_inspector_callback<F>(dc: *const DebugInspector, data: CallbackMutPtr) -> Value
    where
        F: FnOnce(*const DebugInspector) -> Value,
    {
        match unsafe { (*(data as *mut Option<F>)).take() } {
            Some(func) => vm::call_catching_panic(move || func(dc)),
            None => Value::from(0),
        }
    }
}

// Calls `func` with a debug context that is only valid during the call.
pub fn debug_inspector_open<F>(func: F) -> Value
where
    F: FnOnce(*const DebugInspector) -> Value,
{
    let mut func = Some(func);

    unsafe {
        debug::rb_debug_inspector_open(
            debug_inspector_callback::<F>,
            &mut func as *mut Option<F> as *mut c_void,
        )
    }
}

// The `debug_inspector_*` functions take the context `debug_inspector_open`
// passes, and an `index` within its backtrace locations.
pub unsafe fn debug_inspector_backtrace_locations(dc: *const DebugInspector) -> Value {
    debug::rb_debug_inspector_backtrace_locations(dc)
}

pub unsafe fn debug_inspector_frame_self(dc: *const DebugInspector, index: usize) -> Value {
    debug::rb_debug_inspector_frame_self_get(dc, index as c_long)
}

pub unsafe fn debug_inspector_frame_class(dc: *const DebugInspector, index: usize) -> Value {
    debug::rb_debug_inspector_frame_class_get(dc, index as c_long)
}

pub unsafe fn debug_inspector_frame_binding(dc: *const DebugInspector, index: usize) -> Value {
    debug::rb_debug_inspector_frame_binding_get(dc, index as c_long)
}

pub unsafe fn debug_inspector_frame_iseq(dc: *const DebugInspector, index: usize) -> Value {
    debug::rb_debug_inspector_frame_iseq_get(dc, index as c_long)
}

#[cfg(ruby_gte_3_2)]
pub unsafe fn debug_inspector_frame_depth(dc: *const DebugInspector, index: usize) -> Value {
    debug::rb_debug_inspector_frame_depth(dc, index as c_long)
}

#[cfg(ruby_gte_3_2)]
pub fn debug_inspector_current_depth() -> Value {
    unsafe { debug::rb_debug_inspector_current_depth() }
}

// Runs the closure `data` points to; it stays allocated for good, as Ruby
// keeps a preregistered job until the process exits.
//
// An exception (or panic) is caught and discarded here, not left to Ruby:
// `rb_postponed_job_flush` reads a non-`volatile` local after `longjmp`, so
// where the compiler keeps it in a register (clang on arm64 macOS) Ruby
// re-triggers the job that raised and runs it again.
#[cfg(ruby_gte_3_3)]
rutie_callback! {
    fn postponed_job_callback<F>(data: *mut c_void)
    where
        F: Fn() + Send + Sync + 'static,
    {
        let func = unsafe { &*(data as *const F) };

        let _ = vm::protect_value(|| {
            func();

            Value::from(crate::binding::global::RubySpecialConsts::Nil as crate::types::InternalValue)
        });
    }
}

// Returns the job's handle, or `None` when Ruby's job table is full.
#[cfg(ruby_gte_3_3)]
pub fn postponed_job_preregister<F>(func: F) -> Option<debug::PostponedJobHandle>
where
    F: Fn() + Send + Sync + 'static,
{
    let data = Box::into_raw(Box::new(func));
    let handle = unsafe {
        debug::rb_postponed_job_preregister(0, postponed_job_callback::<F>, data as *mut c_void)
    };

    if handle == debug::POSTPONED_JOB_HANDLE_INVALID {
        drop(unsafe { Box::from_raw(data) });

        None
    } else {
        Some(handle)
    }
}

// `handle` must come from `postponed_job_preregister`.
#[cfg(ruby_gte_3_3)]
pub unsafe fn postponed_job_trigger(handle: debug::PostponedJobHandle) {
    debug::rb_postponed_job_trigger(handle)
}