【问题标题】:Changing Calling Convention更改调用约定
【发布时间】:2017-03-12 14:59:06
【问题描述】:

我有一个第三方 C API,它需要一个 __stdcall 回调函数。
我的代码有一个外部提供的 __cdecl 回调函数。

我无法将函数指针传递给 C-API,因为它们被视为不同的类型。
绕过类型系统并使用reinterpret_cast<> 自然会导致运行时错误。

这是来自here的示例:

// C-API
// the stdcall function pointer type:
typedef CTMuint(__stdcall *CTMwritefn)(const void *aBuf, CTMuint aCount, void *aUserData);

// A function needing the callback: 
CTMEXPORT void __stdcall ctmSaveCustom(CTMcontext aContext, CTMwritefn aWriteFn, void *aUserData, int *newvertexindex);
                                                            ^^^^^^^^^^^^^^^^^^^

//////////////////////////////////////////////////////////////////////////////

// C++
CTMuint __cdecl my_func(const void *aBuf, CTMuint aCount, void *aUserData);

// I want to call here:
ctmSaveCustom(context, my_func, &my_data, nullptr);
//                     ^^^^^^^

有没有一种方法可以安全地将具有一种调用约定的函数转换和/或包装成另一种?

我确实找到了一种方法,方法是传递一个调用 second 捕获 lambda 的强制转换的无捕获 lambda。第一个作为回调传递,第二个通过void* user_data。这有效并且是类型安全的。但是对于看起来如此简单的事情来说,它是相当复杂的。

【问题讨论】:

  • 你不能只创建一个 cdecl 包装器来将调用转发到你的真实回调吗?
  • @krzaq:你会怎么做?它并不像看起来那么明显......我将添加一个示例。
  • 你能否——除了函数指针——也传递“用户数据”(通常是void *)?否则,包装函数在线程安全/多个回调方面会有点麻烦。
  • @DanielJour:是的,请参阅上面的扩展问题和解决方法。
  • 但对于看似如此简单的事情却相当复杂 -- 你为什么认为它“简单”?调用约定不仅仅是一个关键字。它定义了如何在低级别传递参数和返回值。一个函数想要一个 __stdcall 函数,没有办法绕过它——它就是这样。

标签: c++ function-pointers calling-convention stdcall cdecl


【解决方案1】:

您可以为不同调用约定之间的转换制作一个包装器:

template<typename Func, Func* callback>
auto make_callback()
{
    return &detail::callback_maker<Func, callback>::call;
}

callback_maker 定义为

template<typename T, T*>
struct callback_maker;

template<typename R, typename... Params, R(*Func)(Params...)>
struct callback_maker<R(Params...), Func>
{
    static R __stdcall call(Params... ps)
    {
        return Func(std::forward<Params>(ps)...);
    }
};

这是一个相当通用的解决方案,允许您指定函数原型。您可以按如下方式使用它:

//  external_api(&not_stdcall_func); // error
external_api(make_callback<void(int,int), &not_stdcall_func>());

demo


如果要在运行时确定指针,您可以将回调保留在用户数据中。你必须正确地管理它的生命周期,但很可能你已经需要这样做了。再次尝试通用解决方案。进行回调并告诉它哪个参数是用户数据指针:

template<typename Callback, size_t N>
auto make_callback()
{
    using callback_maker = detail::callback_maker<Callback, N>;
    return &callback_maker::call;
}

callback_maker 定义为

template<typename T, size_t N>
struct callback_maker;

template<typename R, typename... Params, size_t N>
struct callback_maker<R(*)(Params...), N>
{
    using function_type = R(Params...);

    static R __stdcall call(Params... ps)
    {
        void const* userData = get_nth_element<N>(ps...);
        auto p = static_cast<pair<function_type*, void*> const*>(userData);
        return p->first(ps...);
    }
};

get_nth_element 一样

template<size_t N, typename First, typename... Ts>
decltype(auto) get_nth_element_impl(false_type, First&& f, Ts&&...);

template<size_t N, typename First, typename... Ts>
decltype(auto) get_nth_element_impl(true_type, First&&, Ts&&... ts)
{
    return get_nth_element_impl<N-1>(integral_constant<bool, (N > 1)>{}, forward<Ts>(ts)...);
}

template<size_t N, typename First, typename... Ts>
decltype(auto) get_nth_element_impl(false_type, First&& f, Ts&&...)
{
    return forward<First>(f);
}

