Подтвердить что ты не робот

Каков наилучший способ переноса обратного вызова C с помощью интерфейса С++ 11?

Скажем, что это C-функция, которую нужно обернуть:

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

Две основные ошибки с обратными вызовами функции функции C:

  • Невозможно сохранить выражения привязки
  • Невозможно сохранить захват lambdas

Я хотел бы знать, как наилучшим образом обернуть такие функции, как это сделать. Первое особенно полезно для обратного вызова функции-члена, а второе для встроенного определения, которое использует окружающие переменные, но это не единственное использование.

Другим свойством этих конкретных указателей на функции является необходимость использования соглашения о вызове __stdcall. Это, насколько мне известно, полностью исключает лямбда как вариант, и в любом случае это неприятно. Я хотел бы разрешить хотя бы __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 с помощью обратного вызова указателя функции, чтобы завершенная версия С++:

  • Позволяет использовать любой объект функции
  • Позволяет больше, чем просто вызов вызова callback C (если он критичен, что он то же самое, пользователь может передать что-то с правом вызова)
  • Является потокобезопасным/повторно входящим

Примечание. Очевидным решением, заявленным как часть ответа Микаэла Персона, является использование параметра void *, который должен существовать. Тем не менее, это, к сожалению, не исключение, все-таки вариант, в основном из-за некомпетентности. Какие возможности существуют для тех функций, которые не имеют этой опции, это то, где это может стать интересным, и является основным путем к очень полезному ответу.

4b9b3361

Ответ 1

Вам, к сожалению, не повезло.

Существуют способы генерации кода во время выполнения, например, вы можете читать LLVM trampoline intrinsics, где вы создаете функцию пересылки, которая сохраняет дополнительное состояние, очень сродни lambdas, но определено время выполнения.

К сожалению, ни один из них не является стандартным, и, следовательно, вы застряли.


Простейшим решением для передачи состояния является... фактическое состояние передачи. Ах!

Хорошо определенные C обратные вызовы будут принимать два параметра:

  • Указатель самой функции обратного вызова
  • A void*

Последний не используется самим кодом и просто передается обратному вызову при его вызове. В зависимости от интерфейса может быть передан ответный вызов, чтобы уничтожить его, или поставщик, или даже третья функция "destroy".

С таким интерфейсом вы можете эффективно передавать состояние в потокобезопасном и повторном входе на уровне 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

У этой проблемы две проблемы: одна простая и почти невозможная.

Первой задачей является преобразование статического типа (отображение) из любой вызываемой "вещи" в простой указатель функции. Эта проблема решена с помощью простого шаблона, без особого труда. Это решает проблему вызывающего соглашения (просто обертывая один вид функции другим). Это уже разрешено шаблоном std::function (почему он существует).

Основная проблема заключается в инкапсуляции состояния времени выполнения в простой указатель функции, чья подпись не позволяет указывать указатель "user-data" void* (как обычно имеет любой полупристойный API C). Эта проблема не зависит от языка (C, С++ 03, С++ 11) и почти невозможно решить.

Вам нужно понять фундаментальный факт о любом "родном" языке (и большинстве других). После компиляции код фиксируется, и только данные изменяются во время выполнения. Таким образом, даже функция члена класса, которая выглядит так, как будто она является одной из функций, принадлежащих объекту (состояние времени выполнения), это не так, код фиксирован, изменяется только идентификатор объекта (указатель this).

Другим фундаментальным фактом является то, что все внешние состояния, которые может использовать функция, должны быть глобальными или передаваться как параметр. Если вы устраните последнее, у вас будет только глобальное состояние. И по определению, если функция функции зависит от глобального состояния, она не может быть повторно включена.

