CocosCreator2.0.9的JSB綁定 - 手動綁定

前言

大部分的cocos2d-x的內容都是由官方綁定好的。很方便的使用 cc.Xxx 就可以調用。可是有一些第三方的SDK,或者希望嘗試使用C++的代碼,就可以手動的綁定。其實就是在js裏面用點,用括號,用new,等方式直接調用C++代碼。
具體能做什麼?是否能把一些複雜的JS計算邏輯放入C++?又或者是否能開啓多線程?又或者使用一些C++編碼的sdk?等學會了這招纔好試試看。
文章就是將我一步一步的實現使用手冊裏面手動綁定一個C++類到JS的過程記錄了下來。

準備

建立工程

使用CocosCreator2.0.9直接建立HelloTypeScript工程,在此基礎上進行修改併發布ios的xcode並使用iphone模擬器執行。之後也以這樣的方式進行代碼調試。完成這一步操作後可以先構建出xcode工程進行這個空基礎工程的測試。
真個實驗過程先用ios平臺進行測試,之後再試着發佈到android。

先來個簡單的

打開HelloWorld.ts,將start()裏面的

    this.label.string = this.text;

改爲

    this.label.string = foo;

由於是typescript, foo會被標紅,最後再說。
當然,更好的寫法是

        if (cc.sys.isNative) {
            this.label.string = foo;
        } else {
            this.label.string = this.text;
        }

因爲C++代碼只有原生平臺能使用,做個平臺區分。
此時,我們開始爲js全局範圍內的foo綁定一個值,下面採用C++的代碼來完成:
打開Xcode工程,在Classes目錄下新建一個DefaultJSBind.h文件盒DefaultJSBind.cpp文件:

//
//  DefaultJSBind.h
//  HelloTypeScript
//
//  Created by Wang Yichun on 2019/4/23.
//

#ifndef DefaultJSBind_h
#define DefaultJSBind_h

void defaultBind();

#endif /* DefaultJSBind_h */

//
//  DefaultJSBind.cpp
//  HelloTypeScript-mobile
//
//  Created by Wang Yichun on 2019/4/23.
//

#include "DefaultJSBind.h"

#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"

/** 爲 JS 對象設置一個屬性值 **/
void defaultBind() {
    se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // 這裏爲了演示方便,獲取全局對象
    globalObj->setProperty("foo", se::Value(200)); // 給全局對象設置一個 foo 屬性,值爲 200
}

接着在AppDelegate.cpp 的applicationDidFinishLaunching()中調用defaultBind():

    se->start();
    
    se::AutoHandleScope hs;
    
    defaultBind();
    
    jsb_run_script("jsb-adapter/jsb-builtin.js");
    jsb_run_script("main.js");

接着運行就好了。
在這裏插入圖片描述

綁定整個C++類給JS

這部分對應手冊裏面 註冊一個 CPP 類到 JS 虛擬機中 這一節

同樣在Classes文件夾中建立 SomeClass.h 和 SomeClass.cpp 文件。

//
//  SomeClass.h
//  HelloTypeScript
//
//  Created by Wang Yichun on 2019/4/23.
//

#ifndef SomeClass_h
#define SomeClass_h

#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"

bool js_register_ns_SomeClass(se::Object* global);

#endif /* SomeClass_h */

//
//  SomeClass.cpp
//  HelloTypeScript-mobile
//
//  Created by Wang Yichun on 2019/4/23.
//

#include "SomeClass.h"

#include "cocos/scripting/js-bindings/manual/jsb_module_register.hpp"
#include "cocos/scripting/js-bindings/manual/jsb_global.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"
#include "cocos/scripting/js-bindings/event/EventDispatcher.h"
#include "cocos/scripting/js-bindings/manual/jsb_classtype.hpp"

#include "cocos2d.h"
USING_NS_CC;

