CocosCreator2.0.9的JSB綁定 - 手動綁定
前言
大部分的cocos2d-x的內容都是由官方綁定好的。很方便的使用 cc.Xxx 就可以調用。可是有一些第三方的SDK,或者希望嘗試使用C++的代碼,就可以手動的綁定。其實就是在js裏面用點,用括號,用new,等方式直接調用C++代碼。
具體能做什麼?是否能把一些複雜的JS計算邏輯放入C++?又或者是否能開啓多線程?又或者使用一些C++編碼的sdk?等學會了這招纔好試試看。
文章就是將我一步一步的實現使用手冊裏面手動綁定一個C++類到JS的過程記錄了下來。
準備
- CocosCreator 2.0.9 2019年3月5日發佈: https://www.cocos.com/
- 使用手冊:https://docs.cocos.com/creator/manual/zh/advanced-topics/jsb/JSB2.0-learning.html
- Xcode 10.2
- macOS 10.13.6
- Xcode10.2無法正常安裝在10.13的macOS系統上,由於我自己的原因不能升級系統,因此採用這種方式解決:https://stackoverflow.com/questions/55596733/is-it-possible-to-install-xcode-10-2-on-high-sierra-10-13-6
- Visual Studio Code 1.33.1
建立工程
使用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++的代碼庫進行一些可用性的嘗試。