Изменение соглашения о вызовах

У меня есть сторонний 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);
//                     ^^^^^^^

Есть ли способ безопасно преобразовать и/или обернуть функцию с одним соглашением о вызовах в другое?

Я нашел способ сделать это, передав преобразованную лямбду без захвата, которая вызывает вторую захватывающую лямбду. Первый передается как callback, второй через void* user_data. Это работает и безопасно для типов. Но это довольно запутанно для чего-то, что кажется таким простым.


person Adi Shavit    schedule 30.10.2016    source источник
comment
Не можете ли вы просто создать оболочку cdecl, которая будет перенаправлять вызов на ваш реальный обратный вызов?   -  person krzaq    schedule 30.10.2016
comment
@krzaq: Как бы вы это сделали? Это не так очевидно, как может показаться... Добавлю пример.   -  person Adi Shavit    schedule 30.10.2016
comment
Можете ли вы, помимо указателя на функцию, также передавать пользовательские данные (обычно void *)? В противном случае функция-оболочка будет немного хлопотной в отношении безопасности потоков/множественных обратных вызовов.   -  person Daniel Jour    schedule 30.10.2016
comment
@DanielJour: Да, смотрите мой расширенный вопрос и обходной путь, используя это выше.   -  person Adi Shavit    schedule 30.10.2016
comment
Но это довольно запутанно для чего-то, что кажется таким простым -- Почему вы думаете, что это просто? Соглашение о вызовах — это гораздо больше, чем просто ключевое слово. Он определяет, как передавать параметры и возвращать значения на низком уровне. Функция хочет функцию __stdcall, от этого никуда не деться — это то, что есть.   -  person PaulMcKenzie    schedule 30.10.2016
comment
@AdiShavit: вам нужно это?   -  person krzaq    schedule 30.10.2016
comment
@PaulMcKenzie: я сказал кажется простым — очевидно, это не так. Однако обратите внимание, что лямбда-выражения без захвата могут плавно/автоматически преобразовываться в любой указатель функции соглашения о вызовах, который вы не можете.   -  person Adi Shavit    schedule 30.10.2016
comment
@krzaq: Да! Глядя на образец, думаю, это то, что я хотел - буду тестировать более тщательно. Вы должны поставить это как ответ :-)   -  person Adi Shavit    schedule 30.10.2016


Ответы (4)


Вы можете создать оболочку для перевода между различными соглашениями о вызовах:

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

демонстрация


По предложению Андрея Туркина можно заменить указатель пользовательских данных в списке параметров. Вместе с 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));
}

а вот упрощенная реализация apply С++ 17:

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...>{});
}

демонстрация

person krzaq    schedule 30.10.2016
comment
Я никогда не видел такого способа разложения (отражения) типов функций (указателей). Очень круто использовать шаблонную специализацию. - person Adi Shavit; 30.10.2016
comment
Как обсуждалось в комментариях к ответу @StoryTeller, и, следовательно, ожидалось, что я получаю следующую ошибку: error C2975: 'callback': invalid template argument for 'make_callback', expected compile-time constant expression - person Adi Shavit; 30.10.2016
comment
@AdiShavit добавил мой взгляд на обратный вызов во время выполнения. Может быть, вы найдете это полезным. В любом случае, это был забавный вопрос - person krzaq; 30.10.2016
comment
@krzaq Спасибо за выбор аргумента user_data. Как возможное улучшение - держу пари, что можно заменить N-й аргумент в ps... фактическими пользовательскими данными, таким образом избегая необходимости взлома объединения. - person Andrey Turkin; 30.10.2016
comment
@AndreyTurkin Это определенно возможно. Вопрос о том, стоит ли это усилий по реализации, является спорным - по крайней мере, я не могу придумать простой способ сделать это. При этом не стесняйтесь улучшать этот ответ или публиковать свой собственный (и, пожалуйста, сообщите мне, если вы это сделаете). Я бы с удовольствием поучился. - person krzaq; 30.10.2016
comment
@krzaq: Очень классное универсальное решение - очень похоже на мое нестандартное решение. Я бы, вероятно, по умолчанию использовал N в качестве последнего аргумента, так как это наиболее распространенное соглашение - иметь user_data в качестве последнего аргумента. Жаль, что я не могу проголосовать больше, чем +1. - person Adi Shavit; 30.10.2016
comment
@AndreyTurkin ну, это было проще, чем я думал. Спасибо, что заставил меня задуматься - person krzaq; 30.10.2016
comment
Потрясающий. Заключение: я думаю, что это может быть еще более общим, если бы он брал любой вызываемый N-аргумент и превращал его в функцию __cdecl с N + 1-аргументом. Он возьмет user_data из аргументов (который используется для хранения внутреннего состояния) и вызовет этот callable с оставшимися аргументами. Этот вызываемый объект может/может добавить исходные user_data обратно и вызвать исходный обратный вызов. Было бы по-прежнему очень легко обернуть обратные вызовы с одинаковыми сигнатурами (используя универсальный помощник, вызываемый для вставки user_data в одно и то же место), но таким образом можно также разместить разные сигнатуры (с помощью std::bind или lambda). - person Andrey Turkin; 30.10.2016

