What's the rustful way to write a context manager?

In Python there is a thing called a context manager. It looks like this:

with open('file.txt') as f:
    # setup code runs here
    print f.read()
    # teardown code runs here

do_other_thing(f)  # error: f is not bound here

Inside the indented block the object f is alive. Outside it's not. In addition, the context manager open has the option to run some code just both before entering the indented block, and just before leaving it. In this case it has to do with opening and closing the file behind the scenes.

A different example: say you wanted to time the execution of a few lines of code. You could implement a context manager that does this:

timings = {}

with time_this(timings, "post_and_get"):
    api.post("new thing")
    api.get("new thing")
# timings is now: {"post_and_get", 1.819}

Behind the scenes time_this would start a timer, then run the indented block, stop the timer, and finally insert an entry into the timings dictionary.

Coming back to Rust I'm looking at some server code that I need to instrument with timers to measure the performance of different parts. I'm wondering what the Rust way would be....

Starting from the simple but clunky way:

let mut timings = Timings::new();

timings.start_timer("post_and_get");
api.post("new thing");
let rv = api.get("new thing");
timings.stop_timer("post_and_get");

This is verbose and easy to use incorrectly by failing to match start_timer with stop_timer and not using the same name in both calls.

A C++ inspired variant:

let mut timings = Timings::new();

let rv = {
    // A timer is started here
    let t = Timer::new(&mut timings, "post_and_get"); // like this
    // Timer::new(&mut timings, "post_and_get"); // not like this!!!

    api.post("new thing");
    api.get("new thing")

    // Timer implements Drop, so teardown code runs here
};

Here Timer is a throw-away value that only serves to mutate timings such that on creation it starts the timer, then once it goes out of scope it stops the timer and inserts the result into timings.

This is not bad, but it's easy to use incorrectly, because without the let the Timer value goes out of scope immediately and doesn't time the rest of the block. It also means we have to use a name like t to create the binding, even though we'd rather not have it potentially collide with other variable names we happen to be using in this code already.

Finally, I can think of the macro way:

let mut timings = Timings::new();

let rv = time_this!(&mut timings, "post_and_get", {
    api.post("new thing");
    api.get("new thing")
});

Here the macro would call timings.start_timer and timings.stop_timer behind the scenes for us.

This seems the least error prone, but it feels kind of heavy handed to me syntactically speaking.

Any ideas?

2 Likes

I would use a lambda function.

let mut timings = Timings::new();
let rv = timings.time_this("post_and_get", || {
    //...
});
3 Likes

I think the canonical way to do this is the C++ variant. However,

  1. I'd use timings.time("post_and_get") instead of Timer::new(...).
  2. You can use let _some_name = ... to get rid of the unused binding warning. This also indicates to the programmer that we're using the drop side effect. Note: Do not use let _ = ...; that will drop it immediately.
  3. You can annotate Timer with #[must_use] to ensure that it is used in a let binding.
1 Like