如何编写需要回调的C函数的C ++包装类方法?

时间:2018-09-17 08:42:22

标签: c++ wrapper

给出以下C接口:

IoT_Error_t aws_iot_mqtt_subscribe(AWS_IoT_Client *pClient,
                                   const char *pTopicName,
                                   uint16_t topicNameLen,
                                   QoS qos,
                                   pApplicationHandler_t pApplicationHandler, 
                                   oid *pApplicationHandlerData);

“ {aws_iot_mqtt_subscribe存储其参数以供以后引用-以响应某个事件在稍后的某个时间点调用”

处理程序:

typedef void (*pApplicationHandler_t)(
    AWS_IoT_Client *pClient,
    char *pTopicName,
    uint16_t topicNameLen,
    IoT_Publish_Message_Params *pParams,
    void *pClientData);

我试图将其包装到一个具有以下接口的C ++类中:

class AWS {
// ...
public:
  void subscribe(const std::string &topic,
                 std::function<void(const std::string&)> callback);
// ...
};

我的目标是使传递捕获的lambda函数到AWS::subscribe成为可能。近一个星期以来,我一直在尝试使用不同的方法,但似乎没有一个起作用。

让我知道是否还有其他需要了解的问题,我很乐意更新问题。

2 个答案:

答案 0 :(得分:3)

为什么它不能那样工作

您不能仅将C++函数传递给C API的原因是因为这两个函数可能具有不同的调用约定。 extern "C"语法是告诉C++编译器对单个函数或整个代码块(如C一样使用extern "C" { ... }表示法。

如何使其发挥作用

围绕C API创建一个单例C ++包装器,以负责后者的初始化/完成以及来回转发调用和回调。重要的是,它应该尽量减少进入C API的原始C ++世界指针的数量,以使干净的内存管理成为可能。

godbolt //为拙劣的语法道歉,最近Java太多了:-)

extern "C" {
    void c_api_init();
    void c_api_fini();
    void c_api_subscribe(
        char const* topic,
        void(*cb)(void*),
        void* arg);
}

// this is the key of the trick -- a C proxy
extern "C" void callback_fn(void* arg);

using callaback_t = std::function<void(std::string const&)>;

struct ApiWrapper {
    // this should know how to get the singleton instance
    static std::unique_ptr<ApiWrapper> s_singleton;
    static ApiWrapper& instance() { return *s_singleton; }

    // ctor - initializes the C API
    ApiWrapper(...) { c_api_init(); }

    // dtor - shuts down the C API
    ~ApiWrapper() { c_api_fini(); }

    // this is to unwrap and implement the callback
    void callback(void* arg) {
        auto const sub_id = reinterpret_cast<sub_id_t>(arg);
        auto itr = subs_.find(sub_id);
        if (itr != subs_.end()) {
            itr->second(); // call the actual callback
        }
        else {
           std::clog << "callback for non-existing subscription " << sub_id;
        }
    }

    // and this is how to subscribe
    void subscribe(std::string const& topic, callaback_t cb) {
        auto const sub_id = ++last_sub_id_;
        subs_[sub_id] = [cb = std::move(cb), topic] { cb(topic); };
        c_api_subscribe(topic.c_str(), &callback_fn, reinterpret_cast<void*>(sub_id));
    }

private: 
    using sub_id_t = uintptr_t;
    std::map<sub_id_t, std::function<void()>> subs_;
    sub_id_t last_sub_id_ = 0;
};

创建一个C代理以在C API和C ++包装器之间架起桥梁

// this is the key of the trick -- a C proxy
extern "C" void callback_fn(void* arg) {
    ApiWrapper::instance().callback(arg);
}

答案 1 :(得分:3)

基本方法是将callback的副本存储在某个位置,然后将指针作为pApplicationHandlerData传递给它。

赞:

extern "C"
void application_handler_forwarder(
    AWS_IoT_Client *pClient,
    char *pTopicName,
    uint16_t topicNameLen,
    IoT_Publish_Message_Params *pParams,
    void *pClientData
) {
    auto callback_ptr = static_cast<std::function<void(const std::string&)> *>(pClientData);
    std::string topic(pTopicName, topicNameLen);
    (*callback_ptr)(topic);
}

这是您的(C兼容)通用处理程序函数,仅转发给std::function引用的pClientData

您将在subscribe中将其注册为

void AWS::subscribe(const std::string &topic, std::function<void(const std::string&)> callback) {
    ...
    aws_iot_mqtt_subscribe(pClient, topic.data(), topic.size(), qos,
         application_handler_forwarder, &copy_of_callback);

其中copy_of_callbackstd::function<const std::string &)>

唯一棘手的部分是管理回调对象的生存期。您必须在C ++部分中手动执行此操作,因为只要订阅有效,它就必须一直存在,因为application_handler_forwarder的地址将被调用。

您不能只将指针传递给参数(&callback),因为参数是一个局部变量,函数返回时该局部变量将被销毁。我不知道您的C库,因此无法告诉您何时可以安全删除回调副本。


N.B:显然,即使C代码从未显示过其名称,您也需要在回调上使用extern "C",因为它不仅会影响名称修饰,而且ensures the code uses the calling convention expected by C也是如此。