在 Cocos Creator 中的 Swig 工作流教程

如何为引擎内的新模块添加绑定

添加一个新模块的接口文件

  • 添加一个新模块的接口文件到 native/tools/swig-config 目录, 例如: new-engine-module.i

  • 拷贝 swig-interface-template.i 文件中的内容到 new-engine-module.i

  • 添加必要的配置,可以参考 native/tools/swig-config 目录下现有的 .i 文件配置,或者参考下面的章节内容

修改 swig-config.js 文件

  1. // ......
  2. // 引擎模块配置
  3. const configList = [
  4. [ '2d.i', 'jsb_2d_auto.cpp' ],
  5. // ......
  6. [ 'renderer.i', 'jsb_render_auto.cpp' ],
  7. [ 'new-engine-module.i', 'jsb_new_engine_module_auto.cpp' ], // 添加此行
  8. ];
  9. //......

生成绑定代码

  1. cd engine/native/tools/swig-config
  2. node genbindings.js

修改 engine/native/cocos/CMakeLists.txt

  1. ######## auto
  2. cocos_source_files(
  3. NO_WERROR NO_UBUILD cocos/bindings/auto/jsb_new_engine_module_auto.cpp # 添加此行
  4. cocos/bindings/auto/jsb_new_engine_module_auto.h # 添加此行
  5. NO_WERROR NO_UBUILD cocos/bindings/auto/jsb_cocos_auto.cpp
  6. cocos/bindings/auto/jsb_cocos_auto.h
  7. ......

为脚本引擎注册新的模块

打开 jsb_module_register.cpp ,做如下修改

  1. ......
  2. #if CC_USE_PHYSICS_PHYSX
  3. #include "cocos/bindings/auto/jsb_physics_auto.h"
  4. #endif
  5. #include "cocos/bindings/auto/jsb_new_engine_module_auto.h" // 添加此行
  6. bool jsb_register_all_modules() {
  7. se::ScriptEngine *se = se::ScriptEngine::getInstance();
  8. ......
  9. se->addRegisterCallback(register_all_my_new_engine_module); // 添加此行
  10. se->addAfterCleanupHook([]() {
  11. cc::DeferredReleasePool::clear();
  12. JSBClassType::cleanup();
  13. });
  14. return true;
  15. }

如何为开发者的项目绑定一个新模块

假定我们已经有一个 Cocos Creator 的工程,其位于 /Users/james/NewProject 目录下。

打开 Cocos Creator 的构建面板,构建出一个原生平台的工程,会生成 /Users/james/NewProject/native 目录。

绑定一个简单的类

创建一个简单类

创建一个头文件,其位于 /Users/james/NewProject/native/engine/Classes/MyObject.h , 其内容为:

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. namespace my_ns {
  5. class MyObject {
  6. public:
  7. MyObject() = default;
  8. MyObject(int a, bool b) {}
  9. virtual ~MyObject() = default;
  10. void print() {
  11. CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b);
  12. }
  13. float publicFloatProperty{1.23F};
  14. private:
  15. int _a{100};
  16. bool _b{true};
  17. };
  18. } // namespace my_ns {

编写一个 Swig 接口文件

创建一个名称为 my-module.i 的接口文件,其位于 /Users/james/NewProject/tools/swig-config目录下。

  1. // my-module.i
  2. %module(target_namespace="my_ns") my_module
  3. // %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方
  4. %insert(header_file) %{
  5. #pragma once
  6. #include "bindings/jswrapper/SeApi.h"
  7. #include "bindings/manual/jsb_conversions.h"
  8. #include "MyObject.h" // 添加这行,%include 指令表示让 swig 解析此文件,并且为此文件中的类生成绑定代码。
  9. %}
  10. // %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方
  11. %{
  12. #include "bindings/auto/jsb_my_module_auto.h"
  13. %}
  14. %include "MyObject.h"

编写一个 Swig 配置文件(swig-config.js)

创建一个名为 swig-config.js 的文件,例如: /Users/james/NewProject/tools/swig-config目录下。

  1. // swig-config.js
  2. 'use strict';
  3. const path = require('path');
  4. const configList = [
  5. [ 'my-module.i', 'jsb_my_module_auto.cpp' ],
  6. ];
  7. const projectRoot = path.resolve(path.join(__dirname, '..', '..'));
  8. const interfacesDir = path.join(projectRoot, 'tools', 'swig-config');
  9. const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto');
  10. // includeDirs 意思是 swig 执行时候使用的头文件搜索路径
  11. const includeDirs = [
  12. path.join(projectRoot, 'native', 'engine', 'common', 'Classes'),
  13. ];
  14. module.exports = {
  15. interfacesDir,
  16. bindingsOutDir,
  17. includeDirs,
  18. configList
  19. };

