在android应用中使用luasocket

    最近接手一个移动端应用,要为其android版本扩展支持调用lua脚本解析,而且最好同时能支持luasocket。如果只是希望在android下支持lua标准库的使用,那么androLua这个开源项目就可以解决这个问题。然而在为其扩展支持三方库,如luasocket时,遇到了一些问题,经过一翻折腾,最终解决了这个问题,把折腾的过程记录下来,方便有其他相同需求的人少走弯路。


    基础知识

    Lua是一门用标准C编写的动态脚本语言,如果希望在android上使用,则需要解决两个问题。第一,需要用JNI为Lua的C库进行封装,这样才可能在Java中使用。第二,由于Android系统开发所特有的系统环境限制,Lua三方库的动态加载机制和lua脚本模块的导入机制将不能正常运行,需要进行特殊处理。

    对于第一个问题,LuaJava这个开源项目提供了Java调用Lua C代码的JNI封装接口。

    对于第二个问题,分为两个方面。一方面是lua脚本模块的加载,由于android系统和普通的Linux环境并不相同,如果按照Lua标准的模块查找机制,将无法找到相关的模块文件。AndroLua解决了这个问题,它通过把lua脚本模块放到android应用的asserts目录下,在运行时通过资源类打开这些文件,用dostring来执行lua代码。另一方面是lua三方库如luasocket /luaJson等共享库的路径查找机制,在android系统下无法正常使用。本文主要解决第二个问题的第二个方面。


     让lua能支持三方库的调用思考

    第一,使用Java调用共享库的方式,把三方库编译成so文件,然后再封装一层JNI接口,通过java代码使用。但这种方式还不如直接用Java实现相关功能,在Lua中调用来的简单。

    第二,利用lua自己的so模块加载机制,直接加载三方库。最初,我是希望走这条路的,因为这样不需要修改lua的源代码,但一直未能成功。其方法思想是把三方库编译的单独so文件打到apk包里,这样安装应用后,so文件会有一个路径。这样只需要修改luaconf.h文件,把CPATH的配置修改为so文件的路径,然后用ndk重新编译luaJava。这种方法确实有一定的效果,当我用这种方式把luasocket加载进去后,可以成功调用一次操作,操作返回后应用就崩溃了,查看logcat输出的日志,在调用共享库方法后出现段错误。一度以为是luasocket的实现有缺陷,希望能解决源码的问题,花费了不少无用功。后来,随着我对lua C API了解的增加,这条捷径被我放弃了。决定采用下面的方法。

   第三,直接把luasocket代码静态编译到lua中,也就是说socket将成为lua的标准库,无须动态加载,这样luajava最终编译出来只有一个so文件,其中包含了lua/luasocket的功能。


   具体实现步骤如下

  1  目录结构整理

     在jni目录下创建3个子目录,分别存放lua / luasocket / luajava 的源码

  2  修改jni/lua/linit.c源码

     将luasocket导出加载函数注册到标准库加载函数中,此外增加两个头文件的包含和模块名宏定义 

#define linit_c
#define LUA_LIB

#include "lua.h"

#include "lualib.h"
#include "lauxlib.h"

#include "luasocket.h"
#include "mime.h"

#define LUA_SOCKETLIBNAME "socket"
#define LUA_MIMELIBNAME "mime"

static const luaL_Reg lualibs[] = {
  {"", luaopen_base},
  {LUA_LOADLIBNAME, luaopen_package},
  {LUA_TABLIBNAME, luaopen_table},
  {LUA_IOLIBNAME, luaopen_io},
  {LUA_OSLIBNAME, luaopen_os},
  {LUA_STRLIBNAME, luaopen_string},
  {LUA_MATHLIBNAME, luaopen_math},
  {LUA_DBLIBNAME, luaopen_debug},
  {LUA_MIMELIBNAME,luaopen_mime_core},
  {LUA_SOCKETLIBNAME,luaopen_socket_core},
  {NULL, NULL}
};

  3  修改ndk的构建文件

 创建luasocket/Android.mk将luasocket先编译为静态库,ndk的项目文件和Make的语法一样,只是增加了一些宏和函数,不再此做介绍

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := socket
LOCAL_C_INCLUDES += $(LOCAL_PATH)/../lua/
LOCAL_SRC_FILES := luasocket.c timeout.c buffer.c io.c auxiliar.c options.c inet.c tcp.c udp.c except.c select.c usocket.c mime.c

