[gjs: 4/8] object: Remove getProperty/setProperty hooks



commit 729dad6661c904add043006e21def10b40881f49
Author: Philip Chimento <philip chimento gmail com>
Date:   Fri May 25 01:01:28 2018 -0400

    object: Remove getProperty/setProperty hooks
    
    These hooks are going away in SpiderMonkey 60, so we have to do something
    different. Instead of intercepting the getProperty/setProperty
    operations, we lazily define a JS property for each GObject property or
    field accessed in the resolve hook.
    
    This uses the same native-accessor-with-private-data API used in
    gi/boxed.cpp. Two separate sets of accessors are used for properties and
    fields, which makes them a bit simpler than the previous code.
    
    One behaviour change is that we now can't distinguish whether we're
    setting a readonly property in strict mode or not. Now we always throw
    when setting a readonly property; in effect, we treat the property set as
    if it were always in strict mode. ES6 modules are always treated as
    strict mode, so this is a change that client code will have to go through
    anyway when using ES6 modules.
    
    Closes: #160

 gi/boxed.cpp                                     |  71 +----
 gi/object.cpp                                    | 323 +++++++++++++----------
 gi/proxyutils.cpp                                |  19 ++
 gi/proxyutils.h                                  |   8 +
 gjs/jsapi-class.h                                |  11 +
 gjs/jsapi-dynamic-class.cpp                      |  92 +++++++
 installed-tests/js/testEverythingEncapsulated.js |   9 +-
 installed-tests/js/testGIMarshalling.js          |   8 +
 8 files changed, 335 insertions(+), 206 deletions(-)
---
diff --git a/gi/boxed.cpp b/gi/boxed.cpp
index 69b221c7..e63b9aee 100644
--- a/gi/boxed.cpp
+++ b/gi/boxed.cpp
@@ -40,11 +40,6 @@
 
 #include <girepository.h>
 
