Getting around using an unsafe pointer

Hello.

I designed an animation module where you give it a pointer to some numeric data, your desired final value, and the duration in which you want it to get to that final value, and it will lerp and update the values as long as you keep calling .update() on the Animator.

Pretty happy with the code, here it is in it's full glory:

#[derive(Default)]
pub(crate) struct Animator {
    jobs: Vec<AnimationJob>,
    cleanup: Vec<usize>,
}

impl Animator {
    pub fn animate(&mut self, data: *mut f32, end_value: f32, seconds: f64) {
        self.jobs.push(AnimationJob::new(data, end_value, seconds));
    }

    pub fn update(&mut self) {
        // run jobs
        for (i, job) in self.jobs.iter_mut().enumerate() {
            job.process();

            if job.is_complete {
                self.cleanup.push(i);
            }
        }
        
        // remove any complete jobs
        while let Some(i) = self.cleanup.pop() {
            self.jobs.remove(i);
        }
    }
}

struct AnimationJob {
    data: *mut f32, // pointer to the data I will mutate
    value_start: f32,
    value_end: f32,
    timer: Timer,
    is_complete: bool,
}

impl AnimationJob {
    fn new(data: *mut f32, value_end: f32, seconds: f64) -> Self {
        Self {
            data,
            value_start: unsafe { *data },
            value_end,
            timer: Timer::new(seconds),
            is_complete: false,
        }
    }

    fn process(&mut self) {
        let step = self.timer.step();

        // update value based on lerp of time
        unsafe {
            *self.data = self.lerp(step as f32);
        };

        // determine status of job
        self.is_complete = step >= 1.;
    }

    fn lerp(&self, step: f32) -> f32 {
        self.value_start * (1. - step) + self.value_end * step
    }
}

#[derive(Debug)]
struct Timer {
    time_start: f64,
    time_end: f64,
}

impl Timer {
    fn new(seconds: f64) -> Self {
        let time_start = date::now();
        let time_end = time_start + seconds;
        Self {
            time_start,
            time_end,
        }
    }

    // 0.0 - 1.0 between beginning and end times
    fn step(&self) -> f64 {
        let step = (date::now() - self.time_start) / (self.time_end - self.time_start);
        if step > 1. {
            1.
        } else {
            step
        }
    }
}

But as you can see, there is one glaring problem. It needs to hold an unsafe pointer.

I could have it ask for a borrow every time it wants to update the value (rather than storing the pointer itself) but then my Animator module would have to know about things outside of the module, and how to request it.

I like that my module is completely agnostic.

So how would I get around this?

Given f32 is Copy, is their any reason data can't be passed by value like end_value and seconds

This not only avoids unsafe, but is also smaller than a *mut f32 (4 vs 8 bytes).

Yes because Animator needs to mutate the data.

You could store a closure that’s responsible for the actual updating, and call it in update with the new calculated value. Alternatively, if this value never needs to cross a thread boundary you could use a &Cell<f32> in place of the *mut f32.

1 Like

So the closure would have to hold the lerp logic? Are you suggesting a closure because then from outside the module, I can teach it how to request a borrow of the data?

I'm not very familiar with Cell What is this for / does it solve?

No, the closure holds the borrowing logic that lerp needs to store the calculated values; you’d pass it as a parameter to AnimatonJob::new (and therefore Animator::animate):

anim.animate(start, end, duration, |x| println!("{}", x))

where println is replaced with whatever code is necessary to update the final value. Inside animation_job::process, you replace the unsafe block with something like this:

(self.update_callback)(self.lerp(step as f32));

Cell is the most primitive of the safe interior mutability containers. It lets you change its contents through a shared & reference instead of an exclusive &mut reference. It’s also #[repr(transparent)], so it’s FFI-interchangable with its contents.

2 Likes

Thank you for the detailed answer.

I like the closure idea, i will investigate!

Regarding Cell, So what would it achieve that *mut doesn't already? Also, get and get_mut() return T not Option<T>, so if the location of T moved, that would be use after free, right?

&Cell<f32> is almost exactly the same as *mut f32, but is limited to operations that are always safe to do, and thus doesn’t require you to tag any of your code unsafe.

Cell<f32> itself isn’t a pointer, but an in-place wrapper around a f32 memory location. It lets you copy, swap, or overwrite the inner value through an &Cell reference. Cell::get_mut takes an &mut Cell reference, so the compiler has already verified that this is an exclusive access. In both cases, you’re talking about the value contained within the Cell itself, and so there’s no other pointer that might be freed.

AnimationJob::new is trivial UB. You just have to call the function with std::ptr::null() as *mut _ as an argument (which doesn't require unsafe). The function must be marked as unsafe and have its requirements documented, if you keep it as-is.

Yes, that's why it's the glaring problem I mentioned before, and am trying to find a solution around it.

This sounds like syncronisation. As well as Cell, you also have RefCell, Mutex, RwLock for controlling access to a value accross threads/functions. I don't know enough about you're goals to know which is right for you.

1 Like

What kind of pointer is *mut f32?

Regarding ownership:

  1. Is it an owning pointer, i.e. the animator is responsible for freeing the pointed-at memory or …
  2. is it a referencing pointer, i.e. the caller is responsible for freeing the corresponding memory after the animations are finished or …
  3. is it unclear who has to free the memory?

Regarding mutability:

  1. Is the raw pointer dereferenced and then modified outside of the animator while the raw pointer is also stored within the animator or …
  2. is it dereferenced and then read outside of the animator while the raw pointer is also stored within the animator or …
  3. is it only read from / written to outside the animator while the pointer isn't stored in the animator

Regarding lifetimes:

  1. Is the animator living for the whole program or …
  2. is it constructed and destroyed whenever a batch of animations are finished?

Regarding concurrency:

  1. Is the animator accessed from a single thread or …
  2. from multiple threads?

When dealing with raw pointers, a lot of questions have to be answered, if no sufficient example usage has been provided, otherwise it's easy to introduce UB or a variety of other bugs.

4 Likes

There's also AtomicU32, which you can convert to/from f32 with f32::from_bits/to_bits.

1 Like