What's The Overhead of Calling Function Pointers Over FFI?

Yes, that is a usual question that arises when dealing with plugins: the idea is that the plugin will be providing some behavior, but often the plugin itself would like to internally use some behavior that the "host binary" can offer.

When linking, there is the --export-dynamic linker flag that can help with this (that is, when code inside the plugin calls dynamic_linking_loader_magic_find_symbol(...), the dynamic linking loader will be able to see the functions exported by the "host binary". But relying on such a specific linker flag can hinder portability, so the other approach is to have the plugin offer a registration facility to the "host binary":

fn init_host_binary_functions (
    log: extern "C" fn(&&str),
    sleep: extern "C" fn(usize),
    ...,
);

The host can expect the plugin to provide such as function:

extern "C" { /* Or manually using dlopen & friends */
    fn init_host_binary_functions (
        log: extern "C" fn(&&str),
        sleep: extern "C" fn(usize),
        ...,
    );
}

and the plugin can have a global struct to be overwritten with that:

struct HostFunctions {
    log: extern "C" fn(&&str),
    sleep: extern "C" fn(usize),
    ...,
}

static HOST_FUNCTIONS: OnceCell<HostFunctions> = OnceCell::new();

#[no_mangle] pub extern "C"
fn init_host_binary_functions (
    log: extern "C" fn(&&str),
    sleep: extern "C" fn(usize),
    ...,
)
{
    HOST_FUNCTIONS.set(HostFunctions { log, sleep, ... })
}

// so that it can now access such functions:

fn log (s: &'_ str)
{
    (HOST_FUNCTIONS.get().unwrap().log)(&s)
}

Yeah yeah, the hard-coded assembly implementation is hard to completely describe with the higher-level abstractions from programming languages, so I did mislabel by simplifying the things a bit :sweat_smile:

The closest I can describe that mechanism using higher-level language is that the find_foo is not a PLT entry but a PLT stub, and the real PLT entry is indeed wrapping the global:

unsafe extern "C" fn foo (...) -> _ // PLT
{
    static mut foo_ptr: unsafe extern "C" fn(...) -> _ = { // GOT
        unsafe extern "C" fn find_foo (...) -> _ // PLT stub
        {
            foo_ptr = dynamic_linking_loader_magic_find_symbol(b"foo\0");
            foo_ptr(...)
        }

        find_foo
    };
    foo_ptr(...)
}

EDIT

- log: fn (&'_ str)
+ log: extern "C" fn (&&str)

to make sure the passed function pointers have a stable ABI, so that the host binary and the plugin may be compiled using different versions of the Rust compiler.

1 Like