-/* Reserved slots of JSNative accessor wrappers */
-enum {
-    SLOT_PROP_NAME,
-};
-
 struct Boxed {
     /* prototype info */
     GIBoxedInfo *info;
@@ -579,31 +574,6 @@ get_nested_interface_object(JSContext             *context,
     return true;
 }
 
-static JSObject *
-define_native_accessor_wrapper(JSContext  *cx,
-                               JSNative    call,
-                               unsigned    nargs,
-                               const char *func_name,
-                               uint32_t    id)
-{
-    JSFunction *func = js::NewFunctionWithReserved(cx, call, nargs, 0,
-                                                   func_name);
-    if (!func)
-        return NULL;
-
-    JSObject *func_obj = JS_GetFunctionObject(func);
-    js::SetFunctionNativeReserved(func_obj, SLOT_PROP_NAME,
-                                  JS::PrivateUint32Value(id));
-    return func_obj;
-}
-
-static uint32_t
-native_accessor_slot(JSObject *func_obj)
-{
-    return js::GetFunctionNativeReserved(func_obj, SLOT_PROP_NAME)
-        .toPrivateUint32();
-}
-
 static bool
 boxed_field_getter(JSContext *context,
                    unsigned   argc,
@@ -615,8 +585,9 @@ boxed_field_getter(JSContext *context,
     GArgument arg;
     bool success = false;
 
-    field_info = get_field_info(context, priv,
-                                native_accessor_slot(&args.callee()));
+    uint32_t field_ix = gjs_dynamic_property_private_slot(&args.callee())
+        .toPrivateUint32();
+    field_info = get_field_info(context, priv, field_ix);
     if (!field_info)
         return false;
 
@@ -791,8 +762,9 @@ boxed_field_setter(JSContext *cx,
     GIFieldInfo *field_info;
     bool success = false;
 
-    field_info = get_field_info(cx, priv,
-                                native_accessor_slot(&args.callee()));
+    uint32_t field_ix = gjs_dynamic_property_private_slot(&args.callee())
+        .toPrivateUint32();
+    field_info = get_field_info(cx, priv, field_ix);
     if (!field_info)
         return false;
 
@@ -847,31 +819,14 @@ define_boxed_class_fields(JSContext       *cx,
     for (i = 0; i < n_fields; i++) {
         GIFieldInfo *field = g_struct_info_get_field (priv->info, i);
         const char *field_name = g_base_info_get_name ((GIBaseInfo *)field);
-        GjsAutoChar getter_name = g_strconcat("boxed_field_get::",
-                                              field_name, NULL);
-        GjsAutoChar setter_name = g_strconcat("boxed_field_set::",
-                                              field_name, NULL);
-        g_base_info_unref ((GIBaseInfo *)field);
-
-        /* In order to have one getter and setter for all the properties
-         * we define, we must provide the property index in a "reserved
-         * slot" for which we must unfortunately use the jsfriendapi. */
-        JS::RootedObject getter(cx,
-            define_native_accessor_wrapper(cx, boxed_field_getter, 0,
-                                           getter_name, i));
-        if (!getter)
-            return false;
-
-        JS::RootedObject setter(cx,
-            define_native_accessor_wrapper(cx, boxed_field_setter, 1,
-                                           setter_name, i));
-        if (!setter)
-            return false;
 
-        if (!JS_DefineProperty(cx, proto, field_name, JS::UndefinedHandleValue,
-                               JSPROP_PERMANENT | JSPROP_SHARED | JSPROP_GETTER | JSPROP_SETTER,
-                               JS_DATA_TO_FUNC_PTR(JSNative, getter.get()),
-                               JS_DATA_TO_FUNC_PTR(JSNative, setter.get())))
+        JS::RootedValue private_id(cx, JS::PrivateUint32Value(i));
+        bool ok = gjs_define_property_dynamic(cx, proto, field_name,
+                                              "boxed_field", boxed_field_getter,
+                                              boxed_field_setter, private_id,
+                                              GJS_MODULE_PROP_FLAGS);
+        g_base_info_unref(field);
+        if (!ok)
             return false;
     }
 
diff --git a/gi/object.cpp b/gi/object.cpp
index 37c76c4f..96b5a688 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -319,12 +319,34 @@ object_instance_add_prop(JSContext       *cx,
 }
 
 static bool
-get_prop_from_g_param(JSContext             *context,
-                      JS::HandleObject       obj,
-                      ObjectInstance        *priv,
-                      const char            *name,
-                      JS::MutableHandleValue value_p)
+object_prop_getter(JSContext *cx,
+                   unsigned   argc,
+                   JS::Value *vp)
 {
+    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
+
+    /* FIXME: avoid utf8 conversion on every property access */
+    GjsAutoJSChar name;
+    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
+                            &name))
+        return false;
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Property getter '%s', obj %p priv %p",
+                     name.get(), gjs_debug_object(obj).c_str(), priv);
+
+    if (!priv->gobj)  /* prototype, not an instance. */
+        return true;
+        /* Ignore silently; note that this is different from what we do for
+         * boxed types, for historical reasons */
+
+    if (priv->g_object_finalized) {
+        g_critical("Object %s (%p), has been already finalized. "
+                   "Impossible to get any property from it.",
+                   g_type_name(priv->gtype), priv->gobj);
+        gjs_dumpstack();
+        return true;
+    }
+
     char *gname;
     GParamSpec *param;
     GValue gvalue = { 0, };
@@ -347,14 +369,13 @@ get_prop_from_g_param(JSContext             *context,
     if ((param->flags & G_PARAM_READABLE) == 0)
         return true;
 
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                     "Overriding %s with GObject prop %s",
-                     name, param->name);
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Accessing GObject property %s",
+                     param->name);
 
     g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(param));
     g_object_get_property(priv->gobj, param->name,
                           &gvalue);
-    if (!gjs_value_from_g_value(context, value_p, &gvalue)) {
+    if (!gjs_value_from_g_value(cx, args.rval(), &gvalue)) {
         g_value_unset(&gvalue);
         return false;
     }
@@ -391,12 +412,34 @@ lookup_field_info(GIObjectInfo *info,
 }
 
 static bool
-get_prop_from_field(JSContext             *cx,
-                    JS::HandleObject       obj,
-                    ObjectInstance        *priv,
-                    const char            *name,
-                    JS::MutableHandleValue value_p)
+object_field_getter(JSContext *cx,
+                    unsigned   argc,
+                    JS::Value *vp)
 {
+    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
+
+    /* FIXME: avoid utf8 conversion on every property access */
+    GjsAutoJSChar name;
+    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
+                            &name))
+        return false;
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Field getter '%s', obj %p priv %p",
+                     name.get(), gjs_debug_object(obj).c_str(), priv);
+
+    if (priv->gobj == NULL) /* prototype, not an instance. */
+        return true;
+        /* Ignore silently; note that this is different from what we do for
+         * boxed types, for historical reasons */
+
+    if (priv->g_object_finalized) {
+        g_critical("Object %s (%p), has been already finalized. "
+                   "Impossible to get GObject field from it.",
+                   g_type_name(priv->gtype), priv->gobj);
+        gjs_dumpstack();
+        return true;
+    }
+
     if (priv->info == NULL)
         return true;  /* Not resolved, but no error; leave value_p untouched */
 
