Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C-based support #21

Merged
merged 3 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ cc = { version = "1.0" }

[features]
default = ["test_c_integration"]
use_c_to_interface_with_setjmp = []
test_c_integration = []
10 changes: 10 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ fn main() {
} else {
panic!("did not see it");
}

if std::env::var("CARGO_FEATURE_USE_C_TO_INTERFACE_WITH_SETJMP")
.ok()
.is_some()
{
cc::Build::new()
.file("src/interop_via_c.c")
.compile("interop_via_c");
} else {
}
}
69 changes: 69 additions & 0 deletions src/cee_based.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::{JmpBuf, JmpBufFields};
use crate::{SigJmpBuf, SigJmpBufFields};
use libc::{c_int, c_void};

#[link(name = "interop_via_c", kind="static")]
extern "C" {
fn call_closure_with_setjmp(closure_env_ptr: *mut c_void, closure_code: extern "C" fn(jbuf: *const JmpBufFields, env_ptr: *mut c_void) -> c_int) -> c_int;

fn call_closure_with_sigsetjmp(savemask: c_int, closure_env_ptr: *mut c_void, closure_code: extern "C" fn(jbuf: *const SigJmpBufFields, env_ptr: *mut c_void) -> c_int) -> c_int;
}

/// Covers the usual use case for setjmp: it invokes the callback, and the code
/// of the callback can use longjmp to exit early from the call_with_setjmp.
pub fn call_with_setjmp<F>(mut callback: F) -> c_int
where
F: for<'a> FnOnce(&'a JmpBufFields) -> c_int,
{
extern "C" fn call_from_c_to_rust<F>(jbuf: JmpBuf, closure_env_ptr: *mut c_void) -> c_int
where
F: for<'a> FnOnce(&'a JmpBufFields) -> c_int,
{
let closure_env_ptr: *mut F = closure_env_ptr as *mut F;
unsafe { (closure_env_ptr.read())(&*jbuf) }
}

unsafe {
let closure_env_ptr = std::ptr::addr_of_mut!(callback);

// The callback is now effectively owned by `closure_env_ptr` (i.e., the
// `closure_env_ptr.read()` call in `call_from_c_to_rust` will take a
// direct bitwise copy of its state, and pass that ownership into the
// FnOnce::call_once invocation.)
//
// Therefore, we need to forget about our own ownership of the callback now.
std::mem::forget(callback);

call_closure_with_setjmp(closure_env_ptr as *mut c_void, call_from_c_to_rust::<F>)
}
}

/// TODO
pub fn call_with_sigsetjmp<F>(savemask: bool, mut callback: F) -> c_int
where
F: for<'a> FnOnce(&'a SigJmpBufFields) -> c_int,
{
extern "C" fn call_from_c_to_rust<F>(jbuf: SigJmpBuf, closure_env_ptr: *mut c_void) -> c_int
where
F: for<'a> FnOnce(&'a SigJmpBufFields) -> c_int,
{
let closure_env_ptr: *mut F = closure_env_ptr as *mut F;
unsafe { (closure_env_ptr.read())(&*jbuf) }
}

let savemask: c_int = if savemask { 1 } else { 0 };

unsafe {
let closure_env_ptr = std::ptr::addr_of_mut!(callback);

// The callback is now effectively owned by `closure_env_ptr` (i.e., the
// `closure_env_ptr.read()` call in `call_from_c_to_rust` will take a
// direct bitwise copy of its state, and pass that ownership into the
// FnOnce::call_once invocation.)
//
// Therefore, we need to forget about our own ownership of the callback now.
std::mem::forget(callback);

call_closure_with_sigsetjmp(savemask, closure_env_ptr as *mut c_void, call_from_c_to_rust::<F>)
}
}
1 change: 1 addition & 0 deletions src/glibc_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct SigJmpBufFields {
}

/// This is the type you use to allocate a JmpBuf on the stack.
/// (Glibc puns the two.)
pub type JmpBufStruct = SigJmpBufFields;

/// This is the type you use to allocate a SigJmpBuf on the stack.
Expand Down
22 changes: 22 additions & 0 deletions src/interop_via_c.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <setjmp.h>
#include <stdio.h>

int call_closure_with_setjmp(void* closure_env_ptr, int (*closure_code)(jmp_buf jbuf, void *env_ptr)) {
jmp_buf jbuf;
int val;
if (0 == (val = setjmp(jbuf))) {
return closure_code(jbuf, closure_env_ptr);
} else {
return val;
}
}

