The Tutorial of Swig Workflow in Cocos Creator

How to Bind a New Module in Engine

Add a new module interface file

  • Add a new module interface file to native/tools/swig-config directory, e.g. new-engine-module.i

  • Copy the content in swig-interface-template.i to new-engine-module.i

  • Add necessary configuration, you refer to the existed .i files in native/tools/swig-config directory or refer to the following section

Modify engine/native/cocos/CMakeLists.txt

  1. ######## auto
  2. cocos_source_files(
  3. NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp # Add this line
  4. ${SWIG_OUTPUT}/jsb_cocos_auto.h # Add this line
  5. NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp
  6. ${SWIG_OUTPUT}/jsb_cocos_auto.h
  7. ......

Register the new module to Script Engine

Open jsb_module_register.cpp and do the following modifications

  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" // Add this line
  6. bool jsb_register_all_modules() {
  7. se::ScriptEngine *se = se::ScriptEngine::getInstance();
  8. ......
  9. se->addRegisterCallback(register_all_my_new_engine_module); // Add this line
  10. se->addAfterCleanupHook([]() {
  11. cc::DeferredReleasePool::clear();
  12. JSBClassType::cleanup();
  13. });
  14. return true;
  15. }

How to Bind a New Module in Developer’s Project

Suppose we have a Cocos Creator project located at /Users/james/NewProject directory.

Build a native project in Cocos Creator’s build panel, we get /Users/james/NewProject/native directory.

Bind a simple class

Create a simple class

Create a header file in /Users/james/NewProject/native/engine/Classes/MyObject.h , its content is

  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 {

Write an interface file

Create an interface called my-module.i file in /Users/james/NewProject/tools/swig-config

  1. // my-module.i
  2. %module(target_namespace="my_ns") my_module
  3. // Insert code at the beginning of generated 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" // Add this line
  9. %}
  10. // Insert code at the beginning of generated source file (.cpp)
  11. %{
  12. #include "bindings/auto/jsb_my_module_auto.h"
  13. %}
  14. %include "MyObject.h"

Write a swig config file

Create a file called swig-config.js in /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 means header search path for Swig parser
  11. const includeDirs = [
  12. path.join(projectRoot, 'native', 'engine', 'common', 'Classes'),
  13. ];
  14. module.exports = {
  15. interfacesDir,
  16. bindingsOutDir,
  17. includeDirs,
  18. configList
  19. };

Generate bindings for project

  1. $ cd /Users/james/NewProject/tools/swig-config
  2. $ node < Engine Root >/native/tools/swig-config/genbindings.js

If succeed, the files ( jsb_my_module_auto.cpp/.h ) contain JS binding code will be generated at /Users/james/NewProject/native/engine/bindings/auto directory

Modify project’s CMakeLists.txt

  • Open /Users/james/NewProject/native/engine/common/CMakeLists.txt, add MyObject.h and its binding code

    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. ############### Add the following lines ##############
    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. )
  • Modify /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. ############### Add the following lines ##############
    6. target_include_directories(${EXECUTABLE_NAME} PRIVATE
    7. ${CC_PROJECT_DIR}/../common
    8. )
    9. ########################################################
    10. cc_mac_after_target(${EXECUTABLE_NAME})

Open project

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

Windows: < A specific directory >/NewProject/build/win64/proj/NewProject.sln

Register the new module to Script Engine

Modify Game.cpp :

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

Test binding

  • Add a my-module.d.ts file in the root of project directory to make TS compiler know our binding class.

    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. }
  • Modify /Users/james/NewProject/temp/tsconfig.cocos.json file

    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" // Add this line
    13. ],
    14. // ......
    15. "forceConsistentCasingInFileNames": true
    16. }
    17. }
  • Open NewProject in Cocos Creator, create a cube object in scene and attach a script to cube, the script’s content is

    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(); // Invoke native print method
    8. console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); // Get property defined in native
    9. }
    10. }
  • Run project, if succeed, you could find the following logs in console

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

Section Conclusion

In this section, we have learned how to use Swig tool to bind a simple class, export its public methods and properties to JS. This section also cover the entire flow of binding native classes. Start from next section, we will focus on using more Swig features to satisfy more needs of JS bindings, for example:

  • How to import depended header files
  • How to ignore classes, methods, properties
  • How to rename classes, methods, properties
  • How to define attributes which bind c++ getter and setter as a JS property
  • How to configure C++ modules in .i file