@@ -411,7 +454,7 @@ get_prop_from_field(JSContext             *cx,
     GIArgument arg = { 0 };
 
     gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Overriding %s with GObject field",
-                     name);
+                     name.get());
 
     type = g_field_info_get_type(field);
     tag = g_type_info_get_tag(type);
@@ -422,7 +465,7 @@ get_prop_from_field(JSContext             *cx,
         tag == GI_TYPE_TAG_GHASH ||
         tag == GI_TYPE_TAG_ERROR) {
         gjs_throw(cx, "Can't get field %s; GObject introspection supports only "
-                  "fields with simple types, not %s", name,
+                  "fields with simple types, not %s", name.get(),
                   g_type_tag_to_string(tag));
         retval = false;
         goto out;
@@ -430,11 +473,11 @@ get_prop_from_field(JSContext             *cx,
 
     retval = g_field_info_get_field(field, priv->gobj, &arg);
     if (!retval) {
-        gjs_throw(cx, "Error getting field %s from object", name);
+        gjs_throw(cx, "Error getting field %s from object", name.get());
         goto out;
     }
 
-    retval = gjs_value_from_g_argument(cx, value_p, type, &arg, true);
+    retval = gjs_value_from_g_argument(cx, args.rval(), type, &arg, true);
     /* copy_structs is irrelevant because g_field_info_get_field() doesn't
      * handle boxed types */
 
@@ -445,32 +488,32 @@ out:
     return retval;
 }
 
-/* a hook on getting a property; set value_p to override property's value.
- * Return value is false on OOM/exception.
- */
+/* Dynamic setter for GObject properties. Returns false on OOM/exception.
+ * args.rval() becomes the "stored value" for the property. */
 static bool
-object_instance_get_prop(JSContext              *context,
-                         JS::HandleObject        obj,
-                         JS::HandleId            id,
-                         JS::MutableHandleValue  value_p)
+object_prop_setter(JSContext *cx,
+                   unsigned   argc,
+                   JS::Value *vp)
 {
-    ObjectInstance *priv = priv_from_js(context, obj);
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                     "Get prop '%s' hook, obj %s, priv %p",
-                     gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv);
+    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
 
-    if (priv == nullptr)
-        /* If we reach this point, either object_instance_new_resolve
-         * did not throw (so name == "_init"), or the property actually
-         * exists and it's not something we should be concerned with */
-        return true;
+    /* FIXME: avoid utf8 conversion on every property access */
+    GjsAutoJSChar name;
+    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
+                            &name))
+        return false;
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Property setter '%s', obj %p priv %p",
+                     name.get(), gjs_debug_object(obj).c_str(), priv);
 
     if (priv->gobj == NULL) /* prototype, not an instance. */
         return true;
+        /* Ignore silently; note that this is different from what we do for
+         * boxed types, for historical reasons */
 
     if (priv->g_object_finalized) {
         g_critical("Object %s.%s (%p), has been already finalized. "
-                   "Impossible to get any property from it.",
+                   "Impossible to set any property on it.",
                    priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "",
                    priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype),
                    priv->gobj);
@@ -478,65 +521,63 @@ object_instance_get_prop(JSContext              *context,
         return true;
     }
 