为项目生成自动绑定文件

  1. cd /Users/james/NewProject/tools/swig-config
  2. node < 引擎根目录 >/native/tools/swig-config/genbindings.js

如果成功,包含自动绑定代码的 jsb_my_module_auto.cpp/.h 两个文件将被创建到 /Users/james/NewProject/native/engine/bindings/auto 目录下。

修改项目的 CMakeLists.txt 文件

  • 打开 /Users/james/NewProject/native/engine/common/CMakeLists.txt, 添加 MyObject.h 和自动绑定代码文件

    1. include(${COCOS_X_PATH}/CMakeLists.txt)
    2. list(APPEND CC_COMMON_SOURCES
    3. ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h
    4. ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp
    5. ############### 添加下面几行 ##############
    6. ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h
    7. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h
    8. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp
    9. ########################################################
    10. )
  • 修改 /Users/james/NewProject/native/engine/mac/CMakeLists.txt

    1. cmake_minimum_required(VERSION 3.8)
    2. # ......
    3. cc_mac_before_target(${EXECUTABLE_NAME})
    4. add_executable(${EXECUTABLE_NAME} ${CC_ALL_SOURCES})
    5. ############### 添加下面几行 ##############
    6. target_include_directories(${EXECUTABLE_NAME} PRIVATE
    7. ${CC_PROJECT_DIR}/../common
    8. )
    9. ########################################################
    10. cc_mac_after_target(${EXECUTABLE_NAME})

打开项目工程

macOS: /Users/james/NewProject/build/mac/proj/NewProject.xcodeproj

Windows: < 一个存放项目的目录 >/NewProject/build/win64/proj/NewProject.sln

为脚本引擎注册新的模块

修改 Game.cpp

  1. #include "Game.h"
  2. #include "bindings/auto/jsb_my_module_auto.h" // 添加此行
  3. //......
  4. int Game::init() {
  5. // ......
  6. se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module); // 添加此行
  7. BaseGame::init();
  8. return 0;
  9. }
  10. // ......

测试绑定

  • 在项目的根目录下添加一个 my-module.d.ts 文件,使 TS 编译器识别我们的绑定类型

    1. // my-module.d.ts
    2. declare namespace my_ns {
    3. class MyObject {
    4. constructor();
    5. constructor(a: number, b: number);
    6. publicFloatProperty : number;
    7. print() : void;
    8. }
    9. }
  • 修改 /Users/james/NewProject/temp/tsconfig.cocos.json 文件

    1. {
    2. "$schema": "https://json.schemastore.org/tsconfig",
    3. "compilerOptions": {
    4. "target": "ES2015",
    5. "module": "ES2015",
    6. "strict": true,
    7. "types": [
    8. "./temp/declarations/cc.custom-macro",
    9. "./temp/declarations/jsb",
    10. "./temp/declarations/cc",
    11. "./temp/declarations/cc.env",
    12. "./my-module" // 添加这行
    13. ],
    14. // ......
    15. "forceConsistentCasingInFileNames": true
    16. }
    17. }
  • 在 Cocos Creator 中打开 NewProject 项目, 在场景中添加一个立方体,并且添加一个脚本组件到这个立方体上,脚本的内容是:

    1. import { _decorator, Component } from 'cc';
    2. const { ccclass } = _decorator;
    3. @ccclass('MyComponent')
    4. export class MyComponent extends Component {
    5. start() {
    6. const myObj = new my_ns.MyObject();
    7. myObj.print(); // 调用原生的 print 方法
    8. console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); // 获取原生中定义的属性值
    9. }
    10. }
  • 在 Xcode 或者 Visual Studio 中运行项目, 如果成功,可以看到如下日志输出

    1. 17:31:44 [DEBUG]: ==> a: 100, b: 1
    2. 17:31:44 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863

本节总结

在此节中,我们学会了如何使用 Swig 工具绑定一个简单的 C++ 类,并把它的公有方法与属性导出到 JS 中。从下一节开始,我们将更多地关注如何使用 Swig 的一些特性来满足各式各样的 JS 绑定需求,例如:

  • 如何使用 %import 指令导入头文件依赖?
  • 如何忽略绑定某些特殊的类、方法或属性?
  • 如何重命名类、方法或属性?
  • 如何将 C++ 的 getter 和 setter 函数绑定为 JS 属性?
  • 如何配置 C++ 模块宏

导入头文件依赖

