Android Lua 相互调用
前言
本文基于 Lua 5.3.
Lua 是一个轻量级脚本语言,常用于嵌入其他语言作为补充。关于更多Lua本身的问题不在本文讨论范围之内。
在 Android 中嵌入 Lua 优点很多,借助 Lua 脚本语言的优势,可以轻松实现动态逻辑控制,应用可以随时从服务器读取最新 Lua 脚本文件,在不更新应用的情况下修改程序逻辑。
可惜 Lua 官方只提供了 C API ,而 Android 主要使用 JAVA 作为开发语言。我们可以借助 JNI 来间接实现在 Android 中嵌入 Lua 。
准备
自己实现 JNI 是一件很费力的事情,还好有人已经造好了轮子叫做 Luajava ,并且又有人基于 Luajava 做了 Android 专用库。网上流传最广的是 Androlua ,不过作者已经多年不维护了,对 Lua 的支持依然停留在5.1并且有一些bug,有人Fork了这个项目,并将其更新至 Lua 5.3 :New Androlua ,不过这个项目也存在一些问题,我修复了一下但是作者并没有处理我的 pull request ,各位可以直接使用我修复优化后的:Android-Lua.
在修复bug的同时,我也添加了一些中文注释,减少第一次接触 Lua C API 朋友们的学习记忆成本。
由于最终需要调用 Lua C API,所以请先配置 NDK 开发环境。在 Android Studio 中打开 SDK Manager,切换到 SDK Tools 标签页,勾选CMake
、LLDB
、NDK
下载安装之。
导入工程
仅仅想实现 Android Lua 互相调用,不关心具体过程的,可以直接添加依赖:implementation 'cc.chenhe:android-lua:1.0.2'
然后后边的导入部分可以跳过了。
Clone github 项目到本地并用 Android Studio 打开。大致可以看到下图目录结构。(由于后期更新,结构不一定完全相同)
其中 androidlua
是库,app
是demo工程。库中,lua
下是 Lua 解释引擎,luajava
是 JNI 的有关代码。*.mk
文件是NDK配置文件,详情请参考Google NDK 文档。
你可以将 androidlua
Module 导入自己工程作为依赖库使用。
Lua API 知识普及
此时我们已经可以在 Android 与 Lua 直接互相调用了。但是在开始之前,还要学习下 Lua C API 的有关东西,因为这是与 Lua 交互的基础。
Lua 与 C 依靠一个虚拟的栈来完成数据交换。包括变量、函数、参数、返回值等在内的一切数据,都要放入栈中来共享。为了调用方便,这个栈并不是严格遵循栈的规则。从索引来看,栈底索引为1,往上依次递增。而栈顶索引是-1,往下依次递减。因此,正负索引都是合法的,但是0不可以。下面是栈的示意图:
常用 Lua C API 介绍
Lua 提供了大量的 C API,与其他语言的交互完全依赖这些 API,下面的基础教程中本文会介绍几个基础的,具体可以查看Lua 官方手册
这些函数均由 Lua 提供,在 Luajava 中被封装在 LuaState
类下。
luaL_openlibs
加载 Lua 标准库,一般需要调用一下。
luaL_dostring
执行一段 Lua 脚本。
luaL_dofile
执行给定文件中的 Lua 脚本。
lua_dump
获取当前栈的内容。
java 中对应函数是dumpStack()
,返回String,可以直接输出,便于调试。
lua_pushXXX
将各种类型的数据压入栈,以便未来使用。
lua_toXXX
将栈中指定索引处的值以xxx类型取出。
lua_getglobal
获取 Lua 中的全局变量(包括函数),并压入栈顶,以便未来使用。
参数就是要获取的变量的名字。
lua_getfield
获取 Lua 中 某一 table 的元素,并压入栈顶。
第一个参数是 table 在栈中的索引,第二个参数是要获取元素的 key.
lua_pcall
执行 Lua 函数。
第一个参数是此函数的参数个数,第二个是返回值个数,第三个是错误处理函数在栈中的索引。
Enjoy coding
终于要开始调用了,想想还有点小激动呢~
为了方便说明,我们先定义一个脚本文件叫 test.lua
,然后将其放在assets
目录下,因为这下面的文件不会被编译。
可以使用下面的函数来读取 assets 中文件的内容:
public static String readAssetsTxt( Context context, String fileName ){ try { InputStream is = context.getAssets().open( fileName ); int size = is.available(); /* Read the entire asset into a local byte buffer. */ byte[] buffer = new byte[size]; is.read( buffer ); is.close(); /* Convert the buffer into a string. */ String text = new String( buffer, "utf-8" ); /* Finally stick the string into the text view. */ return(text); } catch ( IOException e ) { e.printStackTrace(); } return("err"); }
创建 Lua 栈
之前说了,依靠一个虚拟的栈来完成数据交换。那么首先我们当然要创建这个栈。
LuaState lua = LuaStateFactory.newLuaState(); //创建栈 lua.openLibs(); //加载标准库 lua.close(); //养成良好习惯,在执行完毕后销毁Lua栈。
执行 Lua 脚本
我们可以使用LdoString()
来执行一段简单的脚本。
String l = "local a = 1"; lua.LdoString(l);
这样就执行了一个很简单的脚本,他声明了一个全局变量l
,值为1.
当然,也可以使用LdoFile()
来加载脚本文件,不过这需要你先把脚本复制到 SD 卡才行。因为在 APK 中是没有“路径”可言的。
读取 Lua 变量与 table
首页要说明的是,只有全局变量(非 local)才可以读取。
test.lua:
a = 111; t = { ["name"] = "Chenhe", [2] = 2222, }
android:
lua.getGlobal("a"); //获取变量a并将值压入栈 Log.i("a", lua.toInteger(-1) + ""); //以int类型取出栈顶的值(也就是a) lua.getGlobal("t"); //获取变量t并压入栈顶,此时table位于栈顶。 lua.getField(-1, "name"); //取出栈顶的table的name元素,压入栈顶。 Log.i("t.name", lua.toString(-1)); //以string类型取出栈顶的值(也就是t.name) Log.i("dump",lua.dumpStack()); //输出当前栈
运行后可以看到log:
I/a: 111 I/t.name: Chenhe I/dump: 1: number = 111.0 2: table 3: string = 'Chenhe'
我们已经成功读取了 Lua 中的变量。
执行 Lua 函数
调用 Lua 函数的一般流程为:
- 获取函数并入栈。
- 压入各个参数(如果有)
- 调用函数,指明参数个数、返回值个数、错误处理函数。
- 获取返回值(如果有)
test.lua:
function test(a, b) return a + b, a - b; end
android:
lua.getGlobal( "test" ); //获取函数并入栈 lua.pushInteger( 5 ); //压入第一个参数a lua.pushInteger( 3 ); //压入第二个参数b lua.pcall( 2, 2, 0 ); //执行函数,有2个参数,2个返回值,不执行错误处理。 Log.i( "r1", lua.toInteger( -2 ) + "" ); //输出第一个返回值 Log.i( "r2", lua.toInteger( -1 ) + "" ); //输出第二个返回值 Log.i( "dump", lua.dumpStack() );
运行后可以看到log:
I/r1: 8 I/r2: 2 I/dump: 1: number = 8.0 2: number = 2.0
这就成功地执行了 Lua 函数,并取得2个返回值。而之前入栈的函数以及参数,在执行的时候 Lua 已经弹出了,所以最后栈里只剩下2个返回值。
传入 Java 对象
得益于 Luajava 以及 Androlua 的封装,我们可以直接将对象作为参数传入,并在 Lua 中直接执行对象的成员函数。
test.lua:
function setText(tv,s) tv:setText("set by Lua."..s); tv:setTextSize(50); end
android:
lua.getGlobal("setText"); //获取函数 lua.pushJavaObject(textView); //把TextView传入 lua.pushString("Demo"); //传入一个字符串 lua.pcall(2,0,0); //执行函数,有2个参数。
注入 Lua 变量
有时我们需要在 android 中创建 Lua 变量,这样就可以在 Lua 中直接使用了。注意,这里创建的变量都是全局的(非 local)。
test.lua:
function setText(tv) tv:setText("set by Lua."..s); --这里的s变量由java注入 tv:setTextSize(50); end
android:
lua.pushString( "from java" ); //压入欲注入变量的值 lua.setGlobal( "s" ); //压入变量名 lua.getGlobal( "setText" ); //获取Lua函数 lua.pushJavaObject( textView ); //压入参数 lua.pcall( 1, 0, 0 ); //执行函数
可以看到 Lua 成功调用了 Java 注入的变量s
.