今天记录一个个人觉得有点意思的问题:在C++代码中调用C API时,对于C回调函数的处理问题,如何识别出调用API时的信息?
这个问题,大部分API都是通过调用时提供传递用户数据的方式实现的,如libcurl中的CURLOPT_WRITEDATA,CURLOPT_READDATA ,通过curl_easy_setopt方式将任意用户数据指针传递给CURL句柄,并在执行上传下载时的回调函数内透传给调用方,这样在例如多线程并发上传下载时就可以很方便的识别出是哪个上传下载任务的回调了。但是,如果API及回调函数内没有提供传递用户数据的接口和回调参数的话,是不是就一定无法完成识别调用上下文信息的功能呢?比如下列的C函数API:
// C-Style callback function without user context
typedef void (*c_api_callback_no_context)(int count);
// C-Style callback function with user context
typedef void (*c_api_callback_context)(int count, void * user_context);
// C API function that will trigger the callback function
void c_api_start_counting_no_context(c_api_callback_no_context callback, int max);
void c_api_start_counting_context(c_api_callback_context callback, int max, void * user_context);
c_api_start_counting_context及配套的c_api_callback_context回调为可以透传用户数据的版本,而c_api_start_counting_no_context及c_api_callback_no_context则只提供了逻辑用回调参数count,没有可用于识别context的用户数据指针,那么如何在不碰C源码的前提下(闭源C库或不想做侵入式修改)做到和带用户数据回调一样的效果呢?目前想到的一种方法是通过C++模板生成不同地址的回调函数配合上下文信息映射来实现:
static std::mutex g_mutex;
static std::map<int, c_api_callback_no_context> mapped_cbs;
template<int x>
void cb_template(int count)
{
std::unique_lock<std::mutex> lock(g_mutex);
std::condition_variable cv;
cv.wait_for(lock, std::chrono::milliseconds(100));
c_api_callback_no_context my_p = cb_template<x>;
auto found = std::find_if(mapped_cbs.cbegin(), mapped_cbs.cend(),
[my_p](const std::pair<int, c_api_callback_no_context> & p)
{
return p.second == my_p;
});
if (mapped_cbs.cend() != found)
{
printf("Counting to %d, from caller %d!\n", count, (*found).first);
}
}
template <size_t N>
struct ForEachCpp11
{
template <size_t I>
static void RegCallback()
{
printf("ForEachCpp11 reg functions %zu %zu %p\n", N, I, cb_template<I>);
mapped_cbs.emplace(static_cast<int>(I), cb_template<I>);
ForEachCpp11<N>::RecursiveReg<I>();
}
template <size_t I>
static typename std::enable_if<I + 1 < N, void>::type
RecursiveReg()
{
ForEachCpp11<N>::RegCallback<I+1>();
}
template <size_t I>
static typename std::enable_if<I + 1 >= N, void>::type
RecursiveReg()
{
}
};
然后在使用需要回调API前先进行回调函数池的生成:
ForEachCpp11<2>::RegCallback<0>();
这样就在映射用map中生成了两个可以用的相同原型,但地址不同的回调函数,在回调函数触发时可以通过回调函数自身地址在map中查找间接找到上下文信息,说白了相当于手写了一堆c_api_callback_no_context函数,只是函数名字不同而已。
这种方法还是有一定局限性的,比如由于使用的template,所以回调函数数量必须在编译期确定,也就是说不能在运行时动态分配回调函数数量,所以,如果需要不同回调函数数量较多时,最好是通过加锁同步的方式延迟API调用,知道回调函数池中有未分配的空闲函数。
最后附上完整示例的GitHub项目:
博主友情提示:
如您在评论中需要提及如QQ号、电子邮件地址或其他隐私敏感信息,欢迎使用>>博主专用加密工具v3<<处理后发布,原文只有博主可以看到。