pub fn execute_in_isolated_process<F, T>(
callable: F,
) -> Result<T, MemIsolateError>
Expand description
Executes a user-supplied callable
in a forked child process so that any
memory changes during execution do not affect the parent. The child
serializes its result (using bincode) and writes it through a pipe, which
the parent reads and deserializes.
§Example
use mem_isolate::execute_in_isolated_process;
let leaky_fn = || {
// Leak 1KiB of memory
let data: Vec<u8> = Vec::with_capacity(1024);
let data = Box::new(data);
Box::leak(data);
};
let _ = execute_in_isolated_process(leaky_fn);
// However, the memory is not leaked in the parent process here
§Errors
Error handling is organized into three levels:
- The first level describes the effect of the error on the
callable
(e.g. did your callable function execute or not) - The second level describes what
mem-isolate
operation caused the error (e.g. did serialization fail) - The third level is the underlying OS error if it is available (e.g. an
io::Error
)
For most applications, you’ll care only about the first level:
use mem_isolate::{execute_in_isolated_process, MemIsolateError};
// Function that might cause memory issues
let result = execute_in_isolated_process(|| {
// Some operation
"Success!".to_string()
});
match result {
Ok(value) => println!("Callable succeeded: {}", value),
Err(MemIsolateError::CallableDidNotExecute(_)) => {
// Safe to retry, callable never executed
println!("Callable did not execute, can safely retry");
},
Err(MemIsolateError::CallableExecuted(_)) => {
// Do not retry unless idempotent
println!("Callable executed but result couldn't be returned");
},
Err(MemIsolateError::CallableStatusUnknown(_)) => {
// Retry only if idempotent
println!("Unknown if callable executed, retry only if idempotent");
}
}
For a more detailed look at error handling, see the documentation in the
errors
module.
§Important Note on Closures
When using closures that capture and mutate variables from their
environment, these mutations only occur in the isolated child process
and do not affect the parent process’s memory. For example, it may seem
surprising that the following code will leave the parent’s counter
variable unchanged:
use mem_isolate::execute_in_isolated_process;
let mut counter = 0;
let result = execute_in_isolated_process(|| {
counter += 1; // This increment only happens in the child process
counter // Returns 1
});
assert_eq!(counter, 0); // Parent's counter remains unchanged
This is the intended behavior as the function’s purpose is to isolate all
memory effects of the callable. However, this can be surprising, especially
for FnMut
or FnOnce
closures.
§Limitations
§Performance & Usability
- Works only on POSIX systems (Linux, macOS, BSD)
- Data returned from the
callable
function must be serialized to and from the child process (usingserde
), which can be expensive for large data. - Excluding serialization/deserialization cost,
execute_in_isolated_process()
introduces runtime overhead on the order of ~1ms compared to a direct invocation of thecallable
.
In performance-critical systems, these overheads can be no joke. However,
for many use cases, this is an affordable trade-off for the memory safety
and snapshotting behavior that mem-isolate
provides.
§Safety & Correctness
The use of fork()
, which this crate uses under the hood, has a slew of
potentially dangerous side effects and surprises if you’re not careful.
- For single-threaded use only: It is generally unsound to
fork()
in multi-threaded environments, especially when mutexes are involved. Only the thread that callsfork()
will be cloned and live on in the new process. This can easily lead to deadlocks and hung child processes if other threads are holding resource locks that the child process expects to acquire. - Signals delivered to the parent process won’t be automatically
forwarded to the child process running your
callable
during its execution. See one of theexamples/blocking-signals-*
files for an example of how to handle this. - Channels can’t be used to communicate between the parent and child processes. Consider using shared mmaps, pipes, or the filesystem instead.
- Shared mmaps break the isolation guarantees of this crate. The child
process will be able to mutate
mmap(..., MAP_SHARED, ...)
regions created by the parent process. - Panics in your
callable
won’t panic the rest of your program, as they would withoutmem-isolate
. That’s as useful as it is harmful, depending on your use case, but it’s worth noting. - Mutable references, static variables, and raw pointers accessible to
your
callable
won’t be modified as you would expect them to. That’s kind of the whole point of this crate… ;)
Failing to understand or respect these limitations will make your code more susceptible to both undefined behavior (UB) and heap corruption, not less.