Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24405) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24406) @@ -1,1018 +1,1048 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx) { m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_formerRealm); } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ JS::RootedValue ret(cx); if (!JS_StructuredClone(rq.cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() != 1 || !args.get(0).isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return false; } ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); args.rval().set(args.get(0)); return true; } bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); args.rval().setUndefined(); return true; } bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileAttribute)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } g_Profiler2.RecordAttribute("%s", name); args.rval().setUndefined(); return true; } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRealm autoRealm(m_cx, m_glob); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(new ScriptInterface_impl(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; if (JS_CallFunctionName(rq.cx, obj, name, argv, ret)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS::NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_DefineProperty(rq.cx, object, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); return JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs); } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { return GetProperty_(obj, name, out); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); if (!GetProperty_(obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { return GetPropertyInt_(obj, name, out); } bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetProperty(rq.cx, object, name, out); } bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetPropertyById(rq.cx, object, nameId, out); } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } +bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) +{ + // Try to get the object as a property of the global object. + JS::RootedObject global(rq.cx, rq.glob); + if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) + { + out.set(JS::NullHandleValue); + return false; + } + + if (!out.isNullOrUndefined()) + return true; + + // Some objects, such as const definitions, or Class definitions, are hidden inside closures. + // We must fetch those from the correct lexical scope. + //JS::RootedValue glob(cx); + JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); + if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) + { + out.set(JS::NullHandleValue); + return false; + } + + if (!out.isNullOrUndefined()) + return true; + + out.set(JS::NullHandleValue); + return false; +} + bool ScriptInterface::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { ScriptRequest rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { ScriptRequest rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); options.setFileAndLine(filenameStr.c_str(), 1); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { ScriptRequest rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; ScriptException::CatchPending(rq); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { ScriptRequest rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { ScriptException::CatchPending(rq); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { ScriptRequest rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); if (JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) return str.stream.str(); // Drop exceptions raised by cyclic values before trying something else JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); ScriptRequest rq(this); JS::RootedValue out(rq.cx); ScriptInterface::StructuredClone structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { ScriptRequest rq(this); ScriptInterface::StructuredClone ret(new JSStructuredCloneData(JS::StructuredCloneScope::SameProcess)); JS::CloneDataPolicy policy; if (!JS_WriteStructuredClone(rq.cx, v, ret.get(), JS::StructuredCloneScope::SameProcess, policy, nullptr, nullptr, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); ScriptException::CatchPending(rq); return ScriptInterface::StructuredClone(); } return ret; } void ScriptInterface::ReadStructuredClone(const ScriptInterface::StructuredClone& ptr, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::CloneDataPolicy policy; if (!JS_ReadStructuredClone(rq.cx, *ptr, JS_STRUCTURED_CLONE_VERSION, ptr->scope(), ret, policy, nullptr, nullptr)) ScriptException::CatchPending(rq); } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24405) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24406) @@ -1,610 +1,619 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "lib/file/vfs/vfs_path.h" #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); ERROR_TYPE(Scripting, RegisterFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class JSStructuredCloneData; class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } /** * RAII structure which encapsulates an access to the context and compartment of a ScriptInterface. * This struct provides: * - a pointer to the context, while acting like JSAutoRequest * - a pointer to the global object of the compartment, while acting like JSAutoRealm * * This way, getting and using those pointers is safe with respect to the GC * and to the separation of compartments. */ class ScriptRequest { public: ScriptRequest() = delete; ScriptRequest(const ScriptRequest& rq) = delete; ScriptRequest& operator=(const ScriptRequest& rq) = delete; ScriptRequest(const ScriptInterface& scriptInterface); ScriptRequest(const ScriptInterface* scriptInterface) : ScriptRequest(*scriptInterface) {} ScriptRequest(shared_ptr scriptInterface) : ScriptRequest(*scriptInterface) {} ~ScriptRequest(); JS::Value globalValue() const; JSContext* cx; JSObject* glob; private: JS::Realm* m_formerRealm; }; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; public: /** * Constructor. * @param nativeScopeName Name of global object that functions (via RegisterFunction) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context); ~ScriptInterface(); struct CmptPrivate { ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions } m_CmptPrivate; void SetCallbackData(void* pCBData); static CmptPrivate* GetScriptInterfaceAndCBData(JSContext* cx); /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; shared_ptr GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number geenrator with a seeded, network-sync'd one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Sets the given value to a new plain JS::Object, converts the arguments to JS::Values and sets them as properties. * This is static so that callers like ToJSVal can use it with the JSContext directly instead of having to obtain the instance using GetScriptInterfaceAndCBData. * Can throw an exception. */ template static bool CreateObject(const ScriptRequest& rq, JS::MutableHandleValue objectValue, Args const&... args) { JS::RootedObject obj(rq.cx); if (!CreateObject_(rq, &obj, args...)) return false; objectValue.setObject(*obj); return true; } /** * Sets the given value to a new JS object or Null Value in case of out-of-memory. */ static void CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length = 0); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the integer-named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant = false, bool enumerate = true) const; /** * Get the named property on the given object. */ template bool GetProperty(JS::HandleValue obj, const char* name, T& out) const; /** * Get the named property of the given object. */ bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const; /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const; /** * Get the named property of the given object. */ bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const; /** * Check the named property has been defined on the given object. */ bool HasProperty(JS::HandleValue obj, const char* name) const; /** + * Get an object from the global scope or any lexical scope. + * This can return globally accessible objects even if they are not properties + * of the global object (e.g. ES6 class definitions). + * @param name - Name of the property. + * @param out The object or null. + */ + static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); + + /** * Returns all properties of the object, both own properties and inherited. * This is essentially equivalent to calling Object.getOwnPropertyNames() * and recursing up the prototype chain. * NB: this does not return properties with symbol or numeric keys, as that would * require a variant in the vector, and it's not useful for now. * @param enumerableOnly - only return enumerable properties. */ bool EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const; bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); bool FreezeObject(JS::HandleValue objVal, bool deep) const; /** * Convert an object to a UTF-8 encoded string, either with JSON * (if pretty == true and there is no JSON error) or with toSource(). * * We have to use a mutable handle because JS_Stringify requires that for unknown reasons. */ std::string ToString(JS::MutableHandleValue obj, bool pretty = false) const; /** * Parse a UTF-8-encoded JSON string. Returns the unmodified value on error * and prints an error message. * @return true on success; false otherwise */ bool ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const; /** * Read a JSON file. Returns the unmodified value on error and prints an error message. */ void ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const; /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ std::string StringifyJSON(JS::MutableHandleValue obj, bool indent = true) const; /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** * Convert a JS::Value to a C++ type. (This might trigger GC.) */ template static bool FromJSVal(const ScriptRequest& rq, const JS::HandleValue val, T& ret); /** * Convert a C++ type to a JS::Value. (This might trigger GC. The return * value must be rooted if you don't want it to be collected.) * NOTE: We are passing the JS::Value by reference instead of returning it by value. * The reason is a memory corruption problem that appears to be caused by a bug in Visual Studio. * Details here: http://www.wildfiregames.com/forum/index.php?showtopic=17289&p=285921 */ template static void ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, T const& val); /** * Convert a named property of an object to a C++ type. */ template static bool FromJSProperty(const ScriptRequest& rq, const JS::HandleValue val, const char* name, T& ret, bool strict = false); /** * MathRandom (this function) calls the random number generator assigned to this ScriptInterface instance and * returns the generated number. * Math_random (with underscore, not this function) is a global function, but different random number generators can be * stored per ScriptInterface. It calls MathRandom of the current ScriptInterface instance. */ bool MathRandom(double& nbr); /** * Structured clones are a way to serialize 'simple' JS::Values into a buffer * that can safely be passed between compartments and between threads. * A StructuredClone can be stored and read multiple times if desired. * We wrap them in shared_ptr so memory management is automatic and * thread-safe. */ using StructuredClone = shared_ptr; StructuredClone WriteStructuredClone(JS::HandleValue v) const; void ReadStructuredClone(const StructuredClone& ptr, JS::MutableHandleValue ret) const; /** * Construct a new value (usable in this ScriptInterface's compartment) by cloning * a value from a different compartment. * Complex values (functions, XML, etc) won't be cloned correctly, but basic * types and cyclic references should be fine. */ JS::Value CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const; /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Converts |a| if needed and assigns it to |handle|. * This is meant for use in other templates where we want to use the same code for JS::RootedValue&/JS::HandleValue and * other types. Note that functions are meant to take JS::HandleValue instead of JS::RootedValue&, but this implicit * conversion does not work for templates (exact type matches required for type deduction). * A similar functionality could also be implemented as a ToJSVal specialization. The current approach was preferred * because "conversions" from JS::HandleValue to JS::MutableHandleValue are unusual and should not happen "by accident". */ template static void AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a); /** * The same as AssignOrToJSVal, but also allows JS::Value for T. * In most cases it's not safe to use the plain (unrooted) JS::Value type, but this can happen quite * easily with template functions. The idea is that the linker prints an error if AssignOrToJSVal is * used with JS::Value. If the specialization for JS::Value should be allowed, you can use this * "unrooted" version of AssignOrToJSVal. */ template static void AssignOrToJSValUnrooted(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { AssignOrToJSVal(rq, handle, a); } /** * Converts |val| to T if needed or just returns it if it's a handle. * This is meant for use in other templates where we want to use the same code for JS::HandleValue and * other types. */ template static T AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret); private: static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj); template static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj, const char* propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, propertyValue); return CreateObject_(rq, obj, args...) && JS_DefineProperty(rq.cx, obj, propertyName, val, JSPROP_ENUMERATE); } bool CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const; bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const; bool GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue value) const; struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; void Register(const char* name, JSNative fptr, size_t nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; boost::random::rand48* m_rng; std::map m_CustomObjectTypes; // The nasty macro/template bits are split into a separate file so you don't have to look at them public: #include "NativeWrapperDecls.h" // This declares: // // template // void RegisterFunction(const char* functionName) const; // // template // static JSNative call; // // template // static JSNative callMethod; // // template // static JSNative callMethodConst; // // template // static size_t nargs(); // // template // bool CallFunction(JS::HandleValue val, const char* name, R& ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const T0&...) const; // // template // bool CallFunctionVoid(JS::HandleValue val, const char* name, const T0&...) const; }; // Implement those declared functions #include "NativeWrapperDefns.h" template inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { ToJSVal(rq, handle, a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::PersistentRootedValue& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal >(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Heap& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::RootedValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::HandleValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSValUnrooted(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Value& a) { handle.set(a); } template inline T ScriptInterface::AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret) { T retVal; ret = FromJSVal(rq, val, retVal); return retVal; } template<> inline JS::HandleValue ScriptInterface::AssignOrFromJSVal(const ScriptRequest& UNUSED(rq), const JS::HandleValue& val, bool& ret) { ret = true; return val; } template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetPropertyInt_(obj, name, val, constant, enumerate); } template bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); if (!GetProperty_(obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); if (!GetPropertyInt_(obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 24405) +++ ps/trunk/source/simulation2/scripting/EngineScriptConversions.cpp (revision 24406) @@ -1,333 +1,333 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "scriptinterface/ScriptConversions.h" #include "graphics/Color.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "ps/CLogger.h" #include "ps/Shapes.h" #include "ps/utf16string.h" #include "simulation2/helpers/CinemaPath.h" #include "simulation2/helpers/Grid.h" #include "simulation2/system/IComponent.h" #include "simulation2/system/ParamNode.h" #define FAIL(msg) STMT(LOGERROR(msg); return false) #define FAIL_VOID(msg) STMT(ScriptException::Raise(rq, msg); return) template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, IComponent* const& val) { if (val == NULL) { ret.setNull(); return; } // If this is a scripted component, just return the JS object directly JS::RootedValue instance(rq.cx, val->GetJSInstance()); if (!instance.isNull()) { ret.set(instance); return; } // Otherwise we need to construct a wrapper object // (TODO: cache wrapper objects?) JS::RootedObject obj(rq.cx); if (!val->NewJSObject(*ScriptInterface::GetScriptInterfaceAndCBData(rq.cx)->pScriptInterface, &obj)) { // Report as an error, since scripts really shouldn't try to use unscriptable interfaces LOGERROR("IComponent does not have a scriptable interface"); ret.setUndefined(); return; } JS_SetPrivate(obj, static_cast(val)); ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, CParamNode const& val) { val.ToJSVal(rq, true, ret); // Prevent modifications to the object, so that it's safe to share between // components and to reconstruct on deserialization if (ret.isObject()) { JS::RootedObject obj(rq.cx, &ret.toObject()); JS_DeepFreezeObject(rq.cx, obj); } } template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CParamNode* const& val) { if (val) ToJSVal(rq, ret, *val); else ret.setUndefined(); } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CColor& out) { if (!v.isObject()) FAIL("CColor has to be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue r(rq.cx); JS::RootedValue g(rq.cx); JS::RootedValue b(rq.cx); JS::RootedValue a(rq.cx); if (!JS_GetProperty(rq.cx, obj, "r", &r) || !FromJSVal(rq, r, out.r)) FAIL("Failed to get property CColor.r"); if (!JS_GetProperty(rq.cx, obj, "g", &g) || !FromJSVal(rq, g, out.g)) FAIL("Failed to get property CColor.g"); if (!JS_GetProperty(rq.cx, obj, "b", &b) || !FromJSVal(rq, b, out.b)) FAIL("Failed to get property CColor.b"); if (!JS_GetProperty(rq.cx, obj, "a", &a) || !FromJSVal(rq, a, out.a)) FAIL("Failed to get property CColor.a"); return true; } template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, CColor const& val) { CreateObject( rq, ret, "r", val.r, "g", val.g, "b", val.b, "a", val.a); } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, fixed& out) { double ret; if (!JS::ToNumber(rq.cx, v, &ret)) return false; out = fixed::FromDouble(ret); // double can precisely represent the full range of fixed, so this is a non-lossy conversion return true; } template<> void ScriptInterface::ToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue ret, const fixed& val) { ret.set(JS::NumberValue(val.ToDouble())); } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CFixedVector3D& out) { if (!v.isObject()) return false; // TODO: report type error JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue p(rq.cx); if (!JS_GetProperty(rq.cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(rq, p, out.X)) return false; if (!JS_GetProperty(rq.cx, obj, "y", &p)) return false; if (!FromJSVal(rq, p, out.Y)) return false; if (!JS_GetProperty(rq.cx, obj, "z", &p)) return false; if (!FromJSVal(rq, p, out.Z)) return false; return true; } template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CFixedVector3D& val) { JS::RootedObject global(rq.cx, rq.glob); JS::RootedValue valueVector3D(rq.cx); - if (!JS_GetProperty(rq.cx, global, "Vector3D", &valueVector3D)) + if (!ScriptInterface::GetGlobalProperty(rq, "Vector3D", &valueVector3D)) FAIL_VOID("Failed to get Vector3D constructor"); JS::RootedValueArray<3> args(rq.cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); args[2].setNumber(val.Z.ToDouble()); JS::RootedObject objVec(rq.cx); if (!JS::Construct(rq.cx, valueVector3D, args, &objVec)) FAIL_VOID("Failed to construct Vector3D object"); ret.setObject(*objVec); } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CFixedVector2D& out) { if (!v.isObject()) return false; // TODO: report type error JS::RootedObject obj(rq.cx, &v.toObject()); JS::RootedValue p(rq.cx); if (!JS_GetProperty(rq.cx, obj, "x", &p)) return false; // TODO: report type errors if (!FromJSVal(rq, p, out.X)) return false; if (!JS_GetProperty(rq.cx, obj, "y", &p)) return false; if (!FromJSVal(rq, p, out.Y)) return false; return true; } template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CFixedVector2D& val) { JS::RootedObject global(rq.cx, rq.glob); JS::RootedValue valueVector2D(rq.cx); - if (!JS_GetProperty(rq.cx, global, "Vector2D", &valueVector2D)) + if (!ScriptInterface::GetGlobalProperty(rq, "Vector2D", &valueVector2D)) FAIL_VOID("Failed to get Vector2D constructor"); JS::RootedValueArray<2> args(rq.cx); args[0].setNumber(val.X.ToDouble()); args[1].setNumber(val.Y.ToDouble()); JS::RootedObject objVec(rq.cx); if (!JS::Construct(rq.cx, valueVector2D, args, &objVec)) FAIL_VOID("Failed to construct Vector2D object"); ret.setObject(*objVec); } template<> void ScriptInterface::ToJSVal >(const ScriptRequest& rq, JS::MutableHandleValue ret, const Grid& val) { u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u8)); JS::RootedObject objArr(rq.cx, JS_NewUint8Array(rq.cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint8ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(rq.cx, JS::ObjectValue(*objArr)); CreateObject( rq, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> void ScriptInterface::ToJSVal >(const ScriptRequest& rq, JS::MutableHandleValue ret, const Grid& val) { u32 length = (u32)(val.m_W * val.m_H); u32 nbytes = (u32)(length * sizeof(u16)); JS::RootedObject objArr(rq.cx, JS_NewUint16Array(rq.cx, length)); // Copy the array data and then remove the no-GC check to allow further changes to the JS data { JS::AutoCheckCannotGC nogc; bool sharedMemory; memcpy((void*)JS_GetUint16ArrayData(objArr, &sharedMemory, nogc), val.m_Data, nbytes); } JS::RootedValue data(rq.cx, JS::ObjectValue(*objArr)); CreateObject( rq, ret, "width", val.m_W, "height", val.m_H, "data", data); } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, TNSpline& out) { if (!v.isObject()) FAIL("Argument must be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); bool isArray; if (!JS::IsArrayObject(rq.cx, obj, &isArray) || !isArray) FAIL("Argument must be an array"); u32 numberOfNodes = 0; if (!JS::GetArrayLength(rq.cx, obj, &numberOfNodes)) FAIL("Failed to get array length"); for (u32 i = 0; i < numberOfNodes; ++i) { JS::RootedValue node(rq.cx); if (!JS_GetElement(rq.cx, obj, i, &node)) FAIL("Failed to read array element"); fixed deltaTime; if (!FromJSProperty(rq, node, "deltaTime", deltaTime)) FAIL("Failed to read Spline.deltaTime property"); CFixedVector3D position; if (!FromJSProperty(rq, node, "position", position)) FAIL("Failed to read Spline.position property"); out.AddNode(position, CFixedVector3D(), deltaTime); } if (out.GetAllNodes().empty()) FAIL("Spline must contain at least one node"); return true; } template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, CCinemaPath& out) { if (!v.isObject()) FAIL("Argument must be an object"); JS::RootedObject obj(rq.cx, &v.toObject()); CCinemaData pathData; TNSpline positionSpline, targetSpline; if (!FromJSProperty(rq, v, "name", pathData.m_Name)) FAIL("Failed to get CCinemaPath.name property"); if (!FromJSProperty(rq, v, "orientation", pathData.m_Orientation)) FAIL("Failed to get CCinemaPath.orientation property"); if (!FromJSProperty(rq, v, "positionNodes", positionSpline)) FAIL("Failed to get CCinemaPath.positionNodes property"); if (pathData.m_Orientation == L"target" && !FromJSProperty(rq, v, "targetNodes", targetSpline)) FAIL("Failed to get CCinemaPath.targetNodes property"); // Other properties are not necessary to be defined if (!FromJSProperty(rq, v, "timescale", pathData.m_Timescale)) pathData.m_Timescale = fixed::FromInt(1); if (!FromJSProperty(rq, v, "mode", pathData.m_Mode)) pathData.m_Mode = L"ease_inout"; if (!FromJSProperty(rq, v, "style", pathData.m_Style)) pathData.m_Style = L"default"; out = CCinemaPath(pathData, positionSpline, targetSpline); return true; } // define vectors JSVAL_VECTOR(CFixedVector2D) #undef FAIL #undef FAIL_VOID