Store arbitrary function pointers to HashMap

Hi I'm new to Rust.

Reading the source code of libloading crate, although I can't fully understand the source code, It seems that it returns function pointer directly from dlsym() call and there is no function pointer caching mechanism when lib.get() is called.

See this link below:
https://github.com/nagisa/rust_libloading/blob/master/src/os/unix/mod.rs

And calling dlsym() many times is not good at performance.
https://stackoverflow.com/questions/10500079/should-i-cache-dlsyms-return-value

What I want to do is write a wrapper struct that holds a hash map and libloading struct, And a function that caches any types of function pointer from dlsym() call to the hash map.

let lib_wrapper = LibWrapper::new("/path/to/library.so");
let some_function: fn(u32) -> u32 = lib_wrapper.get(b"some_function"); // It caches function pointer internally to the hash map.
let some_function2: fn(u32, u32) = lib_wrapper.get(b"some_function2");

The problem is hash map part. How can I make an hash map that accepts any type of function pointers?(is it possible in Rust?)
It would be easy if i could use void pointer in the hash map like this:

let mut hash_map: HashMap<&str, *const void> = HashMap::new();
hash_map.insert("some_function", lib.get(b"some_function") as *const void);

A more type-safe alternative to void pointers is to use trait objects based on the Any trait (&Any or Box<Any> depending on ownership requirements).

It allows the implementation to check that the function pointer is indeed of the expected type, thusly avoiding "calling the wrong function" undefined behaviour.

On the other hand, you will pay a little bit of runtime overhead for this extra safety. But it should not be noticeable considering that you are already using a hashmap keyed on strings, which is a relatively high overhead container to begin with (every lookup needs to scan the string).


EDIT: Alternatively, you may cache each function pointer in isolation, at the individual library wrapper layer, without using a "global" type-erased HashMap. This will be much more efficient (no string-based hashtable lookup and no need for type checks on every access), but requires symbol-specific boilerplate, which can be autogenerated by a macro.

I would create a newtype for each function you want to support, and then store them all in a typemap.

The approach I used here seemed to work as well:
https://github.com/craftytrickster/mock_me/blob/master/src/lib.rs#L71

You cast the functions as usize and then you add them to the hasmap.
When you retrieve the usize value from the hashmap afterwards, you call std::mem::transmute to
convert it back into the desired function.

It's similar to the approach from @HadrienG , without the Any. (And without the type safety!).

1 Like

I tried both approaches but I managed to write codes that only work for @CraftyTrickster approach. Can you show some example codes if you don't mind @HadrienG ?

fn test_function1(x: u32) -> u32 {
    println!("I received {} and returning it", x);
    x
}

fn test_function2(x: u32) {
    println!("I received {} and not returning it", x);
}

fn main() {
    use std::collections::HashMap;
    use std::any::Any;
    use std::mem::transmute;
    /*
     * Panic!: Option::unwrap() on None value
    let mut fp_map: HashMap<&str, &Any> = HashMap::new();
    fp_map.insert("f1", &test_function1);
    fp_map.insert("f2", &test_function2);
    let f1 = *fp_map.get("f1").unwrap();
    let f1 = f1.downcast_ref::<&fn(u32) -> u32>().unwrap();
    */
    /*
    let mut fp_map: HashMap<&str, Box<Any>> = HashMap::new();
    fp_map.insert("f1", Box::new(test_function1));
    fp_map.insert("f2", Box::new(test_function1));
    let f1 = *(fp_map.get("f1").unwrap());
    */
    let mut fp_map: HashMap<&str, *const ()> = HashMap::new();
    fp_map.insert("f1", test_function1 as *const ());
    fp_map.insert("f2", test_function2 as *const ());

    let f1: fn(u32) -> u32 = unsafe {
        transmute::<*const (), fn(u32) -> u32>(*fp_map.get("f1").unwrap())
    };
    f1(100);
    let f2: fn(u32) = unsafe {
        transmute::<*const (), fn(u32)>(*fp_map.get("f2").unwrap())
    };
    f2(10);
}

Edit: I said the code in the comment failed to compile, but it actually compiles and panics.

You need to cast the fn items to an fn pointer: playground

2 Likes

It's interesting that casting make differences!
I don't understand why omitting cast when inserting function to the hash map cause HashMap::get() to return None value. Isn't test_function1 is already type of fn(u32) ->u32 ?

An fn item is not the same thing as an fn pointer; the item is a pointer to a specific function, whereas an fn pointer is a “generic” type that’s not associated with a specific fn. But you can cast an fn item to the fn pointer type to get the generic type.

More info/background: https://github.com/rust-lang/rust/pull/19891

2 Likes

It would be better if trpl mentioned about it..
Thanks for the info!