LiteLoaderBDS-1.16.40/Tools/ScriptX/docs/zh/NativeBinding.md
2023-03-03 10:18:21 -08:00

403 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 注入/绑定 Native实现 类/方法
这里到了一个比较重点的环节如何将Native 类、方法注入到(绑定)到脚本中,使其能正常使用。
## 注入/绑定 方法
先从简单的说起当需要注入一个native的方法的时候可以使用`script::Function::newFunction`方法,接受的参数是一个`script::FunctionCallback`类型,也就是`std::function<void(const Arguments& args)>`即一个回调函数。当js调用相应方法的时候ScriptX就会调用这个回调函数在这个函数里
1. 函数内已经有EngineScope。
2. 函数内可以直接通过抛出 `script::Exception` 向脚本抛异常。
3. 函数的参数通过`Local<Value> Arguments::operator[](size_t)`获得。
4. 函数的返回值通过`return Local<Value>{xxx}`。
相关例子见上文异常一节。
## 注入/绑定 类
类基本和方法同理,形式不同罢了。
在Native.h中有一个模板定义
```c++
template<typename T>
struct ClassDefine {
const std::string className{};
const std::string nameSpace{};
const StaticDefine staticDefine{};
const InstanceDefine<T> instanceDefine{};
NativeRegister getNativeRegister() const;
}
```
使用 `ClassDefine<T>` 来描述一个类的所有信息。
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<typename T>
struct InstanceDefine {
struct PropertyDefine { ... };
struct FunctionDefine {
using FunctionCallback = std::function<void(T*, const Arguments& args)>;
...
};
const Constructor constructor{};
const std::vector<PropertyDefine> properties{};
const std::vector<FunctionDefine> functions{};
const size_t instanceSize = sizeof_helper_v<T>;
};
```
模板参数T必须是是继承了`sctipt::ScriptClass`的类型。
`InstanceDefine<T>`相比起`StaticDefine`则要有些区别,首先这里多了个必选项`constructor`即构造函数的回调。当脚本侧创建class的新实例的时候ScriptX就会回调到这个函数。而这个函数则需要创建并返回一个 `T*` 指针。当对应的实例被GC的时候ScriptX会**自动释放并析构`T*`**。特别的当构造函数参数非法导致不能构造出相应C++实例时,可以考虑抛异常,或者`return nullptr`此时ScriptX会帮你抛异常
另外一个区别是方法回调多了一个参数`T*`用于回传c++的实例指针该指针确保不为null且一定是constructor返回的指针。
上述的`using FunctionCallback = std::function<void(T*, const Arguments& args)>;`借助`std::function`的能力,既可以绑定一个带有两个参数的函数`void (*)(T*, const Arguments&)`也能绑定一个类T的成员函数`void (T::*)(const Arguments&)`。
最后上述struct并非public API只是用来介绍内部逻辑所有的 `ClassDefine<T>`都应该由 `ClassDeinfeBuilder<T>::build` 来构造出来。
示例如下:
```c++
class TestClass : public ScriptClass {
public:
explicit TestClass(const Local<Object>& scriptObj) : ScriptClass(scriptObj) {}
~TestClass() override = default;
virtual Local<Value> greet(const Arguments& args);
Local<Value> age(const Arguments& args);
Local<Value> getSrc();
void setSrc(const Local<Value>& value);
static Local<Value> add(const Arguments& args);
static Local<Value> getVersion();
static void setVersion(const Local<Value>& value);
static TestClass* create(const Arguments& args);
};
const ClassDefine<TestClass> TestClassDefAll =
defineClass<TestClass>("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<ClasType>(className)`来创建一个 `ClassDefineBuilder`
1. `namespace(std::string)` 见上文
2. `property(std::string,std::function<Local<Value>()> getter, std::function<void(const Local<Value>& value)> setter)` 用于添加一个静态属性
3. `function(std::string, std::function<Local<Value>(const Arguments& args)>)` 用于添加一个静态方法
4. `constructor(std::function<T*(const Arguments& args)>)` 用于指定一个构造函数,详见上文
5. `instanceFunction(std::string, std::function<Local<Value>(T*, const Arguments& args)>)` 用于添加一个实例方法
6. `instanceProperty(std::string, std::function<Local<Value>(T*)> getter, std::function<void(T*, const Local<Value>& value)> setter)` 用于添加一个实例属性
7. `build` 用于构造 `ClassDefine`,内部会对数据的合法性做检查,检查不通过会抛异常。
注意,只有当模板参数`T`不是void时也就是该 `ClassDeinfe` 不是静态类的时候方法456才能调用。
### 与 ClassDefine<T> 相关API
如上实例代码所示,`ScriptEngine`中和`ClassDefine<T>`相关的API共三个。
1. newNativeClass在C++中直接创建一个classDefine的对象
2. isInstanceOf判断一个`Local<Value>`是不是某个classDefine的实例
3. getNativeInstance从classDefine的实例中获取其C++对象的指针。
其他的包括`newNativeClass` 的多个重载方法方便API的使用。
```c++
const ClassDefine<TestClass>& def();
EngineScope engineScope(engine);
try {
engine->registerNativeClass<TestClass>(def());
// 1. static property
auto version = engine->eval("script.engine.test.TestClass.version");
ASSERT_TRUE(addRet.isString());
// 2. new instance
Local<Value> ins = engine->newNativeClass<TestClass>();
Local<Value> ret = engine->eval("new script.engine.test.TestClass()");
// 3. 判断一个Script Value是不是某个ClassDefine的实例
auto instanceof = engine->isInstanceOf<TestClass>(ret);
ASSERT_TRUE(instanceof);
// 4. 通过Script value获取C++类指针
TestClass* ptr = engine->getNativeInstance<TestClass>(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<T>)`
这个是ScriptClass的另一个构造函数使用场景是某个绑定类构造时需要很多C++依赖这样再经过一道ScriptX就导致类型转换很多很麻烦。所以提供这个构造函数直接用C++new一个实例出来然后通过 `getScriptObejct` 拿到对应的ScriptObject返回给ScriptX。
请谨慎使用这个能力并详细阅读头文件中的相关注释否则你将面临内存问题导致的crash
```c++
class MyImage : public script::ScriptClass {
public:
MyImage(void* canvas) : script::ScriptClass(script::ScriptClass::ConstructFromCpp<MyImage>{});
void drawTo(void* canvas, int x, int y);
};
script::ClassDefine<MyImage> myClassDefine =
script::defineClass<MyImage>("MyImage")
.constructor(nullptr)
.function("newImage", []() -> Local<Value> {
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<Object>& thiz) : BaseClass(), ScriptClass(thiz) {}
};
const auto baseWrapperDefine =
defineClass<BaseClassScriptWrapper>("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<MyImage> myClassDefine =
script::defineClass<MyImage>("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<Value>`引用的是`TestClass`的绑定对象,那就可以直接转换成 `TestClass*`
# 自定义类型转换器
你可以自定义新的类型转换器,只需要特化模板即可:
```c++
namespace script::converter {
template <>
struct Converter <T, void> {
// convert custom type T to Local<Value>
static Local<Value> toScript(T&& value) { ...; }
// convert Local<Value> to custom type T
static T toCpp(const Local<Value>& value) { ...; }
};
}
```
详见 [NativeConverter](../src/NativeConverter.hpp)
详见 [CustomConverterTest](../test/src/CustomConverterTest.cc)
# 类型转换用于函数调用
ScriptX给其他常用接口也加上了类型转换的能力。如
```c++
Local<Function> func;
func.call({}, "string", 1, classPtr);
Local<Object> 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<int(int)>(overload);
auto o2 = script::selectOverloadedFunc<int(double)>(overload);
EXPECT_EQ(0, o1(0));
EXPECT_EQ(1, o2(0.0));
auto x0 = script::selectOverloadedFunc<int (X::*)(int)>(&X::overload);
auto x1 = script::selectOverloadedFunc<int (X::*)(int&)>(&X::overload);
auto x2 = script::selectOverloadedFunc<int (X::*)(double)>(&X::overload);
auto x3 = script::selectOverloadedFunc<int (X::*)(double) const>(&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)
<details>
<summary>graph code</summary>
```mermaid
flowchart BT;
C++核心能力 -- 绑定 --> ScriptX;
ScriptX -- 扩展+包装 --> JS基础库;
ScriptX -- 扩展+包装 --> Lua基础库;
subgraph JS
JS基础库 <-- API --> JS业务脚本;
end
subgraph Lua
Lua基础库 <-- API --> Lua业务脚本;
end
```
</details>
从底层到上层:
* ScriptX提供基础的绑定能力来暴露C++核心能力;
* 使用者在ScriptX之上再写一套语言相关的**基础库用来扩展能力**(如上述的操作符重载);
* 最后业务脚本会调用基础库和绑定的API实现其功能需求。
这样一来就把语言相关的逻辑放到基础库中了,绑定部分的代码仍然是通用且统一的。