/* Copyright (C) 2023 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 . */ // This file is included directly into actual implementation files. #include "JSInterface_GUIProxy.h" #include "gui/CGUI.h" #include "gui/CGUISetting.h" #include "gui/ObjectBases/IGUIObject.h" #include "ps/CLogger.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptRequest.h" #include #include template JSI_GUIProxy& JSI_GUIProxy::Singleton() { static JSI_GUIProxy s; return s; } // Call this for every specialised type. You will need to override IGUIObject::CreateJSObject() in your class interface. #define DECLARE_GUIPROXY(Type) \ void Type::CreateJSObject() \ { \ ScriptRequest rq(m_pGUI.GetScriptInterface()); \ using ProxyHandler = JSI_GUIProxy>; \ m_JSObject = ProxyHandler::CreateJSObject(rq, this, GetGUI().GetProxyData(&ProxyHandler::Singleton())); \ } \ template class JSI_GUIProxy; // Use a common namespace to avoid duplicating the symbols un-necessarily. namespace JSInterface_GUIProxy { // All proxy objects share a class definition. JSClass& ClassDefinition() { static JSClass c = PROXY_CLASS_DEF("GUIObjectProxy", JSCLASS_HAS_CACHED_PROTO(JSProto_Proxy) | JSCLASS_HAS_RESERVED_SLOTS(1)); return c; } // Default implementation of the cache via unordered_map class MapCache : public GUIProxyProps { public: virtual ~MapCache() {}; virtual bool has(const std::string& name) const override { return m_Functions.find(name) != m_Functions.end(); } virtual JSObject* get(const std::string& name) const override { return m_Functions.at(name).get(); } virtual bool setFunction(const ScriptRequest& rq, const std::string& name, JSFunction* function) override { m_Functions[name].init(rq.cx, JS_GetFunctionObject(function)); return true; } protected: std::unordered_map m_Functions; }; } template<> IGUIObject* IGUIProxyObject::FromPrivateSlot(JSObject* obj) { if (!obj) return nullptr; if (JS::GetClass(obj) != &JSInterface_GUIProxy::ClassDefinition()) return nullptr; return UnsafeFromPrivateSlot(obj); } // The default propcache is a MapCache. template struct JSI_GUIProxy::PropCache { using type = JSInterface_GUIProxy::MapCache; }; template T* JSI_GUIProxy::FromPrivateSlot(const ScriptRequest&, JS::CallArgs& args) { // Call the unsafe version - this is only ever called from actual proxy objects. return IGUIProxyObject::UnsafeFromPrivateSlot(args.thisv().toObjectOrNull()); } template bool JSI_GUIProxy::PropGetter(JS::HandleObject proxy, const std::string& propName, JS::MutableHandleValue vp) const { using PropertyCache = typename PropCache::type; // Since we know at compile time what the type actually is, avoid the virtual call. const PropertyCache* data = static_cast(static_cast(js::GetProxyReservedSlot(proxy, 0).toPrivate())); if (data->has(propName)) { vp.setObjectOrNull(data->get(propName)); return true; } return false; } template std::pair JSI_GUIProxy::CreateData(ScriptInterface& scriptInterface) { using PropertyCache = typename PropCache::type; PropertyCache* data = new PropertyCache(); ScriptRequest rq(scriptInterface); // Functions common to all children of IGUIObject. JSI_GUIProxy::CreateFunctions(rq, data); // Let derived classes register their own interface. if constexpr (!std::is_same_v) CreateFunctions(rq, data); return { &Singleton(), data }; } template template void JSI_GUIProxy::CreateFunction(const ScriptRequest& rq, GUIProxyProps* cache, const std::string& name) { cache->setFunction(rq, name, ScriptFunction::Create(rq, name.c_str())); } template std::unique_ptr JSI_GUIProxy::CreateJSObject(const ScriptRequest& rq, T* ptr, GUIProxyProps* dataPtr) { js::ProxyOptions options; options.setClass(&JSInterface_GUIProxy::ClassDefinition()); auto ret = std::make_unique(); ret->m_Ptr = static_cast(ptr); JS::RootedValue cppObj(rq.cx), data(rq.cx); cppObj.get().setPrivate(ret->m_Ptr); data.get().setPrivate(static_cast(dataPtr)); ret->m_Object.init(rq.cx, js::NewProxyObject(rq.cx, &Singleton(), cppObj, nullptr, options)); js::SetProxyReservedSlot(ret->m_Object, 0, data); return ret; } template bool JSI_GUIProxy::get(JSContext* cx, JS::HandleObject proxy, JS::HandleValue UNUSED(receiver), JS::HandleId id, JS::MutableHandleValue vp) const { ScriptRequest rq(cx); T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) return false; JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return false; std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return false; // Return function properties. Specializable. if (PropGetter(proxy, propName, vp)) return true; // Use onWhatever to access event handlers if (propName.substr(0, 2) == "on") { CStr eventName(propName.substr(2)); std::map>::iterator it = e->m_ScriptHandlers.find(eventName); if (it == e->m_ScriptHandlers.end()) vp.setNull(); else vp.setObject(*it->second.get()); return true; } if (propName == "parent") { IGUIObject* parent = e->GetParent(); if (parent) vp.set(JS::ObjectValue(*parent->GetJSObject())); else vp.set(JS::NullValue()); return true; } else if (propName == "children") { Script::CreateArray(rq, vp); for (size_t i = 0; i < e->m_Children.size(); ++i) Script::SetPropertyInt(rq, vp, i, e->m_Children[i]); return true; } else if (propName == "name") { Script::ToJSVal(rq, vp, e->GetName()); return true; } else if (e->SettingExists(propName)) { e->m_Settings[propName]->ToJSVal(rq, vp); return true; } LOGERROR("Property '%s' does not exist!", propName.c_str()); return false; } template bool JSI_GUIProxy::set(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::HandleValue vp, JS::HandleValue UNUSED(receiver), JS::ObjectOpResult& result) const { T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) { LOGERROR("C++ GUI Object could not be found"); return result.fail(JSMSG_OBJECT_REQUIRED); } ScriptRequest rq(cx); JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return result.fail(JSMSG_BAD_PROP_ID); std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return result.fail(JSMSG_BAD_PROP_ID); if (propName == "name") { std::string value; if (!Script::FromJSVal(rq, vp, value)) return result.fail(JSMSG_BAD_PROP_ID); e->SetName(value); return result.succeed(); } JS::RootedObject vpObj(cx); if (vp.isObject()) vpObj = &vp.toObject(); // Use onWhatever to set event handlers if (propName.substr(0, 2) == "on") { if (vp.isPrimitive() || vp.isNull() || !JS_ObjectIsFunction(&vp.toObject())) { LOGERROR("on- event-handlers must be functions"); return result.fail(JSMSG_NOT_FUNCTION); } CStr eventName(propName.substr(2)); e->SetScriptHandler(eventName, vpObj); return result.succeed(); } if (e->SettingExists(propName)) return e->m_Settings[propName]->FromJSVal(rq, vp, true) ? result.succeed() : result.fail(JSMSG_USER_DEFINED_ERROR); LOGERROR("Property '%s' does not exist!", propName.c_str()); return result.fail(JSMSG_BAD_PROP_ID); } template bool JSI_GUIProxy::delete_(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult& result) const { T* e = IGUIProxyObject::FromPrivateSlot(proxy.get()); if (!e) { LOGERROR("C++ GUI Object could not be found"); return result.fail(JSMSG_OBJECT_REQUIRED); } ScriptRequest rq(cx); JS::RootedValue idval(rq.cx); if (!JS_IdToValue(rq.cx, id, &idval)) return result.fail(JSMSG_BAD_PROP_ID); std::string propName; if (!Script::FromJSVal(rq, idval, propName)) return result.fail(JSMSG_BAD_PROP_ID); // event handlers if (std::string_view{propName}.substr(0, 2) == "on") { CStr eventName(propName.substr(2)); e->UnsetScriptHandler(eventName); return result.succeed(); } LOGERROR("Only event handlers can be deleted from GUI objects!"); return result.fail(JSMSG_BAD_PROP_ID); }