Hone logo
Hone
Problems

Rust Scope Guards: Ensuring Resource Cleanup

In Rust, managing resources effectively and guaranteeing their cleanup, even in the face of errors or early returns, is crucial for robust and safe code. This challenge focuses on implementing the concept of "scope guards," which are small, RAII-style objects that automatically perform an action when they go out of scope. This pattern is invaluable for tasks like releasing locks, closing files, or undoing temporary state changes.

Problem Description

Your task is to create a generic ScopeGuard struct in Rust. This ScopeGuard should encapsulate a piece of data and a closure (a function that can be called later). When the ScopeGuard instance is dropped (goes out of scope), it should automatically execute the provided closure.

Key Requirements:

  1. Generic Data: The ScopeGuard should be able to hold any type of data.
  2. Action Closure: It must store a closure that takes no arguments and returns () (unit). This closure represents the action to be performed upon dropping.
  3. Automatic Execution: The closure must be executed exactly once when the ScopeGuard instance is dropped.
  4. Disabling the Guard: Implement a mechanism to prevent the closure from being executed when the guard is dropped. This is essential for scenarios where the cleanup action has already been performed or is no longer needed.
  5. No Heap Allocation for Guard: The ScopeGuard itself should not require heap allocation.

Expected Behavior:

  • When a ScopeGuard is created, it stores the provided data and the closure.
  • If the ScopeGuard goes out of scope normally, the closure should be executed.
  • If the guard's disabling mechanism is used, the closure should not be executed when it goes out of scope.

Important Edge Cases:

  • What happens if the closure panics? (Rust's Drop trait handling of panics is a relevant concept here, though for this challenge, you don't need to explicitly handle panics within the guard itself, just be aware of the behavior.)
  • Ensuring the closure is only executed once.

Examples

Example 1: Simple Cleanup

use std::sync::{Arc, Mutex};

struct MyResource {
    id: u32,
}

impl MyResource {
    fn new(id: u32) -> Self {
        println!("Creating resource with ID: {}", id);
        MyResource { id }
    }

    fn cleanup(&self) {
        println!("Cleaning up resource with ID: {}", self.id);
    }
}

// Assume ScopeGuard is implemented as described.

fn process_resource(res_id: u32) {
    let resource = MyResource::new(res_id);
    // Create a scope guard that will call resource.cleanup() when it goes out of scope
    let guard = ScopeGuard::new(resource, |r| r.cleanup()); // Simplified closure passing for example

    // ... perform some operations with the resource ...
    println!("Processing resource {}...", res_id);

    // When process_resource finishes, 'guard' will go out of scope,
    // and resource.cleanup() will be called automatically.
}

// In main or elsewhere:
// process_resource(123);

Expected Output for Example 1:

Creating resource with ID: 123
Processing resource 123...
Cleaning up resource with ID: 123

Explanation:

The MyResource is created. A ScopeGuard is associated with it, with a closure that calls cleanup. When process_resource finishes, the guard goes out of scope, triggering the cleanup method.

Example 2: Disabling the Guard

use std::sync::{Arc, Mutex};

struct SomeService {
    name: String,
}

impl SomeService {
    fn new(name: String) -> Self {
        println!("Starting service: {}", name);
        SomeService { name }
    }

    fn shutdown(&self) {
        println!("Shutting down service: {}", self.name);
    }
}

// Assume ScopeGuard is implemented as described, and has a 'cancel()' method.

fn manage_service(service_name: &str) {
    let service = SomeService::new(service_name.to_string());
    let mut guard = ScopeGuard::new(service, |s| s.shutdown());

    println!("Service '{}' is active.", service_name);

    // Simulate a scenario where the service is explicitly shut down early
    if service_name == "Alpha" {
        println!("Explicitly shutting down '{}' early.", service_name);
        // Call the shutdown logic directly and cancel the guard
        guard.cancel_and_drop(); // Or similar method to perform action and disable drop
    } else {
        println!("Service '{}' will be cleaned up automatically.", service_name);
        // The guard will automatically call shutdown when it goes out of scope.
    }
}

// In main or elsewhere:
// manage_service("Alpha");
// manage_service("Beta");

Expected Output for Example 2:

Starting service: Alpha
Service 'Alpha' is active.
Explicitly shutting down 'Alpha' early.
Shutting down service: Alpha
Starting service: Beta
Service 'Beta' is active.
Service 'Beta' will be cleaned up automatically.
Shutting down service: Beta

Explanation:

For "Alpha," the service is shut down early, and the guard is canceled. For "Beta," the guard remains active and performs the shutdown when the function exits. The cancel_and_drop method (or similar) is hypothetical here, representing the idea of executing the cleanup and preventing the Drop impl from running.

Constraints

  • The ScopeGuard must be generic over the type of data it holds.
  • The closure provided to ScopeGuard must accept a reference to the encapsulated data.
  • The closure must be FnOnce to ensure it's executed at most once.
  • The ScopeGuard must implement Drop.
  • The disabling mechanism should prevent the Drop implementation from executing the closure.
  • No external crates are allowed for the core ScopeGuard implementation.

Notes

  • Consider how to pass the data to the closure. Should it be a mutable reference, an immutable reference, or take ownership? Think about what makes the most sense for cleanup actions.
  • The disabling mechanism is a key part of making scope guards truly useful. How can you signal to the Drop implementation that the action has already been taken or is no longer necessary?
  • This exercise is a fundamental building block for more advanced resource management patterns in Rust.
  • You will need to use std::mem::forget or similar techniques carefully to implement the disabling logic. Think about why std::mem::forget works in this context.
Loading editor...
rust