1

我正在尝试为一个名为 libumqtt 的库实现一个包装器。Libumqtt是一个 C 库,它使用 libev 对来自 MQTT 协议的事件进行回调。

直到前几天我才意识到我无法将成员函数传递给需要正常静态函数的函数。这会导致问题,因为我计划启动多个 libumqtt 实例以同时处理多个连接。

我的代码在 C++ 中,因为这是与 Godot 的(游戏引擎)GDNative 模块一起使用最方便的。

在研究一种将 ac 库的多个实例沙箱化或以某种方式使指针正常工作的方法时,我找到了这个答案。我不明白答案中的这句话:

如果您需要访问类的任何非静态成员并且需要使用函数指针,例如,因为该函数是 C 接口的一部分,那么您最好的选择是始终将 void* 传递给采用函数指针的函数并通过转发函数调用您的成员,该函数从 void* 获取对象,然后调用成员函数。

我想要做的是设置回调,当它同时处理可能多达 500 个或更多连接时,libev 将使用该回调将数据发送到我的对象的正确实例。

通过 void* 会帮助我实现我的目标吗?我将如何实现它?另外,转发功能如何工作?

编辑:提供核桃要求的代码示例

下面的这个例子来自我使用静态函数的类的一个版本。如果我尝试在函数不是静态的情况下使用 run this,那么我会收到一个错误,即无法传入成员函数来代替常规函数。

// Client.cpp
void Client::signal_cb(struct ev_loop *loop, ev_signal *w, int revents) {
    ev_break(loop, EVBREAK_ALL);
}

// ...

void Client::do_connect(struct ev_loop *loop, struct ev_timer *w, int revents) {
    //Godot::print("Attempt MQTT Start!!!\n");
    //write_log("debug", "MQTT Wrapper - Attempt MQTT Start!!!");
    struct umqtt_client *cl; // Move to Class Access (Private)?

    cl = umqtt_new(loop, cfg.host, cfg.port, cfg.ssl);
    if (!cl) {
        //Godot::print("Failed To Create Client!!!\n");
        //write_log("debug", "MQTT Wrapper - Failed To Create Client!!!");
        start_reconnect(loop);
        return;
    }

    //Godot::print("Setup Client Callbacks!!!\n");
    //write_log("debug", "MQTT Wrapper - Setup Client Callbacks!!!");

    // For StackOverflow: These cl->... lines do not work because of not being able to pass a member function as a regular function. These are the main callbacks I have trouble with.
    // How do I convert from `void (libumqtt::Client::*)(struct umqtt_client *)` to `void (*)(struct umqtt_client *)`?
    cl->on_net_connected = Client::on_net_connected; // Pass member function as a non-static object
    cl->on_conack = Client::on_conack; // Pass member function as a non-static object
    cl->on_suback = Client::on_suback; // Pass member function as a non-static object
    cl->on_unsuback = Client::on_unsuback; // Pass member function as a non-static object
    cl->on_publish = Client::on_publish; // Pass member function as a non-static object
    cl->on_pingresp = Client::on_pingresp; // Pass member function as a non-static object
    cl->on_error = Client::on_error; // Pass member function as a non-static object
    cl->on_close = Client::on_close; // Pass member function as a non-static object

    //Godot::print("MQTT Start!!!\n");
    //write_log("debug", "MQTT Wrapper - MQTT Start!!!");
}

void Client::initialize() {
    // For StackOverflow: These two lines cannot work in an object as libev expects signal_cb and do_connect to be regular functions. These callbacks are also necessary, but I am not sure how to handle this.
    ev_signal_init(&signal_watcher, Client::signal_cb, SIGINT);
    ev_timer_init(&reconnect_timer, Client::do_connect, 0.1, 0.0); // Fix Me - Make ev.h object

    // ...
}

编辑:我应该提到我是使用 C 和 C++ 的菜鸟。我之前做的最多的是测试缓冲区溢出。因此,如果我显然做错了什么,我将不胜感激评论中的提示。

4

1 回答 1

1

所以问题是它umqtt_client似乎没有提供任何将额外的用户数据传递给回调的方式(void*在你的报价中提到)。它期望回调只接受一个指向umqtt_client实例的指针。(我在这里可能错了,我只是基于快速查看源文件。)


如果您的成员函数实际上没有访问您的类的任何非静态成员,那么您可以简单地使它们static. 然后您可以直接将它们用作普通函数指针。


否则,您需要从指针中获取指向您的实例的umqtt_client*指针。这样做的一种方法是在指针之间维护一个静态映射,例如在Client添加声明中:

static std::map<umqtt_client*, Client*> umqtt_client_map;

并在创建时插入其中Client(我在这里假设您实际上将cl指针维护为 的类成员Client),最好在Client的构造函数中:

umqtt_client_map[cl] = this;

然后 inClient的析构函数(或umqtt_client对象被销毁的地方)从地图中删除相应的元素:

umqtt_client_map.erase(cl);

然后你可以使用一个看起来像这样的 lambda 作为回调传递:

cl->on_net_connected = [](umqtt_client* ptr){
    umqtt_client_map[ptr]->on_net_connected();
};

请注意,on_net_connected如果它是类的成员,则不需要指针作为参数。

这还假设您使类不可复制和不可移动,或者您使用正确的擦除和插入语义来实现复制和移动构造函数和赋值运算符umqtt_client_map


该库似乎提供了一个函数umqtt_init,而不是umqtt_new不分配umqtt_client对象。如果您改用它,您可以执行以下操作:

将其包装umqtt_client在一个小的标准布局类中:

struct umqtt_client_wrapper {
    umqtt_client cl; // must be first member!
    Client* client;
    static_assert(std::is_standard_layout_v<umqtt_client_wrapper>);
};

然后,您将使用它作为成员Client而不是umqtt_client*直接使用该umqtt_client*客户端的构造函数初始化umqtt_init) and客户端。然后您可以在 lambda 中使用强制转换来进行回调:within

cl->on_net_connected = [](umqtt_client* ptr) {
    reinterpret_cast<umqtt_client_wrapper*>(ptr)->client->on_net_connected();
};

请注意,这取决于umqtt_client_wrapper标准布局,这umqtt_client*是它的第一个成员。不遵守这些规则将导致未定义的行为。这static_assert提供了一些保证,至少它的一部分不会被意外违反。它需要#include<type_traits>我在此处使用的形式的 C++17。

同样,这需要特别注意正确实现复制和移动特殊成员函数Client或删除它们,但使用此方法不需要在析构函数中执行任何操作。

这种方法比另一种方法性能更高,原则上,Client如果您确保Client本身是标准布局,则可以避免额外的指针,但这可能过于严格和冒险。


另一种节省额外间接性的方法是将包装器用作以下的基类Client

struct umqtt_client_wrapper {
    umqtt_client cl; // must be first member!    
    static_assert(std::is_standard_layout_v<umqtt_client_wrapper>);
};

然后让Client继承自umqtt_client_wrapper,您可以使用:

cl->on_net_connected = [](umqtt_client* ptr) {
    static_cast<Client*>(reinterpret_cast<umqtt_client_wrapper*>(ptr))
        ->on_net_connected();
};

请注意,这里的第一个强制转换必须是static_cast,否则您很容易导致未定义的行为。

与前面相同的注释适用。

于 2019-12-10T03:20:33.637 回答