template<size_t N, typename... Ts>
decltype(auto) get_nth_element(Ts&&... ts)
{
    return get_nth_element_impl<N>(integral_constant<bool, (N > 0)>{}, forward<Ts>(ts)...);
}

现在,在呼叫站点上

using callback_t = CTMuint(*)(const void *aBuf, CTMuint aCount, void *aUserData);
auto runtime_ptr = &not_stdcall_func;

pair<callback_t, void*> data;
data.first = runtime_ptr;
data.second = nullptr; // actual user data you wanted

auto callback = make_callback<callback_t, 2>();

ctmSaveCustom({}, callback, &data, nullptr);

demo


根据 Andrey Turkin 的建议,您可以替换参数列表中的用户数据指针。与forward_as_tuple 一起,它消除了对get_nth_element 的需求。升级调用功能:

static R __stdcall call(Params... ps)
{
    auto params_tuple = forward_as_tuple(ps...);
    void const* userData = get<N>(params_tuple);
    auto p = static_cast<pair<function_type*, void*> const*>(userData);
    get<N>(params_tuple) = p->second;
    return apply(p->first, move(params_tuple));
}

这是 C++17 的apply 的简单实现:

template<typename Func, typename T, size_t... Is>
decltype(auto) apply_impl(Func f, T&& t, index_sequence<Is...>)
{
    return f(get<Is>(t)...);
}

template<typename Func, typename... Ts>
decltype(auto) apply(Func f, tuple<Ts...>&& tup)
{
    return apply_impl(f, move(tup), index_sequence_for<Ts...>{});
}

demo

【讨论】:

  • 我从未见过这种分解(反映)函数(指针)类型的方式。使用模板专业化非常酷。
  • 正如 @StoryTeller 的答案的 cmets 中所讨论的,因此预计我会收到以下错误:error C2975: 'callback': invalid template argument for 'make_callback', expected compile-time constant expression
  • @AdiShavit 添加了我对运行时回调的看法。也许你会发现这很有用。不管怎样,这是一个有趣的问题
  • @krzaq 感谢选择 user_data 参数。作为一种可能的改进 - 我敢打赌,可以用实际的 userData 替换 ps... 中的第 N 个参数,从而避免使用联合黑客攻击。
  • @krzaq:非常酷的通用解决方案 - 与我的非通用解决方案非常相似。我可能会将N 默认为最后一个参数,因为这是最常见的约定——将user_data 作为最后一个参数。太糟糕了,我不能投票超过 +1。
【解决方案2】:

回答自己,希望有人有更简单的解决方案。
方法和here解释的一样。

我们将使用以下内容:

  1. 可以自动将无捕获 lambda 转换为具有任何所需调用约定的函数指针。
  2. C-API 函数提供了一种void* user_data 方法将数据传递给回调函数。

我们将通过 C-API 两个 labmda:

  1. 一种是无捕获强制转换为正确的调用约定;
  2. 另一个捕获回调 fn-ptr 并作为 user_data 传递给要调用的无捕获 lambda。它捕获both 原始回调 原始user_data 供内部使用。

代码如下:

// This is a lambda that calls the (cdecl) callback via capture list
// However, you can't convert a non-captureless lambda to a function pointer
auto callback_trampoline = [&callback, &user_data](const void *aBuf, CTMuint aCount) -> CTMuint
{
    return callback(aBuf, aCount, user_data);
};

using trampoline_type = decltype(callback_trampoline);

// so we create a capture-less wrapper which will get the lambda as the user data!
// this CAN be cast to a function pointer!
auto callback_wrapper_dispatcher = [](const void *aBuf, CTMuint aCount, void *aUserData) -> CTMuint
{
    auto& lambda = *reinterpret_cast<trampoline_type*>(aUserData);
    return lambda(aBuf, aCount);
};

ctmSaveCustom(context_, callback_wrapper_dispatcher, &callback_trampoline, nullptr);

这是类型安全的,可以按预期工作。

将其制成类似于@krzaq 答案中建议的通用工具会很酷。

更新:
这是一个更简单的公式,只有一个无捕获 lambda,但概念相同:

auto payload = std::tie(callback, user_data);
using payload_type = decltype(payload);
auto dispatcher = [](const void *aBuf, CTMuint aCount, void *aUserData)->CTMuint
{
    // payload_type is visible to the captureless lamda
    auto& payload = *reinterpret_cast<payload_type*>(aUserData);
    return std::get<0>(payload)(aBuf, aCount, std::get<1>(payload));
};
ctmSaveCustom(context_, dispatcher, &payload, nullptr);

