Игровой движок Corona позволяет создавать кроссплатформенные приложения и игры. Но иногда предоставляемого им API бывает недостаточно. Для таких случаев есть Corona Native, позволяющий расширять функциональность с использованием родного кода для каждой платформы.
В статье пойдёт речь об использовании Java в проектах Corona для android
Для понимания происходящего в статье требуются базовые знания Java, Lua и движка Corona
Начало работы
На компьютере должны быть установлены Corona и Android Studio
В папке с установкой Corona также находится шаблон проекта: Native\Project Template\App. Копируем всю папку и переименовываем в имя своего проекта.
Настройка шаблона
Примечание: я использовал последний доступный public build для Corona — 2017.3184. В новых версиях шаблон может измениться, и некоторые приготовления из этой главы перестанут быть нужны.
Для android нам нужны 2 папки внутри: Corona и android
Из папки Corona удаляем Images.xcassets и LaunchScreen.storyboardc — эти папки нам не понадобятся. В файле main.lua также удаляем весь код — мы начнём создание проекта с нуля. Если вы хотите использовать существующий проект, то замените все файлы в папке Corona на свои
Папка android — это готовый проект для Android Studio, нам нужно открыть его. Первым же сообщением от студии будет "Gradle sync failed". Нужно исправить build.gradle:

Чтобы исправить ситуацию, добавляем ссылку на repositories в buildscript. Я также изменил версию в classpath 'com.android.tools.build:gradle' на более новую.
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.1.3' } repositories { jcenter() google() } } allprojects { repositories { jcenter() google() } } task clean(type: Delete) { delete rootProject.buildDir }
Следующим шагом будет изменение gradle-wrapper.properties. Изменить можно вручную, заменив в distributionUrl версию gradle. Или позволить студии всё сделать за вас.

