Skip to content

Commit 4074f51

Browse files
authored
Merge pull request #769 from posit-dev/feature/c-debug
Expose helpers for C/C++/Rust debuggers
2 parents 58f1fc7 + a2f9955 commit 4074f51

File tree

8 files changed

+255
-1
lines changed

8 files changed

+255
-1
lines changed

Cargo.lock

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ark/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ stdext = { path = "../stdext", features = ["testing"] }
6969
tempfile = "3.13.0"
7070

7171
[build-dependencies]
72+
cc = "1.0"
7273
chrono = "0.4.23"
7374
embed-resource = "2.5.0"
7475

crates/ark/build.rs

+3
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,7 @@ fn main() {
5959
.join("manifest")
6060
.join("ark-manifest.rc");
6161
embed_resource::compile_for_everything(resource, embed_resource::NONE);
62+
63+
println!("cargo:rerun-if-changed=src/debug.c");
64+
cc::Build::new().file("src/debug.c").compile("debug");
6265
}

crates/ark/src/debug.c

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Define `SEXP` as opaque struct. This replaces a dependency on `Rinternals.h`.
2+
typedef struct SEXPREC *SEXP;
3+
4+
// C entry points that are visible to debuggers like lldb.
5+
// To ensure these functions are linked in, these must be called from
6+
// `_placeholder()` in `utils.rs`. The actual implementations live in
7+
// `debug.rs`.
8+
9+
const char* ark_print_rs(SEXP x);
10+
const char* ark_inspect_rs(SEXP x);
11+
const char* ark_trace_back_rs();
12+
const char* ark_display_value_rs(SEXP x);
13+
14+
const char* ark_print(SEXP x) {
15+
return ark_print_rs(x);
16+
}
17+
18+
const char* ark_inspect(SEXP x) {
19+
return ark_inspect_rs(x);
20+
}
21+
22+
const char* ark_trace_back() {
23+
return ark_trace_back_rs();
24+
}
25+
26+
const char* ark_display_value(SEXP x) {
27+
return ark_display_value_rs(x);
28+
}