假定我们让 MyObject 类继承于 MyRef 类。但是我们并不想绑定 MyRef 类型。

  1. // MyRef.h
  2. #pragma once
  3. namespace my_ns {
  4. class MyRef {
  5. public:
  6. MyRef() = default;
  7. virtual ~MyRef() = default;
  8. void addRef() { _ref++; }
  9. void release() { --_ref; }
  10. private:
  11. unsigned int _ref{0};
  12. };
  13. } // namespace my_ns {
  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject 继承于 MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. MyObject() = default;
  10. MyObject(int a, bool b) {}
  11. virtual ~MyObject() = default;
  12. void print() {
  13. CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b);
  14. }
  15. float publicFloatProperty{1.23F};
  16. private:
  17. int _a{100};
  18. bool _b{true};
  19. };
  20. } // namespace my_ns {

当 Swig 解析 MyObject.h 的时候, 它并不知道 MyRef 是什么, 因此它会在终端输出一个警告信息。

  1. .../Classes/MyObject.h:7: Warning 401: Nothing known about base class 'MyRef'. Ignored.

要解决此警告也容易,我们只需要使用 %import 指令让 Swig 知道 MyRef 类的存在即可。

  1. // ......
  2. // Insert code at the beginning of generated source file (.cpp)
  3. %{
  4. #include "bindings/auto/jsb_my_module_auto.h"
  5. %}
  6. %import "MyRef.h" // 添加此行
  7. %include "MyObject.h"

尽管 Swig 不再报错了,但是生成的代码却无法编译通过,会出现如下报错:

MyRefCompileError

我们将在下一节中使用 %ignore 指令来修复此问题。

忽略某些类、方法或属性

忽略某些类

在上一节中,我们在 js_register_my_ns_MyObject 函数中碰到了一个编译错误。这是因为 MyRef 类型并不应该被绑定,我们可以用 %ignore 指令来忽略它。

  1. // my-module.i
  2. // ......
  3. %ignore my_ns::MyRef; // 添加此行
  4. %import "MyRef.h"
  5. %include "MyObject.h"

重新生成绑定,现在应该可以编译通过了。

  1. // jsb_my_module_auto.cpp
  2. bool js_register_my_ns_MyObject(se::Object* obj) {
  3. auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); // parentProto will be set to nullptr
  4. cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set));
  5. cls->defineFunction("print", _SE(js_my_ns_MyObject_print));
  6. // ......
  7. }

忽略某些方法和属性

我们为 MyObject 类添加一个名为 methodToBeIgnored 的方法,再添加一个名为 propertyToBeIgnored 的属性。

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject 继承于 MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. // .....
  10. void methodToBeIgnored() {} // 添加此行
  11. float propertyToBeIgnored{345.123F}; // 添加此行
  12. // ......
  13. float publicFloatProperty{1.23F};
  14. private:
  15. int _a{100};
  16. bool _b{true};
  17. };
  18. } // namespace my_ns {

重新生成绑定, 我们可以发现 methodToBeIgnoredpropertyToBeIgnored 的绑定代码已经被自动生成。

  1. // jsb_my_module_auto.cpp
  2. bool js_register_my_ns_MyObject(se::Object* obj) {
  3. auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject));
  4. cls->defineProperty("propertyToBeIgnored", _SE(js_my_ns_MyObject_propertyToBeIgnored_get), _SE(js_my_ns_MyObject_propertyToBeIgnored_set)); // this property should not be bound
  5. cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set));
  6. cls->defineFunction("print", _SE(js_my_ns_MyObject_print));
  7. cls->defineFunction("methodToBeIgnored", _SE(js_my_ns_MyObject_methodToBeIgnored)); // this method should not be bound
  8. // ......
  9. }

修改 my-module.i以忽略绑定这个方法与属性。

  1. // my-module.i
  2. // ......
  3. %ignore my_ns::MyRef;
  4. %ignore my_ns::MyObject::methodToBeIgnored; // 添加此行
  5. %ignore my_ns::MyObject::propertyToBeIgnored; // 添加此行
  6. %import "MyRef.h"
  7. %include "MyObject.h"

重新生成绑定,它们将被忽略。

  1. // jsb_my_module_auto.cpp
  2. bool js_register_my_ns_MyObject(se::Object* obj) {
  3. auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject));
  4. cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set));
  5. cls->defineFunction("print", _SE(js_my_ns_MyObject_print));
  6. // ......
  7. }

重命名类、方法或属性

Swig 定义了一个名为 %rename 指令用于重命名类、方法或者属性。我们继续使用 MyObject 类来展示。

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject 继承于 MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. // ......
  10. void methodToBeRenamed() { // 添加此方法
  11. CC_LOG_DEBUG("==> hello MyObject::methodToBeRenamed");
  12. }
  13. int propertyToBeRenamed{1234}; // 添加此属性
  14. float publicFloatProperty{1.23F};
  15. private:
  16. int _a{100};
  17. bool _b{true};
  18. };
  19. } // namespace my_ns {

