cxx icon indicating copy to clipboard operation
cxx copied to clipboard

how to get a function pointer out of rust::Fn?

Open ian-p-cooke opened this issue 4 years ago • 3 comments

this is a follow up to my previous work on getting PcapPlusPlus to work from Rust.

I'd like to stick this thing in its own crate so I can't use a cxx-generated binding for my callback. I think instead I need to pass in the Rust function as a callback. The library is C++ but they use a function pointer for the callback instead of std::function. So I started with my bridge's extern C++ secion like:

 unsafe fn read_pcap(fileName: &str,
                     onMessageReady: unsafe fn(side: i8, tcpData: &TcpStreamData, userCookie: *mut c_void),
                     userCookie: *mut c_void,
                     bpfFilter: &str);

which makes the C++ side like:

void read_pcap(rust::Str fileName,
               rust::Fn<void(int8_t side, const pcpp::TcpStreamData &tcpData, c_void *userCookie)> onMessageReady,
               c_void *userCookie,
               rust::Str bpfFilter)

but now I have to make PcapPlusPlus invoke my rust::Fn and it wants a function pointer like:

typedef void(*  OnTcpMessageReady) (int8_t side, const TcpStreamData &tcpData, void *userCookie)

what's the best way to get a function pointer out of the rust::Fn?

I found an answer about function pointers and std::function at SO but many answers were either "you can't do that" or "you shouldn't do it". I did use the code from the answer re: fun_ptr_helper and that did work but I'm wary of it. What I did was:

std::function<void(int8_t, const pcpp::TcpStreamData &, c_void *)> onMessageReadyFunction = *onMessageReady;
auto onMessageReadyCallback = get_fn_ptr<0>(onMessageReadyFunction);
pcpp::TcpReassembly tcpReassembly(onMessageReadyCallback, userCookie);

so that works, but is get_fn_ptr legit?

ian-p-cooke avatar Jul 03 '21 01:07 ian-p-cooke

digging into things a little bit more it seems like maybe the private members of Fn could be used to add a public method that returned a function pointer to the underlying bridge function? That sounds a lot better than using that get_fn_ptr code. I'll keep digging.

ian-p-cooke avatar Jul 03 '21 02:07 ian-p-cooke

I think I understand the SO answer now and I see why people don't like it: if it was a library function that UniqueID template parameter can't be made to be unique across libraries including it so it would lead to the wrong function being called sometimes. Also, the static _inst makes it not thread-safe.

I think for my purposes I am going to use something similar since I'm not making that header public and I can control all of the invocations of get_fn_ptr to make sure they're unique. I think making the _inst thread_local is enough to make it thread-safe? I can also get rid of the std::function layer too.

Is there a way that cxx could generate code that would let each instance of rust::Fn be able to return a function pointer? I looked at the code gen and couldn't figure out what to change/add to provide a function pointer to the trampoline without the last argument (fn).

Here's what I'm actually using now but I don't like it a lot:

$ cat include/fn_ptr_helper.hpp
#include "rust/cxx.h"

using rust::Fn;

template <const size_t UniqueId, typename Ret, typename... Args>
struct fn_ptr_helper
{
public:
    typedef Fn<Ret(Args...)> fn_type;

    static void bind(const fn_type *fn) {
        instance().fn_ = fn;
    }

    static Ret invoke(Args... args) {
        return (*instance().fn_)(args...);
    }

    typedef decltype(&fn_ptr_helper::invoke) pointer_type;

    static pointer_type ptr() {
        return &invoke;
    }

private:
    static fn_ptr_helper& instance() {
        thread_local fn_ptr_helper inst_;
        return inst_;
    }

    fn_ptr_helper() : fn_(nullptr) {}

    const fn_type *fn_;
};

template <const size_t UniqueId, typename Ret, typename... Args>
typename fn_ptr_helper<UniqueId, Ret, Args...>::pointer_type
get_fn_ptr(const Fn<Ret(Args...)> *f) {
    fn_ptr_helper<UniqueId, Ret, Args...>::bind(f);
    return fn_ptr_helper<UniqueId, Ret, Args...>::ptr();
}

ian-p-cooke avatar Jul 04 '21 01:07 ian-p-cooke

rust::Fn is not the right type for that use case. They correspond to functions with a Rust ABI, so there is a trampoline to translate the ABI between C and Rust; it's not something that the library you are using could call via a single-word C function pointer. That's why you see the SO hack stashing the necessary context for the trampoline entry point into thread_local.

The type you want is extern "C" fn in Rust — something like:

#[cxx::bridge]
mod ffi {
    extern "C++" {
        include!("...");

        type OnMessageReadyCallback = crate::OnMessageReadyCallback;

        unsafe fn read_pcap(
            fileName: &str,
            onMessageReady: OnMessageReadyCallback,
            userCookie: *mut c_void,
            bpfFilter: &str,
        );
    }
}

use cxx::{type_id, ExternType};

#[repr(transparent)]
pub struct OnMessageReadyCallback(
    pub extern "C" fn(side: i8, tcpData: &TcpStreamData, userCookie: *mut c_void),
);

unsafe impl ExternType for OnMessageReadyCallback {
    type Id = type_id!("OnMessageReadyCallback");
    type Kind = cxx::kind::Trivial;
}

fn main() {
    extern "C" fn callback(side: i8, tcpData: &TcpStreamData, userCookie: *mut c_void) {...}
    let callback = OnMessageReadyCallback(callback);
    ffi::read_pcap("...", callback, userCookie, "...");
}
#pragma once
#include "..."

using OnMessageReadyCallback = void(*)(int8_t side, const TcpStreamData &tcpData, void *userCookie);

void read_pcap(rust::Str fileName,
               OnMessageReadyCallback onMessageReady,
               void *userCookie,
               rust::Str bpfFilter);

dtolnay avatar Sep 05 '21 06:09 dtolnay