В 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 лицензией.
Буду рад увидеть комментарии, советы, фичреквесты и багрепорты.
ссылка на оригинал статьи http://habrahabr.ru/post/165765/
Добавить комментарий