重新生成绑定,我们发现 methodToBeRenamedpropertyToBeRenamed 的绑定代码已经被生成:

  1. // jsb_my_module_auto.cpp
  2. bool js_register_my_ns_MyObject(se::Object* obj) {
  3. auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject));
  4. cls->defineProperty("propertyToBeRenamed", _SE(js_my_ns_MyObject_propertyToBeRenamed_get), _SE(js_my_ns_MyObject_propertyToBeRenamed_set));
  5. cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set));
  6. cls->defineFunction("print", _SE(js_my_ns_MyObject_print));
  7. cls->defineFunction("methodToBeRenamed", _SE(js_my_ns_MyObject_methodToBeRenamed));

如果我们要重命名 propertyToBeRenamedcoolProperty ,重命名 methodToBeRenamedcoolMethod,那么按照下面的方式修改 my-module.i

  1. // my-module.i
  2. // ......
  3. %ignore my_ns::MyRef;
  4. %ignore my_ns::MyObject::methodToBeIgnored;
  5. %ignore my_ns::MyObject::propertyToBeIgnored;
  6. %rename(coolProperty) my_ns::MyObject::propertyToBeRenamed; // 添加此行
  7. %rename(coolMethod) my_ns::MyObject::methodToBeRenamed; // 添加此行
  8. %import "MyRef.h"
  9. %include "MyObject.h"

如果我们还想把 MyObject 类重命名为 MyCoolObject,我猜想你已经知道如何做了吧。没错,只要添加这行:

  1. %rename(MyCoolObject) my_ns::MyObject;

重新生成绑定代码,所有需要被重命名的类、方法与属性都按照我们的意愿被重命名了。

  1. // jsb_my_module_auto.cpp
  2. // MyCoolObject, coolProperty, coolMethod are all what we want now.
  3. bool js_register_my_ns_MyObject(se::Object* obj) {
  4. auto* cls = se::Class::create("MyCoolObject", obj, nullptr, _SE(js_new_MyCoolObject));
  5. cls->defineProperty("coolProperty", _SE(js_my_ns_MyCoolObject_coolProperty_get), _SE(js_my_ns_MyCoolObject_coolProperty_set));
  6. cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyCoolObject_publicFloatProperty_get), _SE(js_my_ns_MyCoolObject_publicFloatProperty_set));
  7. cls->defineFunction("print", _SE(js_my_ns_MyCoolObject_print));
  8. cls->defineFunction("coolMethod", _SE(js_my_ns_MyCoolObject_coolMethod));
  9. // ......
  10. }

