I know the answer is "you shouldn't"... but for the sake of argument, how should you do it?
For example, if you wanted to write an alternative to Vec<T>
that worked differently.
I see that you can make 'something that compiles and runs' by transmuting * mut T
values into u64
and adding to them, then transmuting them back into * mut T
and reading the value at the pointer (see example below). It seems to work, but it leaves a few of open questions:
Will a * mut T
pointer always fit into u64
?
Does write()
ing to an unsafe pointer trigger pointer aliasing issues when the data is an arbitrary (i.e. not a managed type) data block from libc:calloc
?
This only works because I'm using a primitive type (f64
). If this was a real data object, I would have to forget()
the object first; but can you simply write()
a * mut T
into a target and then happily read()
it out again later if the type is complex and has child records?
Is this really the right way of doing this? It seems extremely awkward. I was expecting to find some unsafe ptrtoint()
/ inttoptr()
pair, but I can't find anything like that.
extern crate libc; use std::mem::size_of; use std::ptr::write; use std::ptr::read; use std::mem::transmute; use libc::calloc; use libc::free; use libc::c_void; struct Array { length: usize, data: *mut f64, } impl Array { fn new(length: usize) -> Array { unsafe { Array { length: length, data: calloc(size_of::<f64>(), length) as *mut f64, } } } fn set(&mut self, offset: usize, value: f64) { if offset < self.length { unsafe { let root: *mut f64 = transmute(transmute::<*mut f64, u64>(self.data) + (size_of::<f64>() * offset) as u64); println!("Write: [{:?}] -> {}", root, value); write(root, value); } } else { println!("Write: Nope: [{}] is out of bounds", offset); } } fn get(&self, offset: usize) -> f64 { if offset < self.length { unsafe { let root: *const f64 = transmute(transmute::<*mut f64, u64>(self.data) + (size_of::<f64>() * offset) as u64); let rtn = read::<f64>(root); println!("Read: [{:?}] -> {}", root, rtn); return rtn; } } println!("Read: Nope: [{}] is out of bounds", offset); 0.0 } } impl Drop for Array { fn drop(&mut self) { unsafe { free(self.data as *mut c_void); } } } fn main() { let mut tmp = Array::new(4); tmp.set(0, 100.5); tmp.set(1, 101.5); tmp.set(2, 102.5); tmp.set(3, 103.5); tmp.set(4, 104.5); tmp.get(0); tmp.get(1); tmp.get(2); tmp.get(3); tmp.get(4); }
Write: [0x7f04bdc1e080] -> 100.5 Write: [0x7f04bdc1e088] -> 101.5 Write: [0x7f04bdc1e090] -> 102.5 Write: [0x7f04bdc1e098] -> 103.5 Write: Nope: [4] is out of bounds Read: [0x7f04bdc1e080] -> 100.5 Read: [0x7f04bdc1e088] -> 101.5 Read: [0x7f04bdc1e090] -> 102.5 Read: [0x7f04bdc1e098] -> 103.5 Read: Nope: [4] is out of bounds
Much of Rust's safety comes from compile-time checks, but raw pointers don't have such guarantees, and are unsafe to use. *const T and *mut T are called 'raw pointers' in Rust.
A pointer is a general concept for a variable that contains an address in memory. This address refers to, or “points at,” some other data. The most common kind of pointer in Rust is a reference, which you learned about in Chapter 4. References are indicated by the & symbol and borrow the value they point to.
Address arithmetic is a method of calculating the address of an object with the help of arithmetic operations on pointers and use of pointers in comparison operations. Address arithmetic is also called pointer arithmetic.
Pointers have an offset
method for pointer arithmetic.
fn main() { let items = [1usize, 2, 3, 4]; let ptr = &items[1] as *const usize; println!("{}", unsafe { *ptr }); println!("{}", unsafe { *ptr.offset(-1) }); println!("{}", unsafe { *ptr.offset(1) }); }
Output
2 1 3
https://doc.rust-lang.org/nightly/book/first-edition/unsafe.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With