LuaJavaBridge - Lua 與 Java 互操作的簡單解決方案

參考:http://dualface.github.io/blog/2013/01/01/call-java-from-lua/#luaj20E5AE9EE78EB0E58E9FE79086

最近在遊戲裏要集成中國移動的 SDK,而這些 SDK 都是用 Java 編寫的。由於我們整個遊戲都是使用 Lua 開發的,所以就面對 Lua 與 Java 互操作的問題。

傳統做法是先用 C/C++ 藉助 JNI(Java Native Interface)編寫調用 Java 的接口函數,然後再將這些函數通過 tolua++ 導出給 Lua 使用。這種做法最大的問題就是太繁瑣,而且稍微有一點點修改,就要重新編譯,嚴重降低了開發效率。

我嘗試寫了幾個接口函數後,發現 JNI 提供了完善的接口來操作 Java,比如查找特定的 Class、Method 等等。既然有這些東西,我想完全可以實現一個很薄的轉接層。這個層會提供一些函數,讓 Lua 代碼可以直接調用到 Java 的方法。

經過一番努力,LuaJavaBridge(簡稱 luaj)誕生了。

luaj 主要特徵

  • 可以從 Lua 調用 Java Class Static Method
  • 調用 Java 方法時,支持 int/float/boolean/String/Lua function 五種參數類型
  • 可以將 Lua function 作爲參數傳遞給 Java,並讓 Java 保存 Lua function 的引用
  • 可以從 Java 調用 Lua 的全局函數,或者調用引用指向的 Lua function

luaj 的功能很簡單,但對於集成各種 SDK 來說已經完全滿足需求了。

luaj 用法示例

下面的代碼是我們遊戲中實際使用的中國移動支付 SDK 調用代碼,luaj 好不好用一目瞭然:

Lua 代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
--[[
購買 1000 金幣

Java 方法原型:
public static void GameInterface_doBilling(final String billingIndex,
        final boolean useSms,
        final boolean isRepeated,
        final int luaFunctionId)
]]

-- 用於處理支付結果的函數
local function callback(result)
    if result == "success" then
        game.state:increaseCoins(1000)
        game.state:save()
    end
end

-- 調用 Java 方法需要的參數
local args = {
    "001",    -- billingIndex
    true,     -- useSms
    true,     -- isRepeated
    callback  -- luaFunctionId
}
-- Java 類的名稱
local className = "com/qeeplay/frameworks/ChinaMobile_SDK"
-- 調用 Java 方法
luaj.callStaticMethod(className, "GameInterface_doBilling", args)

上面的代碼就不解釋了,註釋已經寫得非常明白。

luaj 實現原理

luaj 的核心目標有兩個:從 Lua 調用 Java, 從 Java 調用 Lua。整理出來就是如下幾點:

  • 查找並調用指定的 Java 方法
  • 檢查調用結果,並從 Java 方法獲取返回值
  • 將 Lua function 作爲參數傳遞給 Java 方法
  • 在 Java 方法中調用 Lua function

查找並調用指定的 Java 方法

JNI 提供了 FindClass() 方法用於查找指定的 Class,所以 luaj.callStaticMethod() 的第一個參數就是要調用的 Java Class 的完整類名稱(類名稱中的“.”要替換爲“/”)。

找到指定 Class 後,利用 JNI 的 GetStaticMethodID() 方法就可以找到這個類的指定靜態方法,前提是要提供靜態方法的名稱和簽名。

所謂簽名,就是指 Java 方法的參數類型和返回類型定義。例如前面示例代碼中 GameInterface_doBilling() 方法的簽名是 (Ljava/lang/String;ZZI)V 。關於 Java 方法簽名的具體定義,可以參考:JNI Type Signatures

由於簽名寫起來有點囉嗦,所以 luaj 可以根據調用參數自動猜測方法簽名。示例代碼中,luaj.callStaticMethod() 的第二個參數指定了要查找的方法名稱,但並沒有提供方法的簽名,這就是利用了 luaj 的自動猜測簽名功能。

