Понадобилось мне прикрутить Lua к проекту на C++. Писать обертки в ручную — лень (слишком много писать), готовые не подходили по тем или иным причинам. Решил написать свою. А потому задался вопросом, как максимально упростить интерфейс? От одной только мысли об этом в голову лезли жутчайшие конструкции из шаблонов. Так оно в последствии и оказалось, но гораздо проще, чем представлялось.
В C++11 появились шаблоны с переменным числом аргументов, это позволяет писать шаблонные функции/классы так, как в C++03 было невозможно вовсе. Такие шаблоны сильно упрощают задачу.
Первым делом понадобилось написать обертку над простейшими действиями с интерпретатором (можно было бы обойтись простыми вызовами к C API Lua, но держать в памяти кучу индексов различных значений в стеке мне не хочется. Поэтому я обернул их в несколько функций, которые помимо того, что избавляют от необходимости передавать в каждую функцию указатель на состояние интерпретатора, практически не требуют индексов, так как они имеют значения по умолчанию.
В итоге хотелось увидеть интерфейс близкий к следующему:
Можно попробовать. Однако интерфейс будет все-таки чуточку сложнее. Нужно указать интерпретатору имя для экспортируемой функции. И передавать будем адрес на функцию.
Воспользуемся выводом параметров шаблона. Параметры могут быть выведены автоматически, если они будут:
Все эти случаи (и еще несколько других), могут комбинироваться. Можно этим воспользоваться.
Теперь, можно взяться за собственно экспорт функции. Для каждой функции создадим лямбду, которая будет принимать аргументы от интерпретатора, передавать их в функцию, а потом, возвращать интерпретатору результат. Лямбда должна храниться всё время, что работает экземпляр интерпретатора, поэтому указатель на каждую лямбду я сохраняю внутри класса и удаляю в деструкторе.
Выглядит странно. Попробуем разобраться. Для начала надо получить все аргументы от интерпретатора.
Получаем i-ый аргумент и возвращаем его, а с помощью рекурсии получаем остальные аргументы. Но этого мало.
Эту функцию нужно перегрузить, чтобы на последней итерации исполнялся другой код.
Функция arg — очевидна, не буду её приводить, всё что требуется — написать несколько специализаций.
Теперь, когда у нас есть все аргументы в одном кортеже, надо передать их все в функцию.
И нужно специализировать этот шаблон для последней итерации.
Помимо всего этого потребуется еще несколько специализаций (проблемы в типе void).
Получилась вполне рабочая обертка для экспорта C++ функций и классов в lua. Из очевидных минусов вижу всего несколько:
Последние три пункта, возможно, сделаю чуть позже.
А теперь плюсы:
Прежде всего нужно создать объект класса util::Lua, при этом проинициализируется интерпретатор.
После этого можно экспортировать функции/классы.
Всё просто. Мы используем только указатель на функцию и имя, под которым она будет доступна в lua.
Типы всех параметров и возвращаемого значения будут определены и обработаны корректно.
Экспортируемый класс надо подготовить. Для начала нужно унаследовать его от util::LuaClass, чтобы при возвращении объекта интерпретатору был возвращен именно объект, а не userdata. После нужно определить три статических метода.
Функции util::Lua::export_class передаются в качестве параметров шаблона — класс, который мы хотим
экспортировать и его родитель, чтобы экспортировать и его (если это еще не сделано).
Самое интересное творится в методе export_class. К примеру:
Всё просто. Статические методы экспортируем как функции, методы — похожим образом, но через отдельную функцию. Конструктор экспортируется как функция с именем new, типы его аргументов необходимо указать явно в качестве
аргументов шаблона, связано это с тем, что на конструктор нельзя взять указатель. Приятная вещь в том, что объекты созданные посредством вызова такого конструктора из lua будет обрабатывать Garbage Collector. Когда все ссылки на объект будут удалены будет вызван delete для объекта C++.
Весь код выложен на гитхабе github.com/alex-ac/LuaCxx под MIT лицензией.
Буду рад увидеть комментарии, советы, фичреквесты и багрепорты.
Чуть не забыл. Весь код собирается с помощью g++ 4.7.2, также должны работать g++ >= 4.6.4 и clang >= 3.0.
В 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.