static se::Object* __jsb_ns_SomeClass_proto = nullptr;
static se::Class* __jsb_ns_SomeClass_class = nullptr;

namespace ns {
    class SomeClass
    {
    public:
        SomeClass()
        : xxx(0)
        {}
        
        void foo() {
            printf("SomeClass::foo\n");
            
            if (_cb != nullptr) {
                _cb(xxx);
            }
        }
        
        static void static_func() {
            printf("SomeClass::static_func\n");
        }
        
        void setCallback(const std::function<void(int)>& cb) {
            _cb = cb;
            if (_cb != nullptr)
            {
                printf("setCallback(cb)\n");
            }
            else
            {
                printf("setCallback(nullptr)\n");
            }
        }
        
        int xxx;
    private:
        std::function<void(int)> _cb;
    };
} // namespace ns {


static bool js_SomeClass_setCallback(se::State& s)
{
    const auto& args = s.args();
    int argc = (int)args.size();
    if (argc >= 1)
    {
        ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject();
        
        se::Value jsFunc = args[0];
        se::Value jsTarget = argc > 1 ? args[1] : se::Value::Undefined;
        
        if (jsFunc.isNullOrUndefined())
        {
            cobj->setCallback(nullptr);
        }
        else
        {
            assert(jsFunc.isObject() && jsFunc.toObject()->isFunction());
            
            // 如果當前 SomeClass 是可以被 new 出來的類,我們 使用 se::Object::attachObject 把 jsFunc 和 jsTarget 關聯到當前對象中
            s.thisObject()->attachObject(jsFunc.toObject());
            s.thisObject()->attachObject(jsTarget.toObject());
            
            // 如果當前 SomeClass 類是一個單例類,或者永遠只有一個實例的類,我們不能用 se::Object::attachObject 去關聯
            // 必須使用 se::Object::root,開發者無需關係 unroot,unroot 的操作會隨着 lambda 的銷燬觸發 jsFunc 的析構,在 se::Object 的析構函數中進行 unroot 操作。
            // js_cocos2dx_EventDispatcher_addCustomEventListener 的綁定代碼就是使用此方式,因爲 EventDispatcher 始終只有一個實例,
            // 如果使用 s.thisObject->attachObject(jsFunc.toObject);會導致對應的 func 和 target 永遠無法被釋放,引發內存泄露。
            
            // jsFunc.toObject()->root();
            // jsTarget.toObject()->root();
            
            cobj->setCallback([jsFunc, jsTarget](int counter){
                
                // CPP 回調函數中要傳遞數據給 JS 或者調用 JS 函數,在回調函數開始需要添加如下兩行代碼。
                se::ScriptEngine::getInstance()->clearException();
                se::AutoHandleScope hs;
                
                se::ValueArray args;
                args.push_back(se::Value(counter));
                
                se::Object* target = jsTarget.isObject() ? jsTarget.toObject() : nullptr;
                jsFunc.toObject()->call(args, target);
            });
        }
        
        return true;
    }
    
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1);
    return false;
}
SE_BIND_FUNC(js_SomeClass_setCallback)

static bool js_SomeClass_finalize(se::State& s)
{
    ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject();
    delete cobj;
    return true;
}
SE_BIND_FINALIZE_FUNC(js_SomeClass_finalize)

static bool js_SomeClass_constructor(se::State& s)
{
    ns::SomeClass* cobj = new ns::SomeClass();
    s.thisObject()->setPrivateData(cobj);
    return true;
}
SE_BIND_CTOR(js_SomeClass_constructor, __jsb_ns_SomeClass_class, js_SomeClass_finalize)

static bool js_SomeClass_foo(se::State& s)
{
    ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject();
    cobj->foo();
    return true;
}
SE_BIND_FUNC(js_SomeClass_foo)

static bool js_SomeClass_get_xxx(se::State& s)
{
    ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject();
    s.rval().setInt32(cobj->xxx);
    return true;
}
SE_BIND_PROP_GET(js_SomeClass_get_xxx)