现在测试一下吧,更新如下文件 my-module.d.ts and MyComponent.ts

  1. // my-module.d.ts
  2. declare namespace my_ns {
  3. class MyCoolObject {
  4. constructor();
  5. constructor(a: number, b: number);
  6. publicFloatProperty : number;
  7. print() : void;
  8. coolProperty: number;
  9. coolMethod() : void;
  10. }
  11. }
  1. // MyComponent.ts
  2. import { _decorator, Component } from 'cc';
  3. const { ccclass } = _decorator;
  4. @ccclass('MyComponent')
  5. export class MyComponent extends Component {
  6. start() {
  7. const myObj = new my_ns.MyCoolObject(); // 这里改为 MyCoolObject,因为我们在前面重命名了
  8. myObj.print();
  9. console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`);
  10. // Add the follow lines
  11. console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`);
  12. myObj.coolProperty = 666;
  13. console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`);
  14. myObj.coolMethod();
  15. }
  16. }

在 Xcode 或者 Visual Studio 中运行项目,将得到如下日志:

  1. 17:53:28 [DEBUG]: ==> a: 100, b: 1
  2. 17:53:28 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863
  3. 17:53:28 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234
  4. 17:53:28 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666
  5. 17:53:28 [DEBUG]: ==> hello MyObject::methodToBeRenamed

定义一个 attribute

%attribute 指令用于把 C++ 的 gettersetter 函数绑定为一个 JS 属性。

注意:如果 C++ 属性是公有的,那么理论上无需再配置 attribute 了,Swig 会自动绑定类的公有属性。

用法

  1. 定义一个没有 setter 函数的 JS 属性,即只读的 JS 属性。

    1. %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name)
  2. 定义一个有 gettersetter 函数的 JS 属性,即可读可写的 JS 属性。

    1. %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name, cpp_setter_function_name)
  3. 定义一个没有 getter 的 JS 属性,即可写不可读的 JS 属性。

    1. %attribute_writeonly(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_setter_function_name)

示例

为了方便演示,我们为 MyObject 添加两个新方法:setTypegetType

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject 继承于 MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. // ......
  10. void setType(int v) { _type = v; CC_LOG_DEBUG("==> setType: v: %d", v); } // 添加此行
  11. int getType() const { return _type; } // 添加此行
  12. float publicFloatProperty{1.23F};
  13. private:
  14. int _a{100};
  15. bool _b{true};
  16. int _type{333};
  17. };
  18. } // namespace my_ns {
  1. // my-module.i
  2. // ......
  3. %attribute(my_ns::MyObject, int, type, getType, setType); // 添加此行
  4. %import "MyRef.h"
  5. %include "MyObject.h"
  1. // jsb_my_module_auto.cpp
  2. bool js_register_my_ns_MyObject(se::Object* obj) {
  3. // ......
  4. cls->defineProperty("type", _SE(js_my_ns_MyCoolObject_type_get), _SE(js_my_ns_MyCoolObject_type_set));
  5. // ......
  6. }
  1. // MyComponent.ts
  2. import { _decorator, Component } from 'cc';
  3. const { ccclass } = _decorator;
  4. @ccclass('MyComponent')
  5. export class MyComponent extends Component {
  6. start() {
  7. const myObj = new my_ns.MyCoolObject();
  8. myObj.print();
  9. console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`);
  10. console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`);
  11. myObj.coolProperty = 666;
  12. console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`);
  13. myObj.coolMethod();
  14. console.log(`==> old: myObj.type: ${myObj.type}`);
  15. myObj.type = 888;
  16. console.log(`==> new: myObj.type: ${myObj.type}`);
  17. }
  18. }

在 Xcode 或者 Visual Studio 中运行项目:

  1. 18:09:53 [DEBUG]: ==> a: 100, b: 1
  2. 18:09:53 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863
  3. 18:09:53 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234
  4. 18:09:53 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666
  5. 18:09:53 [DEBUG]: ==> hello MyObject::methodToBeRenamed
  6. 18:09:53 [DEBUG]: D/ JS: ==> old: myObj.type: 333
  7. 18:09:53 [DEBUG]: ==> setType: v: 888 // Cool, C++ setType is invoked
  8. 18:09:53 [DEBUG]: D/ JS: ==> new: myObj.type: 888 // Cool, C++ getType is invoked, 888 is return from C++

%attribute_writeonly 指令

%attribute_writeonly 指令是我们为 swig Cocos 后端添加的一个扩展指令,它用于 C++ 只有 setter 函数没有 getter 函数的情况。

例如在 native/tools/swig-config/cocos.i 中有如下定义:

  1. %attribute_writeonly(cc::ICanvasRenderingContext2D, float, width, setWidth);
  2. %attribute_writeonly(cc::ICanvasRenderingContext2D, float, height, setHeight);
  3. %attribute_writeonly(cc::ICanvasRenderingContext2D, float, lineWidth, setLineWidth);
  4. %attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle);
  5. %attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, font, setFont);

它的作用类似于 JS 中如下代码:

  1. Object.defineProperty(MyNewClass.prototype, 'width', {
  2. configurable: true,
  3. enumerable: true,
  4. set(v) {
  5. this._width = v;
  6. },
  7. // No get() for property
  8. });

关于引用类型

如果 C++ 的 get 函数返回的是一个引用数据类型或者 set 函数接受一个引用数据类型,别忘记在 %attribute 或 %attribute_writeonly 指令的编写中添加 & 后缀。以下 ccstd::string& 是一个例子:

  1. %attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle);

如果 & 没有被添加,当绑定函数被调用的时候,一个临时的 ccstd::string 实例将被创建。这种临时对象的创建与销毁是不划算的且是可以完全避免的。

%arg() 指令

有时候 C++ 变量的类型是用模版的方式来修饰的,例如:

  1. class MyNewClass {
  2. public:
  3. const std::map<std::string, std::string>& getConfig() const { return _config; }
  4. void setConfig(const std::map<std::string, std::string> &config) { _config = config; }
  5. private:
  6. std::map<std::string, std::string> _config;
  7. };

我们可能会在 .i 中写一个这样的 %attribute 指令:

  1. %attribute(MyNewClass, std::map<std::string, std::string>&, config, getConfig, setConfig);

但当你执行 node genbindings.js的时候,你将得到如下错误:

  1. Error: Macro '%attribute_custom' expects 7 arguments

