2

I'm trying to implement the following idea:

let command = ...;

let request = match subcommand {
    Some(x) => [command, x as u8, (x >> 8) as u8],
    None => [command],
};

request should be either an array of 1 (in case there is no subcommand) or 3 (in case there is a 2-byte subcommand). Obviously this approach does not work because branches of match expressions "return" different types (either [u8;1] or [u8;3]);

My logical follow-up was to use slices:

let request: &[u8] = match subcommand {
    Some(x) => &[...],
    None => &[...],
};

That works great on constant values such as &[0, 0, 0], but when I'm trying to build arrays using my variables it fails with temporary value dropped while borrowed.

I got that in that case my reference is escaping to the superior code block, but is there a way around it (i.e some lifetime annotation?)

Creating the buffer beforehand and using slices does work but does not feel optimal.

UPD: that's the function I'm trying to debug:

// Read the data from the device.
async fn read<R>(&mut self, command: u8, subcommand: Option<u16>) -> Result<R, ChipError<E>>
where
    R: TryFrom<u16>,
{
    // If the caller has provided a subcommand, build the request with command and subcommand.
    // If there is no subcommand, use the plain command instead
    let request: &[u8] = match subcommand {
        Some(x) => &[command, x as u8, (x >> 8) as u8],
        None => &[command],
    };

    // Send the bytes to the device
    self.i2c.write(self.addr, &request).await?;

    // And read the response...
    let mut response = [0, 0];
    self.i2c
        .write_read(self.addr, &[commands::CONTROL], &mut response)
        .await?;

    match u16::from_le_bytes(response).try_into() {
        Ok(value) => Ok(value),
        Err(_) => Err(ChipError::Value),
    }
}

And that's the compiler output:

error[E0716]: temporary value dropped while borrowed
  --> xxxx/src/lib.rs:69:25
   |
68 |           let request: &[u8] = match subcommand {
   |  ______________________________-
69 | |             Some(x) => &[command, x as u8, (x >> 8) as u8],
   | |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   | |                         |                                |
   | |                         |                                temporary value is freed at the end of this statement
   | |                         creates a temporary value which is freed while still in use
70 | |             None => &[command],
71 | |         };
   | |_________- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value
4
  • Please show us example of your problematic code and full compiler error message. Commented Oct 25, 2023 at 18:06
  • If you want to return references to stack allocated variables from your function this will never work. You will have to allocate memory on the heap (and for example return Box<[u8]>), or you could somehow statically reserve enough memory for both your variants (for example create an enum). Commented Oct 25, 2023 at 18:09
  • @AleksanderKrauze added a complete function and the output Commented Oct 25, 2023 at 18:19
  • @AleksanderKrauze Enums are very interesting but I can't wrap my head around applying them for this task. I'm considering enum with 2 variants but the issue with matching variants to variable-length arrays still stands. I'm new to Rust so picking which features to use is a bit intimidating - in C I would probably just create 2 separate functions ;) Commented Oct 25, 2023 at 21:30

3 Answers 3

3

This doesn't directly answer the question in the title, but assuming your writer is std::io::write (I know it's i2c, but you should be able to adapt), I would make a simple enum, like so:

use std::io::{Write, Error};

enum Request {
    Command(u8),
    CommandWithSubcommand(u8, u16)
}

impl Request {
    fn write_to(&self, mut writer: impl Write) -> Result<(), Error> {
        match self {
            Self::Command(command) => {
                writer.write_all(&[*command])?
            }
            Self::CommandWithSubcommand(command, subcommand) => {
                let [b1, b2] = subcommand.to_le_bytes(); //maybe to_be_bytes()?
                writer.write_all(&[*command, b1, b2])?
            }
        };
        Ok(())
    }
}

...
let request = match subcommand {
    Some(subcommand) => {
        Request::CommandWithSubcommand(command, subcommand)
    }
    None => {
        Request::Command(command)
    }
};
request.write_to(your_std_writer)?;
...

Because we use the write_all() method separately for each branch, and they both return the same result type, we do not have to worry about lifetimes. This can be inelegant, but advantageous.

Technically you can just use separate write calls in match subcommand, but I suggest using enums to help reason about 1-of-N-known-implementation problems.

Sign up to request clarification or add additional context in comments.

2 Comments

I know that this does not directly address the issue in the OP but I'd probably go with that one. Probably the most natural solution - no extra copying or dummy variables.
@RomanLiutko, another cool bonus is you can pass around these enums in function arguments. I find it especially useful for functions that take a value, and other optional values--like your read() does--because you can usually make the extra optional arguments enums variants. Moreso, the compiler makes it hard to forget to an implementation for a variant.
3

There are multiple ways you can do this. Here are some suggestions.

  1. Just use a Vec (or Box<[u8]>). Yes, it is less efficient, but does it matter for you? it is likely to be the clearest solution.
  2. Use a stack-allocated vector, e.g. ArrayVec.
  3. If the type has a simple default value, have an array in the outer scope and initialize part of it, and return a slice to this part:
let mut scratch_space = [0; 3];
let request = match subcommand {
    Some(x) => {
        let data = &mut scratch_space[..3];
        data.copy_from_slice(&[command, x as u8, (x >> 8) as u8]);
        data
    },
    None => {
        scratch_space[0] = command;
        &scratch_space[..1]
    },
};
  1. If this is not possible, declare multiple such arrays and only initialize one. That is:
let scratch_space1;
let scratch_space2;
let request = match subcommand {
    Some(x) => {
        scratch_space1 = [command, x as u8, (x >> 8) as u8];
        &scratch_space1[..]
    },
    None => {
        scratch_space2 = [command];
        &scratch_space2[..]
    },
};

3 Comments

I should've added that heap allocators are not ideal for me because I'm running in no_std environment (this code prepares transactions for I2C bus on a small microcontroller).
@RomanLiutko then points 2 and 3 should fit for you, no?
@cafce25 yup. ArrayVec is an interesting option. Rearranging the code as illustrated works as well
1

The two answers above mine list excellent solutions. As mentioned in their answers:

  1. Rust must know the size of the return value because it exists on the stack. Returning two differently sized values doesn't work because it's not an exact size.
  2. You can't return a reference to data that exists on the stack because that would cause a use after free. Therefore, you can't return a slice.

Here is another solution that does not allocate on the heap.

use either::Either;

fn split(yes: bool) -> Either<[u8; 2], [u8; 4]> {
    if yes {
        Either::Left([1, 2])
    } else {
        Either::Right([1, 2, 3, 4])
    }
}

Either is a useful sum type of any two types. As a sum type, the size is the larger of the two variants plus a discriminant. The total size of the return value is five bytes in this case because [u8; 4] is the larger of the two values and the discriminant is one byte. If you don't want to add another crate just for Either, you can simply write your own since there isn't any magic involved in the type.

pub enum Either<L, R> {
    Left(L),
    Right(R)
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.