示例代碼一共指定了 4 個參數,分別是:字符串、布爾值、布爾值、Lua function。

1
2
3
4
5
6
7
-- 調用 Java 方法需要的參數
local args = {
    "001",          -- billingIndex
    true,           -- useSms
    true,           -- isRepeated
    callback        -- luaFunctionId
}

luaj 根據這 4 個參數,會構造出正確的 GameInterface_doBilling() 方法簽名。注意 Lua function 是以整數的形式傳入 Java 方法,所以 Java 方法的第四個參數是 int 類型)。

不幸的是 Lua 裏沒有辦法準確判斷一個數值是整數還是浮點數,所以 luaj 在猜測方法簽名時,假定所有的數值都是浮點數。因此下面的代碼第二個調用就會失敗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local args = {1} -- 生成的方法簽名是 (F)V

--[[
Java 方法原型:
public static void TestMethod1(final float integerValue)
]]
-- 調用成功
luaj.callStaticMethod(className, "TestMethod1", args)

--[[
Java 方法原型:
public static void TestMethod2(final int integerValue)
]]
-- 調用失敗,正確的方法簽名應該是 (I)V
luaj.callStaticMethod(className, "TestMethod2", args)

爲此,luaj 允許開發者指定完整的方法簽名。而且除了整數和浮點數的情況,在需要從 Java 方法獲得返回值時,也需要開發者指定完整的方法簽名。示例代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local args ={"StringValue", 1, 3.14}

--[[
Java 方法原型:
public static int TestMethod3(final String stringValue,
        final int integerValue,
        final float floatValue)
]]

-- 定義簽名
-- 參數: [S]tring, [I]nteger, [F]loat
-- 返回值: [I]nt
local sig = "(Ljava/lang/String;IF)I"

-- 調用方法並獲得返回值
local ok, ret = luaj.callStaticMethod(className, "TestMethod3", args, sig)

~

簽名使用“(依次排列的參數類型)返回值類型”的格式,幾個例子如下:

簽名 解釋
()V 參數:無,返回值:無
(I)V 參數:int,返回值:無
(Ljava/lang/String;)Z 參數:字符串,返回值:布爾值
(IF)Ljava/lang/String; 參數:整數、浮點數,返回值:字符串

這裏列出不同類型對應的 Java 簽名字符串:

類型名 類型
I 整數,或者 Lua function
F 浮點數
Z 布爾值
Ljava/lang/String; 字符串
V Void 空,僅用於指定一個 Java 方法不返回任何值

Java 方法裏接收 Lua function 的參數必須定義爲 int 類型,具體原因詳見“將 Lua function 作爲參數傳遞給 Java 方法”小節。

~

檢查調用結果,並從 Java 方法獲取返回值

luaj 調用 Java 方法時,可能會出現各種錯誤,因此 luaj 提供了一種機制讓 Lua 調用代碼可以確定 Java 方法是否成功調用。

luaj.callStaticMethod() 會返回兩個值:

  • 當成功時,第一個值爲 true,第二個值是 Java 方法的返回值(如果有)。
  • 當失敗時,第一個值爲 false,第二個值是錯誤代碼。

下面的代碼展示瞭如何檢查返回結果和獲得返回值:

Java 代碼
1
2
3
4
public static int AddTwoNumbers(final int number1,
        final int number2) {
    return number1 + number2;
}
Lua 代碼
1
2
3
4
5
6
7
8
9
local args = {2, 3}
local sig = "(II)I"
local ok, ret = luaj.callStaticMethod(className, "AddTwoNumbers", args, sig)

if not ok then
    print("luaj error:", ret)
else
    print("ret:", ret) -- 輸出 ret: 5
end

~

錯誤代碼定義如下:

錯誤代碼 描述
-1 不支持的參數類型或返回值類型
-2 無效的簽名
-3 沒有找到指定的方法
-4 Java 方法執行時拋出了異常
-5 Java 虛擬機出錯
-6 Java 虛擬機出錯