这是因为 swig 看到 std::map<std::string, std::string>& 的时候并不知道如何处理逗号 (,) ,它将其分割为两部分,即:

  1. std::map<std::string
  2. std::string>&

因此, %attribute 指令这行将被解析为 6 个参数,而不是正确的 5 个参数。

为了避免这种情况出现,我们需要使用 %arg 指令来告诉 swig std::map<std::string, std::string>& 是一个整体。

  1. %attribute(MyNewClass, %arg(std::map<std::string, std::string>&), config, getConfig, setConfig);

重新执行 node genbindings.js,之前的错误即消失了。

不要添加 const

在上一示例中,我们在 %attribute 指令中使用 %arg(std::map<std::string, std::string>&)。你可能会考虑在 std::map 前面添加一个 const 前缀,比如:%arg(const std::map<std::string, std::string>&)。如果你这样做了,你将添加一个 只读的、只绑定 MyNewClass::getConfigconfig 属性。这明显不是我们所期望的。如果我们需要属性是只读的,只需要不配置 setter 函数即可。

  1. // 不配置 setConfig 意味着属性是只读的
  2. %attribute(MyNewClass, %arg(std::map<std::string, std::string>&), config, getConfig);

因此,为了让事情简单化,我们只要记得,永远不要在定义 %attribute 的时候为 C++ 变量类型使用 const 前缀

配置 C++ 模块宏(用于 C++ 模块裁剪)