include $(BUILD_STATIC_LIBRARY)
修改luajava/Android.mk将编译的lua和luasocket静态库一起编译为共享库
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_C_INCLUDES += $(LOCAL_PATH)/../lua
LOCAL_C_INCLUDES += $(LOCAL_PATH)/../luasocket

LOCAL_MODULE     := luajava
LOCAL_SRC_FILES  := luajava.c
LOCAL_STATIC_LIBRARIES := liblua libsocket

LOCAL_LDLIBS    := -ldl -lm

include $(BUILD_SHARED_LIBRARY)

  4 修改socket.lua mime.lua  http.lua源代码

在原始的luasocket中,共享库这一层生成了socket/core.so  mime/core.so,而在lua模块这一层导出了socket.http socket.url  socket.lua mime.lua这4个重要的模块。其中socket.lua中加载socket/core.so,导出了socket模块,mime.lua加载mime/core.so导出了mime模块。

由于android中只能把所有.lua文件存放在assert目录下,且不能有层次结构。因而创建模块时,要把http/url模块从socket中提出来,形成单独的模块。另外luasocket内部注册模块时,把模块名定义为socket和mime,为了不修改luasocket的代码,因而依旧保留了共享库的模块名为socket/mime。这样就和上层lua脚本模块重名,造成冲突。需要修改lua脚本的模块名,分别修改为socketlua.lua,mimelua.lua。这样只需要修改各lua文件的头部模块变量初始化部分的代码即可,各修改部分的代码如下:

socketlua.lua(原socket.lua)文件头部

local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket")
module("socket")
mimelua.lua(原mime.lua)文件头部
local base = _G
local ltn12 = require("ltn12")
local mime = require("mime")
local io = require("io")
local string = require("string")
module("mime")
http.lua(原socket/http.lua)文件头部
require("socketlua")
require("mimelua")
local socket = _G.socket
local mime = _G.mime
local base = _G
local url = require("url")
local ltn12 = require("ltn12")
local string = require("string")
local table = require("table")
module("http")
url.lua(原socket/url)文件头部
local string = require("string")
local base = _G
local table = require("table")
module("url")

5 应用

修改后,在lua中需要使用哪个模块就可以直接引用哪个模块了。下面是个小示例,它用flvxz网站提供的视频解析功能来解析一些主流网站的视频下载地址:

local http=require('http')
local url =require('url')
local mime=require('mime') 
local json=require('json')
local base = _G

module("flvxz")

local flvxz_parse_json = function(jobj)
    for k,item in base.pairs(jobj) do
        files = item['files']
        if #files == 1 then
           if  files[1]['ftype']=="mp4" then
               return files[1]['furl'] 
           end
        end
    end
end
        
flvxz_parse_url = function(url)
        local api="http://api.flvxz.com/jsonp/purejson/url/";
        local surl = base.string.gsub(url,"://",":##");
        local eurl = mime.b64(surl);
        local b,c,h = http.request(api..eurl);
        local succ,data = base.pcall(json.decode,b)
        if not succ then
            return nil;
        end
        return flvxz_parse_json(data)
end

local test = function()
  local urls= {
        "http://v.youku.com/v_show/id_XNTUzMDQzODky.html",
        "http://www.tudou.com/programs/view/YDn_zTq_8gI/",
        "http://www.iqiyi.com/v_19rrh3v2vw.html",
        "http://www.letv.com/ptv/vplay/20047225.html"
       }

  for k,url in base.pairs(urls) do
     base.print(flvxz_parse_url(url),url)
     base.print("-------------\n")
  end
end

--test();

相关链接

Luajava  http://keplerproject.org/luajava/

androLua  https://github.com/mkottman/AndroLua/




你可能感兴趣的:(android,lua,luasocket)