Skip to content

Commit 1643655

Browse files
docs: Improve inline documentation (#11)
1 parent 148df56 commit 1643655

File tree

2 files changed

+136
-15
lines changed

2 files changed

+136
-15
lines changed

src/errors.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Error handling is an important part of the `mem-isolate` crate. If something
22
//! went wrong, we want to give the caller as much context as possible about how
3-
//! that error effected their `callable`, so they are well-equipped to know what
3+
//! that error affected their `callable`, so they are well-equipped to know what
44
//! to do about it.
55
//!
66
//! The primary error type is [`MemIsolateError`], which is returned by
@@ -36,11 +36,17 @@ use thiserror::Error;
3636
/// [`MemIsolateError`] is the **primary error type returned by the crate**. The
3737
/// goal is to give the caller context about what happened to their callable if
3838
/// something went wrong.
39+
///
40+
/// For basic usage, and an introduction of how you should think about error
41+
/// handling with this crate, see
42+
/// [`examples/error-handling-basic.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs)
43+
///
44+
/// For an exhaustive look of all possible error variants, see [`examples/error-handling-complete.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-complete.rs)
3945
// TODO: Consider making this Send + Sync
4046
#[derive(Error, Debug, Serialize, Deserialize)]
4147
pub enum MemIsolateError {
4248
/// Indicates something went wrong before the callable was executed. Because
43-
/// the callable never executed, it should be safe to naively retry the the
49+
/// the callable never executed, it should be safe to naively retry the
4450
/// callable with or without mem-isolate, even if the function is not
4551
/// idempotent.
4652
#[error("an error occurred before the callable was executed: {0}")]
@@ -51,7 +57,7 @@ pub enum MemIsolateError {
5157
#[error("an error occurred after the callable was executed: {0}")]
5258
CallableExecuted(#[source] CallableExecutedError),
5359

54-
/// Indicates something went wrong, but it is unknown wether the callable was
60+
/// Indicates something went wrong, but it is unknown whether the callable was
5561
/// executed. **You should retry the callable only if it is idempotent.**
5662
#[error("the callable process exited with an unknown status: {0}")]
5763
CallableStatusUnknown(#[source] CallableStatusUnknownError),
@@ -93,7 +99,7 @@ pub enum CallableDidNotExecuteError {
9399
// That rules out a ton of overloaded io::Error posibilities. It's more
94100
// precise. WARNING: Serialization will fail if this is not an OS error.
95101
//
96-
/// A system error ocurred while creating the pipe used to communicate with
102+
/// A system error occurred while creating the pipe used to communicate with
97103
/// the child process
98104
#[serde(
99105
serialize_with = "serialize_os_error",
@@ -104,7 +110,7 @@ pub enum CallableDidNotExecuteError {
104110
)]
105111
PipeCreationFailed(#[source] io::Error),
106112

107-
/// A system error ocurred while closing the child process's copy of the
113+
/// A system error occurred while closing the child process's copy of the
108114
/// pipe's read end
109115
#[serde(
110116
serialize_with = "serialize_option_os_error",
@@ -113,7 +119,7 @@ pub enum CallableDidNotExecuteError {
113119
#[error("system error encountered closing the child's copy of the pipe's read end: {}", format_option_error(.0))]
114120
ChildPipeCloseFailed(#[source] Option<io::Error>),
115121

116-
/// A system error ocurred while forking the child process which is used to
122+
/// A system error occurred while forking the child process which is used to
117123
/// execute user-supplied callable
118124
#[serde(
119125
serialize_with = "serialize_os_error",
@@ -124,13 +130,13 @@ pub enum CallableDidNotExecuteError {
124130
}
125131

126132
/// An error indicating that something went wrong in a way where it is difficult
127-
/// or impossible to determine wether the user-supplied callable was executed
133+
/// or impossible to determine whether the user-supplied callable was executed
128134
/// `¯\_(ツ)_/¯`
129135
///
130136
/// You should only retry the callable if it is idempotent.
131137
#[derive(Error, Debug, Serialize, Deserialize)]
132138
pub enum CallableStatusUnknownError {
133-
/// A system error ocurred while closing the parent's copy of the pipe's
139+
/// A system error occurred while closing the parent's copy of the pipe's
134140
/// write end
135141
#[serde(
136142
serialize_with = "serialize_os_error",
@@ -139,15 +145,15 @@ pub enum CallableStatusUnknownError {
139145
#[error("system error encountered closing the parent's copy of the pipe's write end: {0}")]
140146
ParentPipeCloseFailed(#[source] io::Error),
141147

142-
/// A system error ocurred while waiting for the child process to exit
148+
/// A system error occurred while waiting for the child process to exit
143149
#[serde(
144150
serialize_with = "serialize_os_error",
145151
deserialize_with = "deserialize_os_error"
146152
)]
147153
#[error("system error encountered waiting for the child process: {0}")]
148154
WaitFailed(#[source] io::Error),
149155

150-
/// A system error ocurred while reading the child's result from the pipe
156+
/// A system error occurred while reading the child's result from the pipe
151157
#[serde(
152158
serialize_with = "serialize_os_error",
153159
deserialize_with = "deserialize_os_error"

src/lib.rs

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,43 @@
1+
//! # `mem-isolate`: *Run unsafe code safely*
2+
//!
3+
//! It runs your function via a `fork()`, waits for the result, and
4+
//! returns it.
5+
//!
6+
//! This grants your code access to an exact copy of memory and state at the
7+
//! time just before the call, but guarantees that the function will not affect
8+
//! the parent process's memory footprint in any way. It forces functions to be
9+
//! *pure*, even if they aren't.
10+
//!
11+
//! ```
12+
//! use mem_isolate::execute_in_isolated_process;
13+
//!
14+
//! // No heap, stack, or program memory out here...
15+
//! let result = mem_isolate::execute_in_isolated_process(|| {
16+
//! // ...Can be affected by anything in here
17+
//! Box::leak(Box::new(vec![42; 1024]));
18+
//! });
19+
//! ```
20+
//!
21+
//! To keep things simple, this crate exposes only two public interfaces:
22+
//!
23+
//! * [`execute_in_isolated_process`] - The function that executes your code in
24+
//! an isolated process.
25+
//! * [`MemIsolateError`] - The error type that function returns ☝️
26+
//!
27+
//! For more code examples, see [`examples/`](https://github.com/brannondorsey/mem-isolate/tree/main/examples).
28+
//! [This one](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs)
29+
//! in particular shows how you should think about error handling.
30+
//!
31+
//! For more information, see the [README](https://github.com/brannondorsey/mem-isolate).
32+
//!
33+
//! ## Supported platforms
34+
//!
35+
//! Because of its heavy use of POSIX system calls, this crate only
36+
//! supports Unix-like operating systems (e.g. Linux, macOS, BSD).
37+
//!
38+
//! Windows and wasm support are not planned at this time.
39+
#![warn(missing_docs)]
40+
141
#[cfg(not(any(target_family = "unix")))]
242
compile_error!(
343
"Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)"
@@ -28,12 +68,87 @@ pub use serde::{Serialize, de::DeserializeOwned};
2868
/// serializes its result (using bincode) and writes it through a pipe, which
2969
/// the parent reads and deserializes.
3070
///
31-
/// # Safety
32-
/// This code directly calls glibc functions (via the libc crate) and should
33-
/// only be used in a Unix environment.
71+
/// # Example
72+
///
73+
/// ```rust
74+
/// use mem_isolate::execute_in_isolated_process;
75+
///
76+
/// let leaky_fn = || {
77+
/// // Leak 1KiB of memory
78+
/// let data: Vec<u8> = Vec::with_capacity(1024);
79+
/// let data = Box::new(data);
80+
/// Box::leak(data);
81+
/// };
82+
///
83+
/// let _ = execute_in_isolated_process(leaky_fn);
84+
/// // However, the memory is not leaked in the parent process here
85+
/// ```
86+
///
87+
/// # Error Handling
88+
///
89+
/// Error handling is organized into three levels:
90+
///
91+
/// 1. The first level describes the effect of the error on the `callable` (e.g.
92+
/// did your callable function execute or not)
93+
/// 2. The second level describes what `mem-isolate` operation caused the error
94+
/// (e.g. did serialization fail)
95+
/// 3. The third level is the underlying OS error if it is available (e.g. an
96+
/// `io::Error`)
97+
///
98+
/// For most applications, you'll care only about the first level:
99+
///
100+
/// ```rust
101+
/// use mem_isolate::{execute_in_isolated_process, MemIsolateError};
102+
///
103+
/// // Function that might cause memory issues
104+
/// let result = execute_in_isolated_process(|| {
105+
/// // Some operation
106+
/// "Success!".to_string()
107+
/// });
108+
///
109+
/// match result {
110+
/// Ok(value) => println!("Callable succeeded: {}", value),
111+
/// Err(MemIsolateError::CallableDidNotExecute(_)) => {
112+
/// // Safe to retry, callable never executed
113+
/// println!("Callable did not execute, can safely retry");
114+
/// },
115+
/// Err(MemIsolateError::CallableExecuted(_)) => {
116+
/// // Do not retry unless idempotent
117+
/// println!("Callable executed but result couldn't be returned");
118+
/// },
119+
/// Err(MemIsolateError::CallableStatusUnknown(_)) => {
120+
/// // Retry only if idempotent
121+
/// println!("Unknown if callable executed, retry only if idempotent");
122+
/// }
123+
/// }
124+
/// ```
125+
///
126+
/// For a more detailed look at error handling, see the documentation in the
127+
/// [`errors`] module.
128+
///
129+
/// ## Important Note on Closures
130+
///
131+
/// When using closures that capture and mutate variables from their environment,
132+
/// these mutations **only occur in the isolated child process** and do not affect
133+
/// the parent process's memory. For example, it may seem surprising that the
134+
/// following code will leave the parent's `counter` variable unchanged:
135+
///
136+
/// ```rust
137+
/// use mem_isolate::execute_in_isolated_process;
138+
///
139+
/// let mut counter = 0;
140+
/// let result = execute_in_isolated_process(|| {
141+
/// counter += 1; // This increment only happens in the child process
142+
/// counter // Returns 1
143+
/// });
144+
/// assert_eq!(counter, 0); // Parent's counter remains unchanged
145+
/// ```
146+
///
147+
/// This is the intended behavior as the function's purpose is to isolate all
148+
/// memory effects of the callable. However, this can be surprising, especially
149+
/// for [`FnMut`] or [`FnOnce`] closures.
34150
pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
35151
where
36-
// TODO: Consider restricting to disallow FnMut() closures
37152
F: FnOnce() -> T,
38153
T: Serialize + DeserializeOwned,
39154
{
@@ -161,7 +276,7 @@ where
161276
} // The read_fd will automatically be closed when the File is dropped
162277

163278
if buffer.is_empty() {
164-
// TODO: How can we more rigerously know this? Maybe we write to a mem map before and after execution?
279+
// TODO: How can we more rigorously know this? Maybe we write to a mem map before and after execution?
165280
return Err(CallableStatusUnknown(CallableProcessDiedDuringExecution));
166281
}
167282
// Update the deserialization to handle child errors

0 commit comments

Comments
 (0)