From 0c7c93b8e83544abc7eef5abd76526e5c49882f5 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 4 Jun 2014 10:54:35 -0700 Subject: [PATCH] std: Improve non-task-based usage A few notable improvements were implemented to cut down on the number of aborts triggered by the standard library when a local task is not found. * Primarily, the unwinding functionality was restructured to support an unsafe top-level function, `try`. This function invokes a closure, capturing any failure which occurs inside of it. The purpose of this function is to be as lightweight of a "try block" as possible for rust, intended for use when the runtime is difficult to set up. This function is *not* meant to be used by normal rust code, nor should it be consider for use with normal rust code. * When invoking spawn(), a `fail!()` is triggered rather than an abort. * When invoking LocalIo::borrow(), which is transitively called by all I/O constructors, None is returned rather than aborting to indicate that there is no local I/O implementation. * Invoking get() on a TLD key will return None if no task is available * Invoking replace() on a TLD key will fail if no task is available. A test case was also added showing the variety of things that you can do without a runtime or task set up now. In general, this is just a refactoring to abort less quickly in the standard library when a local task is not found. --- src/libstd/local_data.rs | 20 +- src/libstd/rt/rtio.rs | 5 +- src/libstd/rt/unwind.rs | 354 +++++++++++-------- src/libstd/task.rs | 5 +- src/test/run-pass/running-with-no-runtime.rs | 59 ++++ 5 files changed, 282 insertions(+), 161 deletions(-) create mode 100644 src/test/run-pass/running-with-no-runtime.rs diff --git a/src/libstd/local_data.rs b/src/libstd/local_data.rs index 7bf0b19407f..930d1df02f1 100644 --- a/src/libstd/local_data.rs +++ b/src/libstd/local_data.rs @@ -96,22 +96,24 @@ pub type Map = Vec>; type TLSValue = Box; // Gets the map from the runtime. Lazily initialises if not done so already. -unsafe fn get_local_map() -> &mut Map { +unsafe fn get_local_map() -> Option<&mut Map> { use rt::local::Local; + if !Local::exists(None::) { return None } + let task: *mut Task = Local::unsafe_borrow(); match &mut (*task).storage { // If the at_exit function is already set, then we just need to take // a loan out on the TLS map stored inside &LocalStorage(Some(ref mut map_ptr)) => { - return map_ptr; + return Some(map_ptr); } // If this is the first time we've accessed TLS, perform similar // actions to the oldsched way of doing things. &LocalStorage(ref mut slot) => { *slot = Some(vec!()); match *slot { - Some(ref mut map_ptr) => { return map_ptr } + Some(ref mut map_ptr) => { return Some(map_ptr) } None => unreachable!(), } } @@ -156,7 +158,10 @@ impl KeyValue { /// assert_eq!(foo.replace(None), Some(4)); /// ``` pub fn replace(&'static self, data: Option) -> Option { - let map = unsafe { get_local_map() }; + let map = match unsafe { get_local_map() } { + Some(map) => map, + None => fail!("must have a local task to insert into TLD"), + }; let keyval = key_to_key_value(self); // When the task-local map is destroyed, all the data needs to be @@ -223,7 +228,10 @@ impl KeyValue { /// assert_eq!(*key.get().unwrap(), 3); /// ``` pub fn get(&'static self) -> Option> { - let map = unsafe { get_local_map() }; + let map = match unsafe { get_local_map() } { + Some(map) => map, + None => return None, + }; self.find(map).map(|(pos, data, loan)| { *loan += 1; @@ -260,7 +268,7 @@ impl Deref for Ref { #[unsafe_destructor] impl Drop for Ref { fn drop(&mut self) { - let map = unsafe { get_local_map() }; + let map = unsafe { get_local_map().unwrap() }; let (_, _, ref mut loan) = *map.get_mut(self._index).get_mut_ref(); *loan -= 1; diff --git a/src/libstd/rt/rtio.rs b/src/libstd/rt/rtio.rs index a6c60df2642..06db465f7ee 100644 --- a/src/libstd/rt/rtio.rs +++ b/src/libstd/rt/rtio.rs @@ -171,7 +171,10 @@ impl<'a> LocalIo<'a> { // // In order to get around this, we just transmute a copy out of the task // in order to have what is likely a static lifetime (bad). - let mut t: Box = Local::take(); + let mut t: Box = match Local::try_take() { + Some(t) => t, + None => return None, + }; let ret = t.local_io().map(|t| { unsafe { mem::transmute_copy(&t) } }); diff --git a/src/libstd/rt/unwind.rs b/src/libstd/rt/unwind.rs index dc2646102d2..c455648992e 100644 --- a/src/libstd/rt/unwind.rs +++ b/src/libstd/rt/unwind.rs @@ -66,7 +66,7 @@ use option::{Some, None, Option}; use owned::Box; use prelude::drop; use ptr::RawPtr; -use result::{Err, Ok}; +use result::{Err, Ok, Result}; use rt::backtrace; use rt::local::Local; use rt::task::Task; @@ -81,6 +81,11 @@ pub struct Unwinder { cause: Option> } +struct Exception { + uwe: uw::_Unwind_Exception, + cause: Option>, +} + impl Unwinder { pub fn new() -> Unwinder { Unwinder { @@ -94,71 +99,7 @@ impl Unwinder { } pub fn try(&mut self, f: ||) { - use raw::Closure; - use libc::{c_void}; - - unsafe { - let closure: Closure = mem::transmute(f); - let ep = rust_try(try_fn, closure.code as *c_void, - closure.env as *c_void); - if !ep.is_null() { - rtdebug!("caught {}", (*ep).exception_class); - uw::_Unwind_DeleteException(ep); - } - } - - extern fn try_fn(code: *c_void, env: *c_void) { - unsafe { - let closure: || = mem::transmute(Closure { - code: code as *(), - env: env as *(), - }); - closure(); - } - } - - extern { - // Rust's try-catch - // When f(...) returns normally, the return value is null. - // When f(...) throws, the return value is a pointer to the caught - // exception object. - fn rust_try(f: extern "C" fn(*c_void, *c_void), - code: *c_void, - data: *c_void) -> *uw::_Unwind_Exception; - } - } - - pub fn begin_unwind(&mut self, cause: Box) -> ! { - rtdebug!("begin_unwind()"); - - self.unwinding = true; - self.cause = Some(cause); - - rust_fail(); - - // An uninlined, unmangled function upon which to slap yer breakpoints - #[inline(never)] - #[no_mangle] - fn rust_fail() -> ! { - unsafe { - let exception = box uw::_Unwind_Exception { - exception_class: rust_exception_class(), - exception_cleanup: exception_cleanup, - private: [0, ..uw::unwinder_private_data_size], - }; - let error = uw::_Unwind_RaiseException(mem::transmute(exception)); - rtabort!("Could not unwind stack, error = {}", error as int) - } - - extern "C" fn exception_cleanup(_unwind_code: uw::_Unwind_Reason_Code, - exception: *uw::_Unwind_Exception) { - rtdebug!("exception_cleanup()"); - unsafe { - let _: Box = - mem::transmute(exception); - } - } - } + self.cause = unsafe { try(f) }.err(); } pub fn result(&mut self) -> TaskResult { @@ -170,6 +111,92 @@ impl Unwinder { } } +/// Invoke a closure, capturing the cause of failure if one occurs. +/// +/// This function will return `None` if the closure did not fail, and will +/// return `Some(cause)` if the closure fails. The `cause` returned is the +/// object with which failure was originally invoked. +/// +/// This function also is unsafe for a variety of reasons: +/// +/// * This is not safe to call in a nested fashion. The unwinding +/// interface for Rust is designed to have at most one try/catch block per +/// task, not multiple. No runtime checking is currently performed to uphold +/// this invariant, so this function is not safe. A nested try/catch block +/// may result in corruption of the outer try/catch block's state, especially +/// if this is used within a task itself. +/// +/// * It is not sound to trigger unwinding while already unwinding. Rust tasks +/// have runtime checks in place to ensure this invariant, but it is not +/// guaranteed that a rust task is in place when invoking this function. +/// Unwinding twice can lead to resource leaks where some destructors are not +/// run. +pub unsafe fn try(f: ||) -> Result<(), Box> { + use raw::Closure; + use libc::{c_void}; + + let closure: Closure = mem::transmute(f); + let ep = rust_try(try_fn, closure.code as *c_void, + closure.env as *c_void); + return if ep.is_null() { + Ok(()) + } else { + let my_ep = ep as *mut Exception; + rtdebug!("caught {}", (*my_ep).uwe.exception_class); + let cause = (*my_ep).cause.take(); + uw::_Unwind_DeleteException(ep); + Err(cause.unwrap()) + }; + + extern fn try_fn(code: *c_void, env: *c_void) { + unsafe { + let closure: || = mem::transmute(Closure { + code: code as *(), + env: env as *(), + }); + closure(); + } + } + + extern { + // Rust's try-catch + // When f(...) returns normally, the return value is null. + // When f(...) throws, the return value is a pointer to the caught + // exception object. + fn rust_try(f: extern "C" fn(*c_void, *c_void), + code: *c_void, + data: *c_void) -> *uw::_Unwind_Exception; + } +} + +// An uninlined, unmangled function upon which to slap yer breakpoints +#[inline(never)] +#[no_mangle] +fn rust_fail(cause: Box) -> ! { + rtdebug!("begin_unwind()"); + + unsafe { + let exception = box Exception { + uwe: uw::_Unwind_Exception { + exception_class: rust_exception_class(), + exception_cleanup: exception_cleanup, + private: [0, ..uw::unwinder_private_data_size], + }, + cause: Some(cause), + }; + let error = uw::_Unwind_RaiseException(mem::transmute(exception)); + rtabort!("Could not unwind stack, error = {}", error as int) + } + + extern fn exception_cleanup(_unwind_code: uw::_Unwind_Reason_Code, + exception: *uw::_Unwind_Exception) { + rtdebug!("exception_cleanup()"); + unsafe { + let _: Box = mem::transmute(exception); + } + } +} + // Rust's exception class identifier. This is used by personality routines to // determine whether the exception was thrown by their own runtime. fn rust_exception_class() -> uw::_Unwind_Exception_Class { @@ -346,103 +373,124 @@ pub fn begin_unwind(msg: M, file: &'static str, line: uint) -> ! fn begin_unwind_inner(msg: Box, file: &'static str, line: uint) -> ! { - let mut task; - { - let msg_s = match msg.as_ref::<&'static str>() { - Some(s) => *s, - None => match msg.as_ref::() { - Some(s) => s.as_slice(), - None => "Box", - } - }; + // First up, print the message that we're failing + print_failure(msg, file, line); - // It is assumed that all reasonable rust code will have a local task at - // all times. This means that this `try_take` will succeed almost all of - // the time. There are border cases, however, when the runtime has - // *almost* set up the local task, but hasn't quite gotten there yet. In - // order to get some better diagnostics, we print on failure and - // immediately abort the whole process if there is no local task - // available. - let opt_task: Option> = Local::try_take(); - task = match opt_task { - Some(t) => t, - None => { - rterrln!("failed at '{}', {}:{}", msg_s, file, line); - if backtrace::log_enabled() { - let mut err = ::rt::util::Stderr; - let _err = backtrace::write(&mut err); - } else { - rterrln!("run with `RUST_BACKTRACE=1` to see a backtrace"); - } - unsafe { intrinsics::abort() } - } - }; - - // See comments in io::stdio::with_task_stdout as to why we have to be - // careful when using an arbitrary I/O handle from the task. We - // essentially need to dance to make sure when a task is in TLS when - // running user code. - let name = task.name.take(); - { - let n = name.as_ref().map(|n| n.as_slice()).unwrap_or(""); - - match task.stderr.take() { - Some(mut stderr) => { - Local::put(task); - // FIXME: what to do when the task printing fails? - let _err = write!(stderr, - "task '{}' failed at '{}', {}:{}\n", - n, msg_s, file, line); - if backtrace::log_enabled() { - let _err = backtrace::write(stderr); - } - task = Local::take(); - - match mem::replace(&mut task.stderr, Some(stderr)) { - Some(prev) => { - Local::put(task); - drop(prev); - task = Local::take(); - } - None => {} - } - } - None => { - rterrln!("task '{}' failed at '{}', {}:{}", n, msg_s, - file, line); - if backtrace::log_enabled() { - let mut err = ::rt::util::Stderr; - let _err = backtrace::write(&mut err); - } - } - } - } - task.name = name; - - if task.unwinder.unwinding { + let opt_task: Option> = Local::try_take(); + match opt_task { + Some(mut task) => { + // Now that we've printed why we're failing, do a check + // to make sure that we're not double failing. + // // If a task fails while it's already unwinding then we // have limited options. Currently our preference is to // just abort. In the future we may consider resuming // unwinding or otherwise exiting the task cleanly. - rterrln!("task failed during unwinding (double-failure - total drag!)") - rterrln!("rust must abort now. so sorry."); + if task.unwinder.unwinding { + rterrln!("task failed during unwinding (double-failure - \ + total drag!)") + rterrln!("rust must abort now. so sorry."); - // Don't print the backtrace twice (it would have already been - // printed if logging was enabled). - if !backtrace::log_enabled() { + // Don't print the backtrace twice (it would have already been + // printed if logging was enabled). + if !backtrace::log_enabled() { + let mut err = ::rt::util::Stderr; + let _err = backtrace::write(&mut err); + } + unsafe { intrinsics::abort() } + } + + // Finally, we've printed our failure and figured out we're not in a + // double failure, so flag that we've started to unwind and then + // actually unwind. Be sure that the task is in TLS so destructors + // can do fun things like I/O. + task.unwinder.unwinding = true; + Local::put(task); + } + None => {} + } + rust_fail(msg) +} + +/// Given a failure message and the location that it occurred, prints the +/// message to the local task's appropriate stream. +/// +/// This function currently handles three cases: +/// +/// - There is no local task available. In this case the error is printed to +/// stderr. +/// - There is a local task available, but it does not have a stderr handle. +/// In this case the message is also printed to stderr. +/// - There is a local task available, and it has a stderr handle. The +/// message is printed to the handle given in this case. +fn print_failure(msg: &Any:Send, file: &str, line: uint) { + let msg = match msg.as_ref::<&'static str>() { + Some(s) => *s, + None => match msg.as_ref::() { + Some(s) => s.as_slice(), + None => "Box", + } + }; + + // It is assumed that all reasonable rust code will have a local task at + // all times. This means that this `try_take` will succeed almost all of + // the time. There are border cases, however, when the runtime has + // *almost* set up the local task, but hasn't quite gotten there yet. In + // order to get some better diagnostics, we print on failure and + // immediately abort the whole process if there is no local task + // available. + let mut task: Box = match Local::try_take() { + Some(t) => t, + None => { + rterrln!("failed at '{}', {}:{}", msg, file, line); + if backtrace::log_enabled() { let mut err = ::rt::util::Stderr; let _err = backtrace::write(&mut err); + } else { + rterrln!("run with `RUST_BACKTRACE=1` to see a backtrace"); + } + return + } + }; + + // See comments in io::stdio::with_task_stdout as to why we have to be + // careful when using an arbitrary I/O handle from the task. We + // essentially need to dance to make sure when a task is in TLS when + // running user code. + let name = task.name.take(); + { + let n = name.as_ref().map(|n| n.as_slice()).unwrap_or(""); + + match task.stderr.take() { + Some(mut stderr) => { + Local::put(task); + // FIXME: what to do when the task printing fails? + let _err = write!(stderr, + "task '{}' failed at '{}', {}:{}\n", + n, msg, file, line); + if backtrace::log_enabled() { + let _err = backtrace::write(stderr); + } + task = Local::take(); + + match mem::replace(&mut task.stderr, Some(stderr)) { + Some(prev) => { + Local::put(task); + drop(prev); + task = Local::take(); + } + None => {} + } + } + None => { + rterrln!("task '{}' failed at '{}', {}:{}", n, msg, file, line); + if backtrace::log_enabled() { + let mut err = ::rt::util::Stderr; + let _err = backtrace::write(&mut err); + } } - unsafe { intrinsics::abort() } } } - - // The unwinder won't actually use the task at all, so we put the task back - // into TLS right before we invoke the unwinder, but this means we need an - // unsafe reference back to the unwinder once it's in TLS. + task.name = name; Local::put(task); - unsafe { - let task: *mut Task = Local::unsafe_borrow(); - (*task).unwinder.begin_unwind(msg); - } } diff --git a/src/libstd/task.rs b/src/libstd/task.rs index 4824a956107..3b573b87574 100644 --- a/src/libstd/task.rs +++ b/src/libstd/task.rs @@ -176,7 +176,10 @@ impl TaskBuilder { Some(gen) => gen(f), None => f }; - let t: Box = Local::take(); + let t: Box = match Local::try_take() { + Some(t) => t, + None => fail!("need a local task to spawn a new task"), + }; t.spawn_sibling(self.opts, f); } diff --git a/src/test/run-pass/running-with-no-runtime.rs b/src/test/run-pass/running-with-no-runtime.rs new file mode 100644 index 00000000000..1fe99336bc9 --- /dev/null +++ b/src/test/run-pass/running-with-no-runtime.rs @@ -0,0 +1,59 @@ +// Copyright 2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +extern crate native; + +use std::io::process::{Command, ProcessOutput}; +use std::os; +use std::str; +use std::rt::unwind::try; + +local_data_key!(foo: int) + +#[start] +fn start(argc: int, argv: **u8) -> int { + if argc > 1 { + unsafe { + match **argv.offset(1) { + 1 => {} + 2 => println!("foo"), + 3 => assert!(try(|| {}).is_ok()), + 4 => assert!(try(|| fail!()).is_err()), + 5 => assert!(try(|| spawn(proc() {})).is_err()), + 6 => assert!(Command::new("test").spawn().is_err()), + 7 => assert!(foo.get().is_some()), + 8 => assert!(try(|| { foo.replace(Some(3)); }).is_err()), + _ => fail!() + } + } + return 0 + } + + native::start(argc, argv, main) +} + +fn main() { + let args = os::args(); + let me = args.get(0).as_slice(); + + pass(Command::new(me).arg(&[1u8]).output().unwrap()); + pass(Command::new(me).arg(&[2u8]).output().unwrap()); + pass(Command::new(me).arg(&[3u8]).output().unwrap()); + pass(Command::new(me).arg(&[4u8]).output().unwrap()); + pass(Command::new(me).arg(&[5u8]).output().unwrap()); + pass(Command::new(me).arg(&[6u8]).output().unwrap()); +} + +fn pass(output: ProcessOutput) { + if !output.status.success() { + println!("{}", str::from_utf8(output.output.as_slice())); + println!("{}", str::from_utf8(output.error.as_slice())); + } +}