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

chr*_*ris 12 c++ lambda callback stdbind c++11

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

void foo(int(__stdcall *callback)());
Run Code Online (Sandbox Code Playgroud)

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

  • 无法存储绑定表达式
  • 无法存储捕获lambda

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

这些特定函数指针的另一个属性是它们需要使用__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;
    });   
}
Run Code Online (Sandbox Code Playgroud)

然而,存在一个重大缺陷.这不是可重入的.如果变量在使用之前被重新分配,则永远不会调用旧函数(不考虑多线程问题).

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

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

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

  • 允许任何函数对象
  • 不仅允许C回调的调用约定(如果它是相同的,那么用户可以传递具有正确调用约定的东西)
  • 是线程安全的/可重入的

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

Mik*_*son 6

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

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

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

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

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

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

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

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

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

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

* "重复"是因为它仍然指的是"全局"状态,但它是可重入的,因为不同的回调(需要不同的状态)不会相互干扰,就像你原来的问题一样.

  • @ H2CO3:我同意,虽然大部分答案都很明确,但关于代码生成的部分是完全错误的......它甚至不是任何类似于反射的东西!Mikael Persson:我建议您阅读运行时生成的蹦床.它们实际上不是标准的,特别是因为JITting在许多代码和数据严格分开的平台上是不可能的(JITting需要将数据转换成代码),但它们可以用来解决上述问题和一些编译器(如LLVM) )本地支持他们. (2认同)

Mat*_* M. 5

不幸的是,你不幸运.

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

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


传递状态的最简单的解决方案是......实际传递状态.啊!

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

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

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

使用这样的接口,您可以在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);
}
Run Code Online (Sandbox Code Playgroud)

如果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] = {};
Run Code Online (Sandbox Code Playgroud)