RAII for failure-prone initializations?

Design question: I'm working on a library to control hardware devices using a serial protocol.

The natural way to design such drivers is, of course, resource acquisition is initialization.

However, the problem is that initializing the structs that represent these devices is quite complex; it's necessary to initial a serial connection, then send several messages in the device's format and wait for appropriate responses. The devices themselves are not always reliable.

I have some cleanup code that runs in Drop; for the most part, the same code should run if initialization fails partway through, but, of course, the struct itself is not yet available at this point.

Is RAII still the best way to implement this? Are there good examples of ways to modify the code so that I can share cleanup code between failures that occur in init and ordinary Drops?

I would say that this is a good example of when not to use RAII style. The caller should have the opportunity to not block on initialization or block on cleanup.

You can create a value of the type inside your new function prior to the return.

If any of the initialization commands in new below return None, then Drop impl will run cleaning up the structure, and new will also return None.

struct Device { 
    foo: u8,
    /* more fields */ 
}

impl Device {
     fn new() -> Option<Self> {
        let mut this = Self {
            foo: 0, 
            /* initialize fields to minimal value */
        };

        serial_initialize()?;
        this.foo = serial_get_foo()?;
        serial_set_bar()?;
 
        Some(this)
     }
}

impl Drop for Device {
    fn drop(&mut self) {
        if foo != 0 {
            serial_cleanup_foo(self.foo);
        }
        serial_shutdown();
    }
}

If for some reason it is not possible to structure your new function similar to this, you could write a freestanding fn cleanup(foo: u8, /* etc */) function which is called both in drop and if initialization fails in new.

2 Likes

Yes, it's fine, because Rust doesn't have constructors like C++, so a lot of the "RAII vs init" debate doesn't apply.

You can return Result<Self> from your new function, and for the caller it will work like RAII.

Internally, when constructing Self {} literal Rust will drop fields individually if the code returns before all fields were initialized. If you can break down your initialisation into smaller objects with their own Drop you could handle fallibility and partial init this way.

In new you can also make an instance of the type and call methods on it before returning it, so you can ensure that new() calls some that.init() method without being constrained by a new Type() syntax.

There's also a builder pattern. If the init is long and complex and you don't want to have optional fields in the type, init everything in the builder struct, and then move the fields to the final infallible type.

3 Likes

Thank you for the replies. I think that there's a number of good suggestions here. I will have to try a few things to see what will work best for my code. At some point it will hopefully be open-sourced as well (and I might try to post it in the forum).