-    GjsAutoJSChar name;
-    if (!gjs_get_string_id(context, id, &name))
-        return true; /* not resolved, but no error */
-
-    if (!get_prop_from_g_param(context, obj, priv, name, value_p))
-        return false;
-
-    if (!value_p.isUndefined())
-        return true;
-
-    /* Fall back to fields */
-    return get_prop_from_field(context, obj, priv, name, value_p);
-}
-
-static bool
-set_g_param_from_prop(JSContext          *context,
-                      ObjectInstance     *priv,
-                      const char         *name,
-                      bool&               was_set,
-                      JS::HandleValue     value_p,
-                      JS::ObjectOpResult& result)
-{
     GParameter param = { NULL, { 0, }};
-    was_set = false;
 
     GType gtype = G_TYPE_FROM_INSTANCE(priv->gobj);
     GParamSpec *param_spec = param_spec_from_js_prop_name(name, gtype);
     if (!param_spec)
-        return result.succeed();
+        return true;
 
     /* Do not set JS overridden properties through GObject, to avoid
      * infinite recursion (unless constructing) */
     if (g_param_spec_get_qdata(param_spec, gjs_is_custom_property_quark()))
         return true;
 
-    if (!init_g_param_from_property(context, param_spec, value_p, &param))
+    if (!init_g_param_from_property(cx, param_spec, args[0], &param))
         return false;
 
     g_object_set_property(priv->gobj, param.name,
                           &param.value);
 
     g_value_unset(&param.value);
-    was_set = true;
-    return result.succeed();
+    return true;
 }
 
 static bool
-check_set_field_from_prop(JSContext             *cx,
-                          ObjectInstance        *priv,
-                          const char            *name,
-                          JS::MutableHandleValue value_p,
-                          JS::ObjectOpResult&    result)
+object_field_setter(JSContext *cx,
+                    unsigned   argc,
+                    JS::Value *vp)
 {
+    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
+
+    /* FIXME: avoid utf8 conversion on every property access */
+    GjsAutoJSChar name;
+    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
+                            &name))
+        return false;
+
+    if (priv->gobj == NULL) /* prototype, not an instance. */
+        return true;
+        /* Ignore silently; note that this is different from what we do for
+         * boxed types, for historical reasons */
+
+    if (priv->g_object_finalized) {
+        g_critical("Object %s (%p), has been already finalized. "
+                   "Impossible to set GObject field on it.",
+                   g_type_name(priv->gtype), priv->gobj);
+        gjs_dumpstack();
+        return true;
+    }
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Field setter '%s', obj %p priv %p",
+                     name.get(), gjs_debug_object(obj).c_str(), priv);
+
     if (priv->info == NULL)
-        return result.succeed();
+        return true;
 
     GIFieldInfo *field = lookup_field_info(priv->info, name);
     if (field == NULL)
-        return result.succeed();
+        return true;
 
     bool retval = true;
 
@@ -544,77 +585,24 @@ check_set_field_from_prop(JSContext             *cx,
      * writable, so no need to implement this for the time being */
     if (g_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE) {
         g_message("Field %s of a GObject is writable, but setting it is not "
-                  "implemented", name);
-        result.succeed();
+                  "implemented", name.get());
         goto out;
     }
 
-    result.failReadOnly();  /* still return true; error only in strict mode */
+    retval = _gjs_proxy_throw_readonly_field(cx, priv->gtype,
+                                             g_base_info_get_name(field));
 
-    /* We have to update value_p because JS caches it as the property's "stored
+    /* We have to update args.rval(), because JS caches it as the property's "stored
      * value" 
(https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/Stored_value)
      * and so subsequent gets would get the stored value instead of accessing
      * the field */
-    value_p.setUndefined();
+    args.rval().setUndefined();
+
 out:
     g_base_info_unref((GIBaseInfo *) field);
     return retval;
 }
 
-/* a hook on setting a property; set value_p to override property value to
- * be set. Return value is false on OOM/exception.
- */
-static bool
-object_instance_set_prop(JSContext              *context,
-                         JS::HandleObject        obj,
-                         JS::HandleId            id,
-                         JS::MutableHandleValue  value_p,
-                         JS::ObjectOpResult&     result)
-{
-    ObjectInstance *priv;
-    bool ret = true;
-    bool g_param_was_set = false;
-
-    priv = priv_from_js(context, obj);
-
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Set prop '%s' hook, obj %s, priv %p",
-                     gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(),
-                     priv);
-
-    if (priv == nullptr)
-        /* see the comment in object_instance_get_prop() on this */
-        return result.succeed();
-
-    if (priv->gobj == NULL) /* prototype, not an instance. */
-        return result.succeed();
-
-    if (priv->g_object_finalized) {
-        g_critical("Object %s.%s (%p), has been already finalized. "
-                   "Impossible to set any property to it.",
-                   priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "",
-                   priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype),
-                   priv->gobj);
-        gjs_dumpstack();
-        return result.succeed();
-    }
-
-    GjsAutoJSChar name;
-    if (!gjs_get_string_id(context, id, &name)) {
-        return result.succeed();  /* not resolved, but no error */
-    }
-
-    ret = set_g_param_from_prop(context, priv, name, g_param_was_set, value_p, result);
-    if (g_param_was_set || !ret)
-        return ret;
-
-    /* note that the prop will also have been set in JS, which I think
-     * is OK, since we hook get and set so will always override that
-     * value. We could also use JS_DefineProperty though and specify a
-     * getter/setter maybe, don't know if that is better.
-     */
-    return check_set_field_from_prop(context, priv, name, value_p, result);
-}
-
 static bool
 is_vfunc_unchanged(GIVFuncInfo *info,
                    GType        gtype)
