使用C ++ 11接口包装C回调的最佳方法是什么?

时间:2013-08-11 05:52:09

标签: c++ c++11 lambda callback stdbind

假设这是一个要包装的C函数:

void foo(int(__stdcall *callback)());

C函数指针回调的两个主要缺陷是:

  • 无法存储绑定表达式
  • 无法存储抓取lambdas

我想知道包装这些函数的最佳方法。第一个对于成员函数回调特别有用,第二个用于使用周围变量的内联定义,但这些不是唯一用途。

这些特定函数指针的另一个属性是它们需要使用__stdcall调用约定。据我所知,这完全取消了lambda作为一种选择,否则会有点麻烦。我还想允许至少__cdecl

这是我能够提出的最好的东西,没有开始回归依赖于函数指针所没有的支持。它通常在标题中。以下是Coliru上的示例。

#include <functional>

//C function in another header I have no control over
extern "C" void foo(int(__stdcall *callback)()) {
    callback();
}

namespace detail {
    std::function<int()> callback; //pretend extern and defined in cpp

    //compatible with the API, but passes work to above variable
    extern "C" int __stdcall proxyCallback() { //pretend defined in cpp
        //possible additional processing
        return callback();
    }
}

template<typename F> //takes anything
void wrappedFoo(F f) {
    detail::callback = f;
    foo(detail::proxyCallback); //call C function with proxy 
}

int main() {
    wrappedFoo([&]() -> int {
        return 5;
    });   
}
然而,有一个重大缺陷。这不是可重入的。如果变量在使用之前被重新分配,则永远不会调用旧函数(不考虑多线程问题)。

我尝试过的最后一件事就是将std::function作为数据成员存储并使用对象,因此每个都可以在不同的变量上运行,但没有办法将对象传递给代理人。将对象作为参数将导致签名不匹配和绑定,它不会将结果存储为函数指针。

我有一个想法,但没有玩过的是std::function的向量。但是,我认为从中删除它的唯一真正安全时间是在没有使用它时清除它。但是,每个条目首先在wrappedFoo中添加,然后在proxyCallback中使用。我想知道一个计数器在前者中递增并在后者中递减,然后在清除向量之前检查为零会起作用,但它听起来像是一个比必要的更复杂的解决方案。

有没有办法用函数指针回调包装C函数,以便C ++包装版本:

  • 允许任何功能对象
  • 不仅允许C回调的调用约定(如果它是相同的,那么用户可以使用正确的调用约定传递内容)
  • 是线程安全的/可重入的

注意:作为Mikael Persson答案的一部分,明显的解决方案是使用应该存在的void *参数。然而,遗憾的是,这不是一个全部,最终的选择,主要是由于无能。对于那些没有这个选项的函数,存在哪些可能性是有趣的,并且是获得非常有用的答案的主要途径。

3 个答案:

答案 0 :(得分:6)

这个问题有两个挑战:一个容易,一个几乎不可能。

第一个挑战是从任何可调用的“事物”到简单函数指针的静态类型转换(映射)。用一个简单的模板解决了这个问题,没什么大不了的。这解决了调用约定问题(简单地用一种函数包装另一种函数)。这已由std::function模板解决(这就是它存在的原因)。

主要的挑战是将运行时状态封装到普通函数指针中,该函数指针的签名不允许使用“用户数据”void*指针(因为任何半正常的C API通常都会有) 。这个问题与语言无关(C,C ++ 03,C ++ 11),几乎无法解决。

您必须了解任何“本地”语言(以及其他大多数语言)的基本事实。编译后代码已修复,只有数据在运行时更改。因此,即使是一个类成员函数,它看起来好像是属于该对象的一个​​函数(运行时状态),它不是,代码是固定的,只有对象的标识被更改(this指针)

另一个基本事实是函数可以使用的所有外部状态必须是全局的或作为参数传递。如果消除后者,则只能使用全局状态。根据定义,如果函数的操作依赖于全局状态,则它不能重入。

因此,能够创建一个(sort-of)重入*函数,该函数只需一个普通函数指针即可调用,并封装任何通用(state-ful)函数对象(bind') ed调用,lambdas等等,每次调用都需要一段独特的代码(而不是数据)。换句话说,您需要在运行时生成代码,并将指向该代码的指针(回调函数指针)传递给C函数。这就是“几乎不可能”的来源。这是不可能通过任何标准的C ++机制,我是100%肯定的,因为如果在C ++中这是可能的,运行时反射也是可能的(并且它不是)。

从理论上讲,这可能很容易。你需要的只是一段编译的“模板”代码(不是C ++意义上的模板),你可以复制,插入指向你的状态(或函数对象)的指针作为一种硬编码的局部变量,然后放置代码转换为一些动态分配的内存(有一些引用计数或其他任何内容,以确保它存在,只要它是需要的)。但实现这一点显然非常棘手,而且非常“黑客”。说实话,这远远超过了我的技能水平,所以我甚至无法指导你如何做到这一点。

在实践中,现实的选择是甚至不尝试这样做。使用用于传递状态(函数对象)的全局(外部)变量的解决方案在折衷方面正朝着正确的方向发展。你可以拥有一些函数池,每个函数都有自己的全局函数对象来调用,你可以跟踪当前用作回调的函数,并在需要时分配未使用的函数。如果您用尽了有限的功能,则必须抛出异常(或者您喜欢的任何错误报告)。该方案基本上等同于上面的“理论上”解决方案,但是使用了有限数量的并发回调。还有其他类似的解决方案,但这取决于具体应用的性质。

对不起,这个答案没有给你一个很好的解决方案,但有时候根本没有任何银子弹。

另一个选择是避免使用由从未听说过不可避免且非常有用的void* user_data参数的buffoons设计的C API。

*“排序”重入因为它仍然指的是“全局”状态,但它是可重入的,因为不同的回调(需要不同的状态)不会干扰每个另外,这是你原来的问题。

答案 1 :(得分:5)

不幸的是,你不幸运。

有一些方法可以在运行时生成代码,例如,您可以在LLVM trampoline intrinsics上读取,您可以在其中生成一个存储其他状态的转发函数,非常类似于lambdas但运行时已定义。

不幸的是,这些都不是标准的,因此你被搁浅了。


传递状态的最简单方法是......实际传递状态。 啊!

定义良好的C回调将采用两个参数:

  • 指向回调函数本身的指针
  • A void*

后者未被代码本身使用,并在调用时简单地传递给回调。根据接口,回调负责销毁它,或供应商,甚至第三个“销毁”功能都可以通过。

使用这样的界面,您可以在线程安全的&amp;中有效地传递状态。 C级的可重入时尚,因此在C ++中自然地用相同的属性包装它。

template <typename Result, typename... Args)
Result wrapper(void* state, Args... args) {
    using FuncWrapper = std::function<Result(Args...)>;
    FuncWrapper& w = *reinterpret_cast<FuncWrapper*>(state);
    return w(args...);
}