crates/ark/src/debug.rs

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use std::ffi;
2+
use std::sync::atomic::Ordering;
3+
4+
use harp::exec::RFunction;
5+
use harp::exec::RFunctionExt;
6+
use harp::utils::r_str_to_owned_utf8_unchecked;
7+
use harp::utils::r_typeof;
8+
9+
use crate::interface::RMain;
10+
use crate::interface::CAPTURE_CONSOLE_OUTPUT;
11+
12+
// To ensure the compiler includes the C entry points in `debug.c` in the binary,
13+
// we store function pointers in global variables that are declared "used" (even
14+
// though we never actually use them). The compiler is able to follow the chain
15+
// of dependency from these variables to the C functions and ultimately their
16+
// Rust implementations defined below.
17+
extern "C" {
18+
fn ark_print(x: libr::SEXP) -> *const ffi::c_char;
19+
fn ark_inspect(x: libr::SEXP) -> *const ffi::c_char;
20+
fn ark_trace_back() -> *const ffi::c_char;
21+
fn ark_display_value(x: libr::SEXP) -> *const ffi::c_char;
22+
}
23+
24+
#[used]
25+
static _ARK_PRINT: unsafe extern "C" fn(x: libr::SEXP) -> *const ffi::c_char = ark_print;
26+
#[used]
27+
static _ARK_INSPECT: unsafe extern "C" fn(x: libr::SEXP) -> *const ffi::c_char = ark_inspect;
28+
#[used]
29+
static _ARK_TRACE_BACK: unsafe extern "C" fn() -> *const ffi::c_char = ark_trace_back;
30+
#[used]
31+
static _ARK_DISPLAY_VALUE: unsafe extern "C" fn(x: libr::SEXP) -> *const ffi::c_char =
32+
ark_display_value;
33+
34+
// Implementations for entry points in `debug.c`.
35+
36+
#[no_mangle]
37+
pub extern "C-unwind" fn ark_print_rs(x: libr::SEXP) -> *const ffi::c_char {
38+
capture_console_output(|| {
39+
unsafe { libr::Rf_PrintValue(x) };
40+
})
41+
}
42+
43+
/// Inspect structure of R object
44+
///
45+
/// Uses lobstr's `sxp()` function because libr can't find `R_inspect()`.
46+
/// It's an `attribute_hidden` function but since the symbol is visible
47+
/// on macOS (and you can call it in the debugger) I would have expected
48+
/// libr to be able to find it.
49+
///
50+
/// Requires lldb setting:
51+
///
52+
/// ```
53+
/// settings set escape-non-printables false
54+
/// ```
55+
#[no_mangle]
56+
pub extern "C-unwind" fn ark_inspect_rs(x: libr::SEXP) -> *const ffi::c_char {
57+
capture_console_output(|| {
58+
// TODO: Should use C callable when implemented as that would avoid
59+
// messing with namedness and refcounts:
60+
// https://github.com/r-lib/lobstr/issues/77
61+
let out = RFunction::new("lobstr", "sxp").add(x).call().unwrap();
62+
unsafe { libr::Rf_PrintValue(out.sexp) };
63+
})
64+
}
65+
66+
/// Print backtrace via rlang
67+
///
68+
/// Requires lldb setting:
69+
///
70+
/// ```
71+
/// settings set escape-non-printables false
72+
/// ```
73+
#[no_mangle]
74+
pub extern "C-unwind" fn ark_trace_back_rs() -> *const ffi::c_char {
75+
capture_console_output(|| {
76+
// https://github.com/r-lib/rlang/issues/1059
77+
unsafe {
78+
let fun =
79+
get_c_callable_int(c"rlang".as_ptr(), c"rlang_print_backtrace".as_ptr()).unwrap();
80+
fun(1);
81+
};
82+
})
83+
}
84+
85+
#[no_mangle]
86+
pub extern "C-unwind" fn ark_display_value_rs(x: libr::SEXP) -> *const ffi::c_char {
87+
let value = unsafe {
88+
let kind = tidy_kind(r_typeof(x));
89+
let tag = format!("<{kind}>");
90+
91+
match r_typeof(x) {
92+
libr::SYMSXP => format!(
93+
"{tag} ({name})",
94+
name = r_str_to_owned_utf8_unchecked(libr::PRINTNAME(x))
95+
),
96+
// TODO: Show some values (not with ALTREP objects as that could
97+
// materialise or cause side effects)
98+
libr::LGLSXP |
99+
libr::INTSXP |
100+
libr::REALSXP |
101+
libr::CPLXSXP |
102+
libr::RAWSXP |
103+
libr::STRSXP |
104+
libr::VECSXP |
105+
libr::SPECIALSXP |
106+
libr::BUILTINSXP |
107+
libr::PROMSXP => {
108+
format!("{tag} [{len}]", len = libr::Rf_xlength(x))
109+
},
110+
111+
_ => tag,
112+
}
113+
};
114+
115+
ffi::CString::new(value).unwrap().into_raw()
116+
}
117+
118+
pub fn tidy_kind(kind: libr::SEXPTYPE) -> &'static str {
119+
match kind {
120+
libr::NILSXP => "null",
121+
libr::SYMSXP => "sym",
122+
libr::LISTSXP => "list",
123+
libr::CLOSXP => "fn",
124+
libr::ENVSXP => "env",
125+
libr::PROMSXP => "prom",
126+
libr::LANGSXP => "call",
127+
libr::SPECIALSXP => "special",
128+
libr::BUILTINSXP => "builtin",
129+
libr::CHARSXP => "char",
130+
libr::LGLSXP => "lgl",
131+
libr::INTSXP => "int",
132+
libr::REALSXP => "dbl",
133+
libr::CPLXSXP => "cpl",
134+
libr::STRSXP => "chr",
135+
libr::DOTSXP => "dots",
136+
libr::ANYSXP => "any",
137+
libr::VECSXP => "list",
138+
libr::EXPRSXP => "expr",
139+
libr::BCODESXP => "bcode",
140+
libr::EXTPTRSXP => "extptr",
141+
libr::WEAKREFSXP => "weakref",
142+
libr::RAWSXP => "raw",
143+
libr::S4SXP => "s4",
144+
libr::NEWSXP => "new",
145+
libr::FREESXP => "free",
146+
libr::FUNSXP => "fun",
147+
_ => "unknown",
148+
}
149+
}
150+
151+
/// Run closure and capture its console output.
152+
///
153+
/// Useful for debugging. For instance you can use this to call code from the C
154+
/// debugger's interpreter. Output from stdout and stderr is returned instead of
155+
/// being sent over IOPub.
156+
///
157+
/// The closure is run in a `harp::try_catch()` context to prevent R errors and
158+
/// other C longjumps from collapsing the debugging context. If a Rust panic
159+
/// occurs however, it is propagated as normal.
160+
///
161+
/// Note that the resulting string is stored on the Rust heap and never freed.
162+
/// This should only be used in a debugging context where leaking is not an
163+
/// issue.
164+
pub fn capture_console_output(cb: impl FnOnce()) -> *const ffi::c_char {
165+
let old = CAPTURE_CONSOLE_OUTPUT.swap(true, Ordering::SeqCst);
166+
167+
// We protect from panics to correctly restore `CAPTURE_CONSOLE_OUTPUT`'s
168+
// state. The panic is resumed right after.
169+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| harp::try_catch(cb)));
170+
171+
CAPTURE_CONSOLE_OUTPUT.store(old, Ordering::SeqCst);
172+
let mut out = std::mem::take(&mut RMain::get_mut().captured_output);
173+
174+
// Unwrap catch-unwind's result and resume panic if needed
175+
let result = match result {
176+
Ok(res) => res,
177+
Err(err) => {
178+
std::panic::resume_unwind(err);
179+
},
180+
};
181+
182+
// Unwrap try-catch's result
183+
if let Err(err) = result {
184+
out = format!("{out}\nUnexpected longjump in `capture_console_output()`: {err:?}");
185+
}
186+
187+
// Intentionally leaks, should only be used in the debugger
188+
ffi::CString::new(out).unwrap().into_raw()
189+
}
190+
191+
// Cast `DL_FUNC` to correct function type
192+
fn get_c_callable_int(
193+
pkg: *const std::ffi::c_char,
194+
fun: *const std::ffi::c_char,
195+
) -> Option<unsafe extern "C-unwind" fn(std::ffi::c_int) -> *mut std::ffi::c_void> {
196+
unsafe {
197+
std::mem::transmute::<
198+
Option<unsafe extern "C-unwind" fn() -> *mut std::ffi::c_void>,
199+
Option<unsafe extern "C-unwind" fn(std::ffi::c_int) -> *mut std::ffi::c_void>,
200+
>(libr::R_GetCCallable(pkg, fun))
201+
}
202+
}

