auto merge of #14644 : alexcrichton/rust/more-no-runtime-use-cases, r=brson

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.

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.
This commit is contained in:
bors 2014-06-05 08:26:51 -07:00
commit 57e7147f3e
5 changed files with 282 additions and 161 deletions

View File

@ -96,22 +96,24 @@ pub type Map = Vec<Option<(*u8, TLSValue, uint)>>;
type TLSValue = Box<LocalData:Send>;
// 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::<Task>) { 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<T: 'static> KeyValue<T> {
/// assert_eq!(foo.replace(None), Some(4));
/// ```
pub fn replace(&'static self, data: Option<T>) -> Option<T> {
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<T: 'static> KeyValue<T> {
/// assert_eq!(*key.get().unwrap(), 3);
/// ```
pub fn get(&'static self) -> Option<Ref<T>> {
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<T: 'static> Deref<T> for Ref<T> {
#[unsafe_destructor]
impl<T: 'static> Drop for Ref<T> {
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;

View File

@ -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<Task> = Local::take();
let mut t: Box<Task> = match Local::try_take() {
Some(t) => t,
None => return None,
};
let ret = t.local_io().map(|t| {
unsafe { mem::transmute_copy(&t) }
});

View File

@ -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<Box<Any:Send>>
}
struct Exception {
uwe: uw::_Unwind_Exception,
cause: Option<Box<Any:Send>>,
}
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<Any:Send>) -> ! {
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<uw::_Unwind_Exception> =
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<Any:Send>> {
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<Any:Send>) -> ! {
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<Exception> = 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<M: Any + Send>(msg: M, file: &'static str, line: uint) -> !
fn begin_unwind_inner(msg: Box<Any:Send>,
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::<String>() {
Some(s) => s.as_slice(),
None => "Box<Any>",
}
};
// 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<Box<Task>> = 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("<unnamed>");
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<Box<Task>> = 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::<String>() {
Some(s) => s.as_slice(),
None => "Box<Any>",
}
};
// 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<Task> = 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("<unnamed>");
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);
}
}

View File

@ -176,7 +176,10 @@ impl TaskBuilder {
Some(gen) => gen(f),
None => f
};
let t: Box<Task> = Local::take();
let t: Box<Task> = match Local::try_take() {
Some(t) => t,
None => fail!("need a local task to spawn a new task"),
};
t.spawn_sibling(self.opts, f);
}

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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()));
}
}