# 注入/绑定 Native实现 类/方法 这里到了一个比较重点的环节,如何将Native 类、方法注入到(绑定)到脚本中,使其能正常使用。 ## 注入/绑定 方法 先从简单的说起,当需要注入一个native的方法的时候,可以使用`script::Function::newFunction`方法,接受的参数是一个`script::FunctionCallback`类型,也就是`std::function`,即一个回调函数。当js调用相应方法的时候,ScriptX就会调用这个回调函数,在这个函数里: 1. 函数内已经有EngineScope。 2. 函数内可以直接通过抛出 `script::Exception` 向脚本抛异常。 3. 函数的参数通过`Local Arguments::operator[](size_t)`获得。 4. 函数的返回值通过`return Local{xxx}`。 相关例子见上文异常一节。 ## 注入/绑定 类 类基本和方法同理,形式不同罢了。 在Native.h中有一个模板定义: ```c++ template struct ClassDefine { const std::string className{}; const std::string nameSpace{}; const StaticDefine staticDefine{}; const InstanceDefine instanceDefine{}; NativeRegister getNativeRegister() const; } ``` 使用 `ClassDefine` 来描述一个类的所有信息。 1. 模板参数T:即该脚本class在C++中对应的类,脚本类的实例和C++中T的实例是一一对应的,每创建一个脚本class实例,C++中都会创建一个新的T的实例。当这个class只有静态方法属性(类方法属性)即不能创建实例的,用void代替T。 2. className:类的名字 3. nameSpace:即这个类在哪个命名空间下。在JS中比如`nameSpace= "game.ui"; className="ImageView";`则该类在脚本中会存在于`game.ui.ImageView`,使用时`new game.ui.ImageView()`. 为了支持多种语言,命名空间的分隔符统一使用英文句号——`.`。 4. staticDefine:类的静态方法+属性定义 5. instanceDefine:类的实例方法+属性定义 6. getNativeRegister: 由于ClassDefine是模板类,在一些需要存储到容器的场景就比较难实现。因此ClassDefine直接实现了一个**类型擦除**的helper类,`NativeRegister`类是一个普通类,支持拷贝和移动,可以当做指针使用。 通过调用 `ScriptEngine::registerNativeDefine` 注册一个绑定类,需要注意这个ClassDefine的销毁要比ScriptEngien晚,否则会出现野指针问题。通常的做法是ClassDefine作为全局静态变量。 `StaticDefine`中包含方法(方法名+方法回调),属性(属性名+属性getter+属性setter)和绑定方法大同小异,这里不再赘述。 `InstanceDefine` ```c++ template struct InstanceDefine { struct PropertyDefine { ... }; struct FunctionDefine { using FunctionCallback = std::function; ... }; const Constructor constructor{}; const std::vector properties{}; const std::vector functions{}; const size_t instanceSize = sizeof_helper_v; }; ``` 模板参数T必须是是继承了`sctipt::ScriptClass`的类型。 `InstanceDefine`相比起`StaticDefine`则要有些区别,首先这里多了个必选项`constructor`,即构造函数的回调。当脚本侧创建class的新实例的时候ScriptX就会回调到这个函数。而这个函数则需要创建,并返回一个 `T*` 指针。当对应的实例被GC的时候,ScriptX会**自动释放并析构`T*`**。特别的,当构造函数参数非法,导致不能构造出相应C++实例时,可以考虑抛异常,或者`return nullptr`(此时ScriptX会帮你抛异常)。 另外一个区别是方法回调多了一个参数`T*`用于回传c++的实例指针,该指针确保不为null,且一定是constructor返回的指针。 上述的`using FunctionCallback = std::function;`借助`std::function`的能力,既可以绑定一个带有两个参数的函数`void (*)(T*, const Arguments&)`,也能绑定一个类T的成员函数`void (T::*)(const Arguments&)`。 最后上述struct并非public API,只是用来介绍内部逻辑,所有的 `ClassDefine`都应该由 `ClassDeinfeBuilder::build` 来构造出来。 示例如下: ```c++ class TestClass : public ScriptClass { public: explicit TestClass(const Local& scriptObj) : ScriptClass(scriptObj) {} ~TestClass() override = default; virtual Local greet(const Arguments& args); Local age(const Arguments& args); Local getSrc(); void setSrc(const Local& value); static Local add(const Arguments& args); static Local getVersion(); static void setVersion(const Local& value); static TestClass* create(const Arguments& args); }; const ClassDefine TestClassDefAll = defineClass("TestClass") .nameSpace("script.engine.test") .constructor(&TestClass::create) .function("add", &TestClass::add) .property("version", &TestClass::getVersion, &TestClass::setVersion) .instanceFunction("greet", &TestClass::greet) .instanceFunction("age", &TestClass::age) .instanceProperty("src", &TestClass::getSrc, &TestClass::setSrc) .build(); ``` ## ClassDefineBuilder 的API 通过使用 `script::defineClass(className)`来创建一个 `ClassDefineBuilder` 1. `namespace(std::string)` 见上文 2. `property(std::string,std::function()> getter, std::function& value)> setter)` 用于添加一个静态属性 3. `function(std::string, std::function(const Arguments& args)>)` 用于添加一个静态方法 4. `constructor(std::function)` 用于指定一个构造函数,详见上文 5. `instanceFunction(std::string, std::function(T*, const Arguments& args)>)` 用于添加一个实例方法 6. `instanceProperty(std::string, std::function(T*)> getter, std::function& value)> setter)` 用于添加一个实例属性 7. `build` 用于构造 `ClassDefine`,内部会对数据的合法性做检查,检查不通过会抛异常。 注意,只有当模板参数`T`不是void时,也就是该 `ClassDeinfe` 不是静态类的时候,方法4,5,6才能调用。 ### 与 ClassDefine 相关API 如上实例代码所示,`ScriptEngine`中和`ClassDefine`相关的API共三个。 1. newNativeClass:在C++中直接创建一个classDefine的对象 2. isInstanceOf:判断一个`Local`是不是某个classDefine的实例 3. getNativeInstance:从classDefine的实例中,获取其C++对象的指针。 其他的包括`newNativeClass` 的多个重载方法,方便API的使用。 ```c++ const ClassDefine& def(); EngineScope engineScope(engine); try { engine->registerNativeClass(def()); // 1. static property auto version = engine->eval("script.engine.test.TestClass.version"); ASSERT_TRUE(addRet.isString()); // 2. new instance Local ins = engine->newNativeClass(); Local ret = engine->eval("new script.engine.test.TestClass()"); // 3. 判断一个Script Value,是不是某个ClassDefine的实例 auto instanceof = engine->isInstanceOf(ret); ASSERT_TRUE(instanceof); // 4. 通过Script value,获取C++类指针 TestClass* ptr = engine->getNativeInstance(ret); } catch (const Exception& e) { ... } ``` # ScriptClass 如果要绑定实例类,则必须继承`ScriptClass`才行, ScriptClass 提供了一个和脚本对象绑定的C++对象的一些基本操作。 其中需要说明的几个API包括 #### `ScriptClass::getScriptObject` 获取绑定的脚本对象。 #### `ScriptClass::getInternalStore` 使用场景是,有时需要绑定对象需要持有一些其他的脚本对象,比如一个回调函数。 InternalStore是一个被 ScriptObject 所引用的数组,借助这个数组,我们就可以把任何需要存储的对象设置进去。同时也不用担心GC相关的问题。 #### `ScriptClass::~ScriptClass` 析构函数,由ScriptX管理GC并销毁对应的C++类。 ScriptX保证这个方法一定在主线程调用。 ScriptX不保证这个实例一定是在 ScriptObject GC的同时销毁(可能延后)。事实上,对于V8和JSCore,因为两个引擎都要求在finalize里不能调用任何引擎相关的API,所以他们的ScriptClass的销毁都是post到MessageQueue里。 所以当你在C++成持有一个ScriptClass的指针时,会在这个时间间隙里发现 `ScriptClass::getScriptObject` 返回null。 #### `ScriptClass::ScriptClass(ConstructFromCpp)` 这个是ScriptClass的另一个构造函数,使用场景是某个绑定类构造时需要很多C++依赖,这样再经过一道ScriptX就导致类型转换很多很麻烦。所以提供这个构造函数,直接用C++new一个实例出来,然后通过 `getScriptObejct` 拿到对应的ScriptObject返回给ScriptX。 请谨慎使用这个能力,并详细阅读头文件中的相关注释,否则你将面临内存问题导致的crash! ```c++ class MyImage : public script::ScriptClass { public: MyImage(void* canvas) : script::ScriptClass(script::ScriptClass::ConstructFromCpp{}); void drawTo(void* canvas, int x, int y); }; script::ClassDefine myClassDefine = script::defineClass("MyImage") .constructor(nullptr) .function("newImage", []() -> Local { auto img = new MyImage(Render::getInstance()->canvas()); return img.getScriptObject(); }) .function("newImage2", []() -> MyImge* { // 借助Converter,也可以直接返回指针,ScriptX会转换成和上面等价的代码 return MyImage(Render::getInstance()->canvas()); }) .build(); ``` # 对已有C++类做绑定 因为绑定类需要继承 `ScriptClass`,对已经有的C++类似乎不太友好? 幸运的是C++可以多继承,只需要写一个Wrapper类继承已有C++类和 `ScriptClass` 即可。 ```c++ class BaseClass { public: int age = 0; int num = 1; const int length = 180; std::string name() { return "Base"; } int getNum() { return num; } void setNum(int n) { num = n; } }; class BaseClassScriptWrapper : public BaseClass, public ScriptClass { public: explicit BaseClassScriptWrapper(const Local& thiz) : BaseClass(), ScriptClass(thiz) {} }; const auto baseWrapperDefine = defineClass("BaseWrapper") .nameSpace("BindBaseClass") .constructor() .instanceProperty("age", &BaseClass::age) .instanceProperty("length", &BaseClass::length) // const property has getter, no setter .instanceFunction("name", &BaseClass::name) .instanceProperty("num", &BaseClass::getNum, &BaseClass::setNum) .build(); ``` 详见 [NativeTest](../../test/src/NativeTest.cc) # 直接绑定C++函数 尽管上述绑定API已经比较好用了,但是ScriptX还更近了一步,使用现代C++的模板元编程能力,使得ScriptX可以直接绑定普通的C++函数。 如: ```C++ class MyImage : public script::ScriptClass { public: using script::ScriptClass::ScriptClass; static const int age = 0; static const char* name() { return "hello world"; } std::string src; int add(int a, int b) {return a+ b;} }; script::ClassDefine myClassDefine = script::defineClass("MyImage") .constructor() .property("age", &MyImage::age) .function("name", &MyImage::name) .instanceFunction("add", &MyImage::add) .instanceProperty("src", &MyImage::src) .build(); ``` 注意上面,你甚至可以直接对成员变量做绑定。 具体使用原则是,方法**参数**和**返回值**是支持转换的类型。 支持转换的类型列表: 1. void 2. bool 3. any float type : float double 4. any int type : int8_t uint8_t int16_t uint16_t int32_t uint32_t int64_t uint64_t size_t 5. any string type: string string_view char* char8_t* u8string u8string_view 6. all kind of Local reference 7. any pointer of subclass of ScriptClass 注意7,其实支持的是所有绑定类和类指针的转换。 比如`Local`引用的是`TestClass`的绑定对象,那就可以直接转换成 `TestClass*` # 自定义类型转换器 你可以自定义新的类型转换器,只需要特化模板即可: ```c++ namespace script::converter { template <> struct Converter { // convert custom type T to Local static Local toScript(T&& value) { ...; } // convert Local to custom type T static T toCpp(const Local& value) { ...; } }; } ``` 详见 [NativeConverter](../src/NativeConverter.hpp) 详见 [CustomConverterTest](../test/src/CustomConverterTest.cc) # 类型转换用于函数调用 ScriptX给其他常用接口也加上了类型转换的能力。如: ```c++ Local func; func.call({}, "string", 1, classPtr); Local obj; obj.set("hello", 4.13); Object::newObject(type, "name", false); ``` # Tips: 重载函数的选择 直接绑定C++函数时经常面临C++函数重载的情况,这里提供了一个util函数来选择合适的重载版本。 `script::selectOverloadedFunc` ```c++ int overload(int) { return 0; } int overload(double) { return 1; } class X { public: int overload(int) { return 0; } int overload(int&) { return 1; } int overload(double) { return 2; } int overload(double) const { return 3; } }; TEST_F(NativeTest, SelectOverloadedFunction) { auto o1 = script::selectOverloadedFunc(overload); auto o2 = script::selectOverloadedFunc(overload); EXPECT_EQ(0, o1(0)); EXPECT_EQ(1, o2(0.0)); auto x0 = script::selectOverloadedFunc(&X::overload); auto x1 = script::selectOverloadedFunc(&X::overload); auto x2 = script::selectOverloadedFunc(&X::overload); auto x3 = script::selectOverloadedFunc(&X::overload); X x; X* p = &x; int i = 0; EXPECT_EQ(0, (p->*x0)(0)); EXPECT_EQ(1, (p->*x1)(i)); EXPECT_EQ(2, (p->*x2)(0.0)); EXPECT_EQ(3, (p->*x3)(0.0)); } ``` 详见 [NativeTest](../../test/src/NativeTest.cc) # Tips: 不同语言见的差异 不同的脚本语言能力差异较大,比如 JS 不支持重载操作符,但是Lua、Python等语言是允许的。 > ScriptX的设计哲学是:仅提供通用且必要的接口。 比如`ClassDefine`只提供了必要的5个接口:构造函数、静态属性、静态方法、实例属性,实例方法。 有了这五个接口,理论上能力就齐全了。 针对上面提出的问题,ScriptX的解决方案是: > 每一种语言提供一套语言特定的基础库。 ![](../media/multilingual_structure_zh.svg)
graph code ```mermaid flowchart BT; C++核心能力 -- 绑定 --> ScriptX; ScriptX -- 扩展+包装 --> JS基础库; ScriptX -- 扩展+包装 --> Lua基础库; subgraph JS JS基础库 <-- API --> JS业务脚本; end subgraph Lua Lua基础库 <-- API --> Lua业务脚本; end ```
从底层到上层: * ScriptX提供基础的绑定能力来暴露C++核心能力; * 使用者在ScriptX之上再写一套语言相关的**基础库用来扩展能力**(如上述的操作符重载); * 最后业务脚本会调用基础库和绑定的API实现其功能需求。 这样一来就把语言相关的逻辑放到基础库中了,绑定部分的代码仍然是通用且统一的。