1

I have two adjacent slices in memory. I know they are adjacent because I just created them from a single slice with split_at. I want to concatenate them together to get a slice covering the whole range again. Is there any way I can do that safely? Ideally, some function that takes multiple slices, sees if they are adjacent to each other, and merges them together if possible.

fn main() {
    let original_slice: &[u8] = &[1, 2, 3, 4];
    
    let (left, right) = original_slice.split_at(2);
    
    let rejoined_slice = todo!("take left and right and return an Option or similar")
    
    assert_eq!(original_slice, rejoined_slice);
    assert_eq!(original_slice.as_ptr(), rejoined_slice.as_ptr());
}

Playground

This is coming up for me using the zerocopy crate to do some zero-copy parsing. I sometimes end up with multiple slices of the same type that I know are next to each other (after all, I just parsed them, checked their alignment, etc.), and it would be useful to fuse them together.

Not a duplicate of this question: I don't want to end up with a new Vec, I want to fuse the already-adjacent slices in place without copying.

ddulaney
  • 843
  • 7
  • 19

1 Answers1

3

This is actually somewhat of a controversial topic: it is not decided whether this kind of capability should be possible at all, and the current consensus (from my interpretation of discussions) seems to lean against it. From the documentation in std::ptr:

Provenance

This section is non-normative and is part of the Strict Provenance experiment.

Pointers are not simply an “integer” or “address”. For instance, it’s uncontroversial to say that a Use After Free is clearly Undefined Behaviour, even if you “get lucky” and the freed memory gets reallocated before your read/write (in fact this is the worst-case scenario, UAFs would be much less concerning if this didn’t happen!). To rationalize this claim, pointers need to somehow be more than just their addresses: they must have provenance.

...

Shrinking provenance cannot be undone: even if you “know” there is a larger allocation, you can’t derive a pointer with a larger provenance. Similarly, you cannot “recombine” two contiguous provenances back into one (i.e. with a fn merge(&[T], &[T]) -> &[T]).


If it is ultimately decided that widening provenance is allowed, you could accomplish this with a bit of pointer operations:

/// SAFETY: Both parameters must refer to the same allocated object.
unsafe fn rejoin<'a, T>(a: &'a [T], b: &'a [T]) -> Option<&'a [T]> {
    if a.as_ptr().add(a.len()) == b.as_ptr() {
        Some(std::slice::from_raw_parts(a.as_ptr(), a.len() + b.len()))
    } else {
        None
    }
}

But note that I've still made the function unsafe and left a comment documenting so. Two slices could theoretically be adjacent while being separate objects. For example, let a = [1, 2]; and let b = [3, 4]; can reasonably be contiguous variables on the stack, but it would be undefined behavior to access them as a single slice. Because of this, the function should be unsafe and the caller must guarantee they are from the same originating object.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • That makes sense. I can see why it's not safely possible, and I'm going to go with a fully-safe codepath that doubles up on some parsing work until I have evidence that I really need this. Thanks for the explanation and the example! – ddulaney Apr 15 '23 at 18:42