[gjs: 4/8] object: Remove getProperty/setProperty hooks
- From: Cosimo Cecchi <cosimoc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs: 4/8] object: Remove getProperty/setProperty hooks
- Date: Fri, 1 Jun 2018 20:07:07 +0000 (UTC)
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, ¶m))
+ if (!init_g_param_from_property(cx, param_spec, args[0], ¶m))
return false;
g_object_set_property(priv->gobj, param.name,
¶m.value);
g_value_unset(¶m.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]