Android Lua 相互调用
前言
本文基于 Lua 5.3.
Lua 是一个轻量级脚本语言,常用于嵌入其他语言作为补充。关于更多Lua本身的问题不在本文讨论范围之内。 在 Android 中嵌入 Lua 优点很多,借助 Lua 脚本语言的优势,可以轻松实现动态逻辑控制,应用可以随时从服务器读取最新 Lua 脚本文件,在不更新应用的情况下修改程序逻辑。 可惜 Lua 官方只提供了 C API ,而 Android 主要使用 JAVA 作为开发语言。我们可以借助 JNI 来间接实现在 Android 中嵌入 Lua 。 <!-- more -->
准备
自己实现 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
.
Lua 调用 java
Lua 调用 java 函数相对复杂,毕竟 java 不是脚本语言。我们需要将 java 函数包装成一个 JavaFunction
类,实例化之后注册到 lua,这样才可以从 lua 调用。下面看一个例子:
public class MyJavaFunction extends JavaFunction {
public MyJavaFunction(LuaState luaState) {
super(luaState);
}
@Override
public int execute() {
// 获取Lua传入的参数,注意第一个参数固定为上下文环境。
String str = L.toString(2);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(System.currentTimeMillis());
L.pushString(simpleDateFormat.format(date) + str);
return 1; // 返回值的个数
}
public void register() {
try {
// 注册为 Lua 全局函数
register("testJava");
} catch (LuaException e) {
e.printStackTrace();
}
}
}
我们创建一个类继承 JavaFunction
并实现 execute
方法,它将在被 lua 调用时执行。前面说过,lua 与 c 靠栈来交换数据,故调用函数所传的参数也会入栈。需要注意的是第一个参数恒为 lua 的上下文环境,实际传入的参数是从2开始。在这个简单的例子中,我们取得了 lua 传递的字符串,并将其拼接在由 java 获取的时间字符串后边一起返回给 lua。
与传参类似,返回值也是直接入栈即可。最后我们需要返回返回值的个数,这样 lua 就知道从栈里取出几个元素作为返回值了。这些元素会被自动出栈。这里我们利用 L.pushString()
将拼接后的字符串返回给 lua,并返回 1
表示有1个返回值。
最后,调用 register
方法注册到 lua,传入的字符串参数就是在 lua 中的函数名。
new MyJavaFunction(lua).register();
像这样实例化刚才包装的类就可以成功注册了。现在我们可以在 lua 中调用 testJava(String)
,它将返回一个字符串,内容是当前的时间加上我们传入的字符串。