Import depended header files

Suppose we let MyObject class be inherited from MyRef class. But we don’t want to bind MyRef class.

  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 inherits from 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 {

When Swig parses MyObject.h, it will not know what MyRef is, it will output a warning in console.

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

It’s simple to fix this issue, we need to let Swig know that MyRef exists by using %import directive.

  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" // Add this line to fix the warning
  7. %include "MyObject.h"

Although Swig doesn’t report the error now, the binding code will not be compiled, the error is:

MyRefCompileError

We fix this in the next section by %ignore directive.

Ignore classes, methods, properties

Ignore classes

In last section, we got a compile error in js_register_my_ns_MyObject. Since MyRef should not be bound, we could use%ignore directive to ignore it.

  1. // my-module.i
  2. // ......
  3. %ignore my_ns::MyRef; // Add this line
  4. %import "MyRef.h"
  5. %include "MyObject.h"

Generate binding again, it compiles ok.

  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. }

Ignore methods and properties

We add a new method methodToBeIgnored and a new property propertyToBeIgnored to MyObject class.

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

Re-generate bindings, we’ll get methodToBeIgnored and propertyToBeIgnored bound.

  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. }

Modify my-module.i to skip binding them.

  1. // my-module.i
  2. // ......
  3. %ignore my_ns::MyRef;
  4. %ignore my_ns::MyObject::methodToBeIgnored; // Add this line
  5. %ignore my_ns::MyObject::propertyToBeIgnored; // Add this line
  6. %import "MyRef.h"
  7. %include "MyObject.h"

Re-generate bindings, they’re ignored now.

  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. }

Rename classes, methods, properties

Swig has defined a directive called %rename to rename classes, methods or properties. To demonstrate, we modify MyObject again.

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject inherits from MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. // ......
  10. void methodToBeRenamed() { // Add this method
  11. CC_LOG_DEBUG("==> hello MyObject::methodToBeRenamed");
  12. }
  13. int propertyToBeRenamed{1234}; // Add this property
  14. float publicFloatProperty{1.23F};
  15. private:
  16. int _a{100};
  17. bool _b{true};
  18. };
  19. } // namespace my_ns {

Generate bindings, we get:

  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));

If we want to rename propertyToBeRenamed to coolProperty and rename methodToBeRenamed to coolMethod, modify my-module.i as follows:

  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; // Add this line
  7. %rename(coolMethod) my_ns::MyObject::methodToBeRenamed; // Add this line
  8. %import "MyRef.h"
  9. %include "MyObject.h"

If we want to rename MyObject class to MyCoolObject, I guess you have already known how to do. Yes, add this line:

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

Re-generate bindings, get the correct name exported to JS.

  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. }

Test it, update my-module.d.ts and MyComponent.ts. The code example is as follows:

  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(); // Renamed to 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. }

Build and run project, get log:

  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

Define an attribute

%attribute directive is used for bind C++ getter and setter functions as a JS property.

Usage

  1. Define an attribute (JS property) without setter

    1. %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name)
  2. Define an attribute (JS property) with getter and setter

    1. %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name, cpp_setter_function_name)
  3. Define an attribute (JS property) without getter

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

Demo

To demonstrate, we add two new methods for MyObject class.

  1. // MyObject.h
  2. #pragma once
  3. #include "cocos/cocos.h"
  4. #include "MyRef.h"
  5. namespace my_ns {
  6. // MyObject inherits from MyRef
  7. class MyObject : public MyRef {
  8. public:
  9. // ......
  10. void setType(int v) { _type = v; CC_LOG_DEBUG("==> setType: v: %d", v); } // Add this line
  11. int getType() const { return _type; } // Add this line
  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); // Add this line
  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. }

Build and run project

  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 directive

%attribute_writeonly directive is an extension we added in swig Cocos backend, it’s used for the purpose that C++ class only has a set function and there isn’t a get function.

In native/tools/swig-config/cocos.i, there are:

  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);

This is the similar functionality in 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. });

Reference type

If C++ get function returns a reference data type or set function accesses a reference data type, don’t forget to add & suffix in %attribute or %attribute_writeonly directives. The following ccstd::string& is an example.

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

If & is missing, a temporary ccstd::string instance will be created while the binding function is invoked.

%arg() directive

Sometimes, the type of C++ variable is a describled by C++ template, for instance:

  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. };