【讨论】:

  • @krzaq:使用单个 lambda 查看更新后的公式,基本上是您建议的,但不是通用的。
【解决方案3】:

如果在编译时回调未知,您有以下选项:

  • 使用单个包装函数并在user_data 中传递目标回调。 Pro - 相当容易使用; con - 需要user_data 供自己使用;需要非常相似的函数签名
  • 使用包装类,分配类的实例并在user_data中传递this。 Pro - 更通用,因为它可以在每个实例中捕获一些数据(例如,它可以存储 user_data 用于目标回调或将其他数据传递给目标回调); con - 需要管理包装器实例的生命周期
  • 为每个不同的目标回调构建单独的 thunk。 Pro - 不需要使用user_data; con - 相当低级且相当不可移植(在操作系统中的两个编译器中);可能很难做到;不借助汇编很难在 C++ 中完成。

第一个选项看起来像这样(无耻地扯掉@krzaq):

template<typename T> struct callback_maker;
template<typename R, typename... Params> struct callback_maker<R(Params...)> {
    static R __stdcall call_with_userdata_as_last_parameter(Params... ps, void* userData) {
        R(__cdecl *Func)(Params...) = reinterpret_cast<R(__cdecl *)(Params...)>(userData);
        return Func(std::forward<Params>(ps)...);
    }
};
template<typename Func> constexpr auto make_callback() {
    return &callback_maker<Func>::call_with_userdata_as_last_parameter;
}

...
extern void external_api(void(__stdcall*)(int,int,void*), void* userdata);
extern void __cdecl not_stdcall_func(int,int);
external_api(make_callback<void(int,int)>(), &not_stdcall_func);

可能对您不可用,因为您需要 userData 进行两个回调。

第二个选项:

template<typename T> struct CallbackWrapper;
template<typename R, typename... Params> struct CallbackWrapper<R(Params...)> {
    using stdcall_callback_t = R(__stdcall*)(Params..., void*);
    using cdecl_callback_t = R(__cdecl*)(Params..., void*);
    using MyType = CallbackWrapper<R(Params...)>;
    CallbackWrapper(cdecl_callback_t target, void* target_userdata) : _target(target), _target_userdata(target_userdata) {}
    stdcall_callback_t callback() const { return &MyType::callback_function; }
private:
    static R __stdcall callback_function(Params... ps, void* userData) {
        auto This = reinterpret_cast<MyType*>(userData);
        return This->_target(std::forward<Params>(ps)..., This->_target_userdata);
    }
    cdecl_callback_t _target;
    void* _target_userdata;
};

...
extern void external_api(void(__stdcall*)(int,int,void*), void* userdata);
extern void __cdecl not_stdcall_func(int,int, void*);

void * userdata_for_not_stdcall_func = nullptr;
CallbackWrapper<void(int, int)> wrapper(&not_stdcall_func, userdata_for_not_stdcall_func);
external_api(wrapper.callback(), &wrapper);
// make sure wrapper is alive for as long as external_api is using the callback!

【讨论】:

    【解决方案4】:

    在 Visual c++(从 VC11 开始)的情况下,无状态 lambda 实现了一个转换运算符,用于所有调用约定的函数指针。

    所以this,也可以正常工作

    #include <iostream>
    using namespace std;
    
    int __cdecl foo()
    {
        return 2;
    }
    
    void bar (int (__stdcall *pFunc)() )
    {
        cout << pFunc()*2;
    }
    
    int main() {
    
        bar([](){ return foo(); });
    
        return 0;
    }
    

    【讨论】:

    • 实际上,虽然原理是正确的,但这不能像你展示的那样工作,因为在我的例子中 foo() 不是函数声明(可以在无捕获 lambda 中使用),而是函数指针需要将其作为参数传递给 lambda,使其捕获完整,因此不可转换。
    • @AdiShavit,如果您(和编译器)无法访问完整的函数定义,那么整个前提是不可能的。如果您在运行时获得指针,即使 krzaq 的答案也不起作用。
    • @AdiShavit 他说了什么。请注意,我使用函数指针作为模板参数 - 它必须在编译(嗯,链接?)时知道
    • @krzaq:是的,你是对的,我现在意识到了这一点。我确实在运行时获得了 fn 指针,所以我想这两种解决方案都行不通。
    • @AdiShavit 我可以假设您总是在您控制的回调中获得userData 指针吗?
    猜你喜欢
    • 2020-02-27
    • 2011-07-06
    • 1970-01-01
    • 2021-12-20
    • 1970-01-01
    • 2017-05-13
    • 2020-11-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多