改变调用约定

10

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

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

下面是来自这里的示例:

// 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);
//                     ^^^^^^^

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

我找到了一种方法,通过传递一个调用第二个捕获lambda的转换后的无捕获lambda,来实现这一点。第一个被作为回调函数传递,第二个通过 void* user_data 传递。这是可行且类型安全的。但对于看起来如此简单的事情而言,这显得相当复杂。


1
你能否创建一个cdecl包装器,将调用转发到你的真实回调函数? - krzaq
@krzaq:你会怎么做呢?这并不像看起来那么明显...我会添加一个例子。 - Adi Shavit
除了函数指针,你能否还传递“用户数据”(通常是 void *)?否则,关于线程安全/多个回调,包装函数会有点麻烦。 - Daniel Jour
@DanielJour:是的,请查看我的扩展问题和使用上述方法的解决办法。 - Adi Shavit
但是对于看起来很简单的东西来说,它相当复杂。你为什么认为它是“简单”的呢?调用约定远远不止是一个关键字。它以低级别定义了如何传递参数和返回值。如果函数需要一个__stdcall函数,那就没有任何绕过它的方法 - 它本来就是这样的。 - PaulMcKenzie
显示剩余3条评论
4个回答

11
你可以创建一个包装器来在不同的调用约定之间进行翻译:
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>());

演示


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

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);

演示


根据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


1
我从未见过这种分解(反映)函数(指针)类型的方式。使用模板特化非常酷。 - Adi Shavit
正如在@StoryTeller答案的评论中所讨论的那样,因此我得到了以下错误:error C2975: 'callback': invalid template argument for 'make_callback', expected compile-time constant expression - Adi Shavit
@AdiShavit 加入了我的运行时回调想法。也许你会发现这很有用。无论如何,这是一个有趣的问题。 - krzaq
1
@krzaq,你选择了user_data参数,真不错。作为可能的改进,我敢打赌,在ps...中用实际的userData替换第N个参数是可行的,从而避免使用union hack。 - Andrey Turkin
1
@krzaq:非常酷的通用解决方案 - 非常类似于我的非通用解决方案。我可能会将“N”默认为最后一个参数,因为这是最常见的约定 - 将“user_data”作为最后一个参数。太遗憾了,我不能投超过+1的票。 - Adi Shavit
显示剩余3条评论

4
在Visual C++(从VC11开始)中,无状态lambda实现了一个转换运算符,可以将其转换为所有调用约定的函数指针。
因此,这个同样适用。
#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的函数指针,使其成为全捕获,因此无法强制转换。 - Adi Shavit
1
@AdiShavit,如果您(和编译器)无法访问完整的函数定义,则整个前提是不可能的。即使是krzaq的答案,在运行时获取指针也无法工作。 - StoryTeller - Unslander Monica
@AdiShavit 他说的没错。请注意,我使用函数指针作为模板参数 - 它必须在编译(好吧,链接?)时知道。 - krzaq
@krzaq:是的,你说得对,我现在意识到了。我在运行时确实获得了fn指针,所以我想两种解决方案都不会起作用。 - Adi Shavit
@krzaq:在我的情况下,我确实有userData,并且我使用了这里解释的方法来解决它(http://bannalia.blogspot.com/2016/07/passing-capturing-c-lambda-functions-as.html)。我只是希望有一种更简单的方法来做到这一点。 - Adi Shavit
显示剩余2条评论

4
如果回调函数在编译时未知,则有以下选项:
  • 使用单个包装器函数并在user_data中传递目标回调函数。优点-使用相对容易;缺点-需要user_data进行自身使用;需要非常相似的函数签名。
  • 使用包装类,分配类的实例并将this传递给user_data。优点-更加灵活,因为它可以在每个实例中捕获一些数据(例如,它可以存储目标回调函数的user_data或向目标回调函数传递其他数据);缺点-需要管理包装器实例的生命周期。
  • 为每个不同的目标回调函数构建单独的thunks。优点-不需要使用user_data;缺点-相当低级和非常不可移植(在编译器和操作系统中都是如此);可能很难做到;在不使用汇编语言的情况下,在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!

3
回答自己的问题,希望有人有更简单的解决方案。
这种方法与这里所述相同。
我们将使用以下内容:
  1. 无捕获lambda可以自动转换为具有任何所需调用约定的函数指针。
  2. C-API函数提供了一种void* user_data方式将数据传递给回调。
我们将传递两个lambda表达式给C-API:
  1. 一个是无捕获的lambda,转换为正确的调用约定;
  2. 另一个捕获回调fn-ptr,并作为user_data传递给无捕获lambda进行调用。它捕获了原始的回调和原始的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表达式,基本上是您建议的但不是通用的。 - Adi Shavit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接