Понадобилось мне прикрутить 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 лицензией.
Буду рад увидеть комментарии, советы, фичреквесты и багрепорты.
Автор: alexac