use std::{convert::From, fmt, marker::PhantomData, time::Duration};
use crate::{
binding::{debug, io, thread, vm},
rubysys::thread as rubysys_thread,
types::{c_void, Value},
};
#[cfg(any(unix, windows))]
use crate::types::RawFd;
use crate::{
AnyException, AnyObject, Class, Exception, Float, NilClass, Object, ProfileFrame,
VerifiedObject, IO,
};
#[derive(Debug)]
#[repr(C)]
pub struct Thread {
value: Value,
}
impl Thread {
pub fn new<F, R>(func: F) -> Self
where
F: FnMut() -> R,
R: Object,
{
Self::from(thread::create(func))
}
#[cfg(any(unix, windows))]
#[deprecated(since = "0.12.0", note = "use Thread::wait_readable")]
pub fn wait_fd(fd: RawFd) {
thread::wait_fd(fd);
}
pub fn call_without_gvl<F, R, G>(func: F, unblock_func: Option<G>) -> R
where
F: FnMut() -> R,
G: FnMut(),
{
thread::call_without_gvl(func, unblock_func)
}
pub fn call_without_gvl2<F, R, G>(func: F, unblock_func: Option<G>) -> R
where
F: FnMut() -> R,
G: FnMut(),
{
thread::call_without_gvl2(func, unblock_func)
}
pub fn call_with_gvl<F, R>(func: F) -> R
where
F: FnMut() -> R,
{
thread::call_with_gvl(func)
}
pub fn call_without_gvl_io<F, R>(func: F) -> R
where
F: FnMut() -> R,
{
thread::call_without_gvl_io(func)
}
pub fn current() -> Self {
Thread::from(thread::current())
}
pub fn main() -> Self {
Thread::from(thread::main())
}
pub fn is_alone() -> bool {
thread::is_alone()
}
pub fn pass() {
thread::schedule()
}
pub fn sleep(duration: Duration) {
thread::sleep_for(duration)
}
pub fn check_interrupts() {
thread::check_interrupts()
}
#[cfg(any(unix, windows))]
#[deprecated(since = "0.12.0", note = "use Thread::wait_writable")]
pub fn wait_fd_writable(fd: RawFd) {
thread::wait_fd_writable(fd);
}
pub fn wait_readable(io: &IO, timeout: Option<Duration>) -> Result<bool, AnyException> {
Self::wait_io(io, io::RUBY_IO_READABLE, timeout)
}
pub fn wait_writable(io: &IO, timeout: Option<Duration>) -> Result<bool, AnyException> {
Self::wait_io(io, io::RUBY_IO_WRITABLE, timeout)
}
fn wait_io(io: &IO, events: i32, timeout: Option<Duration>) -> Result<bool, AnyException> {
let io_value = io.value();
let timeout = match timeout {
Some(timeout) => Float::new(timeout.as_secs_f64()).value(),
None => NilClass::new().value(),
};
vm::protect_value(|| io::wait(io_value, events, timeout))
.map(|ready| !ready.is_false())
.map_err(AnyException::from)
}
pub fn join(&self) -> Result<Thread, AnyException> {
self.protect_send("join", &[])
.map(|thread| Thread::from(thread.value()))
}
pub fn join_value(&self) -> Result<AnyObject, AnyException> {
self.protect_send("value", &[])
}
pub fn is_alive(&self) -> bool {
unsafe { self.send("alive?", &[]) }.value().is_true()
}
pub fn kill(&self) {
thread::kill(self.value());
}
pub fn wakeup(&self) -> Result<(), AnyException> {
let thread_value = self.value();
vm::protect_value(|| thread::wakeup(thread_value))
.map(|_| ())
.map_err(AnyException::from)
}
pub fn local_get(&self, name: &str) -> AnyObject {
AnyObject::from(thread::local_get(self.value(), name))
}
pub fn local_set<T: Object>(&self, name: &str, value: T) -> AnyObject {
AnyObject::from(thread::local_set(self.value(), name, value.value()))
}
#[cfg(ruby_gte_3_3)]
pub fn profile_frames(&self, start: usize, limit: usize) -> Vec<ProfileFrame> {
ProfileFrame::from_frames(debug::profile_thread_frames(self.value(), start, limit))
}
#[cfg(ruby_gte_3_2)]
pub fn add_internal_event_hook<F>(events: u32, func: F) -> Option<InternalThreadEventHook>
where
F: Fn(&InternalThreadEvent) + Send + Sync + 'static,
{
let callback = move |flag: u32, data: *const rubysys_thread::InternalThreadEventData| {
let event = InternalThreadEvent::new(flag, data);
func(&event)
};
thread::internal_thread_add_event_hook(events, callback).map(|(hook, data)| {
InternalThreadEventHook {
remove: Box::new(move || unsafe {
thread::internal_thread_remove_event_hook(hook, data);
}),
_not_send: PhantomData,
}
})
}
#[cfg(ruby_gte_3_3)]
pub fn internal_specific(&self, key: InternalThreadSpecificKey) -> *mut c_void {
thread::internal_thread_specific_get(self.value(), key.0)
}
#[cfg(ruby_gte_3_3)]
pub fn set_internal_specific(&self, key: InternalThreadSpecificKey, data: *mut c_void) {
thread::internal_thread_specific_set(self.value(), key.0, data)
}
}
#[cfg(ruby_gte_3_2)]
#[derive(Debug, Clone, Copy)]
pub struct InternalThreadEvent {
flag: u32,
#[cfg(ruby_gte_3_3)]
thread: Value,
}
#[cfg(ruby_gte_3_2)]
impl InternalThreadEvent {
pub const STARTED: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_STARTED;
pub const READY: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_READY;
pub const RESUMED: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_RESUMED;
pub const SUSPENDED: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_SUSPENDED;
pub const EXITED: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_EXITED;
pub const ALL: u32 = rubysys_thread::RUBY_INTERNAL_THREAD_EVENT_MASK;
fn new(flag: u32, data: *const rubysys_thread::InternalThreadEventData) -> Self {
#[cfg(not(ruby_gte_3_3))]
let _ = data;
InternalThreadEvent {
flag,
#[cfg(ruby_gte_3_3)]
thread: unsafe { (*data).thread },
}
}
pub fn flag(&self) -> u32 {
self.flag
}
#[cfg(ruby_gte_3_3)]
pub fn thread(&self) -> Thread {
Thread::from(self.thread)
}
}
#[cfg(ruby_gte_3_2)]
pub struct InternalThreadEventHook {
remove: Box<dyn FnMut()>,
_not_send: PhantomData<*mut ()>,
}
#[cfg(ruby_gte_3_2)]
impl Drop for InternalThreadEventHook {
fn drop(&mut self) {
(self.remove)()
}
}
#[cfg(ruby_gte_3_2)]
impl fmt::Debug for InternalThreadEventHook {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("InternalThreadEventHook")
}
}
#[cfg(ruby_gte_3_3)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InternalThreadSpecificKey(rubysys_thread::InternalThreadSpecificKey);
#[cfg(ruby_gte_3_3)]
impl InternalThreadSpecificKey {
pub fn new() -> Result<Self, AnyException> {
let mut key = 0;
let created = vm::protect_value(|| {
key = thread::internal_thread_specific_key_create();
NilClass::new().value()
});
match created {
Err(exception) => Err(AnyException::from(exception)),
Ok(_) if key >= rubysys_thread::RB_INTERNAL_THREAD_SPECIFIC_KEY_MAX => Err(
AnyException::new("ThreadError", Some("too many thread specific keys")),
),
Ok(_) => Ok(InternalThreadSpecificKey(key)),
}
}
}
impl From<Value> for Thread {
fn from(value: Value) -> Self {
Thread { value }
}
}
impl Into<Value> for Thread {
fn into(self) -> Value {
self.value
}
}
impl Into<AnyObject> for Thread {
fn into(self) -> AnyObject {
AnyObject::from(self.value)
}
}
impl Object for Thread {
#[inline]
fn value(&self) -> Value {
self.value
}
}
impl VerifiedObject for Thread {
fn is_correct_type<T: Object>(object: &T) -> bool {
object.class() == Class::thread()
}
fn error_message() -> &'static str {
"Error converting to Thread"
}
}
impl PartialEq for Thread {
fn eq(&self, other: &Self) -> bool {
self.equals(other)
}
}
#[cfg(test)]
mod tests {
use crate::{Array, Exception, Fixnum, Object, RString, Thread, IO, VM};
use std::time::{Duration, Instant};
#[test]
fn test_thread_management() {
crate::on_ruby_thread(|| {
assert!(Thread::current() == Thread::main());
assert!(Thread::current().is_alive());
let worker = Thread::new(|| Fixnum::new(21 * 2));
assert_eq!(
worker.join_value().unwrap().try_convert_to::<Fixnum>(),
Ok(Fixnum::new(42))
);
assert!(!worker.is_alive());
assert!(worker.join().is_ok());
let failing = VM::eval(
"Thread.new { Thread.current.report_on_exception = false; raise ArgumentError, 'bad' }",
)
.unwrap()
.try_convert_to::<Thread>()
.unwrap();
assert_eq!(failing.join_value().unwrap_err().message(), "bad");
let sleeper = VM::eval("Thread.new { sleep }")
.unwrap()
.try_convert_to::<Thread>()
.unwrap();
sleeper.kill();
sleeper.join().unwrap();
assert!(!sleeper.is_alive());
assert!(sleeper.wakeup().is_err());
let current = Thread::current();
current.local_set("rutie_thread_test", RString::new_utf8("local"));
assert_eq!(
current
.local_get("rutie_thread_test")
.try_convert_to::<RString>()
.unwrap()
.to_str(),
"local"
);
let start = Instant::now();
Thread::sleep(Duration::from_millis(5));
assert!(start.elapsed() >= Duration::from_millis(4));
Thread::pass();
Thread::check_interrupts();
assert!(Thread::is_alone());
assert_eq!(Thread::call_without_gvl_io(|| 2 + 2), 4);
});
}
#[test]
fn test_gvl_release_and_fd_waits() {
crate::on_ruby_thread(|| {
let sum = Thread::call_without_gvl(|| (1..=10u64).sum::<u64>(), Some(|| {}));
assert_eq!(sum, 55);
let product = Thread::call_without_gvl2(|| 6 * 7, None::<fn()>);
assert_eq!(product, 42);
let text = Thread::call_without_gvl(
|| Thread::call_with_gvl(|| RString::new_utf8("with gvl").to_string()),
None::<fn()>,
);
assert_eq!(text, "with gvl");
let fds = VM::eval("r, w = IO.pipe; $rutie_fd_pipe = [r, w]; [r.fileno, w.fileno]")
.unwrap()
.try_convert_to::<crate::Array>()
.unwrap();
let fd = |i| {
fds.at(i)
.try_convert_to::<crate::Fixnum>()
.unwrap()
.to_i32()
};
#[allow(deprecated)]
Thread::wait_fd_writable(fd(1));
VM::eval("$rutie_fd_pipe[1].write('x')").unwrap();
#[allow(deprecated)]
Thread::wait_fd(fd(0));
VM::eval("$rutie_fd_pipe.each(&:close); $rutie_fd_pipe = nil").unwrap();
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
let (mut writer, reader) = UnixStream::pair().unwrap();
#[allow(deprecated)]
Thread::wait_fd_writable(writer.as_raw_fd());
writer.write_all(b"x").unwrap();
#[allow(deprecated)]
Thread::wait_fd(reader.as_raw_fd());
}
});
}
#[test]
fn test_wait_readable_and_writable() {
crate::on_ruby_thread(|| {
let pipe = Array::from(VM::eval("IO.pipe").unwrap().value());
let reader = pipe.at(0).try_convert_to::<IO>().unwrap();
let writer = pipe.at(1).try_convert_to::<IO>().unwrap();
let short = Some(Duration::from_millis(5));
assert_eq!(Thread::wait_readable(&reader, short), Ok(false));
assert_eq!(Thread::wait_writable(&writer, short), Ok(true));
writer.write(&RString::new_utf8("x")).unwrap();
assert_eq!(Thread::wait_readable(&reader, short), Ok(true));
assert_eq!(Thread::wait_readable(&reader, None), Ok(true));
reader.close().unwrap();
writer.close().unwrap();
let error = Thread::wait_readable(&reader, None).unwrap_err();
assert_eq!(error.class().name().unwrap().to_string(), "IOError");
assert!(Thread::wait_writable(&writer, short).is_err());
});
}
#[cfg(ruby_gte_3_2)]
#[test]
fn test_internal_event_hook() {
use crate::InternalThreadEvent;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
crate::on_ruby_thread(|| {
let counts: Arc<Vec<AtomicUsize>> =
Arc::new((0..5).map(|_| AtomicUsize::new(0)).collect());
let count = |i: usize| counts[i].load(Ordering::SeqCst);
let odd_flags = Arc::new(AtomicUsize::new(0));
let panicked = Arc::new(std::sync::atomic::AtomicBool::new(false));
let hook = {
let (counts, odd_flags, panicked) =
(counts.clone(), odd_flags.clone(), panicked.clone());
Thread::add_internal_event_hook(InternalThreadEvent::ALL, move |event| {
let flag = event.flag();
if flag.count_ones() != 1 || flag > InternalThreadEvent::EXITED {
odd_flags.fetch_add(1, Ordering::SeqCst);
return;
}
counts[flag.trailing_zeros() as usize].fetch_add(1, Ordering::SeqCst);
if flag == InternalThreadEvent::EXITED && !panicked.swap(true, Ordering::SeqCst)
{
panic!("a panic in a thread event hook is ignored");
}
})
};
VM::eval("2.times.map { Thread.new { Thread.pass; 1 } }.each(&:join)").unwrap();
VM::eval("Thread.new {}.join").unwrap();
if cfg!(windows) {
assert!(hook.is_none());
assert_eq!(Arc::strong_count(&counts), 1);
return;
}
let hook = hook.unwrap();
assert_eq!(odd_flags.load(Ordering::SeqCst), 0);
assert_eq!(count(0), 3, "started");
assert!(count(1) >= 3, "ready");
assert!(count(2) >= 3, "resumed");
assert!(count(3) >= 3, "suspended");
assert_eq!(format!("{:?}", hook), "InternalThreadEventHook");
let second_calls = Arc::new(AtomicUsize::new(0));
let second = {
let second_calls = second_calls.clone();
Thread::add_internal_event_hook(InternalThreadEvent::STARTED, move |_| {
second_calls.fetch_add(1, Ordering::SeqCst);
})
.unwrap()
};
VM::eval("Thread.new {}.join").unwrap();
assert_eq!(second_calls.load(Ordering::SeqCst), 1);
assert_eq!(count(0), 4);
drop(second);
assert_eq!(Arc::strong_count(&second_calls), 1);
VM::eval("Thread.new {}.join").unwrap();
assert_eq!(second_calls.load(Ordering::SeqCst), 1);
assert_eq!(count(0), 5);
drop(hook);
assert_eq!(Arc::strong_count(&counts), 1);
VM::eval("Thread.new {}.join").unwrap();
assert_eq!(count(0), 5);
});
}
#[cfg(ruby_gte_3_3)]
#[test]
fn test_internal_specific_and_event_thread() {
use crate::{InternalThreadEvent, InternalThreadSpecificKey};
use std::sync::{Arc, Mutex};
crate::on_ruby_thread(|| {
let key = InternalThreadSpecificKey::new().unwrap();
let main = Thread::current();
assert!(main.internal_specific(key).is_null());
main.set_internal_specific(key, 42 as *mut crate::types::c_void);
assert_eq!(main.internal_specific(key) as usize, 42);
let seen = Arc::new(Mutex::new(Vec::new()));
let hook = {
let seen = seen.clone();
Thread::add_internal_event_hook(InternalThreadEvent::RESUMED, move |event| {
let thread = event.thread();
seen.lock()
.unwrap()
.push((thread.value(), thread.internal_specific(key) as usize));
})
};
let worker = VM::eval("q = $rutie_specific_queue = Queue.new; Thread.new { q.pop }")
.unwrap()
.try_convert_to::<Thread>()
.unwrap();
worker.set_internal_specific(key, 7 as *mut crate::types::c_void);
VM::eval("$rutie_specific_queue << 1; $rutie_specific_queue = nil").unwrap();
worker.join().unwrap();
drop(hook);
assert_eq!(worker.internal_specific(key) as usize, 7);
assert_eq!(main.internal_specific(key) as usize, 42);
main.set_internal_specific(key, std::ptr::null_mut());
if cfg!(not(windows)) {
let seen = seen.lock().unwrap();
assert!(seen.contains(&(main.value(), 42)));
assert!(seen.iter().any(|&(thread, _)| thread == worker.value()));
}
});
}
#[cfg(ruby_gte_3_3)]
#[test]
fn test_profile_frames_of_another_thread() {
crate::on_ruby_thread(|| {
let worker = VM::eval(
"$rutie_profile_queue = Queue.new
t = Thread.new { def rutie_parked(q) = q.pop; rutie_parked($rutie_profile_queue) }
Thread.pass until t.status == 'sleep'
t",
)
.unwrap()
.try_convert_to::<Thread>()
.unwrap();
let frames = worker.profile_frames(0, 10);
let labels: Vec<String> = frames
.iter()
.map(|frame| frame.full_label().unwrap().to_string())
.collect();
assert_eq!(labels[0], "Thread::Queue#pop");
assert_eq!(labels[1], "Object#rutie_parked");
assert!(frames[1].line() > 0);
assert_eq!(
worker.profile_frames(1, 1)[0]
.full_label()
.unwrap()
.to_str(),
"Object#rutie_parked"
);
assert!(worker.profile_frames(0, 0).is_empty());
VM::eval("$rutie_profile_queue << 1; $rutie_profile_queue = nil").unwrap();
worker.join().unwrap();
assert!(worker.profile_frames(0, 10).is_empty());
});
}
}