有时候是否需要让一个类参与编译依赖于某个宏是否启用。比如,我们在 MyObject.h 文件中添加一个 MyFeatureObject 类:

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. #ifndef USE_MY_FEATURE
  6. #define USE_MY_FEATURE 1 // Enable USE_MY_FEATURE
  7. #endif
  8. namespace my_ns {
  9. #if USE_MY_FEATURE
  10. class MyFeatureObject {
  11. public:
  12. void foo() {
  13. CC_LOG_DEBUG("==> MyFeatureObject::foo");
  14. }
  15. };
  16. #else
  17. class MyFeatureObject;
  18. #endif
  19. // MyObject 继承于 MyRef
  20. class MyObject : public MyRef {
  21. public:
  22. //......
  23. MyFeatureObject* getFeatureObject() {
  24. #if USE_MY_FEATURE // getFeatureObject 只在宏 USE_MY_FEATURE 启用的情况下返回有效值
  25. if (_featureObject == nullptr) {
  26. _featureObject = new MyFeatureObject();
  27. }
  28. #endif
  29. return _featureObject;
  30. }
  31. private:
  32. int _a{100};
  33. bool _b{true};
  34. int _type{333};
  35. MyFeatureObject* _featureObject{nullptr}; // 添加此行
  36. };
  37. } // namespace my_ns {
  1. // my-module.i
  2. // ......
  3. %rename(MyCoolObject) my_ns::MyObject;
  4. %attribute(my_ns::MyObject, int, type, getType, setType);
  5. %module_macro(USE_MY_FEATURE) my_ns::MyFeatureObject; // 添加此行,用于让 Swig 知道生成出来的 MyFeatureObject 类的绑定代码需要被包在 USE_MY_FEATURE 下
  6. %module_macro(USE_MY_FEATURE) my_ns::MyObject::getFeatureObject; // 添加此行,用于让 Swig 知道生成出来的 MyObject::getFeatureObject 方法的绑定代码需要被包在 USE_MY_FEATURE 下
  7. #define USE_MY_FEATURE 1 // 这里定义为 1 是骗过 Swig,让它帮我们生成绑定代码。注意,这行必须在 %module_macro 之后
  8. %import "MyRef.h"
  9. %include "MyObject.h"
  1. // my-module.d.ts
  2. declare namespace my_ns {
  3. class MyFeatureObject {
  4. foo() : void;
  5. }
  6. class MyCoolObject {
  7. constructor();
  8. constructor(a: number, b: number);
  9. publicFloatProperty : number;
  10. print() : void;
  11. coolProperty: number;
  12. coolMethod() : void;
  13. type: number;
  14. getFeatureObject() : MyFeatureObject;
  15. }
  16. }
  1. // MyComponent.ts
  2. import { _decorator, Component } from 'cc';
  3. const { ccclass } = _decorator;
  4. @ccclass('MyComponent')
  5. export class MyComponent extends Component {
  6. start() {
  7. const myObj = new my_ns.MyCoolObject();
  8. myObj.print();
  9. console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`);
  10. console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`);
  11. myObj.coolProperty = 666;
  12. console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`);
  13. myObj.coolMethod();
  14. console.log(`==> old: myObj.type: ${myObj.type}`);
  15. myObj.type = 888;
  16. console.log(`==> new: myObj.type: ${myObj.type}`);
  17. const featureObj = myObj.getFeatureObject();
  18. console.log(`==> featureObj: ${featureObj}`);
  19. if (featureObj) {
  20. featureObj.foo();
  21. }
  22. }
  23. }

重新生成绑定代码,自动绑定代码如下:

  1. #if USE_MY_FEATURE // 注意,现在所有 MyFeatureObject 相关的绑定代码都被包在 USE_MY_FEATURE 宏下面了。
  2. se::Class* __jsb_my_ns_MyFeatureObject_class = nullptr;
  3. se::Object* __jsb_my_ns_MyFeatureObject_proto = nullptr;
  4. SE_DECLARE_FINALIZE_FUNC(js_delete_my_ns_MyFeatureObject)
  5. static bool js_my_ns_MyFeatureObject_foo(se::State& s)
  6. {
  7. // ......
  8. }
  9. // ......
  10. bool js_register_my_ns_MyFeatureObject(se::Object* obj) {
  11. auto* cls = se::Class::create("MyFeatureObject", obj, nullptr, _SE(js_new_my_ns_MyFeatureObject));
  12. // ......
  13. }
  14. #endif // USE_MY_FEATURE
  15. // ......
  16. static bool js_my_ns_MyCoolObject_getFeatureObject(se::State& s)
  17. {
  18. #if USE_MY_FEATURE // getFeatureObject 函数的绑定代码也被包在 USE_MY_FEATURE 宏下面了。
  19. // ......
  20. ok &= nativevalue_to_se(result, s.rval(), s.thisObject() /*ctx*/);
  21. SE_PRECONDITION2(ok, false, "MyCoolObject_getFeatureObject, Error processing arguments");
  22. SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval());
  23. #endif // USE_MY_FEATURE
  24. return true;
  25. }
  26. SE_BIND_FUNC(js_my_ns_MyCoolObject_getFeatureObject)
  27. // ......
  28. bool register_all_my_module(se::Object* obj) {
  29. // Get the ns
  30. se::Value nsVal;
  31. if (!obj->getProperty("my_ns", &nsVal, true))
  32. {
  33. se::HandleObject jsobj(se::Object::createPlainObject());
  34. nsVal.setObject(jsobj);
  35. obj->setProperty("my_ns", nsVal);
  36. }
  37. se::Object* ns = nsVal.toObject();
  38. /* Register classes */
  39. #if USE_MY_FEATURE
  40. js_register_my_ns_MyFeatureObject(ns); // js_register_my_ns_MyFeatureObject 也被包在 USE_MY_FEATURE 宏下面了。
  41. #endif // USE_MY_FEATURE
  42. js_register_my_ns_MyObject(ns);
  43. return true;
  44. }

在 Xcode 或者 Visual Studio 中运行项目,会得到如下输出:

  1. 18:32:20 [DEBUG]: D/ JS: ==> featureObj: [object Object] // 在 USE_MY_FEATURE 启用的情况下,featureObj 是个有效值
  2. 18:32:20 [DEBUG]: ==> MyFeatureObject::foo // 调用 C++ foo 方法

当我们不需要 MyFeatureObject 类的时候,把宏设置为 0 即可,代码示例如下:

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. #ifndef USE_MY_FEATURE
  6. #define USE_MY_FEATURE 0 // Disable USE_MY_FEATURE
  7. #endif

在 Xcode 或者 Visual Studio 中运行项目:

  1. 18:54:00 [DEBUG]: D/ JS: ==> featureObj: undefined // getFeatureObject returns undefined if USE_MY_FEATURE is disabled.

多个 Swig 模块的配置

我们创建另外一个头文件,名为 MyAnotherObject.h

  1. // MyAnotherObject.h
  2. #pragma once
  3. namespace my_another_ns {
  4. struct MyAnotherObject {
  5. float a{135.246};
  6. int b{999};
  7. };
  8. } // namespace my_another_ns {

更新 MyObject.h

  1. // MyObject.h
  2. //......
  3. class MyObject : public MyRef {
  4. public:
  5. // ......
  6. void helloWithAnotherObject(const my_another_ns::MyAnotherObject &obj) {
  7. CC_LOG_DEBUG("==> helloWithAnotherObject, a: %f, b: %d", obj.a, obj.b);
  8. }
  9. // ......
  10. };
  11. } // namespace my_ns {

创建 /Users/james/NewProject/tools/swig-config/another-module.i

  1. // another-module.i
  2. %module(target_namespace="another_ns") another_module
  3. // %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方
  4. %insert(header_file) %{
  5. #pragma once
  6. #include "bindings/jswrapper/SeApi.h"
  7. #include "bindings/manual/jsb_conversions.h"
  8. #include "MyAnotherObject.h" // 添加此行
  9. %}
  10. // %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方
  11. %{
  12. #include "bindings/auto/jsb_another_module_auto.h"
  13. %}
  14. %include "MyAnotherObject.h"

修改 /Users/james/NewProject/tools/swig-config/swig-config.js

  1. 'use strict';
  2. const path = require('path');
  3. const configList = [
  4. [ 'my-module.i', 'jsb_my_module_auto.cpp' ],
  5. [ 'another-module.i', 'jsb_another_module_auto.cpp' ], // 添加此行
  6. ];
  7. const projectRoot = path.resolve(path.join(__dirname, '..', '..'));
  8. const interfacesDir = path.join(projectRoot, 'tools', 'swig-config');
  9. const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto');
  10. const includeDirs = [
  11. path.join(projectRoot, 'native', 'engine', 'common', 'Classes'),
  12. ];
  13. module.exports = {
  14. interfacesDir,
  15. bindingsOutDir,
  16. includeDirs,
  17. configList
  18. };

修改 /Users/james/NewProject/native/engine/common/CMakeLists.txt

  1. # /Users/james/NewProject/native/engine/common/CMakeLists.txt
  2. list(APPEND CC_COMMON_SOURCES
  3. ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h
  4. ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp
  5. ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h
  6. ${CMAKE_CURRENT_LIST_DIR}/Classes/MyAnotherObject.h # Add this line
  7. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h
  8. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp
  9. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.h # Add this line
  10. ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.cpp # Add this line
  11. )

重新生成绑定。

更新 Game.cpp:

  1. #include "Game.h"
  2. #include "bindings/auto/jsb_my_module_auto.h"
  3. #include "bindings/auto/jsb_another_module_auto.h" // Add this line
  4. //......
  5. int Game::init() {
  6. //......
  7. se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module);
  8. se::ScriptEngine::getInstance()->addRegisterCallback(register_all_another_module); // Add this line
  9. //
  10. BaseGame::init();
  11. return 0;
  12. }

在 Xcode 或者 Visual Studio 中编译,但是得到如下错误:

Swig 示例 - 图2

因为 MyObject 类依赖了 MyAnotherObject 类,而 MyAnotherObject 类是被定义在另外一个模块中的。我们需要修改 my-module.i 并添加 #include "bindings/auto/jsb_another_module_auto.h"

  1. // my-module.i
  2. %module(target_namespace="my_ns") my_module
  3. // %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方
  4. %insert(header_file) %{
  5. #pragma once
  6. #include "bindings/jswrapper/SeApi.h"
  7. #include "bindings/manual/jsb_conversions.h"
  8. #include "MyObject.h"
  9. %}
  10. // %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方
  11. %{
  12. #include "bindings/auto/jsb_my_module_auto.h"
  13. #include "bindings/auto/jsb_another_module_auto.h" // Add this line
  14. %}
  15. // ......

在 Xcode 或者 Visual Studio 中编译项目,现在应该可以正常编译了。

下一步,我们需要更新 .d.ts 文件:

  1. // my-module.d.ts
  2. declare namespace my_ns {
  3. class MyFeatureObject {
  4. foo() : void;
  5. }
  6. class MyCoolObject {
  7. constructor();
  8. constructor(a: number, b: number);
  9. publicFloatProperty : number;
  10. print() : void;
  11. coolProperty: number;
  12. coolMethod() : void;
  13. type: number;
  14. getFeatureObject() : MyFeatureObject;
  15. helloWithAnotherObject(obj: another_ns.MyAnotherObject) : void; // 添加这行
  16. }
  17. }
  18. // 添加以下行
  19. declare namespace another_ns {
  20. class MyAnotherObject {
  21. a: number;
  22. b: number;
  23. }
  24. }

添加更多的用于读取 MyAnotherObject 类属性的测试代码:

  1. // MyComponent.ts
  2. import { _decorator, Component } from 'cc';
  3. const { ccclass } = _decorator;
  4. @ccclass('MyComponent')
  5. export class MyComponent extends Component {
  6. start() {
  7. const myObj = new my_ns.MyCoolObject();
  8. // ......
  9. const anotherObj = new another_ns.MyAnotherObject(); // 添加此行
  10. myObj.helloWithAnotherObject(anotherObj); // 添加此行
  11. }
  12. }

在 Xcode 或者 Visual Studio 中编译项目,得到如下输出:

  1. 15:05:36 [DEBUG]: ==> helloWithAnotherObject, a: 135.246002, b: 999