Pull to refresh

Шаблоны с переменным количеством аргументов на примере обертки для Lua

Reading time 5 min
Views 20K
Понадобилось мне прикрутить Lua к проекту на C++. Писать обертки в ручную — лень (слишком много писать), готовые не подходили по тем или иным причинам. Решил написать свою. А потому задался вопросом, как максимально упростить интерфейс? От одной только мысли об этом в голову лезли жутчайшие конструкции из шаблонов. Так оно в последствии и оказалось, но гораздо проще, чем представлялось.

В C++11 появились шаблоны с переменным числом аргументов, это позволяет писать шаблонные функции/классы так, как в C++03 было невозможно вовсе. Такие шаблоны сильно упрощают задачу.

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

В итоге хотелось увидеть интерфейс близкий к следующему:

lua.export_function(some_function);


Можно попробовать. Однако интерфейс будет все-таки чуточку сложнее. Нужно указать интерпретатору имя для экспортируемой функции. И передавать будем адрес на функцию.

lua.export_function("some_function", &some_function);

Воспользуемся выводом параметров шаблона. Параметры могут быть выведены автоматически, если они будут:
  • Возвращаемым значением колбэка:
    template <typename T>
    void some_function(T (*callback)()) {}
    

  • Параметром колбэка:
    template <typename T>
    void some_function(void (*callback)(T)) {}
    

  • Классом, которому принадлежит метод.
    template <typename T>
    void some_function(void (T::*method)()) {}
    


Все эти случаи (и еще несколько других), могут комбинироваться. Можно этим воспользоваться.

template <typename R, typename... Args>
void export_function(const std::string& name, T (*function)(Args...)) {
}

Теперь, можно взяться за собственно экспорт функции. Для каждой функции создадим лямбду, которая будет принимать аргументы от интерпретатора, передавать их в функцию, а потом, возвращать интерпретатору результат. Лямбда должна храниться всё время, что работает экземпляр интерпретатора, поэтому указатель на каждую лямбду я сохраняю внутри класса и удаляю в деструкторе.

template <typename R, typename... Args>
void export_function(const std::string& name, T (*function)(Args...)) {
    auto function = new std::function<int(Lua&)>([function](Lua& vm) -> int {
        auto tuple = args<Args...>();
        return apply_function<std::tuple_size<decltype(tuple)>::value>
            ::apply(function, tuple);
    });
    lambda(function);
}

Выглядит странно. Попробуем разобраться. Для начала надо получить все аргументы от интерпретатора.

template <typename T, typename T1, typename... Args>
std::tuple<T, T1, Args...> args(const int i = 1) {
    T t = arg<T>(i);
    return std::tuple_cat(t, args<T1, Args...>(i+1));
}

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

template <typename T>
std::tuple<T> args(const int i = 1) {
    return std::tuple<T>(arg<T>(i));
}

Функция arg — очевидна, не буду её приводить, всё что требуется — написать несколько специализаций.
Теперь, когда у нас есть все аргументы в одном кортеже, надо передать их все в функцию.

template <int N> struct apply_function {
    template <typename R, typename... FunctionArgs, typename... TupleArgs,
        typename... Args>
    static R apply(R (*function)(Args...), std::tuple<TupleArgs...>,
        Args... args) {
        return apply_function<N-1>::apply(function, tuple, std::get<N-1>::value, args);
    }
};

И нужно специализировать этот шаблон для последней итерации.

template <> struct apply_function<0> {
    template <typename R, typename... FunctionArgs, typename... TupleArgs,
        typename... Args>
    static R apply(R (*function)(Args...), std::tuple<TupleArgs...>,
        Args... args) {
        return (*function)(args...);
    }
};

Помимо всего этого потребуется еще несколько специализаций (проблемы в типе void).

Результаты


Получилась вполне рабочая обертка для экспорта C++ функций и классов в lua. Из очевидных минусов вижу всего несколько:
  • Лямбды все-таки медленнее колбэков, при желании можно переписать код без них, но получится больше шаблонных функций.
  • При каждом вызове функции/метода мы получаем две рекурсии, глубина которых равна количеству аргументов функций. Возможно компилятор сделает всю эту орду шаблонных функций инлайновыми, я не проверял (и не уверен в этом).
  • Шаблоны сильно сказываются на времени компиляции. Но даже на моем довольно слабом ноутбуке сборка этой обертки и кода, который её использует, занимает гораздо меньше времени, чем сборка кода, который использует boost, так что это не критично.
  • Нет поддержки множественного наследования — слишком муторно его делать.
  • Нет доступа к метатаблицам, а значит нет переопределения операторов.
  • Нет поддержки перегрузки функций, но можно просто дать перегружаемым функциям разные имена.

Последние три пункта, возможно, сделаю чуть позже.
А теперь плюсы:
  • Простой интерфейс.
  • Решение на чистом C++11, не требует генерации дополнительного кода дополнительными инструментами.

Как использовать

Прежде всего нужно создать объект класса util::Lua, при этом проинициализируется интерпретатор.

util::Lua vm;

После этого можно экспортировать функции/классы.

Функции

Всё просто. Мы используем только указатель на функцию и имя, под которым она будет доступна в lua.

some_function();

vm.export_function("some_function", &some_function);

Типы всех параметров и возвращаемого значения будут определены и обработаны корректно.

Классы

Экспортируемый класс надо подготовить. Для начала нужно унаследовать его от util::LuaClass, чтобы при возвращении объекта интерпретатору был возвращен именно объект, а не userdata. После нужно определить три статических метода.
  • Метод export_class должен экспортировать все методы/функции класса.
  • Метод export_me должен вызывать функцию Lua::export_class<A, B>()
  • Метод class_name должен возвращать имя класса.

class A : public util::LuaClass {
public:
    static void export_class(Lua& vm);
    static void export_me(Lua& vm);
    static const std::string class_name();
};

void A::export_me(Lua& vm) {
    vm.export_class<A>();
}

class B: public A {
public:
    static void export_class(Lua& vm);
    static void export_me(Lua& vm);
    static const std::string class_name();
};

void B::export_me(Lua& vm) {
    vm.export_class<B, A>();
}

Функции util::Lua::export_class передаются в качестве параметров шаблона — класс, который мы хотим
экспортировать и его родитель, чтобы экспортировать и его (если это еще не сделано).

Самое интересное творится в методе export_class. К примеру:

vm.export_constructor<A, int>();
vm.export_function("static_method", &A::static_method);
vm.export_method("method", &A::method);

Всё просто. Статические методы экспортируем как функции, методы — похожим образом, но через отдельную функцию. Конструктор экспортируется как функция с именем new, типы его аргументов необходимо указать явно в качестве
аргументов шаблона, связано это с тем, что на конструктор нельзя взять указатель. Приятная вещь в том, что объекты созданные посредством вызова такого конструктора из lua будет обрабатывать Garbage Collector. Когда все ссылки на объект будут удалены будет вызван delete для объекта C++.

Код

Весь код выложен на гитхабе github.com/alex-ac/LuaCxx под MIT лицензией.
Буду рад увидеть комментарии, советы, фичреквесты и багрепорты.

UPD

Чуть не забыл. Весь код собирается с помощью g++ 4.7.2, также должны работать g++ >= 4.6.4 и clang >= 3.0.
Tags:
Hubs:
+19
Comments 4
Comments Comments 4

Articles