Как написать метод класса оболочки C ++ для функции C, которая принимает обратный вызов? - PullRequest
0 голосов
/ 17 сентября 2018

Учитывая следующий интерфейс 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);
// ...
};

Моя цель - сделать возможным передать лямбда-функцию захвата в AWS::subscribe. Я уже почти неделю пытаюсь использовать разные подходы, но ни один из них, похоже, не сработал.

Дайте мне знать, если что-то еще понадобится, чтобы понять проблему, я с удовольствием обновлю вопрос.

Ответы [ 2 ]

0 голосов
/ 17 сентября 2018

Основной подход - хранить где-то копию 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, поэтому не могу сказать вам, когда можно безопасно удалить копию обратного вызова.


Примечание: очевидно, вам нужно extern "C" для обратного вызова, даже еслиего имя никогда не просматривается кодом C, поскольку оно не только влияет на искажение имени, но также гарантирует, что код использует соглашение о вызовах, ожидаемое C .

0 голосов
/ 17 сентября 2018

Почему это не работает просто так

Причина, по которой вы не можете просто передать функцию C++ в C API, заключается в том, что у них потенциально разные соглашения о вызовах. Синтаксис extern "C" указывает компилятору C++ использовать нотацию C для отдельной функции или для всего блока кода, если используется как extern "C" { ... }.

Как заставить это работать

Создает одноэлементную оболочку C ++ вокруг C API, отвечающую за инициализацию / финализацию последних и переадресацию вызовов и обратных вызовов туда и обратно. Важно отметить, что следует попытаться минимизировать количество необработанных мировых указателей C ++ в C API, чтобы сделать возможным чистое управление памятью.

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);
}
...