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类进行测试性的调用。
这是相关的输出结果,注意CocosCreator构建发布时勾选“调试模式”
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++的代码库进行一些可用性的尝试。