-1

I'm working with a library that offers a RAII transaction API shaped like this:

struct Dataset {}

struct Transaction<'a> {
    dataset: &'a mut Dataset,
}

struct Error;

impl Dataset {
    fn start_transaction(&mut self) -> Result<Transaction<'_>, Error> {
        Ok(Transaction { dataset: self }) // In real life, may also return Err.
    }
}

Now I want to write some wrapper code that tries to start a Transaction on a dataset, but if that fails because the underlying data source does not support transactions, it returns the Dataset as-is. (Then everything is eventually resolved to a Dataset through DerefMut implementations, but that's beside the point.)

Here's my attempt:

enum TransactionIfSupported<'a> {
    Transaction(Transaction<'a>),
    Dataset(&'a mut Dataset),
}

fn start_transaction_if_supported<'a>(dataset: &'a mut Dataset) -> TransactionIfSupported<'a> {
    if let Ok(txn) = dataset.start_transaction() {
        return TransactionIfSupported::Transaction(txn);
    }
    TransactionIfSupported::Dataset(dataset)
}

Sadly the borrow checker frowns upon it:

error[E0499]: cannot borrow `*dataset` as mutable more than once at a time
  --> src/lib.rs:28:37
   |
24 | fn start_transaction_if_supported<'a>(dataset: &'a mut Dataset) -> TransactionIfSupported<'a> {
   |                                   -- lifetime `'a` defined here
25 |     if let Ok(txn) = dataset.start_transaction() {
   |                      --------------------------- first mutable borrow occurs here
26 |         return TransactionIfSupported::Transaction(txn);
   |                ---------------------------------------- returning this value requires that `*dataset` is borrowed for `'a`
27 |     }
28 |     TransactionIfSupported::Dataset(dataset)
   |                                     ^^^^^^^ second mutable borrow occurs here

I don't quite understand why this happens. If start_transaction returns the Err variant, I'd expect any borrows from the start_transaction call to no longer be in scope after the if block. I'd expect this to hold even if there was no unconditional return statement inside the if.

What's going on and how do I resolve it?

Playground link

Thomas
  • 174,939
  • 50
  • 355
  • 478
  • Applying the code of `start_transaction_if_supported` inline, as suggested in the link, [also works](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=27b8810f16d44ef79384dff37d39469e). – E_net4 Mar 15 '22 at 10:11

1 Answers1

0

It looks like the borrow checker is not smart enough to handle this case yet. The linked question contains a solution for the HashMap case specifically, which will not work here because there is no contains_key equivalent.

Instead, we need some unsafe code to express it:

fn start_transaction_if_supported<'a, 'b>(dataset: &'b mut Dataset) -> TransactionIfSupported<'a, 'b> {
    unsafe {
        let dataset_ptr: *mut Dataset = dataset;
        if let Ok(txn) = (*dataset_ptr).start_transaction() {
            return TransactionIfSupported::Transaction(txn);
        }
        TransactionIfSupported::Dataset(&mut *dataset)
    }
}
Thomas
  • 174,939
  • 50
  • 355
  • 478