static bool js_SomeClass_set_xxx(se::State& s)
{
    const auto& args = s.args();
    int argc = (int)args.size();
    if (argc > 0)
    {
        ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject();
        cobj->xxx = args[0].toInt32();
        return true;
    }
    
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1);
    return false;
}
SE_BIND_PROP_SET(js_SomeClass_set_xxx)

static bool js_SomeClass_static_func(se::State& s)
{
    ns::SomeClass::static_func();
    return true;
}
SE_BIND_FUNC(js_SomeClass_static_func)

bool js_register_ns_SomeClass(se::Object* global)
{
    // 保證 namespace 對象存在
    se::Value nsVal;
    if (!global->getProperty("ns", &nsVal))
    {
        // 不存在則創建一個 JS 對象,相當於 var ns = {};
        se::HandleObject jsobj(se::Object::createPlainObject());
        nsVal.setObject(jsobj);
        
        // 將 ns 對象掛載到 global 對象中,名稱爲 ns
        global->setProperty("ns", nsVal);
    }
    se::Object* ns = nsVal.toObject();
    
    // 創建一個 Class 對象,開發者無需考慮 Class 對象的釋放,其交由 ScriptEngine 內部自動處理
    auto cls = se::Class::create("SomeClass", ns, nullptr, _SE(js_SomeClass_constructor)); // 如果無構造函數,最後一個參數可傳入 nullptr,則這個類在 JS 中無法被 new SomeClass()出來
    
    // 爲這個 Class 對象定義成員函數、屬性、靜態函數、析構函數
    cls->defineFunction("foo", _SE(js_SomeClass_foo));
    cls->defineProperty("xxx", _SE(js_SomeClass_get_xxx), _SE(js_SomeClass_set_xxx));
    
    cls->defineFunction("setCallback", _SE(js_SomeClass_setCallback));
    
    cls->defineFinalizeFunction(_SE(js_SomeClass_finalize));
    
    // 註冊類型到 JS VirtualMachine 的操作
    cls->install();
    
    // JSBClassType 爲 Cocos 引擎綁定層封裝的類型註冊的輔助函數,此函數不屬於 ScriptEngine 這層
    JSBClassType::registerClass<ns::SomeClass>(cls);
    
    // 保存註冊的結果,便於其他地方使用,比如類繼承
    __jsb_ns_SomeClass_proto = cls->getProto();
    __jsb_ns_SomeClass_class = cls;
    
    // 爲每個此 Class 實例化出來的對象附加一個屬性
    __jsb_ns_SomeClass_proto->setProperty("yyy", se::Value("helloyyy"));
    
    // 註冊靜態成員變量和靜態成員函數
    se::Value ctorVal;
    if (ns->getProperty("SomeClass", &ctorVal) && ctorVal.isObject())
    {
        ctorVal.toObject()->setProperty("static_val", se::Value(200));
        ctorVal.toObject()->defineFunction("static_func", _SE(js_SomeClass_static_func));
    }
    
    // 清空異常
    se::ScriptEngine::getInstance()->clearException();
    return true;
}

對比手冊裏的代碼已經做了一些修改,去掉了一個Director的計時器,因爲這個版本使用手冊的代碼找不到Director。
接着修改 Helloworld.ts 文件爲:

const {ccclass, property} = cc._decorator;

@ccclass
export default class Helloworld extends cc.Component {

    @property(cc.Label)
    label: cc.Label = null;

    @property
    text: string = 'hello2';

    start() {
        // init logic
        if (cc.sys.isNative) {
            this.label.string = foo;
            this.testSomeClass();
        } else {
            this.label.string = this.text;
        }
    }

