如何编写接受回调的 C 函数的 C++ 包装器 class 方法?
How to write a C++ wrapper class method of a C function that takes callback?
给定以下 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 中:
class AWS {
// ...
public:
void subscribe(const std::string &topic,
std::function<void(const std::string&)> callback);
// ...
};
我的目标是能够将捕获 lambda 函数传递给 AWS::subscribe
。近一个星期以来,我一直在尝试不同的方法,但 none 似乎有效。
让我知道是否还需要了解问题,我很乐意更新问题。
为什么它不能像那样工作
您不能将 C++
函数传递给 C
API 的原因是因为两者可能具有不同的调用约定。 extern "C"
语法是告诉 C++
编译器对单个函数或整个代码块使用 C
表示法,如果像 extern "C" { ... }
.
如何让它发挥作用
围绕 C API 创建一个单独的 C++ 包装器,负责后者的 initialization/finalization 并来回转发调用和回调。重要的是,它应该尽量减少原始 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);
}
基本方法是在某处存储 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 兼容的)通用处理程序函数,它仅转发到 pClientData
引用的 std::function
。
您可以在 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, ©_of_callback);
其中 copy_of_callback
是 std::function<const std::string &)>
。
唯一棘手的部分是管理回调对象的生命周期。您必须在 C++ 部分手动执行此操作,因为只要订阅有效,它就需要保持活动状态,因为 application_handler_forwarder
将使用其地址进行调用。
你不能只传递一个指向参数的指针(&callback
),因为参数是一个局部变量,在函数returns时被销毁。我不知道你的 C 库,所以我不能告诉你什么时候删除回调的副本是安全的。
N.B:显然您需要在回调中使用 extern "C"
,即使 C 代码从未见过它的名称,因为它不仅会影响名称修饰,还会影响 ensures the code uses the calling convention expected by C。
给定以下 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 中:
class AWS {
// ...
public:
void subscribe(const std::string &topic,
std::function<void(const std::string&)> callback);
// ...
};
我的目标是能够将捕获 lambda 函数传递给 AWS::subscribe
。近一个星期以来,我一直在尝试不同的方法,但 none 似乎有效。
让我知道是否还需要了解问题,我很乐意更新问题。
为什么它不能像那样工作
您不能将 C++
函数传递给 C
API 的原因是因为两者可能具有不同的调用约定。 extern "C"
语法是告诉 C++
编译器对单个函数或整个代码块使用 C
表示法,如果像 extern "C" { ... }
.
如何让它发挥作用
围绕 C API 创建一个单独的 C++ 包装器,负责后者的 initialization/finalization 并来回转发调用和回调。重要的是,它应该尽量减少原始 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);
}
基本方法是在某处存储 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 兼容的)通用处理程序函数,它仅转发到 pClientData
引用的 std::function
。
您可以在 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, ©_of_callback);
其中 copy_of_callback
是 std::function<const std::string &)>
。
唯一棘手的部分是管理回调对象的生命周期。您必须在 C++ 部分手动执行此操作,因为只要订阅有效,它就需要保持活动状态,因为 application_handler_forwarder
将使用其地址进行调用。
你不能只传递一个指向参数的指针(&callback
),因为参数是一个局部变量,在函数returns时被销毁。我不知道你的 C 库,所以我不能告诉你什么时候删除回调的副本是安全的。
N.B:显然您需要在回调中使用 extern "C"
,即使 C 代码从未见过它的名称,因为它不仅会影响名称修饰,还会影响 ensures the code uses the calling convention expected by C。