/* * Tencent is pleased to support the open source community by making ScriptX available. * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include "test.h" namespace script::test { DEFINE_ENGINE_TEST(NativeTest); namespace { std::string version = "0.1.0"; class TestClass : public ScriptClass { public: std::string src = "https://github.com/LanderlYoung"; explicit TestClass(const Local& scriptObj) : ScriptClass(scriptObj) {} ~TestClass() override = default; std::string greetStr; // intentionally make this virtual virtual Local greet(const Arguments& args) { if (args.size() > 0 && args[0].isString()) { greetStr = args[0].asString().toString().c_str(); } return {}; } int ageInt = -1; Local age(const Arguments& args) { if (args.size() > 0 && args[0].isNumber()) { ageInt = args[0].asNumber().toInt32(); return Number::newNumber(ageInt); } return {}; } Local getSrc() { return String::newString(src); } void setSrc(const Local& value) { src = value.asString().toString(); } static Local add(const Arguments& args) { if (args.size() >= 2 && args[0].isNumber() && args[1].isNumber()) { return Number::newNumber(args[0].asNumber().toDouble() + args[1].asNumber().toDouble()); } return {}; } static int add_1(int a, int b) { return a + b; } static Local getVersion() { return String::newString(version); } static void setVersion(const Local& value) { version = value.asString().toString(); } static TestClass* create(const Arguments& args) { return new TestClass(args.thiz()); } }; const ClassDefine TestClassDefAll = defineClass("TestClass") .nameSpace("script.engine.test") .constructor() .function("add", &TestClass::add) .function("add1", &TestClass::add_1) .property("version", &TestClass::getVersion, &TestClass::setVersion) .instanceFunction("greet", &TestClass::greet) .instanceFunction("age", &TestClass::age) .instanceProperty("src", &TestClass::getSrc, &TestClass::setSrc) .build(); const ClassDefine TestClassDefStatic = defineClass("TestClass") .nameSpace("script.engine.test") .function("add", &TestClass::add) .property("version", &TestClass::getVersion, &TestClass::setVersion) .build(); const ClassDefine TestClassDefInstance = defineClass("TestClass") .nameSpace("script.engine.test") .constructor(TestClass::create) .instanceFunction("greet", &TestClass::greet) .instanceFunction("age", &TestClass::age) .instanceProperty("src", &TestClass::getSrc, &TestClass::setSrc) .build(); void testStatic(ScriptEngine* engine) { // prop: get version auto getVersion = engine->eval(TS().js("script.engine.test.TestClass.version") .lua("return script.engine.test.TestClass.version") .select()); ASSERT_TRUE(getVersion.isString()); EXPECT_EQ(getVersion.asString().toString(), version); // prop: set version engine->eval("script.engine.test.TestClass['version'] = '1.0-beta' "); EXPECT_EQ(std::string("1.0-beta"), version); // function: add auto addRet = engine->eval(TS().js("script.engine.test.TestClass.add(1, 2)") .lua("return script.engine.test.TestClass.add(1, 2)") .select()); ASSERT_TRUE(addRet.isNumber()); EXPECT_EQ(addRet.asNumber().toInt32(), 3); } void testInstance(ScriptEngine* engine, const ClassDefine& def) { auto ret = engine->eval(TS().js("new script.engine.test.TestClass()") .lua("return script.engine.test.TestClass()") .select()); ASSERT_TRUE(ret.isObject()); ASSERT_TRUE(engine->isInstanceOf(ret)); EXPECT_FALSE(engine->isInstanceOf({})); EXPECT_FALSE(engine->isInstanceOf(script::Object::newObject())); TestClass* instance = engine->getNativeInstance(ret); ASSERT_TRUE(instance != nullptr); engine->set("instance", ret); auto srcRet = engine->eval(TS().js("instance.src").lua("return instance.src").select()); ASSERT_TRUE(srcRet.isString()); EXPECT_STREQ(srcRet.asString().toString().c_str(), instance->src.c_str()); engine->eval("instance.src = 'new_src'"); EXPECT_STREQ(instance->src.c_str(), "new_src"); auto greet1Ret = engine->eval(TS().js("instance.greet('gh')").lua("return instance:greet('gh')").select()); EXPECT_TRUE(greet1Ret.isNull()); EXPECT_STREQ(instance->greetStr.c_str(), "gh"); engine->eval(TS().js("instance.greet('hello world')") .lua("return instance:greet('hello world')") .select()); EXPECT_STREQ(instance->greetStr.c_str(), "hello world"); auto age1Ret = engine->eval(TS().js("instance.age()").lua("return instance:age()").select()); ASSERT_TRUE(age1Ret.isNull()); EXPECT_EQ(instance->ageInt, -1); auto age2Ret = engine->eval(TS().js("instance.age(18)").lua("return instance:age(18)").select()); ASSERT_TRUE(age2Ret.isNumber()); EXPECT_EQ(age2Ret.asNumber().toInt32(), 18); EXPECT_EQ(instance->ageInt, 18); engine->set("instance", {}); } } // namespace TEST_F(NativeTest, All) { script::EngineScope engineScope(engine); engine->registerNativeClass(TestClassDefAll); auto ret = engine->eval( TS().js("script.engine.test.TestClass").lua("return script.engine.test.TestClass").select()); ASSERT_TRUE(ret.isObject()); testStatic(engine); testInstance(engine, TestClassDefAll); auto ins = engine->newNativeClass(); ASSERT_TRUE(engine->isInstanceOf(ins)); ASSERT_TRUE(engine->getNativeInstance(ins) != nullptr); } TEST_F(NativeTest, Static) { script::EngineScope engineScope(engine); engine->registerNativeClass(TestClassDefStatic); auto ret = engine->eval( TS().js("script.engine.test.TestClass").lua("return script.engine.test.TestClass").select()); ASSERT_TRUE(ret.isObject()); testStatic(engine); } TEST_F(NativeTest, Instance) { script::EngineScope engineScope(engine); try { engine->registerNativeClass(TestClassDefInstance); auto ret = engine->eval(TS().js("script.engine.test.TestClass") .lua("return script.engine.test.TestClass") .select()); ASSERT_TRUE(ret.isObject()); testInstance(engine, TestClassDefInstance); } catch (const Exception& e) { FAIL() << e; } } #ifdef SCRIPTX_LANG_JAVASCRIPT TEST_F(NativeTest, ProtoType) { script::EngineScope engineScope(engine); try { engine->registerNativeClass(TestClassDefInstance); auto ret = engine->eval(R"( const obj = new script.engine.test.TestClass(); if (Object.getPrototypeOf(obj) !== script.engine.test.TestClass.prototype) { throw Error("instance prototype != Class.prototype"); } )"); } catch (const Exception& e) { FAIL() << e; } } #endif namespace { auto NativeRegisterDef = TestClassDefAll; } TEST_F(NativeTest, NativeRegister) { script::EngineScope engineScope(engine); auto reg = NativeRegisterDef.getNativeRegister(); reg.registerNativeClass(engine); auto ret = engine->eval( TS().js("script.engine.test.TestClass").lua("return script.engine.test.TestClass").select()); ASSERT_TRUE(ret.isObject()); testStatic(engine); testInstance(engine, NativeRegisterDef); // clear engine->set("script", {}); auto ins = engine->newNativeClass(); EXPECT_TRUE(engine->isInstanceOf(ins)); EXPECT_TRUE(engine->getNativeInstance(ins) != nullptr); } TEST_F(NativeTest, NativeRegister2) { script::EngineScope engineScope(engine); auto reg = NativeRegisterDef.getNativeRegister(); engine->registerNativeClass(reg); auto ret = engine->eval( TS().js("script.engine.test.TestClass").lua("return script.engine.test.TestClass").select()); ASSERT_TRUE(ret.isObject()); testStatic(engine); testInstance(engine, NativeRegisterDef); // clear engine->set("script", {}); auto ins = engine->newNativeClass(); EXPECT_TRUE(engine->isInstanceOf(ins)); EXPECT_TRUE(engine->getNativeInstance(ins) != nullptr); } TEST_F(NativeTest, NotRegistered) { script::EngineScope engineScope(engine); EXPECT_THROW( { // forget to register // engine->registerNativeClass(TestClassDefAll); engine->newNativeClass(); }, Exception); } namespace { class MyImage : public script::ScriptClass { public: using script::ScriptClass::getScriptObject; using script::ScriptClass::ScriptClass; MyImage() : script::ScriptClass(script::ScriptClass::ConstructFromCpp{}) {} int drawTo(void* canvas, int x, int y) const { return x + y; } }; script::ClassDefine myClassDefine = script::defineClass("MyImage").constructor().build(); } // namespace TEST_F(NativeTest, ScriptClassConverter) { EngineScope scope(engine); try { engine->registerNativeClass(myClassDefine); // use MyImage* as argument auto drawImagePointer = Function::newFunction( [](MyImage* image, int x, int y) -> int { return image->drawTo(nullptr, x, y); }); auto drawImagePointerConst = Function::newFunction( [](MyImage* const image, int x, int y) -> int { return image->drawTo(nullptr, x, y); }); auto drawImageConstPointer = Function::newFunction( [](const MyImage* image, int x, int y) -> int { return image->drawTo(nullptr, x, y); }); auto drawImageReference = Function::newFunction( [](MyImage& image, int x, int y) -> int { return image.drawTo(nullptr, x, y); }); auto drawImageConstReference = Function::newFunction( [](const MyImage& image, int x, int y) -> int { return image.drawTo(nullptr, x, y); }); // return MyImage* auto factory = Function::newFunction([]() -> MyImage* { return new MyImage(); }); auto myClassValue = factory.call(); auto myClass = engine->getNativeInstance(myClassValue); ASSERT_TRUE(myClass != nullptr); for (auto&& func : std::initializer_list>{ drawImagePointer, drawImageConstPointer, drawImagePointerConst, drawImageReference, drawImageConstReference}) { auto ret = func.call({}, myClass, 1, 2); ASSERT_TRUE(ret.isNumber()); EXPECT_EQ(ret.asNumber().toInt32(), 3); } } catch (const Exception& e) { FAIL() << e; } } namespace { auto SanityCheckDef = TestClassDefAll; class SanityCheck2 : public script::ScriptClass { public: using ScriptClass::ScriptClass; }; auto SanityCheck2Def = defineClass("SanityCheck2").constructor().build(); } // namespace TEST_F(NativeTest, SanityCheck) { script::EngineScope engineScope(engine); engine->registerNativeClass(SanityCheckDef); engine->registerNativeClass(SanityCheck2Def); #ifdef SCRIPTX_LANG_JAVASCRIPT EXPECT_THROW( { // can't call as function engine->eval(u8"script.engine.test.TestClass(); new script.engine.test.TestClass();"); }, Exception); #endif return; EXPECT_THROW( { // wrong receiver engine->eval(TS().js( R"( (function() { const t = new script.engine.test.TestClass(); const obj = {}; obj.g = t.greet; obj.g(); })() )") .lua(R"( local t = script.engine.test.TestClass() local obj = {} obj.g = t.greet obj.g() )") .select()); }, Exception); EXPECT_THROW( { // wrong receiver engine->eval(TS().js( R"( (function() { const t = new script.engine.test.TestClass(); const obj = new SanityCheck2(); obj.g = t.greet; obj.g(); })() )") .lua(R"( local t = script.engine.test.TestClass() local obj = SanityCheck2() obj.g = t.greet obj.g() )") .select()); }, Exception); } namespace { ClassDefine gns = defineClass("GnS").property("src", []() { return String::newString(u8"hello"); }).build(); } TEST_F(NativeTest, GetNoSet) { script::EngineScope engineScope(engine); try { engine->registerNativeClass(gns); engine->eval(u8"GnS.src = 'x';"); engine->eval(TS().js("if (GnS.src !== 'hello') throw new Error(GnS.src);") .lua("if GnS.src ~= 'hello' then error(GnS.src) end") .select()); } catch (const Exception& e) { FAIL() << e; } } namespace { ClassDefine sng = defineClass("SnG") .property("src", nullptr, [](const Local& val) { EXPECT_TRUE(val.isString()); EXPECT_STREQ(val.asString().toString().c_str(), "hello"); }) .build(); } TEST_F(NativeTest, SetNoGet) { script::EngineScope engineScope(engine); engine->registerNativeClass(sng); try { engine->eval(TS().js(u8"if (SnG.src !== undefined) throw new Error();") .lua(u8"if SnG.src ~= nil then error() end") .select()); } catch (Exception& e) { FAIL() << e; } engine->eval(u8"SnG.src = 'hello';"); try { engine->eval(TS().js(u8"if (SnG.src !== undefined) throw new Error();") .lua(u8"if SnG.src ~= nil then error() end") .select()); } catch (Exception& e) { FAIL() << e; } engine->set("SnG", {}); } TEST_F(NativeTest, OverloadedBind) { auto f1 = [](int) { return "number"; }; auto f2 = [](const std::string&) { return "string"; }; auto f3 = [](const Local&) { return "func"; }; auto f4 = [](const Arguments&) -> Local { return String::newString("other"); }; EngineScope scope(engine); auto overload = script::adaptOverLoadedFunction(f1, f2, f3, f4); auto func = Function::newFunction(overload); auto ret = func.call({}, Number::newNumber(0)); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "number"); ret = func.call({}, String::newString("hello")); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "string"); ret = func.call({}, func); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "func"); ret = func.call({}, Object::newObject()); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "other"); } TEST_F(NativeTest, OverloadedInsBind) { class Ins : public ScriptClass { public: explicit Ins(const Local& scriptObject) : ScriptClass(scriptObject) {} }; auto f1 = [](Ins*, int) { return "number"; }; auto f2 = [](Ins*, const std::string&) { return "string"; }; auto f3 = [](Ins*, const Local&) { return "func"; }; EngineScope scope(engine); auto overload = script::adaptOverloadedInstanceFunction(f1, f2, f3); static auto define = defineClass("OverloadedInsBuild").constructor().instanceFunction("f", overload).build(); engine->registerNativeClass(define); auto ins = engine->newNativeClass(); auto func = ins.get("f").asFunction(); auto ret = func.call(ins, Number::newNumber(0)); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "number"); ret = func.call(ins, String::newString("hello")); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "string"); ret = func.call(ins, func); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "func"); } TEST_F(NativeTest, NewNativeClass) { class Ins : public ScriptClass { public: std::string name{}; double score{}; explicit Ins(const Arguments& args) : ScriptClass(args.thiz()) { name = args[0].asString().toString(); score = args[1].asNumber().toDouble(); } }; static auto define = defineClass("InsNewNativeClassTest").constructor().build(); EngineScope scope(engine); engine->registerNativeClass(define); std::initializer_list> insList{ engine->newNativeClass(String::newString("hello"), Number::newNumber(3.14)), engine->newNativeClass(String::newString("hello"), 3.14), engine->newNativeClass("hello", 3.14), }; for (auto& ins : insList) { auto ptr = engine->getNativeInstance(ins); EXPECT_EQ(ptr->name, "hello"); EXPECT_EQ(ptr->score, 3.14); } } namespace { class CppNew : public ScriptClass { public: explicit CppNew(std::vector words); const std::string& greet() const { return words_; } using ScriptClass::getScriptObject; private: std::string words_; }; Local create() { // pure cpp constructor auto ptr = new CppNew({"hello", "world", "cpp"}); return ptr->getScriptObject(); } static auto define = defineClass("CppNewTest") .nameSpace("cppnewtest") .constructor([](const Arguments&) -> CppNew* { throw Exception("this class can't be created in script"); // or return nullptr, has the same effect }) .instanceFunction("greet", &CppNew::greet) .function("create", create) .build(); CppNew::CppNew(std::vector words) : ScriptClass(ScriptClass::ConstructFromCpp{}), words_() { for (auto& w : words) { words_ += w; words_ += " "; } } TEST_F(NativeTest, CppNew) { EngineScope scope(engine); engine->registerNativeClass(define); auto ret = engine->eval(TS().js( R"( (function() { const ins = cppnewtest.CppNewTest.create(); return ins.greet(); })() )") .lua( R"( local ins = cppnewtest.CppNewTest.create(); return ins:greet(); )") .select()); ASSERT_TRUE(ret.isString()); EXPECT_STREQ(ret.asString().toString().c_str(), "hello world cpp "); } } // namespace TEST_F(NativeTest, BindExceptionTest) { auto f1 = [](int i) { if (i == 42) { throw Exception("nonono"); } return "number"; }; EngineScope scope(engine); auto funcHasThrow = Function::newFunction(f1, false); auto funcNoThrow = Function::newFunction(f1, true); auto ret = funcHasThrow.call({}, Number::newNumber(0)); ASSERT_TRUE(ret.isString()); EXPECT_EQ(ret.asString().toString(), "number"); // invalid arguments EXPECT_THROW({ funcHasThrow.call({}, String::newString("hello")); }, Exception); try { funcHasThrow.call({}, String::newString("hello")); // no reach here EXPECT_TRUE(false); } catch (const Exception& e) { EXPECT_TRUE(e.message().find("can't cast value as Number") != std::string::npos); } try { funcHasThrow.call({}, Number::newNumber(42)); // no reach here EXPECT_TRUE(false); } catch (const Exception& e) { EXPECT_TRUE(e.message().find("nonono") != std::string::npos); } EXPECT_NO_THROW({ funcNoThrow.call({}, String::newString("hello")); }); EXPECT_THROW({ funcNoThrow.call({}, Number::newNumber(42)); }, Exception); } class InternalStorageTest : public ScriptClass { public: explicit InternalStorageTest(const Local& scriptObject) : ScriptClass(scriptObject) {} void setVal(const Local& val) { getInternalStore().set(0, val); } Local getVal() { return getInternalStore().get(0); } }; ClassDefine internalStorageTest = defineClass("InternalStorageTest") .constructor() .instanceProperty("val", &InternalStorageTest::getVal, &InternalStorageTest::setVal) .build(); TEST_F(NativeTest, InternalStorage) { script::EngineScope engineScope(engine); engine->registerNativeClass(internalStorageTest); try { auto val = engine->eval(TS().js( R"( var x = new InternalStorageTest(); x.val = "hello"; x.val; )") .lua(R"( local x = InternalStorageTest(); x.val = "hello"; return x.val; )") .select()); ASSERT_TRUE(val.isString()); EXPECT_STREQ(val.asString().toString().c_str(), "hello"); } catch (Exception& e) { FAIL() << e; } } namespace { 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(); } // namespace TEST_F(NativeTest, BindBaseClass) { EngineScope engineScope(engine); try { engine->registerNativeClass(baseWrapperDefine); auto base = engine->newNativeClass(); auto ptr = engine->getNativeInstance(base); engine->set("base", base); engine->eval("base.age = 10"); EXPECT_EQ(ptr->age, 10); // length is const, so no setter available engine->eval("base.length = 0"); EXPECT_EQ(ptr->length, 180); ptr->setNum(42); auto num = engine->eval(TS().js("base.num").lua("return base.num").select()); ASSERT_TRUE(num.isNumber()); EXPECT_EQ(ptr->getNum(), num.asNumber().toInt32()); } catch (const Exception& e) { FAIL() << e; } } namespace { class InstanceOfTest : public script::ScriptClass { public: using ScriptClass::ScriptClass; }; auto instanceOfTestDefine = defineClass("InstanceOfTest").constructor().build(); } // namespace TEST_F(NativeTest, InstanceOfTest) { EngineScope scope(engine); try { engine->registerNativeClass(instanceOfTestDefine); // script created object auto ins = engine->eval(TS().js("new InstanceOfTest()").lua("return InstanceOfTest()").select()) .asObject(); EXPECT_TRUE(engine->isInstanceOf(ins)); auto func = engine ->eval(TS().js( R"( (function() { return function (ins) { return ins instanceof InstanceOfTest; } })(); )") .lua("return function (ins) return ScriptX.isInstanceOf(ins, " "InstanceOfTest) end") .select()) .asFunction(); auto scriptCreatedIsInstance = func.call({}, ins); EXPECT_TRUE(scriptCreatedIsInstance.asBoolean().value()); // native create ins = engine->newNativeClass(); ASSERT_TRUE(engine->isInstanceOf(ins)); auto scriptInstanceOfFunc = func.call({}, ins); EXPECT_TRUE(scriptInstanceOfFunc.asBoolean().value()); EXPECT_FALSE(func.call({}, Object::newObject()).asBoolean().value()); EXPECT_FALSE(func.call({}, Number::newNumber(0)).asBoolean().value()); } catch (const Exception& e) { FAIL() << e; } } TEST_F(NativeTest, TypedAPIClassDefineTest) { EngineScope engineScope(engine); engine->registerNativeClass(instanceOfTestDefine); auto ins = engine->newNativeClass(); EXPECT_TRUE(engine->isInstanceOf(ins)); EXPECT_TRUE(engine->getNativeInstance(ins) != nullptr); EXPECT_THROW({ engine->newNativeClass(); }, Exception); EXPECT_THROW({ engine->isInstanceOf(ins); }, Exception); EXPECT_THROW({ engine->getNativeInstance(ins); }, Exception); } TEST_F(NativeTest, MissMatchedType) { class Instance : public ScriptClass { public: using ScriptClass::ScriptClass; std::string name; static int getAge() { return 0; } static void setAge(int) {} void fun(int) {} static void sfun(int) {} }; EngineScope scope(engine); auto def = defineClass("Instance") .constructor() .function("sfun", &Instance::sfun) .instanceFunction("fun", &Instance::fun) .build(); engine->registerNativeClass(def); auto sfun = engine->eval(TS().js("Instance.sfun;").lua("return Instance.sfun").select()).asFunction(); auto ins = engine->newNativeClass(); auto fun = ins.get("fun").asFunction(); #ifdef SCRIPTX_NO_EXCEPTION_ON_BIND_FUNCTION ASSERT_TRUE(sfun.call().isNull()); ASSERT_TRUE(sfun.call({}, "empty").isNull()); ASSERT_TRUE(sfun.call({}, 0, 0).isNull()); ASSERT_TRUE(fun.call().isNull()); ASSERT_TRUE(fun.call(ins).isNull()); ASSERT_TRUE(fun.call(ins, "empty").isNull()); ASSERT_TRUE(fun.call(ins, 0, 0).isNull()); #else EXPECT_THROW({ sfun.call(); }, Exception); EXPECT_THROW({ sfun.call({}, "empty"); }, Exception); EXPECT_THROW({ sfun.call({}, 0, 0); }, Exception); EXPECT_THROW({ fun.call(); }, Exception); EXPECT_THROW({ fun.call(ins); }, Exception); EXPECT_THROW({ fun.call(ins, "empty"); }, Exception); EXPECT_THROW({ fun.call(ins, 0, 0); }, Exception); #endif } namespace { static const bool gender = true; class Class : public ScriptClass { std::string name = "Native"; public: int age = 0; explicit Class(const Local& thiz) : ScriptClass(thiz) {} static Local hello0(const Arguments& args) { if (args.size() == 1 && args[0].isString()) { return String::newString(std::string("hello ") + args[0].asString().toString()); } return {}; } static std::string hello(std::string_view who) { return std::string("hello ") + std::string(who); } Local helloMe0(const Arguments& args) { if (args.size() == 1 && args[0].isString()) { return String::newString(name + " hello " + args[0].asString().toString()); } return {}; } std::string helloMe(std::string_view who) { return name + std::string(" hello ") + std::string(who); } static Local getName0() { return String::newString("bala bala bala"); } static std::string getName00() { return "bala bala bala"; } static void setName0(const Local& value) { if (value.isString()) { /*xxx = */ value.asString().toString(); } } static void setName00(const std::string& value) {} std::string getName() { return name; } void setName(std::string name) { this->name = std::move(name); } }; static ClassDefine def = defineClass("BindInstanceFunc") .nameSpace("test") .constructor() .function("hello0", &Class::hello0) .function("hello", &Class::hello, false) .property("name0", &Class::getName0, &Class::setName0) .property("name00", &Class::getName00, &Class::setName0) .property("name01", &Class::getName00, &Class::setName00) .property("name02", &Class::getName0, &Class::setName00) .property("gender", &gender) .instanceFunction("helloMe0", &Class::helloMe0) .instanceFunction("helloMe", &Class::helloMe) .instanceProperty("name", &Class::getName, &Class::setName) .instanceProperty("age", &Class::age) .build(); // these are all the supported types static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); #ifdef __cpp_lib_char8_t static_assert(converter::isConvertible); static_assert(converter::isConvertible); static_assert(converter::isConvertible); #endif static_assert(converter::isConvertible>); static_assert(converter::isConvertible>); static_assert(converter::isConvertible>); static_assert(converter::isConvertible>); static_assert(converter::isConvertible>); static_assert(converter::isConvertible>); TEST_F(NativeTest, ClassDefineBuilder) { EngineScope engineScope(engine); engine->registerNativeClass(def); auto ret1 = engine->eval(TS().js("test.BindInstanceFunc.hello('js');") .lua("return test.BindInstanceFunc.hello('js');") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "hello js"); EXPECT_THROW({ engine->eval("test.BindInstanceFunc.hello(0);"); }, Exception); ret1 = engine->eval(TS().js("test.BindInstanceFunc.hello0('js');") .lua("return test.BindInstanceFunc.hello0('js');") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "hello js"); ret1 = engine->eval( TS().js("test.BindInstanceFunc.name0;").lua("return test.BindInstanceFunc.name0;").select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "bala bala bala"); ret1 = engine->eval(TS().js("test.BindInstanceFunc.gender;") .lua("return test.BindInstanceFunc.gender;") .select()); ASSERT_TRUE(ret1.isBoolean()); ASSERT_EQ(ret1.asBoolean().value(), true); ret1 = engine->eval(TS().js("new test.BindInstanceFunc().helloMe0(\"js\");") .lua("return test.BindInstanceFunc():helloMe0(\"js\");") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "Native hello js"); ret1 = engine->eval(TS().js("new test.BindInstanceFunc().helloMe('js');") .lua("return test.BindInstanceFunc():helloMe('js');") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "Native hello js"); ret1 = engine->eval(TS().js("new test.BindInstanceFunc().name;") .lua("return test.BindInstanceFunc().name;") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "Native"); ret1 = engine->eval(TS().js(R"""( var i = new test.BindInstanceFunc(); i.name = "What"; i.name; )""") .lua(R"""( local i = test.BindInstanceFunc(); i.name = "What"; return i.name; )""") .select()); ASSERT_TRUE(ret1.isString()); ASSERT_EQ(ret1.asString().toString(), "What"); ret1 = engine->eval(TS().js("new test.BindInstanceFunc().age;") .lua("return test.BindInstanceFunc().age;") .select()); ASSERT_TRUE(ret1.isNumber()); ASSERT_EQ(ret1.asNumber().toInt32(), 0); } } // namespace namespace { 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; } }; } // namespace TEST_F(NativeTest, NativeFounction) { EngineScope scope(engine); auto func = Function::newFunction([](int) {}); func.call({}, 0); EXPECT_THROW({ func.call(); }, Exception); EXPECT_THROW({ func.call({}, 1, 2); }, Exception); EXPECT_THROW({ func.call({}, ""); }, Exception); class Instance : public ScriptClass { public: using ScriptClass::ScriptClass; }; auto def = defineClass("Instance") .constructor() .instanceFunction("f", [](Instance*, int) {}) .build(); engine->registerNativeClass(def); auto ins = engine->newNativeClass(); auto insFunc = ins.get("f"); func.call({}, 0); EXPECT_THROW({ func.call(ins); }, Exception); EXPECT_THROW({ func.call(ins, 1, 2); }, Exception); EXPECT_THROW({ func.call(ins, ""); }, Exception); } TEST_F(NativeTest, OverloadedFunction) { EngineScope scope(engine); auto func1 = [](int) { return 1; }; auto func2 = [](const std::string&) { return 2; }; auto func3 = [](int, int) { return 3; }; auto overloaded = script::adaptOverLoadedFunction(func1, func2, func3); auto fun = Function::newFunction(overloaded); auto ret = fun.call({}, 0); EXPECT_EQ(ret.asNumber().toInt32(), 1); ret = fun.call({}, 3.14); EXPECT_EQ(ret.asNumber().toInt32(), 1); ret = fun.call({}, "hello"); EXPECT_EQ(ret.asNumber().toInt32(), 2); ret = fun.call({}, 1, 2); EXPECT_EQ(ret.asNumber().toInt32(), 3); EXPECT_THROW({ fun.call({}, false); }, Exception); } 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)); } TEST_F(NativeTest, FunctionWrapper) { std::function add; { EngineScope scope(engine); auto func = engine ->eval(TS().js("(function (ia, ib) { return ia + ib;})") .lua("return function (ia, ib) return ia + ib end") .select()) .asFunction(); auto f = func.wrapper(); EXPECT_EQ(f(1, 2), 3); add = std::move(f); func.wrapper(); auto wrongRetType = func.wrapper(); EXPECT_THROW({ wrongRetType(1, 2); }, Exception); auto wrongParamType = func.wrapper(); EXPECT_THROW({ wrongParamType("hello", 2); }, Exception); } EXPECT_EQ(add(1, 1), 2) << "Out of EngineScope test"; } TEST_F(NativeTest, FunctionWrapperReceiver) { EngineScope scope(engine); try { auto func = engine ->eval( TS().js("(function () { if (this && this.num) return this.num; else return -1 ;})") .lua("return function (self) if self ~= nil then return self.num else return " "-1 end end") .select()) .asFunction(); auto receiver = engine->eval(TS().js("({ num: 42})").lua("num = {}; num.num = 42; return num;").select()) .asObject(); auto withReceiver = func.wrapper(receiver); EXPECT_EQ(withReceiver(), 42); auto noReceiver = func.wrapper(); EXPECT_EQ(noReceiver(), -1); } catch (const Exception& e) { FAIL() << e; } } TEST_F(NativeTest, ValidateClassDefine) { // static & instance are empty EXPECT_THROW({ defineClass("hello").build(); }, std::runtime_error); { EngineScope scope(engine); EXPECT_THROW({ defineClass("hello").build(); }, Exception); } // empty func name EXPECT_THROW({ defineClass("hello").function("", []() {}).build(); }, std::runtime_error); // empty prop name EXPECT_THROW({ defineClass("hello").property("", &version).build(); }, std::runtime_error); // empty func func EXPECT_THROW( { defineClass("hello").function("x", std::function(const Arguments&)>()).build(); }, std::runtime_error); EXPECT_TRUE(std::function() == nullptr); // empty prop func EXPECT_THROW({ defineClass("hello").property("x", std::function()>()).build(); }, std::runtime_error); // no ctor EXPECT_THROW( { defineClass("hello") .instanceFunction("x", &InternalStorageTest::getVal) .build(); }, std::runtime_error); // empty func name EXPECT_THROW( { defineClass("hello") .constructor() .instanceFunction("", &InternalStorageTest::getVal) .build(); }, std::runtime_error); // empty prop name EXPECT_THROW( { defineClass("hello").constructor().instanceProperty("", &TestClass::src).build(); }, std::runtime_error); // empty func func EXPECT_THROW( { defineClass("hello") .constructor() .instanceFunction("x", std::function(const TestClass*, const Arguments&)>()) .build(); }, std::runtime_error); // empty prop func EXPECT_THROW( { defineClass("hello") .constructor() .instanceProperty("x", std::function(TestClass*)>()) .build(); }, std::runtime_error); } TEST_F(NativeTest, ClassDefineVisitor) { static std::ostringstream out; class V : public ClassDefineVisitor { public: void beginClassDefine(const std::string& name, const std::string& nameSpace) override {} void endClassDefine() override {} void visitConstructor(const std::type_info& type) override { out << "constructor: " << std::endl << type.name() << std::endl << std::endl; } void visitStaticProperty(const std::string& name, const std::type_info& getter, const std::type_info& setter) override { out << "prop: " << std::endl << name << std::endl << getter.name() << std::endl << setter.name() << std::endl; } void visitStaticFunction(const std::string& name, const std::type_info& callback) override { out << "function: " << std::endl << name << std::endl << callback.name() << std::endl; } void visitInstanceProperty(const std::string& name, const std::type_info& getter, const std::type_info& setter) override { out << "insProp: " << std::endl << name << std::endl << getter.name() << std::endl << setter.name() << std::endl; } void visitInstanceFunction(const std::string& name, const std::type_info& callback) override { out << "insFunction: " << std::endl << name << std::endl << callback.name() << std::endl; } }; #ifdef __cpp_rtti V v; TestClassDefAll.visit(v); EXPECT_FALSE(out.str().empty()); TestClassDefAll.getNativeRegister().visit(v); #endif } #ifdef SCRIPTX_BACKEND_WEBASSEMBLY class WasmScriptClassDestroyScriptClass : public ScriptClass { public: static bool destroyed_; using ScriptClass::ScriptClass; ~WasmScriptClassDestroyScriptClass() override { destroyed_ = true; } }; bool WasmScriptClassDestroyScriptClass::destroyed_ = false; static ClassDefine wasmScriptClassDestroyScriptClassDefine = defineClass("WasmScriptClassDestroyScriptClass") .constructor() .build(); TEST_F(NativeTest, WasmScriptClassDestroyTest) { EngineScope scope(engine); engine->registerNativeClass(wasmScriptClassDestroyScriptClassDefine); WasmScriptClassDestroyScriptClass::destroyed_ = false; auto ins = engine->newNativeClass(); wasm_interop::destroyScriptClass(ins); EXPECT_TRUE(WasmScriptClassDestroyScriptClass::destroyed_); WasmScriptClassDestroyScriptClass::destroyed_ = false; engine->eval(R"( const ins = new WasmScriptClassDestroyScriptClass(); ScriptX.destroyScriptClass(ins); )"); EXPECT_TRUE(WasmScriptClassDestroyScriptClass::destroyed_); } #endif } // namespace script::test