Corona Native for Android - using arbitrary Java code in a game written in Corona

The game engine Corona allows you to create cross-platform applications and games. But sometimes the API it provides is not enough. For such cases, there is Corona Native , which allows you to extend the functionality using native code for each platform.


The article will discuss the use of Java in projects Corona for android


To understand what is happening in the article requires a basic knowledge of Java, Lua and the engine Corona


Beginning of work


Corona and Android Studio must be installed on the computer


The folder with the installation of Corona is also a project template: Native \ Project Template \ App. Copy the entire folder and rename it to the name of your project.


Template setting


Note: I used the latest available public build for Corona - 2017.3184 . In new versions, the pattern may change, and some of the preparations from this chapter will no longer be needed.


For android, we need 2 folders inside: Corona and android


From the Corona folder, delete the Images.xcassets and LaunchScreen.storyboardc - we will not need these folders. In the main.lua file we also delete all the code - we will start the creation of the project from scratch. If you want to use an existing project, replace all the files in the Corona folder with your own


The android folder is a ready project for Android Studio, we need to open it. The first message from the studio will be "Gradle sync failed". Need to fix build.gradle:


build gradle


To fix the situation, add a link to the repositories in buildscript. I also changed the version in the classpath 'com.android.tools.build:gradle' to a newer one.


Build.gradle code
// 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 } 

The next step is to change gradle-wrapper.properties . You can change it manually by replacing the gradle version in distributionUrl . Or let the studio do everything for you.


gradle-wrapper.properties


Additionally, you need to fix the build.gradle for the app module: in cleanAssets , add the line delete "$ projectDir / build / intermediates / jniLibs" , without which you have to clean the project before each launch ( taken from here )


Now the synchronization succeeded, only a few warnings remained, related to the outdated buildToolsVersion and the old syntax in the configuration. To fix them is not difficult.


Now in the studio we see 2 modules: app and plugin. It is worth renaming the application (com.mycompany.app) and plugin (plugin.library) before continuing.


In the following code, the plugin will be called plugin.habrExamplePlugin.


The default plugin contains the LuaLoader class - it will be responsible for handling calls from the lua code. There is already some code there, but let's clean it up.


LuaLoader code
 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; } } 

Using plugin code from lua code


For binding between java and lua code in Corona Native, jnlua is used. LuaLoader implements the jnlua.JavaFunction interface, so its invoke method is available from the lua code. To make sure everything is in order, add the logging code to LuaLoader.invoke and make the require of the plugin in 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) 

Running the application, among the logs we will see the following 2 lines:


D / Corona native: Lua Loader invoke called
I / Corona: test true

So, our application loaded the plugin, and require returns true. Now we will try to return from Java code the lua-table with functions.


To add functions to the module, use the jnlua.NamedJavaFunction interface. An example of a simple function with no arguments and no return value:


 class HelloHabrFunction implements NamedJavaFunction { @Override public String getName() { return "helloHabr"; } @Override public int invoke(LuaState L) { Log.d("Corona native", "Hello Habr!"); return 0; } } 

To register our new function in lua, use the LuaState.register method:


 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; } 

This code requires additional explanation:


LuaState, a parameter of the invoke method, essentially represents a wrapper over a Lua virtual machine (please correct me if I put it wrong). For those familiar with using lua code from C, LuaState is the same as the lua_State pointer in C.


For those who want to delve into the wilds of working with lua, I recommend reading the manual, starting with The Application Program Interface


So, when invoke function is called, we get LuaState. It has a stack that contains the parameters passed to our function from the lua code. In this case, this is the name of the module, since the LuaLoader is executed when the require ("plugin.habrExamplePlugin") call is called.


The number returned by the invoke function indicates the number of variables from the stack, which will be returned to the lua code. In the case of the require call, this number does not affect anything, but we will use this knowledge later by creating a function that returns several values.


Adding fields to the module


In addition to functions, we can also add additional fields to the module, for example a version:


  luaState.register(libName, luaFunctions); //   ,       luaState.pushString("0.1.2"); //     luaState.setField(-2, "version"); //   version   . 

In this case, we used the index -2 to indicate that the field should be installed in our module. A negative index means that the countdown begins at the end of the stack. -1 will point to the string "0.1.2" (in lua, indices start with one).


In order not to clutter up the stack, after setting the field, I recommend calling luaState.pop (1) - throwing 1 element off the stack.


Full code 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; } } 

Function examples


An example of a function that accepts multiple strings and concatenates them through the String builder

Implementation:


 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; } } 

Use in lua:


 local joinedString = habrPlugin.stringJoin("this", " ", "was", " ", "concated", " ", "by", " ", "Java", "!", " ", "some", " ", "number", " : ", 42); print(joinedString) 

Example of returning multiple values

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 - using Java classes directly in lua


In the jnlua library there is a special class JavaReflector, which is responsible for creating the lua table from a java object. Thus, you can write classes in java and give them to lua code for further use.


This is quite simple:


Class example


 @SuppressWarnings({"unused"}) public class Calculator { public int sum(int number1, int number2){ return number1 + number2; } public static int someStaticMethod(){ return 4; } } 

Adding an instance of this class to our module


  luaState.pushJavaObject(new Calculator()); luaState.setField(-2, "calc"); luaState.pop(1); 

Use in 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()) 

Notice the colon in the class method call. For static methods you also need to use a colon.


Here I noticed an interesting feature of the reflector: if we pass only a class instance to lua, then the call of its static method is possible via getClass (). But after the call via getClass (), subsequent calls will be triggered on the object itself:


 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         

Also, using getClass (), we can create new objects right in lua:


 local newInstance = calc:getClass():new() 

Unfortunately, I could not save Calculator.class in the module field due to "java.lang.IllegalArgumentException: illegal type" inside setField .


Creating and calling lua functions on the fly


This section appeared because the crown does not provide the ability to access functions from its api directly in Java. But jnlua.LuaState allows you to load and execute arbitrary lua code:


 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; } } 

Do not forget to register the function via LuaLoader.invoke, similar to the previous examples.


Call lua:


 habrPlugin.createText("Hello Habr!") 

Conclusion


Thus, your application on android can use all the native features of the platform. The only drawback of this solution is that you lose the ability to use the Corona Simulator, which slows down the development (restarting the simulator is almost instantaneous, unlike debugging on an emulator or device that requires build + install)


useful links


  1. Full code is available on github


  2. Corona Native Documentation



3) One of the jnlua repositories . He helped me to understand the appointment of some functions.

Source: https://habr.com/ru/post/416079/


All Articles