~

將 Lua function 作爲參數傳遞給 Java 方法

很多時候,我們需要一種方法讓 Java 代碼可以向 Lua 代碼傳遞一些消息。例如在大部分遊戲平臺的 SDK 中,涉及支付的部分都是異步操作的。在支付操作結束後,Java 代碼需要通知 Lua 支付成功與否。

Lua 虛擬機中,Lua function 以值的形式保存。但這個值無法直接給 Java 用,所以 luaj 做了一個 Lua function 引用表。當一個 Lua function 傳遞給 Java 時,這個 function 對應的值會被存在引用表中,並獲得一個唯一的引用 ID (整數)。Java 代碼拿到這個引用 ID 後,就可以很方便的調用該 Lua function 了。

回顧最開始的示例代碼,GameInterface_doBilling() 函數用於接收 Lua function 的參數就是 int 類型。因爲實際傳入 Java 函數的值是 Lua function 的引用 Id。

~

在 Java 方法中調用 Lua function

在 Java 代碼中拿到 Lua function 的引用 ID 後,就可以很方便的調用該 Lua function 了:

1
LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "hello");

這裏出現的 LuaJavaBridge 是 luaj 的 Java 部分定義的工具 class。 callLuaFunctionWithString() 方法可以將一個字符串參數傳遞給指定的 Lua function。

LuaJavaBridge 還提供了 callLuaGlobalFunctionWithString() 方法,可以直接調用 Lua 中指定名字的全局函數。這樣可以在沒有 Lua function 引用 ID 的情況下和 Lua 代碼交互。

由於自己的項目暫時沒更多需求,所以目前 luaj 只支持向 Lua function 傳遞單個字符串參數。

~

GL 線程和 UI 線程的協調

cocos2d-x for Android 運行在多線程環境下,所以在 Lua 和 Java 交互時需要注意選擇適當的線程。

~

cocos2d-x 在 Android 上以兩個線程來運行,分別是負責圖像渲染的 GL 線程和負責 Android 系統用戶界面的 UI 線程。

  • 在 cocos2d-x 啓動後,Lua 代碼將由 GL 線程調用,因此從 Lua 中調用的 Java 方法如果涉及到系統用戶界面的顯示、更新操作,那麼就必須讓這部分代碼切換到 UI 線程上去運行。
  • 反之亦然,從 Java 調用 Lua 代碼時,需要讓這個調用在 GL 線程上執行,否則 Lua 代碼雖然執行了,但會無法更新 cocos2d-x 內部狀態。

下面是 GameInterface_doBilling() 方法的主要代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void GameInterface_doBilling(final String billingIndex,
    final boolean useSms,
    final boolean isRepeated,
    final int luaFunctionId) {
  context.runOnUiThread(new Runnable() {
    @Override
    public void run() {
      GameInterface.doBilling(useSms, isRepeated, billingIndex, new BillingCallback() {

        ...

        @Override
        public void onBillingSuccess() {
          context.runOnGLThread(new Runnable() {
            @Override
            public void run() {
              LuaJavaBridge.callLuaFunctionWithString(luaFunctionId, "success");
              LuaJavaBridge.releaseLuaFunction(luaFunctionId);
            }
          });
        }

        ...

      });
    }
  });
}

~

方法中,構造了一個 Runnable 對象,用來包裝需要執行的 Java 代碼。這個 Runnable 對象被指定運行在 UI 線程上。這樣當調用 GameInterface.doBilling() 方法時就可以正確顯示出支付界面。

當用戶支付成功後,GameInterface.doBilling() 會調用 BillingCallback.onBillingSuccess() 方法。這個方法裏構造了另一個 Runnable 對象,包裝了調用 Lua function 的代碼。

看上去代碼不少,實際上就是在兩個線程間互相切換。確保 Lua function 跑在 GL 線程,Java 代碼跑在 UI 線程。

~

