haxpanel
haxpanel

Reputation: 4678

How to write a C++ wrapper class method of a C function that takes callback?

Given the following C interface:

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 stores its arguments for latter reference - to call, in response to some event at some later point in time"

Handler:

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

I am trying to wrap this into a C++ class that would have the following interface:

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

My goal is to make it possible to pass a capturing lambda function to AWS::subscribe. I have been trying with different approaches for a nearly a week now but none of them seemed to work.

Let me know if anything else needed to understand the problem, I'm happy to update the question.

Upvotes: 10

Views: 1378

Answers (2)

bobah
bobah

Reputation: 18864

Why it does not work just like that

The reason you can not just pass a C++ function into a C API is because the two have potentially different calling conventions. The extern "C" syntax is to tell the C++ compiler to use the C notation for a single function or for the whole code block if used like extern "C" { ... }.

How to make it work

Create a singleton C++ wrapper around the C API responsible for the initialization/finalization of the latter and forwarding calls and callbacks back and forth. Importantly it should try minimising the amount of raw C++ world pointers into the C API to make clean memory management possible.

godbolt // apologies for the clumsy syntax, too much Java recently :-)

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

Create a C-proxy to bridge between the C API and the C++ wrapper

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

Upvotes: 3

melpomene
melpomene

Reputation: 85767

The basic approach is to store a copy of callback somewhere, then pass a pointer to it as your pApplicationHandlerData.

Like this:

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

This is your (C compatible) generic handler function that just forwards to a std::function referenced by pClientData.

You'd register it in subscribe as

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

where copy_of_callback is a std::function<const std::string &)>.

The only tricky part is managing the lifetime of the callback object. You must do it manually in the C++ part because it needs to stay alive for as long as the subscription is valid, because application_handler_forwarder will be called with its address.

You can't just pass a pointer to the parameter (&callback) because the parameter is a local variable that is destroyed when the function returns. I don't know your C library, so I can't tell you when it is safe to delete the copy of the callback.


N.B: Apparently you need extern "C" on the callback even if its name is never seen by C code because it doesn't just affect name mangling, it also ensures the code uses the calling convention expected by C.

Upvotes: 3

Related Questions