crates/ark/src/interface.rs

+15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::collections::HashMap;
1616
use std::ffi::*;
1717
use std::os::raw::c_uchar;
1818
use std::result::Result::Ok;
19+
use std::sync::atomic::AtomicBool;
20+
use std::sync::atomic::Ordering;
1921
use std::sync::Arc;
2022
use std::sync::Mutex;
2123
use std::task::Poll;
@@ -125,6 +127,7 @@ use crate::sys::console::console_to_utf8;
125127
use crate::ui::UiCommMessage;
126128
use crate::ui::UiCommSender;
127129

130+
pub static CAPTURE_CONSOLE_OUTPUT: AtomicBool = AtomicBool::new(false);
128131
static RE_DEBUG_PROMPT: Lazy<Regex> = Lazy::new(|| Regex::new(r"Browse\[\d+\]").unwrap());
129132

130133
/// An enum representing the different modes in which the R session can run.
@@ -236,6 +239,10 @@ pub struct RMain {
236239
/// Raw error buffer provided to `Rf_error()` when throwing `r_read_console()` errors.
237240
/// Stored in `RMain` to avoid memory leakage when `Rf_error()` jumps.
238241
r_error_buffer: Option<CString>,
242+
243+
/// `WriteConsole` output diverted from IOPub is stored here. This is only used
244+
/// to return R output to the debugger.
245+
pub(crate) captured_output: String,
239246
}
240247

241248
/// Represents the currently active execution request from the frontend. It
@@ -567,6 +574,7 @@ impl RMain {
567574
pending_lines: Vec::new(),
568575
banner: None,
569576
r_error_buffer: None,
577+
captured_output: String::new(),
570578
}
571579
}
572580

@@ -1668,6 +1676,13 @@ impl RMain {
16681676

16691677
/// Invoked by R to write output to the console.
16701678
fn write_console(buf: *const c_char, _buflen: i32, otype: i32) {
1679+
if CAPTURE_CONSOLE_OUTPUT.load(Ordering::SeqCst) {
1680+
RMain::get_mut()
1681+
.captured_output
1682+
.push_str(&console_to_utf8(buf).unwrap());
1683+
return;
1684+
}
1685+
16711686
let content = match console_to_utf8(buf) {
16721687
Ok(content) => content,
16731688
Err(err) => panic!("Failed to read from R buffer: {err:?}"),

crates/ark/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod control;
1212
pub mod coordinates;
1313
pub mod dap;
1414
pub mod data_explorer;
15+
pub mod debug;
1516
pub mod errors;
1617
pub mod fixtures;
1718
pub mod help;

crates/libr/src/r.rs

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ functions::generate! {
4141
externalRoutines: *const R_ExternalMethodDef
4242
) -> std::ffi::c_int;
4343

44+
pub fn R_GetCCallable(pkg: *const std::ffi::c_char, fun: *const std::ffi::c_char) ->
45+
DL_FUNC;
46+
4447
pub fn vmaxget() -> *mut std::ffi::c_void;
4548

4649
pub fn vmaxset(arg1: *const std::ffi::c_void);

0 commit comments

Comments
 (0)