Lua function 的引用計數器

Lua 虛擬機具有自動垃圾回收機制。Lua function 既然是值,那麼在沒有被使用時自然會被回收掉。所以 luaj 提供了 retainLuaFunction() 和 releaseLuaFunction() 兩個函數用於增減 Lua function 的引用計數。

將一個 Lua function 以引用 ID 的形式傳入 Java 時,luaj 會自動增加引用 ID 的計數器,所以在 Java 方法裏可以放心的異步調用 Lua function。但在不需要使用該 Lua function 後,一定要調用 releaseLuaFunction() 減少該引用 ID 的計數器。當計數器爲 0 時,會自動釋放該 Lua function。

如果瞭解 cocos2d-x 中 CCObject 的 autorelease 機制,那麼對引用計數應該很熟悉,兩者是完全相同的實現機制。

~

連接第三方 SDK 和 cocos2d-x 的中間層

雖然 luaj 可以讓開發者從 Lua 中直接調用 Java 代碼。但大部分第三方 SDK 在初始化時都需要指定當前應用程序的 Activity 對象,並且還要切換不同線程,所以對於大多數第三方 SDK,我們仍然要寫一箇中間層用於 Lua 和 Java 的交互。

與使用 JNI 做中間層相比,配合 luja 的中間層是使用 Java 來編寫的,不但更簡單明瞭,而且處理線程切換也非常簡單。

~

要實現一箇中間層,只有兩個步驟:

  • 實現供 luaj 調用的 Java 接口
  • 修改遊戲的 Java 入口文件,將應用程序的 Activity 對象傳入 SDK

第一步請參考:“中國移動遊戲基地和短信支付 SDK”中間層源代碼

第二步也相當簡單,只需要在遊戲的 onCreate() 中調用 中間層 class 的 setContext() 方法:

1
2
3
4
5
6
7
8
9
10
public class mygame extends Cocos2dxActivity {

  protected void onCreate(Bundle savedInstanceState) {
    ChinaMobile_SDK.setContext(this); // init sdk
    super.onCreate(savedInstanceState);
  }

  ...

}

~

做好一切準備工作後,在遊戲的 Lua 代碼裏訪問 SDK 功能就很簡單了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
local luaj = require("luaj")

local className = "com/qeeplay/frameworks/ChinaMobile_SDK"

-- 初始化 SDK
local args = {
  CHINA_MOBILE_SP_APP_NAME,
  CHINA_MOBILE_SP_CP_NAME,
  CHINA_MOBILE_SP_TEL
}
luaj.callStaticMethod(className, "GameInterface_initializeApp", args)

-- 支付
local function callback(result)
  if result == "success" then
    -- 支付成功
  end
end

local args = {
  billingIndex,
  true,
  true,
  callback
}
luaj.callStaticMethod(className, "GameInterface_doBilling", args)

-- 顯示遊戲基地界面
luaj.callStaticMethod(className, "GameCommunity_launchGameCommunity")

-- 提交玩家的遊戲成績
local args = {
  "0",            -- 排行榜Id
  newBestScores,  -- 新的最佳成績
}
local sig = "(Ljava/lang/String;I)V"
luaj.callStaticMethod(className, "GameCommunity_commitScoreWithRank", args, sig)

~

安裝 luaj

luaj 分爲三個部分:

  • LuaJavaBridge.java, com_qeeplay_frameworks_LuaJavaBridge.h/.cpp - 供 Java 端使用的工具類,包含 Java 接口定義文件和 JNI 實現。
  • LuaJavaBridge.h/.cpp - 供 Lua 端使用的工具類。
  • luaj.lua - LuaJavaBridge 的 Lua 包裝,提供更簡單和靈活的接口。

下載地址:

~

步驟:

  • 將 LuaJavaBridge.java 添加到 Android 項目中;
  • 修改 proj.android/jni/Android.mk:
1
2
3
4
5
6
LOCAL_SRC_FILES := ... \
    luaj/jni/com_qeeplay_frameworks_LuaJavaBridge.cpp \
    luaj/luabinding/LuaJavaBridge.cpp

LOCAL_C_INCLUDES := ... \
    luaj
  • 修改 AppDelegate.cpp,加入以下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "LuaJavaBridge.h"
#endif

bool AppDelegate::applicationDidFinishLaunching()
{

  ...

  CCLuaEngine* pEngine = CCLuaEngine::defaultEngine();
  CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
  LuaJavaBridge_luabinding_open(pEngine->getLuaState());
#endif

  ...
}
  • 修改proj.android/jni/hellocpp/main.cpp,加入以下代碼:
1
2
3
4
5
6
7
8
9
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{

  ...

  LuaJavaBridge_setJavaVM(vm);

  return JNI_VERSION_1_4;
}

~

luaj 方法參考

  • [Lua] luaj.callStaticMethod(className, methodName, args, methodSig)

    調用指定的 Java class static method,允許傳入 int/float/boolean/string/function 五種類型的參數。

  • [Java] LuaJavaBridge.callLuaFunctionWithString(int luaFunctionId, String value)

    調用引用 ID 指向的 Lua function,並傳入一個字符串作爲參數。

  • [Java] LuaJavaBridge.callLuaGlobalFunctionWithString(int luaFunctionId, String value)

    調用指定名字的 Lua 全局函數,並傳入一個字符串作爲參數。

  • [Java] LuaJavaBridge.retainLuaFunction(int luaFunctionId)

    增加引用 ID 的計數,確保 Lua function 不會被 Lua 虛擬機自動回收。

  • [Java] LuaJavaBridge.releaseLuaFunction(int luaFunctionId)

    減少引用 ID 的計數,當計數等於 0 時,引用 ID 指向的 Lua function 將被回收。

~

未來改進

因爲我們自己的項目暫時還沒有更復雜的需求,所以 luaj 目前的實現很簡單。但要在這個基礎上進行完善是很容易的事情,luaj 已經解決了幾個關鍵性問題。

未來計劃會增加的主要特性就是支持更多的類型,例如將一個以字符串爲鍵名的 Lua table 以 Java Map 集合的形式傳遞給 Java。同樣,從 Java 調用 Lua 函數時,也應該支持多個參數,以及更多的參數類型。

至於將 Java 對象傳入 Lua,並在 Lua 中調用 Java 對象的方法,目前沒這個打算。因爲 luaj 的主要目的是爲 cocos2d-x 遊戲服務,而 cocos2d-x 的多線程模式要求 Lua 和 Java 代碼必須在不同的線程裏運行。如果在 Lua 中調用 Java 對象方法將面對許多複雜的問題。與其花大量時間去解決這個問題(還不一定能保證最後簡單易用),不如簡單寫一箇中間層。

最後,luaj 已經被集成到了 quick-cocos2d-x 這個基於 cocos2d-x 的快速遊戲開發引擎中。quick-cocos2d-x 讓開發者可以使用 Lua 語言開發高質量的商業遊戲,同時又保持 cocos2d-x 的高性能、開放性、可擴展能力。並且 quick-cocos2d-x 使用最新的 LuaJIT 實現,可以讓 Lua 腳本獲得數倍到數十倍的性能提升。

慣例爲廣大程序猿送上福利美圖一張 :-)

~

關於 quick-cocos2d-x

LuaJavaBridge 已經集成到 quick-cocos2d-x 這個開源項目中,用法說明 .

quick-cocos2d-x 是一個可以讓您覺得“爽快”的 cocos2d-x 的擴展版。基於 cocos2d-x,完全的跨平臺能力、優異的性能和可靠性。而 quick-cocos2d-x 在這一切的基礎上,添加了完善的 Lua 腳本語言支持,讓開發者可以使用 Lua 這種簡單易用的腳本語言完成商業品質的移動遊戲。

https://github.com/dualface/quick-cocos2d-x

- EOF -

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章