Итак, чтобы иметь возможность создать (сортировку) ретранслятора * функцию, которая может быть вызвана с помощью простого указателя на функцию и инкапсулировать любой общий (state-ful) объект функции (bind'ed calls, lambdas или что-то еще), для каждого вызова вам понадобится уникальный код (а не данные). Другими словами, вам нужно сгенерировать код во время выполнения и доставить указатель на этот код (указатель функции обратного вызова) на функцию C. То, откуда происходит "почти невозможное". Это невозможно с помощью каких-либо стандартных механизмов С++, я на 100% уверен в этом, потому что, если это было возможно на С++, отражение во время выполнения также было бы возможно (и это не так).

Теоретически это может быть легко. Все, что вам нужно, это фрагмент скомпилированного "шаблонного" кода (не шаблон в смысле С++), который вы можете скопировать, вставить указатель на свое состояние (или объект функции) как своего рода жестко закодированную локальную переменную, а затем поместить это кода в некоторую динамически распределенную память (с некоторым подсчетом ссылок или тем, что обеспечит ее существование до тех пор, пока это необходимо). Но сделать это происходит, очевидно, очень сложно и очень "взломать". И, честно говоря, это намного опережает мой уровень мастерства, поэтому я даже не смог бы проинструктировать вас о том, как именно вы могли бы это сделать.

На практике реалистичный вариант - даже не пытаться это сделать. Ваше решение с глобальной (внешней) переменной, которую вы используете для передачи состояния (объект функции), идет в правильном направлении с точки зрения компромисса. У вас может быть что-то вроде пула функций, каждый из которых имеет свой собственный глобальный функциональный объект для вызова, и вы отслеживаете, какая функция в настоящее время используется в качестве обратного вызова, и при необходимости выделяйте неиспользуемые. Если у вас закончится ограниченный набор функций, вам придется выбросить исключение (или любую информацию об ошибках, которую вы предпочитаете). Эта схема была бы по существу эквивалентна описанному выше "теоретическому" решению, но с ограниченным числом одновременных обратных вызовов. Существуют другие решения в аналогичном ключе, но это зависит от характера конкретного приложения.

Мне жаль, что этот ответ не дает вам отличного решения, но иногда просто нет серебряных пуль.

Другой вариант - избегать использования API-интерфейса C, который был разработан буффонадами, которые никогда не слышали о неизбежном и чрезвычайно полезном параметре void* user_data.

* "sort-of" re-entrant, поскольку он по-прежнему относится к "глобальному" состоянию, но он повторен в том смысле, что разные обратные вызовы (которые требуют разного состояния) не мешают друг другу, как ваша оригинальная проблема.

Ответ 3

Как сказано выше, указатель функции C не содержит никакого состояния, поэтому функция обратного вызова, вызываемая без аргументов, может обращаться только к глобальному состоянию. Следовательно, такая функция "без учета состояния" может использоваться только в одном контексте, где контекст хранится в глобальной переменной. Затем объявляйте разные обратные вызовы для разных контекстов.

Если количество требуемых обратных вызовов изменяется динамически (например, в графическом интерфейсе, где для каждого окна, открытого пользователем, требуется новый обратный вызов для обработки ввода в это окно), тогда предопределите большой пул простых состояний обратные вызовы, которые сопоставляются с обратным вызовом 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, а не в С++ файле), вы можете создавать новые функции обратного вызова даже "на лету", используя не очень известную функцию GCC, вложенные функции:

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

В этом случае GCC создает для вас код необходимого генерации кода. Недостатком является то, что он ограничивает вас коллекцией компиляторов GNU и что бит NX больше не может быть использован в стеке, так как даже ваш код потребует новый код в стеке.

makecallback() вызывается из кода высокого уровня для создания новой анонимной функции обратного вызова с инкапсулированным состоянием. Если эта новая функция вызывается, она вызывается функцией statefull callback cb с состоянием arg. Новая функция анонимного обратного вызова используется, как долго, поскольку makecallback() не возвращается. Поэтому makecallback() возвращает управление вызывающему коду, вызывая переданную функцию "cont". В этом примере предполагается, что фактический обратный вызов cb() и нормальная функция продолжения cont() используют одно и то же состояние "состояние". Также возможно использовать два разных указателя void для передачи другого состояния в оба.

Функция "cont" может возвращаться (и СЛЕДУЕТ также возвращаться, чтобы избежать утечек памяти), когда обратный вызов больше не требуется. Если ваше приложение многопоточное и требует различных обратных вызовов в основном для его различных потоков, то вы должны иметь возможность каждого потока при запуске распределять требуемые обратные вызовы через makecallback().

Однако, если ваше приложение многопоточно, и если у вас есть (или можно установить) строгое отношение обратного вызова к потоку, вы можете использовать потоковые локальные вары для передачи требуемого состояния. Конечно, это будет работать, только если ваш lib вызывает обратный вызов в нужном потоке.