Игровой движок 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:


build gradle


Чтобы исправить ситуацию, добавляем ссылку на repositories в buildscript. Я также изменил версию в classpath 'com.android.tools.build:gradle' на более новую.


Код 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. Или позволить студии всё сделать за вас.


gradle-wrapper.properties


Дополнительно нужно поправить build.gradle для модуля app: в cleanAssets нужно добавить строчку delete "$projectDir/build/intermediates/jniLibs", без которой придётся делать clean проекта перед каждым запуском (взято отсюда)


Теперь синхронизация удалась, осталось только несколько warnings, связанных с устаревшей buildToolsVersion и старым синтаксисом в конфигурации. Поправить их не составит труда.


Теперь в студии мы видим 2 модуля: app и plugin. Стоит переименовать приложение (com.mycompany.app) и плагин (plugin.library), перед тем как продолжить работу.


Далее в коде плагин будет называться plugin.habrExamplePlugin


В плагине по умолчанию находится класс LuaLoader — именно он будет отвечать за обработку вызовов из lua кода. Там уже есть некий код, но давайте его очистим.


Код LuaLoader
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 элемент.


Полный код LuaLoader
@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;
    }
}

Примеры функций


Пример функции, которая принимает несколько строк и конкатенирует их через String builder

Реализация:


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)


Полезные ссылки


  1. Полный код доступен на гитхабе


  2. Документация по Corona Native



3) Один из репозиториев jnlua. Помог мне разобраться в назначении некоторых функций.

Комментарии (2)


  1. mon
    04.07.2018 10:42

    спасибо! хорошо бы похожую статью про Corona Native для iOS


    1. Shchvova
      04.07.2018 15:02

      Там похоже — открываете `ios/App` проект, и нажимаете кнопку запуска в Xcode. Если запускаете на устройстве придется выбрать подпись.