GUIProxy: Support property introspection for debugging

This commit enables proper property enumeration and inspection for GUI
proxy objects in debugging sessions using the SpiderMonkey Debugger API.

Interface (IGUIProxyObject):
- Added a pure virtual method getPropsNames() to expose cached property
  names from the GUI object implementation.

Proxy handler (JSI_GUIProxy):
- Implemented ownPropertyKeys() to enumerate all visible properties of
  the proxy, including: -- Built-in GUI fields: "name", "parent",
  "children". -- Dynamic settings stored in m_Settings. -- Script event
  handlers prefixed with "on" from m_ScriptHandlers. -- Function
  properties returned by getPropsNames().
- Implemented getOwnPropertyDescriptor() to synthesize descriptors for
  debugger queries: -- Returns undefined if the property is not defined.
  -- Returns a read-only enumerable descriptor otherwise.
- Both methods are marked final and override SpiderMonkey's
  BaseProxyHandler.

Why:
- SpiderMonkey’s Debugger API requires ownPropertyKeys and
  getOwnPropertyDescriptor for proxy objects to be introspectable in dev
  tools like VS Code.
- Without these, properties of GUI objects are hidden during debugging.
- This change improves the developer experience by making all meaningful
  GUI object fields visible and explorable at runtime.
This commit is contained in:
trompetin17
2025-06-25 08:05:32 -05:00
parent 20b7c3f9b8
commit 0f156e3544
2 changed files with 96 additions and 12 deletions
+6 -12
View File
@@ -101,6 +101,7 @@ public:
// @return the JSFunction matching @param name. Must call has() first as it can assume existence.
virtual JSObject* get(const std::string& name) const = 0;
virtual bool setFunction(const ScriptRequest& rq, const std::string& name, JSFunction* function) = 0;
virtual std::vector<std::string_view> getPropsNames() const = 0;
};
/**
@@ -177,24 +178,17 @@ protected:
// The following methods are not provided by BaseProxyHandler.
// We provide defaults that do nothing (some raise JS exceptions).
// The JS code will see undefined when querying a property descriptor.
virtual bool getOwnPropertyDescriptor(JSContext*, JS::HandleObject /*proxy*/, JS::HandleId,
JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>>) const override
{
return true;
}
virtual bool getOwnPropertyDescriptor(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> desc) const override final;
// Throw an exception is JS code attempts defining a property.
virtual bool defineProperty(JSContext*, JS::HandleObject /*proxy*/, JS::HandleId,
JS::Handle<JS::PropertyDescriptor>, JS::ObjectOpResult& /*result*/) const override
{
return false;
}
// No accessible properties.
virtual bool ownPropertyKeys(JSContext*, JS::HandleObject /*proxy*/,
JS::MutableHandleIdVector) const override
{
return true;
}
virtual bool ownPropertyKeys(JSContext* cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const override final;
// Nothing to enumerate.
virtual bool enumerate(JSContext*, JS::HandleObject /*proxy*/,
JS::MutableHandleIdVector /*props*/) const override
@@ -104,6 +104,17 @@ public:
return true;
}
virtual std::vector<std::string_view> getPropsNames() const override
{
std::vector<std::string_view> result;
result.reserve(m_Functions.size());
for (const auto& [key, value] : m_Functions)
result.emplace_back(key);
return result;
}
protected:
std::unordered_map<std::string, JS::PersistentRootedObject> m_Functions;
};
@@ -344,4 +355,83 @@ bool JSI_GUIProxy<T>::delete_(JSContext* cx, JS::HandleObject proxy, JS::HandleI
LOGERROR("Only event handlers can be deleted from GUI objects!");
return result.fail(JSMSG_BAD_PROP_ID);
}
template<typename T>
bool JSI_GUIProxy<T>::ownPropertyKeys(JSContext* cx, JS::HandleObject proxy, JS::MutableHandleIdVector props) const
{
ScriptRequest rq(cx);
T* e = IGUIProxyObject::FromPrivateSlot<T>(proxy.get());
if (!e)
return false;
// Add common properties.
static constexpr std::array<const char*, 3> keys = {
"name",
"parent",
"children"
};
for (const char* key : keys)
{
JS::RootedString str(cx, JS_NewStringCopyZ(cx, key));
if (!str)
return false;
JS::RootedId id(cx);
if (!JS_StringToId(cx, str, &id))
return false;
if (!props.append(id))
return false;
}
// Add settings.
for (const auto& [name, setting] : e->m_Settings)
{
JS::RootedString str(cx, JS_NewStringCopyZ(cx, name.c_str()));
JS::RootedId id(cx);
if (!str || !JS_StringToId(cx, str, &id) || !props.append(id))
return false;
}
// Add script handlers.
for (const auto& [name, scriptHandler] : e->m_ScriptHandlers)
{
JS::RootedString str(cx, JS_NewStringCopyZ(cx, fmt::format("on{}", name).c_str()));
JS::RootedId id(cx);
if (!str || !JS_StringToId(cx, str, &id) || !props.append(id))
return false;
}
// Add properties from the cache.
using PropertyCache = typename PropCache::type;
const PropertyCache* data = static_cast<const PropertyCache*>(static_cast<const GUIProxyProps*>(js::GetProxyReservedSlot(proxy, 0).toPrivate()));
for (const auto& key : data->getPropsNames()) {
JS::RootedString str(cx, JS_NewStringCopyZ(cx, key.data()));
JS::RootedId id(cx);
if (!str || !JS_StringToId(cx, str, &id) || !props.append(id))
return false;
}
return true;
}
template<typename T>
bool JSI_GUIProxy<T>::getOwnPropertyDescriptor(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::MutableHandle<mozilla::Maybe<JS::PropertyDescriptor>> desc) const
{
JS::RootedValue value(cx);
if (!this->get(cx, proxy, JS::UndefinedHandleValue, id, &value))
return false;
if (value.isUndefined())
{
desc.set(mozilla::Nothing());
return true;
}
desc.set(mozilla::Some(JS::PropertyDescriptor::Data(value, JSPROP_ENUMERATE | JSPROP_READONLY)));
return true;
}
#endif // INCLUDED_JSI_GUIPROXY_IMP