@@ -781,12 +769,17 @@ is_gobject_property_name(GIObjectInfo *info,
 
 static bool
 is_gobject_field_name(GIObjectInfo *info,
-                      const char   *name)
+                      const char   *name,
+                      GIFieldInfo **field_info_out)
 {
     GIFieldInfo *field_info = lookup_field_info(info, name);
     if (!field_info)
         return false;
-    g_base_info_unref(field_info);
+
+    if (field_info_out)
+        *field_info_out = field_info;
+    else
+        g_base_info_unref(field_info);
     return true;
 }
 
@@ -894,15 +887,65 @@ object_instance_resolve(JSContext       *context,
          * method resolution. */
     }
 
-    /* If the name refers to a GObject property or field, don't resolve.
-     * Instead, let the getProperty hook handle fetching the property from
-     * GObject. */
-    if (is_gobject_property_name(priv->info, name) ||
-        is_gobject_field_name(priv->info, name)) {
+    /* If the name refers to a GObject property or field, lazily define the
+     * property in JS, on the prototype. */
+    if (is_gobject_property_name(priv->info, name)) {
+        JS::RootedObject proto(context);
+        JS_GetPrototype(context, obj, &proto);
+
+        bool found = false;
+        if (!JS_AlreadyHasOwnPropertyById(context, proto, id, &found))
+            return false;
+        if (found) {
+            /* Already defined, so *resolved = false because we didn't just
+             * define it */
+            *resolved = false;
+            return true;
+        }
+
         gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                         "Breaking out of %p resolve, '%s' is a GObject prop",
-                         obj.get(), name.get());
-        *resolved = false;
+                         "Defining lazy GObject property '%s' in prototype of %s",
+                         gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str());
+
+        JS::RootedValue private_id(context, JS::StringValue(JSID_TO_STRING(id)));
+        if (!gjs_define_property_dynamic(context, proto, name, "gobject_prop",
+                                         object_prop_getter, object_prop_setter,
+                                         private_id, GJS_MODULE_PROP_FLAGS))
+            return false;
+
+        *resolved = true;
+        return true;
+    }
+
+    GIFieldInfo *field_info;
+    if (is_gobject_field_name(priv->info, name, &field_info)) {
+        JS::RootedObject proto(context);
+        JS_GetPrototype(context, obj, &proto);
+
+        bool found = false;
+        if (!JS_AlreadyHasOwnPropertyById(context, proto, id, &found))
+            return false;
+        if (found) {
+            *resolved = false;
+            return true;
+        }
+
+        gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
+                         "Defining lazy GObject field '%s' in prototype of %s",
+                         gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str());
+
+        unsigned flags = GJS_MODULE_PROP_FLAGS;
+        if (!(g_field_info_get_flags(field_info) & GI_FIELD_IS_WRITABLE))
+            flags |= JSPROP_READONLY;
+
+        JS::RootedValue private_id(context, JS::StringValue(JSID_TO_STRING(id)));
+        if (!gjs_define_property_dynamic(context, proto, name, "gobject_field",
+                                         object_field_getter,
+                                         object_field_setter, private_id,
+                                         flags))
+            return false;
+
+        *resolved = true;
         return true;
     }
 
