sebind 使用教程

sebind 之前,不论是手动绑定还是自动绑定,开发者需要比较多的步骤才能完成绑定,而且需要对 JSB 有比较多了解。sebind 利用 C++ 的模板,最大化地减少了中间代码。

简单的示例

我们将在全局空间中定义一个对象 simpleMath,并且添加一个 lerp 方法:

let v = simpleMath.lerp(a, b, t);

这个方法在 C++ 中实现。

准备工作

我们需要创建一个空的工程,保存场景,并且新建任一原生平台的构建任务。本示例使用的是 Windows 平台。

第一步:添加绑定代码

native/engine/common/Classes 目录,新建文件 HelloSEBind.cpp,写入以下内容:

// HelloSEBind.cpp
#include "bindings/sebind/sebind.h"

namespace {
struct Empty {}; // act as a namespace
float lerp(float a, float b, float t) { return (1 - t) * a + t * b; }
} // namespace

bool jsb_register_simple_math(se::Object *globalThis) {

  sebind::class_<Empty> demoMathClass("simpleMath");
  {
    // invoke through simpleMath.lerp(a, b, t);
    demoMathClass.staticFunction("lerp", &lerp).install(globalThis);
  }
  return true;
}

native/engine/common/CMakeLists.txt 中加入源文件 HelloSEBind.cpp


# ... 

list(APPEND CC_COMMON_SOURCES
    ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h
    ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp
    ${CMAKE_CURRENT_LIST_DIR}/Classes/HelloSEBind.cpp # new file
)

第二步:注册到 ScriptEngine

编辑 Game.cpp,位于 native/engine/common/Classes 目录:

// ...

// declare registry function
extern bool jsb_register_simple_math(se::Object *);       // #1

Game::Game() = default;


