Questions on design for swappable functions

I thought up the following code for swapping out functions at runtime (like a function that by default doesn't output logs while in production, but can be swapped with one that does to debug in-situ) but have a few questions about whether it's valid or not

use std::{sync::atomic::{AtomicUsize, Ordering}, marker::PhantomData};

pub struct SwappableFn<I, O> {
    current: AtomicUsize,
    __marker: PhantomData<fn(I) -> O>,
}

impl<I, O> SwappableFn<I, O> {
    pub fn new(default: fn(I) -> O) -> Self {
        Self { current: AtomicUsize::new(default as usize), __marker: PhantomData }
    }

    pub fn get(&self) -> fn(I) -> O {
        let ptr = self.current.load(Ordering::SeqCst) as *mut ();
        let fn_ptr = (&ptr) as *const *mut () as *const fn(I) -> O;
        unsafe { *fn_ptr }
    }

    pub fn set(&self, new: fn(I) -> O) {
        self.current.store(new as usize, Ordering::SeqCst)
    }
}

Main questions:

  • Is the function pointer returned from get valid?
  • Are there any issues for swapping the function while it is being run on another thread?
  • Should I use AtomicPtr<()> instead of AtomicUsize?

Welcome to point out anything else you notice, though :slight_smile:

(yeah i know the API is kinda awkward, such as requiring tuples to have multiple arguments. it's more of a "can I do this?" rather than "should I do this?")

There's no need to re-invent the transmute_copy yourself.

    let ptr = self.current.load(Ordering::SeqCst);
    unsafe { std::mem::transmute::<usize, fn(I) -> O>(ptr) }

You're not modifying the machine code in memory. What's stored within the AtomicUsize is an address to the machine code which is the start of the function.

AFAIK, yes

Could you please tell how you'd be doing the swap?

I don't think it would make a real difference. An AtomicUsize is okay.

I tend to try to avoid using transmute wherever I can, but if it's okay then sure

I'm aware of that, that's what I thought, thanks :slight_smile:

Exactly as is shown in the snippet, AtomicUsize::store

So if I am not mistaken, you'd be doing something like this:

// Assume the log functions have a "type"
// fn(&'static str) -> ()
let log_fn = SwappableFn::new(prod_log);
// To actually call the following line, you'd need to make log_fn
// Send and Sync.
thread::spawn(|| loop {
    let logger = log_fn.get();
    logger("Hola!");
});
thread::sleep(Duration::from_secs(5)); // Just for kicks
log_fn.set(debug_log);

Since you're using atomics to load/store the function pointer, I think it'd work.

Yep, exactly!

What you wrote is a reimplemtation of the transmute_copy, which is even worse as it doesn't check the size. Also I disagree implementing your own transmute would be better than using the one in the stdlib.

3 Likes

Oh yeah, that is almost exactly the same, I'll use that instead. Thanks :+1:

Um, why do you prefer one with less static check?

I thought you were encouraging using that one..? Sorry, I must've misunderstood.

transmute checks the size, not transmute_copy

Yes, it will be easier to verify if you stay pointers, because you can't accidentally fall into this potential source of UB.

See What about: Pointer-to-integer transmutes? · Issue #286 · rust-lang/unsafe-code-guidelines · GitHub

I doubt function pointers have provenance. I guess it doesn't make sense to get some function pointer from another function pointer using pointer arithmetics, which is the purpose of the provenance.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.