@@ -2023,8 +2066,8 @@ to_string_func(JSContext *context,
 static const struct JSClassOps gjs_object_class_ops = {
     object_instance_add_prop,
     NULL,  /* deleteProperty */
-    object_instance_get_prop,
-    object_instance_set_prop,
+    nullptr,  /* getProperty */
+    nullptr,  /* setProperty */
     NULL,  /* enumerate */
     object_instance_resolve,
     nullptr,  /* mayResolve */
diff --git a/gi/proxyutils.cpp b/gi/proxyutils.cpp
index d90aa4d0..6a3295f3 100644
--- a/gi/proxyutils.cpp
+++ b/gi/proxyutils.cpp
@@ -74,3 +74,22 @@ _gjs_proxy_to_string_func(JSContext             *context,
     g_string_free (buf, true);
     return ret;
 }
+
+bool
+_gjs_proxy_throw_nonexistent_field(JSContext  *cx,
+                                   GType       gtype,
+                                   const char *field_name)
+{
+    gjs_throw(cx, "No property %s on %s", field_name, g_type_name(gtype));
+    return false;
+}
+
+bool
+_gjs_proxy_throw_readonly_field(JSContext  *cx,
+                                GType       gtype,
+                                const char *field_name)
+{
+    gjs_throw(cx, "Property %s.%s is not writable", g_type_name(gtype),
+              field_name);
+    return false;
+}
diff --git a/gi/proxyutils.h b/gi/proxyutils.h
index 79e15fa7..e8cf6d26 100644
--- a/gi/proxyutils.h
+++ b/gi/proxyutils.h
@@ -36,6 +36,14 @@ bool _gjs_proxy_to_string_func(JSContext             *context,
                                gpointer               native_address,
                                JS::MutableHandleValue ret);
 
+bool _gjs_proxy_throw_nonexistent_field(JSContext  *cx,
+                                        GType       gtype,
+                                        const char *field_name);
+
+bool _gjs_proxy_throw_readonly_field(JSContext  *cx,
+                                     GType       gtype,
+                                     const char *field_name);
+
 G_END_DECLS
 
 #endif  /* __GJS_OBJECT_H__ */
diff --git a/gjs/jsapi-class.h b/gjs/jsapi-class.h
index c3c8fa97..6f76704c 100644
--- a/gjs/jsapi-class.h
+++ b/gjs/jsapi-class.h
@@ -55,6 +55,15 @@ JSObject *gjs_construct_object_dynamic(JSContext                  *cx,
                                        JS::HandleObject            proto,
                                        const JS::HandleValueArray& args);
 
+bool gjs_define_property_dynamic(JSContext       *cx,
+                                 JS::HandleObject proto,
+                                 const char      *prop_name,
+                                 const char      *func_namespace,
+                                 JSNative         getter,
+                                 JSNative         setter,
+                                 JS::HandleValue  private_slot,
+                                 unsigned         flags);
+
 /*
  * Helper methods to access private data:
  *
@@ -335,4 +344,6 @@ gjs_##name##_constructor(JSContext  *context,           \
 
 G_END_DECLS
 
+JS::Value gjs_dynamic_property_private_slot(JSObject *accessor_obj);
+
 #endif /* GJS_JSAPI_CLASS_H */
diff --git a/gjs/jsapi-dynamic-class.cpp b/gjs/jsapi-dynamic-class.cpp
index cb343495..30e48894 100644
--- a/gjs/jsapi-dynamic-class.cpp
+++ b/gjs/jsapi-dynamic-class.cpp
@@ -35,6 +35,11 @@
 #include <string.h>
 #include <math.h>
 
+/* Reserved slots of JSNative accessor wrappers */
+enum {
+    DYNAMIC_PROPERTY_PRIVATE_SLOT,
+};
+
 bool
 gjs_init_class_dynamic(JSContext              *context,
                        JS::HandleObject        in_object,
@@ -194,3 +199,90 @@ gjs_construct_object_dynamic(JSContext                  *context,
 
     return JS_New(context, constructor, args);
 }
+
+static JSObject *
+define_native_accessor_wrapper(JSContext      *cx,
+                               JSNative        call,
+                               unsigned        nargs,
+                               const char     *func_name,
+                               JS::HandleValue private_slot)
+{
+    JSFunction *func = js::NewFunctionWithReserved(cx, call, nargs, 0, func_name);
+    if (!func)
+        return nullptr;
+
+    JSObject *func_obj = JS_GetFunctionObject(func);
+    js::SetFunctionNativeReserved(func_obj, DYNAMIC_PROPERTY_PRIVATE_SLOT,
+                                  private_slot);
+    return func_obj;
+}
+
+/**
+ * gjs_define_property_dynamic:
+ * @cx: the #JSContext
+ * @proto: the prototype of the object, on which to define the property
+ * @prop_name: name of the property or field in GObject, visible to JS code
+ * @func_namespace: string from which the internal names for the getter and
+ *   setter functions are built, not visible to JS code
+ * @getter: getter function
+ * @setter: setter function
+ * @private_slot: private data in the form of a #JS::Value that the getter and
+ *   setter will have access to
+ * @flags: additional flags to define the property with (other than the ones
+ *   required for a property with native getter/setter)
+ *
+ * When defining properties in a GBoxed or GObject, we can't have a separate
+ * getter and setter for each one, since the properties are defined dynamically.
+ * Therefore we must have one getter and setter for all the properties we define
+ * on all the types. In order to have that, we must provide the getter and
+ * setter with private data, e.g. the field index for GBoxed, in a "reserved
+ * slot" for which we must unfortunately use the jsfriendapi.
+ *
+ * Returns: %true on success, %false if an exception is pending on @cx.
+ */
+bool
+gjs_define_property_dynamic(JSContext       *cx,
+                            JS::HandleObject proto,
+                            const char      *prop_name,
+                            const char      *func_namespace,
+                            JSNative         getter,
+                            JSNative         setter,
+                            JS::HandleValue  private_slot,
+                            unsigned         flags)
+{
+    GjsAutoChar getter_name = g_strconcat(func_namespace, "_get::", prop_name, nullptr);
+    GjsAutoChar setter_name = g_strconcat(func_namespace, "_set::", prop_name, nullptr);
+
+    JS::RootedObject getter_obj(cx,
+        define_native_accessor_wrapper(cx, getter, 0, getter_name, private_slot));
+    if (!getter_obj)
+        return false;
+
+    JS::RootedObject setter_obj(cx,
+        define_native_accessor_wrapper(cx, setter, 1, setter_name, private_slot));
+    if (!setter_obj)
+        return false;
+
+    flags |= JSPROP_SHARED | JSPROP_GETTER | JSPROP_SETTER;
+
+    return JS_DefineProperty(cx, proto, prop_name, JS::UndefinedHandleValue, flags,
+                             JS_DATA_TO_FUNC_PTR(JSNative, getter_obj.get()),
+                             JS_DATA_TO_FUNC_PTR(JSNative, setter_obj.get()));
+}
+
+/**
+ * gjs_dynamic_property_private_slot:
+ * @accessor_obj: the getter or setter as a function object, i.e.
+ *   `&args.callee()` in the #JSNative function
+ *
+ * For use in dynamic property getters and setters (see
+ * gjs_define_property_dynamic()) to retrieve the private data passed there.
+ *
+ * Returns: the JS::Value that was passed to gjs_define_property_dynamic().
+ */
+JS::Value
+gjs_dynamic_property_private_slot(JSObject *accessor_obj)
+{
+    return js::GetFunctionNativeReserved(accessor_obj,
+                                         DYNAMIC_PROPERTY_PRIVATE_SLOT);
+}
diff --git a/installed-tests/js/testEverythingEncapsulated.js 
b/installed-tests/js/testEverythingEncapsulated.js
index 1039f0e0..ec8215c1 100644
--- a/installed-tests/js/testEverythingEncapsulated.js
+++ b/installed-tests/js/testEverythingEncapsulated.js
@@ -216,14 +216,7 @@ describe('Introspected GObject', function () {
         expect(() => obj.function_ptr).toThrow();
     });
 
-    it('silently does not set read-only fields', function () {
-        obj.some_int8 = 41;
-        expect(obj.some_int8).toEqual(42);
-        expect(obj.int).toEqual(42);
-    });
-
-    it('throws an error in strict mode when setting a read-only field', function () {
-        'use strict';
+    it('throws when setting a read-only field', function () {
         expect(() => obj.some_int8 = 41).toThrow();
     });
 
diff --git a/installed-tests/js/testGIMarshalling.js b/installed-tests/js/testGIMarshalling.js
index 713de013..b7565c54 100644
--- a/installed-tests/js/testGIMarshalling.js
+++ b/installed-tests/js/testGIMarshalling.js
@@ -678,4 +678,12 @@ describe('GObject properties', function () {
         obj.some_gvalue = 'foo';
         expect(obj.some_gvalue).toEqual('foo');
     });
+
+    xit('gets a read-only property', function () {
+        expect(obj.some_readonly).toEqual(42);
+    }).pend('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/32');
+
+    xit('throws when setting a read-only property', function () {
+        expect(() => obj.some_readonly = 35).toThrow();
+    }).pend('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/32');
 });


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]