int Game::init() {

// ...
  _xxteaKey = SCRIPT_XXTEAKEY;

  // add callback to script engine
  auto *seengine = se::ScriptEngine::getInstance();        // #2
  seengine->addRegisterCallback(jsb_register_simple_math); // #3

  BaseGame::init();
  //....

按照上述标记(#1、2、3)依次添加编辑对应的代码。

第三步:验证

启动程序,在 Debug 模式下通过 Chrome devtools 验证:

devtool

此时可观察到导出成功。

相比之前的做法,对 se 接口的使用大大减少,代码量也随之减少。

更复杂的绑定示例

sebind 以类为单位进行接口绑定,每一个 JS 类都需要构造对应的 sebind::class_ 实例。所有的类的绑定都是通过 sebind::class_ 所提供的方法来完成的。

下面,我们通过导出示例代码的 User 类来熟悉 sebind 的绑定流程。

注意:示例代码只用作说明 sebind 的用法, 内部接口设计和实现在实际开发中无参考价值。

class User {
private:
  static int userCount;

public:
  // static methods
  static int doubleUserCount() { return 2 * userCount; }
  // static attributes
  static int getUserCount() { return userCount; }
  static void setUserCount(int v) { userCount = v; }

  // constructors with different parameters
  User() { userCount++; }
  User(const std::string &name_) : User() { name = name_; }
  User(const std::string &name_, const std::string &token)
      : User() {
    name = name_;
    _token = token;
  }
  User(const std::string &name_, const std::string &token, int credit)
      : User(name, token) {
    _credit = credit;
    name = name_;
    _token = token;
  }


  ~User() = default;

  // attributes 
  std::string getToken() const { return _token; }
  void setToken(const std::string &t) { _token = t; }

  // override function
  std::string toString() const { return name + ":" + _token; }
  std::string toString(const std::string &tag) const {
    return "[" + tag + "]:" + name + ":" + _token;
  }

  // function args with bound type
  std::string mergeName1(User &other) { return name + "|" + other.name; }
  std::string mergeName2(User *other) { return name + "|" + other->name; }
  std::string mergeName3(const std::shared_ptr<User> &other) {
    return name + "|" + other->name;
  }
  // public fields
  std::string name{"unset"};

private:
  std::string _token{"unset"};
  int _credit{-1};
};

int User::userCount = 0;

} // namespace

实例化 sebind::class_

关联 C++ 类和指定 JS 类名

    // 定义绑定类和 JS 类名
  sebind::class_<User> userClass("User");

绑定构造函数

userClass.constructor<>() // JS: new User
    .constructor<const std::string &>() // JS: new User("Jone")
    .constructor<const std::string &, const std::string &>() // JS: new User("Jone", "343453")
    .constructor<const std::string &, const std::string &, int>() //JS:  new User("Jone", "343453", 5678)

这里声明了 4 个构造模式:分别为 0、1、2、3 个参数. 每一个模板参数都对应于构造函数的参数类型。

在 JS 中调用 new User(...) 时, 会根据参数的数目触发对应的 C++ 构造函数。

注意:如果不声明任何的 constructor, sebind:class_ 会使用默认的无参构造函数。

我们也可以将普通函数定义为构造函数, 比如:

User *createUser(int credit) {
  return = new User("Lambda", "ctor", credit);
}
// ...
  .constructor(&createUser) // JS: new User(234)

其返回值需要是一个 User* 类型. 这里相当于在 JS 中声明了 构造函数 constructor(credit:number)

导出成员属性

把 C++ 公开的字段定义为 JS 中的属性,代码示例如下:

.property("name", &User::name)  // JS: user.name

也可以将 getter/setter 函数定义为属性。这里的 getter 函数需要有返回值, 且无参。setter 函数接受一个参数。

.property("token", &User::getToken, &User::setToken) // JS: user.token

注意:这里的 getter/setter 可以只提供一个,另一个为 nullptr,但不能同时为 nullptr

普通函数,第一个参数是 User*,可以作为成员函数使用。如:

std::string tokenLong_get(User *u) {
  return "token[" + u->getToken() + "]";
}
void tokenLong_set(User *u, const std::string &s) {
  u->setToken("token[" + u->getToken() + "]");
}
//...
.property("tokenPrefix", &tokenLong_get, &tokenLong_set) // JS: user.tokenPrefix

导出成员函数

绑定成员函数,代码示例如下:

.function("mergeName1", &User::mergeName1) // JS: user1.mergeName1(user2)
.function("mergeName2", &User::mergeName2) // JS: user2.mergeName1(user2)
.function("mergeName3", &User::mergeName3) // JS: user3.mergeName1(user2)

JS 中绑定类型的实例可以作为参数传递给 C++ 绑定函数。C++ 函数可以使用 引用指针 或者 智能指针 的方式接收绑定对象实例。这里如果 User 继承了 cc::RefCounted, 我们可以使用 cc::IntrusivePtr<User> 持有。如果没有继承 cc::RefCounted,正如现在的情况,我们也可以通过 std::shared_ptr<User> 持有。持有后,就是关联的 JS 对象被 GC 了,C++ 层持有的对象不会被析构。

注意:绑定类型需要在调用 sebind 接口之前,通过宏 JSB_REGISTER_OBJECT_TYPE(User); 进行注册。后续的 jsb_conversions 方法才能正确处理类型转换。

如果对函数进行了重载,我们需要通过 static_cast 指定函数指针对应的具体类型。

.function("toString", static_cast<std::string(User::*)() const>(&User::toString))   ///JS: (new User).toString()
.function("toString", static_cast<std::string(User::*)(const std::string&) const>(&User::toString))  //JS: (new User).toString("1111")

和构造函数类似,重载函数是根据参数的数目进行匹配的,应该避免相同参数的情形。如果需要运行时判断参数类型可以参考绑定 SE 函数

导出类静态函数

通过下方的代码示例,可以导出类的静态函数:

.staticFunction("doubleUserCount", &User::doubleUserCount) //JS: User.doubleUserCount()

同样可以把普通函数导出为类静态函数:

int  static_add(int a, int b) { return a + b; }
///...
  .staticFunction("add", &static_add) //JS: User.add(1,2)

类静态属性

将类静态函数导出为类的静态属性,代码示例如下:

.staticProperty("userCount", &User::getUserCount, &User::setUserCount)  //JS: User.userCount

或普通函数:

int gettime() { return time(nullptr); }
/// ...
.staticProperty("time", &gettime, nullptr) //JS: User.time

注册析构回调

注册绑定对象被 GC 时的回调示例如下:

...
.finalizer([](User *usr) {
  std::cout << "release " << usr->name << std::endl;
})

导出到 JS 全局对象

User 类挂载到 globalThis对象, 完成导出. JS 脚本中可在全局访问.

.install(globalThis);

继承

sebind::class_ 的构造函数,第二个参数为父类的 prototype 对象。这里的 SuperUser 类继承了 User 类。

sebind::class_<User> superUser("SuperUser", userClass.prototype());
{
  superUser.constructor<const std::string &>()
      .function(
          "superName", +[](User *user) { return user->name + ".super";
          }) //JS:  (new SuperUser("Mok")).superName()
      .install(globalThis);
}

注意:需要注意的是,父类的静态方法不会被子类继承。

其他用法

C++ 调用 JS 函数

从 3.6.1 起,通过 sebind::bindFunction 可以将 se::Value 对象,绑定为 C++ 中的 std::function,不需要处理参数的转换。 类似地,可以使用 sebind::callFunction 直接调用 JS 函数。

示例如下:

demo.staticFunction(
  "add",
  +[](const se::Value &func, int a, int b) {
    // bind js function as a std::function<int(int,int)>
    auto addFunc = sebind::bindFunction<int(int, int)>(func);
    // ..
    // invoke std::function
    auto result = addFunc(a, b);

    // call JS function with automatic arguments assembling
    auto result2 = sebind::callFunction<int, int, int>(func, a, b);
    auto result3 = sebind::callFunction<int, int, int>(func, 6, 8);

    // argument type computing
    auto result4 = sebind::callFunction<int>(func, a, b);
    auto result5 = sebind::callFunction<int>(func, 6, 8);

    std::cout << "result 1 " << result << std::endl;
    std::cout << "result 2 " << result2 << std::endl;
  });

绑定抽象类

sebind::class_ 要求提供构造函数,但抽象类的构造函数不可用。通过提供空的构造函数解决此冲突,实现抽象类型的注册。

示例如下:


class AbstractClass {
public:
  virtual bool tick() = 0;
};

class SubClass : public AbstractClass {
public:
  bool tick() override { return true; }
};

AbstractClass *fakeConstructor() {
  assert(false); // Abstract class cannot be instantiated
  return nullptr;
}

//..
sebind::class_<AbstractClass> base("AbstractBase");

base.constructor<>(&fakeConstructor) // add constructor
    .function("tick", &AbstractClass::tick)
    .install(globalThis);

sebind::class_<SubClass> sub("SubClass", base.prototype());
sub.install(globalThis);

手动类型转换

sebind 支持绑定传统 SE 函数,实现手动执行转换,代码示例如下:

bool jsb_sum(se::State &state) {
  double result = 0;
  auto &args = state.args();
  for (int i = 0; i < args.size(); i++) {
    result += (args[i].isNumber() ? args[i].toDouble() : 0);
  }
  state.rval().setDouble(result);
  return true;
}
/// 
.staticFunction("sum", &jsb_sum) // JS: User.sum(1,2,3,4,5)

这样就可以支持变长参数 和 灵活的参数转换。相比自动转换,这个方式更灵活同时也要求开发者对 SE API 更熟悉。

获取 JS this 对象

在 C++ 构造函数中获取对应的 JS this 对象是一个常见的需求,也能简化了从 C++ 到 JS 的访问流程。

我们只需要在 constructor 的参数类型中指定占位符 sebind::ThisObject 同时将对应构造函数的参数类型声明为 se::Object *

// constructor
User(se::Object *self, const std::string &name_) {
  self->setProperty("fromNative", se::Value(true));
  name = name_;
}
/// ...
    superUser.constructor<sebind::ThisObject, const std::string &>() // JS: new SuperUser("Jone")

JS 中调用对应构造函数的时候,需要忽略 sebind::ThisObject 参数。

sebind::ThisObject

HelloSEBind.cpp 完整代码如下:


#include "bindings/sebind/sebind.h"
#include <iostream>

namespace {


struct Empty {}; // act as a namespace
float lerp(float a, float b, float t) { return (1 - t) * a + t * b; }

class User {
  static int userCount;

public:
  static int doubleUserCount() { return 2 * userCount; }
  static int getUserCount() { return userCount; }
  static void setUserCount(int v) { userCount = v; }

  User() { userCount++; }
  // User(const std::string &name_) : User() { name = name_; }
  User(const std::string &name_, const std::string &token) : User() {
    name = name_;
    _token = token;
  }
  User(const std::string &name_, const std::string &token, int credit)
      : User(name, token) {
    _credit = credit;
    name = name_;
    _token = token;
  }

  User(se::Object *self, const std::string &name_) {
    self->setProperty("fromNative", se::Value(true));
    name = name_;
  }

  ~User() { userCount--; }

  std::string getToken() const { return _token; }
  void setToken(const std::string &t) { _token = t; }
  std::string toString() const { return name + ":" + _token; }
  std::string toString(const std::string &tag) const {
    return "[" + tag + "]:" + name + ":" + _token;
  }

  std::string mergeName1(User &other) { return name + "|" + other.name; }
  std::string mergeName2(const std::shared_ptr<User> &other) {
    return name + "|" + other->name;
  }
  std::string mergeName3(User *other) { return name + "|" + other->name; }

  std::string name{"unset"};

private:
  std::string _token{"unset"};
  int _credit{-1};
};

int User::userCount = 0;


class UserExt : public User {
public:
    using User::User;
};

/////////////////////////////////////////////////////



User *createUser(int credit) { return new User("Lambda", "ctor", credit); }

std::string tokenLong_get(User *u) { return "token[" + u->getToken() + "]"; }
void tokenLong_set(User *u, const std::string &s) {
  u->setToken("token[" + u->getToken() + "]");
}

int static_add(int a, int b) { return a + b; }

bool jsb_sum(se::State &state) {
  double result = 0;
  auto &args = state.args();
  for (int i = 0; i < args.size(); i++) {
    result += (args[i].isNumber() ? args[i].toDouble() : 0);
  }
  state.rval().setDouble(result);
  return true;
}

int gettime() { return time(nullptr); }


} // namespace

JSB_REGISTER_OBJECT_TYPE(User);

bool jsb_register_simple_math(se::Object *globalThis) {

  sebind::class_<Empty> demoMathClass("simpleMath");
  {
    // invoke through simpleMath.lerp(a, b, t);
    demoMathClass.staticFunction("lerp", &lerp).install(globalThis);
  }

  sebind::class_<User> userClass("User");
  {

    userClass
        .constructor<>()
        .constructor<const std::string &>()
        .constructor<const std::string &, const std::string &>()
        .constructor<const std::string &, const std::string &, int>()
        // .constructor(&createUser)
        .property("name", &User::name)
        .property("token", &User::getToken, &User::setToken)
        .property("tokenPrefix", &tokenLong_get, nullptr)
        .function("mergeName1", &User::mergeName1)
        .function("mergeName2", &User::mergeName2)
        // .function("mergeName3", &User::mergeName3)
        .function("toString",
                  static_cast<std::string (User::*)() const>(&User::toString))
        .function("toString",
                  static_cast<std::string (User::*)(const std::string &) const>(
                      &User::toString))
        .staticFunction("doubleUserCount", &User::doubleUserCount)
        .staticProperty("userCount", &User::getUserCount, &User::setUserCount)
        .staticProperty("time", &gettime, nullptr)
        .staticFunction("sum", &jsb_sum)
        .finalizer([](User *usr) {
          std::cout << "release " << usr->name << std::endl;
        })
        .install(globalThis);
  }

  sebind::class_<UserExt> superUser("SuperUser", userClass.prototype());
  {
    superUser.constructor<sebind::ThisObject, const std::string &>()
        .function(
            "superName", +[](UserExt *user) { return user->name + ".super"; })
        .install(globalThis);
  }

  return true;
}

条与 "" 相匹配的结果

    没有与 "" 匹配的结果