143 lines
5.3 KiB
Rust
143 lines
5.3 KiB
Rust
|
|
use libc::c_void;
|
||
|
|
use std::io;
|
||
|
|
use std::ptr;
|
||
|
|
use windows_sys::Win32::Foundation::BOOL;
|
||
|
|
use windows_sys::Win32::System::Memory::VirtualQuery;
|
||
|
|
use windows_sys::Win32::System::Threading::{
|
||
|
|
ConvertFiberToThread, ConvertThreadToFiber, CreateFiber, DeleteFiber, IsThreadAFiber,
|
||
|
|
SetThreadStackGuarantee, SwitchToFiber,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Make sure the libstacker.a (implemented in C) is linked.
|
||
|
|
// See https://github.com/rust-lang/rust/issues/65610
|
||
|
|
#[link(name = "stacker")]
|
||
|
|
extern "C" {
|
||
|
|
fn __stacker_get_current_fiber() -> *mut c_void;
|
||
|
|
}
|
||
|
|
|
||
|
|
struct FiberInfo<F> {
|
||
|
|
callback: std::mem::MaybeUninit<F>,
|
||
|
|
panic: Option<Box<dyn std::any::Any + Send + 'static>>,
|
||
|
|
parent_fiber: *mut c_void,
|
||
|
|
}
|
||
|
|
|
||
|
|
unsafe extern "system" fn fiber_proc<F: FnOnce()>(data: *mut c_void) {
|
||
|
|
// This function is the entry point to our inner fiber, and as argument we get an
|
||
|
|
// instance of `FiberInfo`. We will set-up the "runtime" for the callback and execute
|
||
|
|
// it.
|
||
|
|
let data = &mut *(data as *mut FiberInfo<F>);
|
||
|
|
let old_stack_limit = crate::get_stack_limit();
|
||
|
|
crate::set_stack_limit(guess_os_stack_limit());
|
||
|
|
let callback = data.callback.as_ptr();
|
||
|
|
data.panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback.read())).err();
|
||
|
|
|
||
|
|
// Restore to the previous Fiber
|
||
|
|
crate::set_stack_limit(old_stack_limit);
|
||
|
|
SwitchToFiber(data.parent_fiber);
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn _grow(stack_size: usize, callback: &mut dyn FnMut()) {
|
||
|
|
// Fibers (or stackful coroutines) is the only official way to create new stacks on the
|
||
|
|
// same thread on Windows. So in order to extend the stack we create fiber and switch
|
||
|
|
// to it so we can use it's stack. After running `callback` within our fiber, we switch
|
||
|
|
// back to the current stack and destroy the fiber and its associated stack.
|
||
|
|
unsafe {
|
||
|
|
let was_fiber = IsThreadAFiber() == 1 as BOOL;
|
||
|
|
let mut data = FiberInfo {
|
||
|
|
callback: std::mem::MaybeUninit::new(callback),
|
||
|
|
panic: None,
|
||
|
|
parent_fiber: {
|
||
|
|
if was_fiber {
|
||
|
|
// Get a handle to the current fiber. We need to use a C implementation
|
||
|
|
// for this as GetCurrentFiber is an header only function.
|
||
|
|
__stacker_get_current_fiber()
|
||
|
|
} else {
|
||
|
|
// Convert the current thread to a fiber, so we are able to switch back
|
||
|
|
// to the current stack. Threads coverted to fibers still act like
|
||
|
|
// regular threads, but they have associated fiber data. We later
|
||
|
|
// convert it back to a regular thread and free the fiber data.
|
||
|
|
ConvertThreadToFiber(ptr::null_mut())
|
||
|
|
}
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
if data.parent_fiber.is_null() {
|
||
|
|
panic!(
|
||
|
|
"unable to convert thread to fiber: {}",
|
||
|
|
io::Error::last_os_error()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
let fiber = CreateFiber(
|
||
|
|
stack_size as usize,
|
||
|
|
Some(fiber_proc::<&mut dyn FnMut()>),
|
||
|
|
&mut data as *mut FiberInfo<&mut dyn FnMut()> as *mut _,
|
||
|
|
);
|
||
|
|
if fiber.is_null() {
|
||
|
|
panic!("unable to allocate fiber: {}", io::Error::last_os_error());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Switch to the fiber we created. This changes stacks and starts executing
|
||
|
|
// fiber_proc on it. fiber_proc will run `callback` and then switch back to run the
|
||
|
|
// next statement.
|
||
|
|
SwitchToFiber(fiber);
|
||
|
|
DeleteFiber(fiber);
|
||
|
|
|
||
|
|
// Clean-up.
|
||
|
|
if !was_fiber && ConvertFiberToThread() == 0 {
|
||
|
|
// FIXME: Perhaps should not panic here?
|
||
|
|
panic!(
|
||
|
|
"unable to convert back to thread: {}",
|
||
|
|
io::Error::last_os_error()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(p) = data.panic {
|
||
|
|
std::panic::resume_unwind(p);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[inline(always)]
|
||
|
|
fn get_thread_stack_guarantee() -> Option<usize> {
|
||
|
|
let min_guarantee = if cfg!(target_pointer_width = "32") {
|
||
|
|
0x1000
|
||
|
|
} else {
|
||
|
|
0x2000
|
||
|
|
};
|
||
|
|
let mut stack_guarantee = 0;
|
||
|
|
unsafe {
|
||
|
|
// Read the current thread stack guarantee
|
||
|
|
// This is the stack reserved for stack overflow
|
||
|
|
// exception handling.
|
||
|
|
// This doesn't return the true value so we need
|
||
|
|
// some further logic to calculate the real stack
|
||
|
|
// guarantee. This logic is what is used on x86-32 and
|
||
|
|
// x86-64 Windows 10. Other versions and platforms may differ
|
||
|
|
let ret = SetThreadStackGuarantee(&mut stack_guarantee);
|
||
|
|
if ret == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
Some(std::cmp::max(stack_guarantee, min_guarantee) as usize + 0x1000)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[inline(always)]
|
||
|
|
pub unsafe fn guess_os_stack_limit() -> Option<usize> {
|
||
|
|
// Query the allocation which contains our stack pointer in order
|
||
|
|
// to discover the size of the stack
|
||
|
|
//
|
||
|
|
// FIXME: we could read stack base from the TIB, specifically the 3rd element of it.
|
||
|
|
type QueryT = windows_sys::Win32::System::Memory::MEMORY_BASIC_INFORMATION;
|
||
|
|
let mut mi = std::mem::MaybeUninit::<QueryT>::uninit();
|
||
|
|
let res = VirtualQuery(
|
||
|
|
psm::stack_pointer() as *const _,
|
||
|
|
mi.as_mut_ptr(),
|
||
|
|
std::mem::size_of::<QueryT>() as usize,
|
||
|
|
);
|
||
|
|
if res == 0 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(mi.assume_init().AllocationBase as usize + get_thread_stack_guarantee()? + 0x1000)
|
||
|
|
}
|