It is incorrect to return reference to stack-allocated data, as it will immediately outlive the object it refers to. The only kind of references that can always be returned, no questions asked, are those whose lifetime is 'static
- which Rust will carefully check. References to freshly allocated data are definitely not 'static
.
Fortunately, there is a way around it: it is safe to return a reference when Rust can prove that the reference outlives the data. For example:
// Memory backed by a Vec
struct VecMemory {
data: Vec<u8>
}
impl VecMemory {
fn as_slice(&self) -> &[u8] {
&self.data
}
}
as_slice()
may return a reference because that reference provably outlives the object it refers to. If we undo the lifetime elision, the signature of as_slice()
would be:
fn as_slice<'a>(&'a self) -> &'a [u8]
The next question is what should the closure return? If it returned a Vec
, as suggested by @E_net4, or a VecMemory
(which again just holds a Vec
), then the use of a vector as the underlying storage would be baked into the interface. To support different storage types, the closure should return what other languages would call an interface. The closest Rust equivalent is a trait object, which is specified in return context as Box<SomeTrait>
.
With this design, the closure effectively heap-allocates a resource management object and returns a two-pointer-sized box that provides ownership and a uniform interface to the heap-allocated value. The user of the box is communicating with the implementation only through the box, which uses an internal vtable to talk to the implementation. (Pointer to the vtable is the reason why the Box
itself takes up two pointers, not one.) In other words, return value of the closure is such that it erases the concrete type returned.
use std::fs::File;
use std::io::prelude::*;
trait Memory {
fn as_slice(&self) -> &[u8];
// a real-life trait would likely also define
// as_slice(&mut self) -> &mut [u8]
}
// Memory backed by a Vec
struct VecMemory {
data: Vec<u8>
}
impl Memory for VecMemory {
fn as_slice(&self) -> &[u8] {
&self.data
}
}
fn main() {
test(&|path| {
if false {
let mut data: Vec<u8> = Vec::new();
let mut file = File::open(path).unwrap();
file.read_to_end(&mut data).unwrap();
return Some(Box::new(VecMemory { data: data }));
}
None
});
}
// loader returns a boxed trait object whose underlying memory
// can be accessed as long as the box is alive.
fn test<'a>(_loader: &Fn(&str) -> Option<Box<Memory>>) {}
To use mmap
for the storage, one would write a different Memory
implementation, say Mmap
. This one would store the raw pointer and the size of the memory returned by mmap()
. It would call mmap()
in new()
and munmap()
in Drop::drop
. Most importantly, Mmap
would implement Memory
using an unsafe block to construct a slice from the stored pointer and length. Again, this is safe because lifetime of the reference will be tied to the lifetime of Mmap
.