Expand description
Description
At the bare minimum, with no other features enabled, this crate is a collection of traits which make error handling on fallible drops easier, although there is no built-in way to handle these errors, at least in this case.
With all (default) features enabled, this crate is collection of types and traits which not only allow you to make error handling on fallible drops easier, but contains a large set of strategies[1] on how to handle these errors.
[1] Don’t know what that means? don’t worry, we’ll get to that in a bit.
Motivation
Say you have a structure which performs external operations, to let’s say a filesystem, which can fail.
use std::io;
struct Structure { /* fields */ }
impl Structure {
pub fn do_external_io(&self) -> Result<(), io::Error> {
// do some external io
}
}
You then think to yourself, “What if I want to run that external IO at the end of a scope?”
Seems simple, you create a guard structure.
struct StructureGuard<'s> {
structure: &'s Guard,
// other fields
}
impl<'s> StructureGuard<'s> {
pub fn new(structure: &'s Structure) -> Self {
StructureGuard {
structure,
// other fields
}
}
}
You then proceed to write the Drop
implementation for the guard.
impl<'s> Drop for StructureGuard<'s> {
fn drop(&mut self) {
if let Err(e) = self.structure.do_external_io() {
// ...how can i handle this error?
}
}
}
But, alas! you need to handle the error.
First option: Just panic. But, we’d like to handle the error.
Second option: Print to standard error.
Second option: Create a finish function
and then check in the Drop
implementation if its finished.
struct StructureGuard<'s> {
structure: &'s Structure,
finished: bool,
// other fields
}
impl<'s> StructureGuard<'s> {
pub fn new(structure: &'s Structure) -> Self {
StructureGuard {
structure,
finished: false,
// other fields
}
}
pub fn finish(&mut self) -> io::Result<()> {
if self.finished {
Ok(())
} else {
self.structure.do_external_io()?;
self.finished = true;
}
}
}
impl<'s> Drop for StructureGuard<'s> {
fn drop(&mut self) {
if !self.finished {
panic!("you must finish the guard before dropping it");
}
}
}
…but what if the user forgets to finish the guard? It would become a runtime error instead of a compile error.
And, what if an error occurs in another part of the code before the guard is finished?
{
let guard = StructureGuard::new(&structure);
do_some_thing_that_might_fail()?;
guard.finish()?
}
Then it won’t finish the guard, and it will panic.
This is exactly the problem I was having when I was making the ideapad
project. In fact, this project was spun off
a module which was a part of the ideapad
project.
I originally meant to only leave it in the ideapad
project, but eventually I started needing this pattern in other
projects, so I decided to make it a standalone library.
Overview
try-drop
is based loosely off of how the x86 architecture handles exceptions, there is a primary exception handler,
and then there is an exception handler for the primary exception handler.
Except it isn’t exception handlers, it’s try drop strategies.
But what is a try drop?
Try Drop
A try drop or a TryDrop
is a trait which defines a destructor which can fail.
pub trait TryDrop {
type Error;
unsafe fn try_drop(&mut self) -> Result<(), Self::Error>;
}
You may have noticed that TryDrop::try_drop
is unsafe. This is intentional:
See, the Drop
trait has a special characteristic in that you can’t call it’s drop
method directly. The reason is
that you may call the destructor twice, which could potentially cause a double free.
struct T;
impl Drop for T {
fn drop(&mut self) {
// ...
}
}
let t = T;
t.drop(); // this is not allowed and will result in a compile error
However, as far as I know, there is no way to implement this characteristic in your own traits. So you can do this if
try_drop
wasn’t marked unsafe:
struct CWrapper(pub *mut some_library_type_t);
impl TryDrop for CWrapper {
type Error = Error;
fn try_drop(&mut self) -> Result<(), Self::Error> {
let rc = c_wrapper_sys::some_library_type_t_free(self.0);
if rc != 0 {
Err(Error::from_rc(rc))
} else {
Ok(())
}
}
}
// meanwhile, in another crate...
fn do_stuff() -> Result<(), Error> {
let mut c_wrapper = CWrapper(c_wrapper_sys::some_library_type_t_new());
c_wrapper.try_drop()?;
c_wrapper.try_drop()?; // oops!
Ok(())
}
So we have to trust that the user will never call TryDrop::try_drop
directly, and we want to make this invariant
explicit, so that’s why I’ve made the TryDrop::try_drop
method unsafe. You must only ever call this method from a
Drop
implementation.
There is a way to make it safe, though.
Drop Adapter
A drop adapter is basically the glue between the Drop
trait and the TryDrop
trait. This handles the error handling
for you.
fn do_stuff() {
let c_wrapper = CWrapper(c_wrapper_sys::some_library_type_t_new());
let c_wrapper = DropAdapter(c_wrapper);
// let c_wrapper = c_wrapper.adapt(); // you can also do this
// you can't cause a double free as `DropAdapter` doesn't implement `TryDrop`[1]
// do stuff with c_wrapper
// this results in a compile error as the method does not exist
// c_wrapper.try_drop()?;
} // ... after this, the drop adapter handles the error
// [1]: well, kinda. we'll explain more later
But how can we specify how DropAdapter
handles the error? Well…
Try Drop Strategies
A try drop strategy or a TryDropStrategy
is a trait which defines how to handle an error which occurs in a drop.
pub trait TryDropStrategy {
fn handle_error(&self, error: try_drop::Error);
}
Simply implement it to your type, then boom, Bob’s your uncle.
struct MyStrategy;
impl TryDropStrategy for MyStrategy {
fn handle_error(&self, error: try_drop::Error) {
println!("{}", error);
}
}
A try drop strategy can also be fallible, meaning that it can fail.
pub trait FallibleTryDropStrategy {
type Error;
fn handle_error(&self, error: Self::Error) -> Result<(), Self::Error>;
}
Implementing it is like so:
impl FallibleTryDropStrategy for MyStrategy {
type Error = io::Error;
fn handle_error(&self, error: Self::Error) -> Result<(), Self::Error> {
io::stdout().write_all(error.to_string().as_bytes())
}
}
But what if that strategy fails? You can provide a last resort strategy.
Fallback Try Drop Strategies
A fallback try drop strategy or a FallbackTryDropStrategy
is a trait which defines how to handle an error which
occurs from another try drop strategy.
While the FallbackTryDropStrategy
does exist, any type which implements TryDropStrategy
will automatically
implement FallbackTryDropStrategy
too. So MyStrategy
already implements FallbackTryDropStrategy
!
// yeah, cool, but like, how do i use the strategies in the drop adapter in the first place?
let c_wrapper = DropAdapter(CWrapper(c_wrapper_sys::some_library_type_t_new()));
There are two ways.
Ways to use the strategies
Provide it as a generic parameter
You can provide the drop strategies as a generic parameter for, in our case, CWrapper
.
struct CWrapper<D, DD>
where
D: TryDropStrategy,
DD: FallbackTryDropStrategy,
{
inner: *mut some_library_type_t,
strategy: D,
fallback_strategy: DD,
}
And then you provide references to it in the TryDrop
trait!
“…but there is no way to provide it,” you may ask.
Well, actually, there are two version of the TryDrop
trait.
Impure Try Drop
This try drop, ignoring it’s kinda ugly name, allows you to not provide any strategy.
pub trait ImpureTryDrop {
type Error;
unsafe fn try_drop(&mut self) -> Result<(), Self::Error>;
}
…seems familiar? that’s because it’s actually aliased to TryDrop
!
Pure Try Drop
This try drop, on the other hand, is a bit more verbose. You need to explicitly provide the strategies to use.
pub trait PureTryDrop {
type Error;
type FallbackTryDropStrategy;
type TryDropStrategy;
fn try_drop_strategy(&self) -> &Self::TryDropStrategy;
fn fallback_try_drop_strategy(&self) -> &Self::FallbackTryDropStrategy;
unsafe fn try_drop(&mut self) -> Result<(), Self::Error>;
}
We can now implement our drop strategy for CWrapper
!
impl<D, DD> PureTryDrop for CWrapper<D, DD>
where
D: TryDropStrategy,
DD: FallbackTryDropStrategy,
{
type Error = Error;
type FallbackTryDropStrategy = DD;
type TryDropStrategy = D;
fn try_drop_strategy(&self) -> &Self::TryDropStrategy {
&self.strategy
}
fn fallback_try_drop_strategy(&self) -> &Self::FallbackTryDropStrategy {
&self.fallback_strategy
}
unsafe fn try_drop(&mut self) -> Result<(), Self::Error> {
let rc = c_wrapper_sys::some_library_type_t_free(self.0);
if rc != 0 {
Err(Error::from_rc(rc))
} else {
Ok(())
}
}
}
Actually, all types that implement ImpureTryDrop
also implement PureTryDrop
!
…wait, what types are used then for
FallBackTryDropStrategy
andTryDropStrategy
?
I’m glad you asked.
Use it as the global drop strategy
By default, try-drop
has the global
feature enabled which, as you may have guessed, provides a global drop strategy.
However, it is only initialized if the ds-write
(write drop strategy) and the ds-panic
(panic drop strategy)
features are enabled, which are the main drop strategy and fallback drop strategy respectively. You’d need to provide a
drop strategy for each global if you don’t have those features enabled.
We can install our MyStrategy
as the main drop strategy like so:
try_drop::global::install(MyStrategy);
We can also install it as the fallback drop strategy:
try_drop::fallback::install(MyStrategy);
Going back to our (previous) CWrapper
, it should already be using our global drop strategy.
Other features
Repeatable Try Drops
If you’re 100% sure that your TryDrop
implementation is safe, you can implement the RepeatableTryDrop
marker trait.
pub unsafe trait RepeatableTryDrop: PureTryDrop {}
Implement it like so:
unsafe impl RepeatableTryDrop for T {}
With this, a safe version of try_drop
will be provided.
// look ma, no `unsafe`!
let t = T;
t.safe_try_drop();
t.safe_try_drop();
Providing DropAdapter
with this type will make DropAdapter
implement TryDrop
and RepeatableTryDrop
, therefore
allowing you to nest DropAdapter
s.
let t = DropAdapter(DropAdapter(T));
Dependencies
At the bare minimum, there is only one dependency–anyhow
. With all default features enabled, there are six
dependencies.
anyhow
: used as the main error type.downcast-rs
: used to support downcasting the drop strategies in globals. Is optional.once_cell
: used to support the global drop strategy and for the OnceCell drop strategy. Is optional.shrinkwraprs
: used for more flexible newtypes. Is optional.tokio
: used for the broadcast drop strategy. Is optional.
Features
Here is a tree of the features (which aren’t optional dependencies) and their explanations.
default
: Enables the global try drop strategy, downcasting of try drop strategies, standard library, newtype derefs,derives
s for most types, and the default try drop strategies.global
: This enables the global try drop strategy without nothing set to it.OnceCell
is required for lazy initialization of the global and parking lot to write to the global. By default, there are… defaults, which are…global-defaults
: This enables the default try drop and fallback try drop strategies, the write drop strategy and panic drop strategy.
std
: Enable types which require the standard library to work.derives
: DerivesDebug
,Copy
,Clone
,PartialEq
,Eq
,PartialOrd
,Ord
,Hash
, andDefault
to all public types if possible.drop-strategies
: Enables the default drop strategies. Each drop strategy is explained below. Note thatds
stands for drop strategies, due to Rust’s lack of feature namespacing (I think). You can find these drop strategies in thetry_drop::drop_strategies
module.ds-abort
: A drop strategy which aborts the current program if called.ds-broadcast
: A drop strategy which broadcasts the error to all receivers. This is a heavy drop strategy; it depends on the Tokio broadcast channel and therefore the runtime as it provides a good implementation of a broadcast channel, with the cost of overhead and code bloat.ds-exit
: A drop strategy which exits the program without calling any destructors with the specified exit code if called.ds-noop
: A drop strategy which does nothing when called.ds-panic
: A drop strategy which panics if called. This is used as the default global fallback drop strategy if it’s available.ds-write
: A drop strategy which writes the error to a writer if called. This is used as the default global drop strategy if it’s available.ds-adhoc
: A drop strategy which calls a function if called. This only supportsFn
s, which is the strictest type of function trait based on its trait bounds. If you want a less strict version, use…ds-adhoc-mut
: A drop strategy which calls a function if called. This supportsFnMut
s, which has more flexible trait bounds compared toFn
, at the cost of using aMutex
internally, in order to call it, since try drop strategies must take self by reference.
ds-once-cell
: A drop strategy which stores an error value once. This is primarily useful forstruct
s which own their drop strategy and is adjustable.
License
This project is licensed under the MIT License.
Re-exports
pub use fallback::FallbackTryDropStrategyHandler;
pub use fallback::FallbackTryDropStrategyRef;
pub use crate::fallback::global::GlobalFallbackTryDropStrategyHandler;
pub use crate::global::GlobalTryDropStrategyHandler;
pub use self::ImpureTryDrop as TryDrop;
Modules
Numerous strategies for handling drop errors.
Type and traits for fallback try drop strategies.
Manage the global try drop strategy.
Most commonly used traits.
Structs
The Error
type, a wrapper around a dynamic error type.
A reference to a type which implements FallibleTryDropStrategy
. Used as a workaround for
implementing FallibleTryDropStrategy
on references.
This type is an adapter for types which implement TryDrop
which allow their
TryDrop::try_drop
functions to be repeated multiple times.
Enums
The error type for errors that can never happen.
Traits
A trait which signifies a try drop strategy which can fail. Can be dynamically dispatched.
A trait which signifies a try drop strategy which can fail.
A trait which signifies a try drop strategy which can fail, can be dynamically dispatched, and can be used as the global try drop strategy.
A trait for types which can be dropped, but which may fail to do so.
A trait for types which can be dropped, but which may fail to do so.
Marker trait signifying that the implementing type can repeatedly call its TryDrop::try_drop
method.
A trait which signifies a thread safe type. Can be used in a static
.
A trait which signifies a try drop strategy. This can never fail. If it can, use
FallibleTryDropStrategy
instead.
Functions
Install a drop strategy and fallback drop strategy globally.
Install a drop strategy and fallback drop strategy globally. They both need to be a dynamic trait object.