В случае Visual C++ (начиная с VC11) лямбда-выражения без состояния реализуют оператор преобразования в указатели функций всех соглашений о вызовах.

Так что это тоже может работать.

#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;
}
person StoryTeller - Unslander Monica    schedule 30.10.2016
comment
На самом деле, хотя принцип верен, это не может работать, как вы показываете, поскольку в моем случае foo() - это не объявление функции (которое можно использовать в лямбде без захвата), а указатель на функцию, который нужно передать в качестве параметра в lambda, что делает его полным захватом и, следовательно, неприменимым. - person Adi Shavit; 30.10.2016
comment
@AdiShavit, если у вас (и у компилятора) нет доступа к полному определению функции, вся предпосылка невозможна. Даже ответ krzaq не сработает, если вы получите указатель во время выполнения. - person StoryTeller - Unslander Monica; 30.10.2016
comment
@AdiShavit, что он сказал. Обратите внимание, что я использую указатель на функцию в качестве параметра шаблона — он должен быть известен во время компиляции (ну, ссылка?) - person krzaq; 30.10.2016
comment
@krzaq: Да, ты прав, теперь я это понимаю. Я действительно получаю указатель fn во время выполнения, поэтому я думаю, что ни одно из решений не будет работать. - person Adi Shavit; 30.10.2016
comment
@AdiShavit Могу ли я предположить, что вы всегда получаете указатель userData в обратном вызове, которым вы управляете? - person krzaq; 30.10.2016
comment
@krzaq: В моем случае у меня есть userData, и я использовал его для решения, как объяснено здесь Я просто надеялся, что есть более простой способ сделать это. - person Adi Shavit; 30.10.2016
comment
@AdiShavit, чем опубликовать это в качестве ответа и принять. Хорошо иметь для потомков. - person StoryTeller - Unslander Monica; 30.10.2016

Если обратный вызов неизвестен во время компиляции, у вас есть следующие варианты:

  • Используйте одну функцию-оболочку и передайте целевой обратный вызов в user_data. Pro - достаточно прост в использовании; con - нуждается в user_data для собственного использования; требует очень похожих сигнатур функций
  • Используйте класс-оболочку, выделите экземпляр класса и передайте this в user_data. Pro — более универсальный, поскольку он может собирать некоторые данные в каждом экземпляре (например, он может хранить user_data для целевого обратного вызова или передавать дополнительные данные для целевого обратного вызова); con - необходимо управлять временем жизни экземпляра оболочки
  • Создайте отдельные преобразователи для каждого отдельного целевого обратного вызова. Pro - не требует использования user_data; con - довольно низкоуровневый и довольно непереносимый (как в компиляторе, так и в ОС); может быть трудно сделать; трудно сделать на С++, не прибегая к ассемблеру.

Первый вариант будет выглядеть примерно так (беззастенчиво обдирая @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!
person Andrey Turkin    schedule 30.10.2016

Отвечаю сам себе, надеясь, что у кого-то есть более простое решение.
Подход такой же, как описано здесь.

Мы собираемся использовать следующее:

  1. Лямбда-выражения без захвата могут быть автоматически приведены к указателям на функции с любым соглашением о вызовах.
  2. Функция C-API предоставляет void* user_data способ передачи данных обратному вызову.

Мы пройдем через C-API две лабмады:

  1. Один из них без захвата приведен к правильному соглашению о вызовах;
  2. Другой захватывает обратный вызов fn-ptr и передается как user_data лямбде без захвата для вызова. Он захватывает и исходный обратный вызов, и исходный 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.

ОБНОВЛЕНИЕ:
Вот более простая формулировка с одной лямбдой без захвата, но с той же концепцией:

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);
person Adi Shavit    schedule 30.10.2016
comment
@krzaq: см. обновленную формулировку с одной лямбдой, в основном то, что вы предложили, но не общее. - person Adi Shavit; 30.10.2016