Дополнительно нужно поправить build.gradle для модуля app: в cleanAssets нужно добавить строчку delete "$projectDir/build/intermediates/jniLibs", без которой придёт��я делать clean проекта перед каждым запуском (взято отсюда)
Теперь синхронизация удалась, осталось только несколько warnings, связанных с устаревшей buildToolsVersion и старым синтаксисом в конфигурации. Поправить их не составит труда.
Теперь в студии мы видим 2 модуля: app и plugin. Стоит переименовать приложение (com.mycompany.app) и плагин (plugin.library), перед тем как продолжить работу.
Далее в коде плагин будет называться plugin.habrExamplePlugin
В плагине по умолчанию находится класс LuaLoader — именно он будет отвечать за обработку вызовов из lua кода. Там уже есть некий код, но давайте его очистим.
package plugin.habrExamplePlugin; import com.naef.jnlua.JavaFunction; import com.naef.jnlua.LuaState; @SuppressWarnings({"WeakerAccess", "unused"}) public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { return 0; } }
Использование кода плагина из lua кода
Для биндинга между java и lua кодом в Corona Native используется jnlua. LuaLoader реализует интерфейс jnlua.JavaFunction, таким образом его метод invoke доступен из lua кода. Чтобы удостовериться, что всё в порядке, добавим логгирующий код в LuaLoader.invoke и сделаем require плагина в main.lua
@Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); return 0; }
local habrPlugin = require("plugin.habrExamplePlugin") print("test:", habrPlugin)
Запустив приложение, среди логов увидим следующие 2 строчки:
D/Corona native: Lua Loader invoke called
I/Corona: test true
Итак, наше приложение загрузило плагин, а require возвращает true. Теперь попробуем вернуть из Java кода lua-таблицу с функциями.
Для добавления функций в модуль воспользуемся интерфейсом jnlua.NamedJavaFunction. Пример простой функции без аргументов и без возвращаемого значения:
class HelloHabrFunction implements NamedJavaFunction { @Override public String getName() { return "helloHabr"; } @Override public int invoke(LuaState L) { Log.d("Corona native", "Hello Habr!"); return 0; } }
Для регистрации нашей новой функции в lua используем метод LuaState.register:
public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); String libName = luaState.toString(1); // получаем имя модуля из стека (первый параметр require) NamedJavaFunction[] luaFunctions = new NamedJavaFunction[]{ new HelloHabrFunction(), // создаём экземпляр нашей функции }; luaState.register(libName, luaFunctions); // регистрируем наш модуль, он помещается наверх стека // Цифра 1 показывает сколько аргументов из стека вернётся в lua код. // Но в случае с require это ни на что не повлияет, require вернёт только наш модуль return 1; }
Данный код требует дополнительных пояснений:
LuaState, параметр метода invoke, по сути представляет обёртку над виртуальной машиной Lua (прошу меня скорректировать если я неверно выразился). Для тех, кто знаком с использованием lua кода из C, LuaState представляет собой то же, что и указатель lua_State в C.
Для тех, кто хочет углубиться в дебри работы с lua, рекомендую почитать мануал, начиная с The Application Program Interface
Итак, при вызове функции invoke мы получаем LuaState. У него есть стек, который содержит параметры, переданные в нашу функцию из lua кода. В данном случае это имя модуля, поскольку LuaLoader исполняется в момент вызова require("plugin.habrExamplePlugin").
Возвращаемое функцией invoke число показывает количество переменных из стека, которое вернётся в lua код. В случае с вызовом require это число ни на что не влияет, но мы воспользуемся этим знанием позже, создав функцию, возвращающую несколько значений
Добавление полей в модуль
Помимо функций мы также можем добавить в модуль дополнительные поля, например версию:
luaState.register(libName, luaFunctions); // регистрируем наш модуль, он будет расположен на вершине стека luaState.pushString("0.1.2"); // кладём в стек строку luaState.setField(-2, "version"); // установка поля version у нашего модуля.
В данном случае мы воспользовались индексом -2, чтобы указать, что поле нужно установить у нашего модуля. Отрицательный индекс означает, что отсчёт начинается с конца стека. -1 будет указывать на строку "0.1.2" (в lua индексы начинаются с единицы).
Чтобы не засорять стек, после установки поля я рекомендую вызывать luaState.pop(1) — выбрасывает из стека 1 элемент.
@SuppressWarnings({"WeakerAccess", "unused"}) public class LuaLoader implements JavaFunction { @Override public int invoke(LuaState luaState) { Log.d("Corona native", "Lua Loader invoke called"); String libName = luaState.toString(1); // получаем имя модуля из стека (первый параметр require) NamedJavaFunction[] luaFunctions = new NamedJavaFunction[]{ new HelloHabrFunction(), // создаём экземпляр нашей функции }; luaState.register(libName, luaFunctions); // регистрируем наш модуль, он помещается наверх стека luaState.register(libName, luaFunctions); // регистрируем наш модуль, он будет расположен на вершине стека luaState.pushString("0.1.2"); // кладём в стек строку luaState.setField(-2, "version"); // установка поля version у нашего модуля. // Цифра 1 показывает сколько аргументов из стека вернётся в lua код. // Но в случае с require это ни на что не повлияет, require вернёт только наш модуль return 0; } }
Примеры функций
Реализация:
class StringJoinFunction implements NamedJavaFunction{ @Override public String getName() { return "stringJoin"; } @Override public int invoke(LuaState luaState) { int currentStackIndex = 1; StringBuilder stringBuilder = new StringBuilder(); while (!luaState.isNone(currentStackIndex)){ String str = luaState.toString(currentStackIndex); if (str != null){ //toString возвращает null для non-string и non-number, игнорируем stringBuilder.append(str); } currentStackIndex++; } luaState.pushString(stringBuilder.toString()); return 1; } }
Использование в lua:
local joinedString = habrPlugin.stringJoin("this", " ", "was", " ", "concated", " ", "by", " ", "Java", "!", " ", "some", " ", "number", " : ", 42); print(joinedString)
class SumFunction implements NamedJavaFunction{
Override
public String getName() {
return "sum";
}
@Override public int invoke(LuaState luaState) { if (!luaState.isNumber(1) || !luaState.isNumber(2)){ luaState.pushNil(); luaState.pushString("Arguments should be numbers!"); return 2; } int firstNumber = luaState.toInteger(1); int secondNumber = luaState.toInteger(1); luaState.pushInteger(firstNumber + secondNumber); return 1; }
}
Java Reflection — использование Java классов напрямую в lua
В библиотеке jnlua есть специальный класс JavaReflector, который отвечает за создание lua таблицы из java объекта. Таким образом можно писать классы на java и отдавать их в lua код для дальнейшего использования.
Сделать это достаточно просто:
Пример класса
@SuppressWarnings({"unused"}) public class Calculator { public int sum(int number1, int number2){ return number1 + number2; } public static int someStaticMethod(){ return 4; } }
Добавление экземпляра этого класса к нашему модулю
luaState.pushJavaObject(new Calculator()); luaState.setField(-2, "calc"); luaState.pop(1);
Использование в Lua:
local calc = habrPlugin.calc print("call method of java object", calc:sum(3,4)) print("call static method of java object", calc:getClass():someStaticMethod())
Обратите внимание на двоеточие в вызове метода класса. Для статических методов также нужно использовать двоеточие.
Тут я заметил интересную особенность рефлектора: если мы передаём в lua только экземпляр класса, то вызов его статического метода возможен через getClass(). Но после вызова через getClass() последующие вызовы буду срабатывать и на самом объекте:
print("call method of java object", calc:sum(3,4)) -- ok print("exception here", calc:someStaticMethod()) -- бросает исключение "com.naef.jnlua.LuaRuntimeException: no method of class plugin.habrExamplePlugin.Calculator matches 'someStaticMethod()'" print("call static method of java object", calc:getClass():someStaticMethod()) -- ok print("hmm", calc:someStaticMethod()) -- после вызова через getClass мы получили возможность работать с этим методом напрямую
Также, используя getClass(), мы можем создавать новые объекты прямо в lua:
local newInstance = calc:getClass():new()
К сожалению, сохранить Calculator.class в поле модуля мне не удалось из-за "java.lang.IllegalArgumentException: illegal type" внутри setField.
Создание и вызов lua функций "на лету"
Этот раздел появился по причине того, что корона не предоставляет возможность обратиться к функциям из своего api напрямую в Java. Но jnlua.LuaState позволяет загружать и выполнять произвольный lua код:
class CreateDisplayTextFunction implements NamedJavaFunction{ // Вызываем функцию из API короны private static String code = "local text = ...;" + "return display.newText({" + "text = text," + "x = 160," + "y = 200," + "});"; @Override public String getName() { return "createText"; } @Override public int invoke(LuaState luaState) { luaState.load(code,"CreateDisplayTextFunction code"); // загружаем код в стек, создавая из него функцию luaState.pushValue(1); // помещаем первый параметр функции на вершину стека luaState.call(1, 1); // вызываем нашу функцию, указываем что она должна получить 1 параметр, а также вернуть 1 return 1; } }
Не забудьте зарегистрировать функцию через LuaLoader.invoke, аналогично предыдущим примерам
Вызов в lua:
habrPlugin.createText("Hello Habr!")
Заключение
Таким образом, ваше приложение на android может использовать все нативные возможности платформы. Единственный недостаток этого решения — вы лишаетесь возможности использовать Corona Simulator, что замедляет разработку (перезапуск симулятора практически мгновенен, в отличие от отладки на эмуляторе или устройстве, который требует build + install)
Полезные ссылки
Полный код доступен на гитхабе
3) Один из репозиториев jnlua. Помог мне разобраться в назначении некоторых функций.