We may write an %attribute in .i file like:

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

You will get an error while invoking node genbindings.js.

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

This is because swig doesn’t know how to deal with comma (,) in std::map<std::string, std::string>&, it will split it to two parts:

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

Therefore, this line of %attribute directive will be parsed with 6 arguments instead of 5.

To avoid making swig confused, we need to use %arg directive to tell swig that std::map<std::string, std::string>& is a complete declaration.

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

Re-run node genbindings.js, the error will no longer be reported.

Don’t add const

In the above sample, %arg(std::map<std::string, std::string>&) is used as a C++ data type in %attribue directive. You may consider to add a const prefix before std::map like %arg(const std::map<std::string, std::string>&). If you do that, you will make a readyonly config property which only binds MyNewClass::getConfig. That’s obviously not what we expect. If we need a readonly property, just don’t assign a set function.

  1. // Don't assign setConfig means the property doesn't need a setter.
  2. %attribute(MyNewClass, %arg(std::map<std::string, std::string>&), config, getConfig);

So to keep things simple, never add const prefix while writing a %attribute directive.

Configure C++ modules in .i file

Sometimes, whether to compile a class depends on whether a macro is enabled. For example, we add a MyFeatureObject class in MyObject.h

  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 inherits from MyRef
  20. class MyObject : public MyRef {
  21. public:
  22. //......
  23. MyFeatureObject* getFeatureObject() {
  24. #if USE_MY_FEATURE // getFeatureObject only returns valid value when USE_MY_FEATURE is enabled
  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}; // Add this line
  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; // Add this line to let Swig know the generated code for MyFeatureObject needs to be wrapped by USE_MY_FEATURE macro
  6. %module_macro(USE_MY_FEATURE) my_ns::MyObject::getFeatureObject; // Add this line to let Swig know the generated code for MyObject::getFeatureObject should be wrapped by USE_MY_FEATURE macro
  7. #define USE_MY_FEATURE 1 // Must be 1 to trick Swig that we need to generate binding code
  8. // even this macro is disabled in C++. NOTE: this line should be after %module_macro
  9. %import "MyRef.h"
  10. %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. }

After generating bindings, the binding code is as follows:

  1. #if USE_MY_FEATURE // NOTE THAT, all binding code of MyFeatureObject is wrapped by USE_MY_FEATURE macro
  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 function is also wrapped by 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 is wrapped by USE_MY_FEATURE
  41. #endif // USE_MY_FEATURE
  42. js_register_my_ns_MyObject(ns);
  43. return true;
  44. }

Build and run the project, the output is as follows:

  1. 18:32:20 [DEBUG]: D/ JS: ==> featureObj: [object Object] // featureObj is valid if USE_MY_FEATURE macro is enabled
  2. 18:32:20 [DEBUG]: ==> MyFeatureObject::foo // Invoke C++ foo method

When we don’t need MyFeatureObject, assign the macro to 0, the code is as follows:

  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

Build and run the project, the following output can be seen.

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

Multiple swig modules configuration

Let’s create another header file 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 {

Update 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 {

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

  1. // another-module.i
  2. %module(target_namespace="another_ns") another_module
  3. // Insert code at the beginning of generated 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" // Add this line
  9. %}
  10. // Insert code at the beginning of generated source file (.cpp)
  11. %{
  12. #include "bindings/auto/jsb_another_module_auto.h"
  13. %}
  14. %include "MyAnotherObject.h"

Modify /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' ], // Add this line
  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. };

Modify /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. )

Generate bindings again.

Update 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. }

Build and compile, but the following errors will be reported:

another-module-compile-error

Since MyObject class depends on MyAnotherObject which is defined on another module. We need to update my-module.i and add #include "bindings/auto/jsb_another_module_auto.h".

  1. // my-module.i
  2. %module(target_namespace="my_ns") my_module
  3. // Insert code at the beginning of generated 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. // Insert code at the beginning of generated source file (.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. // ......

Compile project. It should compile success now.

Next, we update .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; // Add this line
  16. }
  17. }
  18. // Add the following lines
  19. declare namespace another_ns {
  20. class MyAnotherObject {
  21. a: number;
  22. b: number;
  23. }
  24. }

We add some more test code of reading the properties of 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(); // Add this line
  10. myObj.helloWithAnotherObject(anotherObj); // Add this line
  11. }
  12. }

Build and run project, the following output should be seen.

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