    testSomeClass() {
        cc.log('testSomeClass');

        var myObj = new ns.SomeClass();
        myObj.foo();
        ns.SomeClass.static_func();
        cc.log("ns.SomeClass.static_val: " + ns.SomeClass.static_val);
        cc.log("Old myObj.xxx:" + myObj.xxx);
        myObj.xxx = 1234;
        cc.log("New myObj.xxx:" + myObj.xxx);
        cc.log("myObj.yyy: " + myObj.yyy);

        var delegateObj = {
            onCallback: function (counter) {
                cc.log("Delegate obj, onCallback: " + counter + ", this.myVar: " + this.myVar);
                this.setVar();
            },

            setVar: function () {
                this.myVar++;
            },

            myVar: 100
        };

        myObj.setCallback(delegateObj.onCallback, delegateObj);

        setTimeout(function () {
            myObj.setCallback(null);
        }, 6000); // 6 秒後清空 callback


        myObj.foo();
    }
}

也就是對C++那邊綁定的ns名字空間中等SomeClass類進行測試性的調用。
這是相關的輸出結果

JS: testSomeClass
SomeClass::foo
SomeClass::static_func
JS: ns.SomeClass.static_val: 200
JS: Old myObj.xxx:0
JS: New myObj.xxx:1234
JS: myObj.yyy: helloyyy
setCallback(cb)
SomeClass::foo
JS: Delegate obj, onCallback: 1234, this.myVar: 100
setCallback(nullptr)

關於TypeScript裏面的自動提示

由於這些JS裏面的變量也好,類也好。都是在C++中生成的。因此TS的編輯器肯定認不出來。爲了彌補這個缺陷,以不至於VSCode裏面全屏的紅線。需要加入一個globals.d.ts文件,放在項目Assets同級目錄下。可以看見放在了creator.d.ts文件旁邊。

declare var foo: any;

declare namespace ns {
	export class SomeClass {
		constructor();

		xxx: Number;
		yyy: Number;

		foo(): void;

		static static_func(): void;
		static static_val: Number;

		setCallback(callback: Function): void;
	}
}

這樣即消除了紅線,又可以在編碼時擁有自動提示,提高編碼效率。

在Android設備上

之後我又在android平臺上做了一些測試,期間遇到的兩個問題分別做了解決:

問題: /Users/frsyrup/Documents/Projs/HelloTypeScript/build/jsb-link/frameworks/runtime-src/proj.android-studio/app/jni/…/…/…/Classes/AppDelegate.cpp:71: error: undefined reference to ‘defaultBind()’
/Users/frsyrup/android-ndk-r16b/sources/cxx-stl/llvm-libc++/include/new:234: error: undefined reference to ‘js_register_ns_SomeClass(se::Object*)’
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
解決:在Android.mk加入

LOCAL_SRC_FILES := hellojavascript/main.cpp \../../../Classes/AppDelegate.cpp \
				   ../../../Classes/jsb_module_register.cpp \
				   ../../../Classes/SomeClass.cpp \
				   ../../../Classes/DefaultJSBind.cpp \

問題:Cannot create a handle without a HandleScope
解決:AppDelegate.cpp中把我們的代碼defaultBind()調用一定要放到se::AutoHandleScope hs之後

	//...
    jsb_register_all_modules();
    se->addRegisterCallback(js_register_ns_SomeClass);
    se->start();
    se::AutoHandleScope hs;
    defaultBind();
    jsb_run_script("jsb-adapter/jsb-builtin.js");
    jsb_run_script("main.js");
    //...

總結

從接觸CocosCreator1.9.1以來,積累了JS與OC,JS與Java的代碼相互調用的經驗,經常爲了去調用原生平臺的一些API,接入原生SDK等。看到過綁定C++代碼以形成 JS與C++的互調,不過一直沒有去試驗過。直到今天雖然做了ios和android平臺上的實驗。但並沒有在實際的上線項目中使用過,還有沒有什麼坑不太清楚。
後面可以實驗性的接入一些C++的代碼庫進行一些可用性的嘗試。

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