int call_closure_with_sigsetjmp(int savemask, void* closure_env_ptr, int (*closure_code)(sigjmp_buf jbuf, void *env_ptr)) {
sigjmp_buf jbuf;
int val;
if (0 == (val = sigsetjmp(jbuf, savemask))) {
return closure_code(jbuf, closure_env_ptr);
} else {
return val;
}
}
100 changes: 85 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ extern "C" {
/// non-zero value is the only way for the internal machinery to distinguish
/// between the first return from the initial call versus a non-local
/// return).
///
/// FIXME: include safety note here, including the issues with destructors
pub fn longjmp(jbuf: JmpBuf, val: c_int) -> !;

/// Given a calling environment `jbuf` (which one can acquire via
Expand All @@ -226,12 +228,24 @@ extern "C" {
/// non-zero value is the only way for the internal machinery to distinguish
/// between the first return from the initial call versus a non-local
/// return).
///
/// FIXME: include safety note here, including the issues with destructors
pub fn siglongjmp(jbuf: SigJmpBuf, val: c_int) -> !;
}

// FIXME: figure out how to access feature cfg'ing. (And then, look into linting
// against people trying to do "the obvious things".)

#[cfg(not(feature = "use_c_to_interface_with_setjmp"))]
mod asm_based;
#[cfg(not(feature = "use_c_to_interface_with_setjmp"))]
pub use asm_based::{call_with_setjmp, call_with_sigsetjmp};

#[cfg(feature = "use_c_to_interface_with_setjmp")]
mod cee_based;
#[cfg(feature = "use_c_to_interface_with_setjmp")]
pub use cee_based::{call_with_setjmp, call_with_sigsetjmp};

#[cfg(test)]
mod tests {
// longjmp never returns, and its signature reflects that. But its noisy to
Expand Down Expand Up @@ -380,31 +394,87 @@ mod tests {
assert_eq!(cinfo.sigjb_size, core::mem::size_of::<SigJmpBufStruct>());
assert_eq!(cinfo.sigjb_align, core::mem::align_of::<SigJmpBufStruct>());
}
}

#[test]
fn does_ptr_read_cause_a_double_drop() {
use std::sync::atomic::{AtomicUsize, Ordering};
struct IncrementOnDrop(&'static str, &'static AtomicUsize);
impl IncrementOnDrop {
fn new(name: &'static str, state: &'static AtomicUsize) -> Self {
println!("called new for {name}");
IncrementOnDrop(name, state)
}
#[cfg(test)]
mod tests_of_drop_interaction {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{call_with_setjmp, call_with_sigsetjmp};
struct IncrementOnDrop(&'static str, &'static AtomicUsize);
impl IncrementOnDrop {
fn new(name: &'static str, state: &'static AtomicUsize) -> Self {
println!("called new for {name}");
IncrementOnDrop(name, state)
}
impl Drop for IncrementOnDrop {
fn drop(&mut self) {
println!("called drop on {}", self.0);
self.1.fetch_add(1, Ordering::Relaxed);
}
}
impl Drop for IncrementOnDrop {
fn drop(&mut self) {
println!("called drop on {}", self.0);
self.1.fetch_add(1, Ordering::Relaxed);
}
}

#[test]
fn does_ptr_read_cause_a_double_drop_for_setjmp() {
static STATE: AtomicUsize = AtomicUsize::new(0);
let iod = IncrementOnDrop::new("iod", &STATE);
call_with_setjmp(move |_env| {
println!("at callback start: {}", iod.1.load(Ordering::Relaxed));
println!("at callback 1 start: {}", iod.1.load(Ordering::Relaxed));
let _own_it = iod;
0
});
println!("callback done, drop counter: {}", STATE.load(Ordering::Relaxed));
assert_eq!(STATE.load(Ordering::Relaxed), 1);
let iod = IncrementOnDrop::new("iod", &STATE);
call_with_setjmp(move |_env| {
println!("at callback 2 start: {}", iod.1.load(Ordering::Relaxed));
let _own_it = iod;
0
});
println!("callback done, drop counter: {}", STATE.load(Ordering::Relaxed));
assert_eq!(STATE.load(Ordering::Relaxed), 2);
}

#[test]
fn does_ptr_read_cause_a_double_drop_for_sigsetjmp() {
static STATE: AtomicUsize = AtomicUsize::new(0);
let iod = IncrementOnDrop::new("iod", &STATE);
call_with_sigsetjmp(false, move |_env| {
println!("at callback 3 start: {}", iod.1.load(Ordering::Relaxed));
let _own_it = iod;
0
});
println!("callback done, drop counter: {}", STATE.load(Ordering::Relaxed));
assert_eq!(STATE.load(Ordering::Relaxed), 1);
let iod = IncrementOnDrop::new("iod", &STATE);
call_with_sigsetjmp(true, move |_env| {
println!("at callback 4 start: {}", iod.1.load(Ordering::Relaxed));
let _own_it = iod;
0
});
println!("callback done, drop counter: {}", STATE.load(Ordering::Relaxed));
assert_eq!(STATE.load(Ordering::Relaxed), 2);
}

// FIXME: This test probably shouldn't be written this way. The intended safety property
// for calling longjmp is that there *are no* destructors waiting to run between the
// longjmp and its associated setjmp (and that we otherwise have UB).
#[test]
fn mix_drop_with_longjmp() {
use crate::longjmp;

static STATE: AtomicUsize = AtomicUsize::new(0);
// The above cases were checking that "normal" control flow,
// with no longjmp's involved, would not cause a double-drop.
// But as soon as longjmp is in the mix, we can no lonbger
// guarantee that the closure passed into call_with_setjmp will be dropped
let iod = IncrementOnDrop::new("iod", &STATE);
call_with_setjmp(move |env1| {
println!("at callback 1 start: {}", iod.1.load(Ordering::Relaxed));
let _own_it = iod;
unsafe { longjmp(env1, 4) }
});
println!("callback done, drop counter: {}", STATE.load(Ordering::Relaxed));
assert_eq!(STATE.load(Ordering::Relaxed), 0);
}
}