template <typename Result, typename... Args)
auto make_wrapper(std::function<Result(Args...)>& func)
    -> std::pair<Result (*)(Args...), void*>
{
    void* state = reinterpret_cast<void*>(&func);
    return std::make_pair(&wrapper<Result, Args...>, state);
}

如果C接口没有提供这样的设施,你可以稍微破解,但最终你是非常有限的。如前所述,一种可能的解决方案是使用全局变量在外部保持状态,并尽力避免争用。

粗略草图在这里:

// The FreeList, Store and Release functions are up to you,
// you can use locks, atomics, whatever...
template <size_t N, typename Result, typename... Args>
class Callbacks {
public:
    using FunctionType = Result (*)(Args...);
    using FuncWrapper = std::function<Result(Args...)>;

    static std::pair<FunctionType, size_t> Generate(FuncWrapper&& func) {
        // 1. Using the free-list, find the index in which to store "func"
        size_t const index = Store(std::move(state));

        // 2. Select the appropriate "Call" function and return it
        assert(index < N);
        return std::make_pair(Select<0, N-1>(index), index);
    } // Generate

    static void Release(size_t);

private:
    static size_t FreeList[N];
    static FuncWrapper State[N];

    static size_t Store(FuncWrapper&& func);

    template <size_t I, typename = typename std::enable_if<(I < N)>::type>
    static Result Call(Args...&& args) {
        return State[I](std::forward<Args>(args)...);
    } // Call

    template <size_t L, size_t H>
    static FunctionType Select(size_t const index) {
        static size_t const Middle = (L+H)/2;

        if (L == H) { return Call<L>; }

        return index <= Middle ? Select<L, Middle>(index)
                               : Select<Middle + 1, H>(index);
    }

}; // class Callbacks

// Static initialization
template <size_t N, typename Result, typename... Args>
static size_t Callbacks<N, Result, Args...>::FreeList[N] = {};

template <size_t N, typename Result, typename... Args>
static Callbacks<N, Result, Args...>::FuncWrapper Callbacks<N, Result, Args...>::State[N] = {};

答案 2 :(得分:1)

如前所述,C函数指针不包含任何状态,因此不带参数调用的回调函数只能访问全局状态。因此,这种“无状态”回调函数只能在一个上下文中使用,其中上下文存储在全局变量中。然后为不同的上下文声明不同的回调。

如果所需的回调数量动态变化(例如,在GUI中,用户打开的每个窗口都需要一个新的回调来处理该窗口的输入),那么预先定义一个简单无状态的大型池回调,映射到statefull回调。在C中,可以按如下方式完成:

struct cbdata { void (*f)(void *); void *arg; } cb[10000];
void cb0000(void) { (*cb[0].f)(cb[0].arg); }
void cb0001(void) { (*cb[1].f)(cb[1].arg); }
...
void cb9999(void) { (*cb[9999].f)(cb[99999].arg); }
void (*cbfs[10000])(void) =
    { cb0000, cb0001, ... cb9999 };

然后使用一些更高级别的模块来保留可用回调列表。

使用GCC(但不是G ++,所以以下内容需要在严格的C语言而不是C ++文件中),即使动态使用不太知名的GCC功能,您也可以创建新的回调函数,嵌套函数:

void makecallback(void *state, void (*cb)(void *), void (*cont)(void *, void (*)()))
{
    void mycallback() { cb(state); }
    cont(state, mycallback);
}

在这种情况下,GCC会为您创建必要的代码生成代码。缺点是,它限制了你对GNU编译器集合的限制,并且NX位不能再在堆栈中使用,因为即使你的代码也需要堆栈上的新代码。

从高级代码调用makecallback()以创建具有封装状态的新匿名回调函数。如果调用此新函数,它将使用arg状态调用statefull回调函数cb。只要makecallback()不返回,新的匿名回调函数就可以使用了。因此,makecallback()通过调用传入的“cont”函数将控制权返回给调用代码。此示例假定实际回调cb()和正常继续函数cont()都使用相同的状态“state”。也可以使用两个不同的void指针将不同的状态传递给它们。

当不再需要回调时,“cont”函数可能只返回(并且应该返回以避免内存泄漏)。如果您的应用程序是多线程的,并且需要各种回调主要用于其各种线程,那么您应该能够让每个启动时的线程通过makecallback()分配其所需的回调。

但是,如果您的应用程序仍然是多线程的,并且您已经(或可以建立)严格的回调到线程关系,那么您可以使用线程局部变量来传递所需的状态。当然,只有当你的lib在正确的线程中调用回调时,这才有效。