1 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 // SPDX-FileCopyrightText: 2008 litl, LLC
4 
5 #include <config.h>
6 
7 #include <unordered_map>
8 #include <utility>  // for move, pair
9 
10 #include <glib-object.h>
11 #include <glib.h>
12 
13 #include <js/PropertyDescriptor.h>  // for JSPROP_READONLY
14 #include <js/RootingAPI.h>
15 #include <js/TypeDecls.h>
16 #include <js/Value.h>
17 #include <js/ValueArray.h>
18 #include <jsapi.h>  // for JS_New, JSAutoRealm, JS_GetProperty
19 
20 #include "gi/gobject.h"
21 #include "gi/object.h"
22 #include "gi/value.h"
23 #include "gjs/context-private.h"
24 #include "gjs/context.h"
25 #include "gjs/jsapi-util.h"
26 #include "gjs/macros.h"
27 
28 static std::unordered_map<GType, AutoParamArray> class_init_properties;
29 
current_js_context()30 [[nodiscard]] static JSContext* current_js_context() {
31     GjsContext* gjs = gjs_context_get_current();
32     return static_cast<JSContext*>(gjs_context_get_native_context(gjs));
33 }
34 
push_class_init_properties(GType gtype,AutoParamArray * params)35 void push_class_init_properties(GType gtype, AutoParamArray* params) {
36     class_init_properties[gtype] = std::move(*params);
37 }
38 
pop_class_init_properties(GType gtype,AutoParamArray * params_out)39 bool pop_class_init_properties(GType gtype, AutoParamArray* params_out) {
40     auto found = class_init_properties.find(gtype);
41     if (found == class_init_properties.end())
42         return false;
43 
44     *params_out = std::move(found->second);
45     class_init_properties.erase(found);
46     return true;
47 }
48 
49 GJS_JSAPI_RETURN_CONVENTION
jsobj_set_gproperty(JSContext * cx,JS::HandleObject object,const GValue * value,GParamSpec * pspec)50 static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
51                                 const GValue* value, GParamSpec* pspec) {
52     JS::RootedValue jsvalue(cx);
53     if (!gjs_value_from_g_value(cx, &jsvalue, value))
54         return false;
55 
56     GjsAutoChar underscore_name = gjs_hyphen_to_underscore(pspec->name);
57 
58     if (pspec->flags & G_PARAM_CONSTRUCT_ONLY) {
59         unsigned flags = GJS_MODULE_PROP_FLAGS | JSPROP_READONLY;
60         GjsAutoChar camel_name = gjs_hyphen_to_camel(pspec->name);
61 
62         if (g_param_spec_get_qdata(pspec, ObjectBase::custom_property_quark())) {
63             JS::Rooted<JS::PropertyDescriptor> jsprop(cx);
64 
65             // Ensure to call any associated setter method
66             if (!g_str_equal(underscore_name.get(), pspec->name)) {
67                 if (!JS_GetPropertyDescriptor(cx, object, underscore_name, &jsprop))
68                     return false;
69                 if (jsprop.setter() &&
70                     !JS_SetProperty(cx, object, underscore_name, jsvalue))
71                     return false;
72             }
73 
74             if (!g_str_equal(camel_name.get(), pspec->name)) {
75                 if (!JS_GetPropertyDescriptor(cx, object, camel_name, &jsprop))
76                     return false;
77                 if (jsprop.setter() &&
78                     !JS_SetProperty(cx, object, camel_name, jsvalue))
79                     return false;
80             }
81 
82             if (!JS_GetPropertyDescriptor(cx, object, pspec->name, &jsprop))
83                 return false;
84             if (jsprop.setter() &&
85                 !JS_SetProperty(cx, object, pspec->name, jsvalue))
86                 return false;
87         }
88 
89         return JS_DefineProperty(cx, object, underscore_name, jsvalue, flags) &&
90                JS_DefineProperty(cx, object, camel_name, jsvalue, flags) &&
91                JS_DefineProperty(cx, object, pspec->name, jsvalue, flags);
92     }
93 
94     return JS_SetProperty(cx, object, underscore_name, jsvalue);
95 }
96 
gjs_object_base_init(void * klass)97 static void gjs_object_base_init(void* klass) {
98     auto* priv = ObjectPrototype::for_gtype(G_OBJECT_CLASS_TYPE(klass));
99     if (priv)
100         priv->ref_vfuncs();
101 }
102 
gjs_object_base_finalize(void * klass)103 static void gjs_object_base_finalize(void* klass) {
104     auto* priv = ObjectPrototype::for_gtype(G_OBJECT_CLASS_TYPE(klass));
105     if (priv)
106         priv->unref_vfuncs();
107 }
108 
gjs_object_constructor(GType type,unsigned n_construct_properties,GObjectConstructParam * construct_properties)109 static GObject* gjs_object_constructor(
110     GType type, unsigned n_construct_properties,
111     GObjectConstructParam* construct_properties) {
112     JSContext* cx = current_js_context();
113     GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
114 
115     if (!gjs->object_init_list().empty()) {
116         GType parent_type = g_type_parent(type);
117 
118         /* The object is being constructed from JS:
119          * Simply chain up to the first non-gjs constructor
120          */
121         while (G_OBJECT_CLASS(g_type_class_peek(parent_type))->constructor ==
122                gjs_object_constructor)
123             parent_type = g_type_parent(parent_type);
124 
125         return G_OBJECT_CLASS(g_type_class_peek(parent_type))
126             ->constructor(type, n_construct_properties, construct_properties);
127     }
128 
129     /* The object is being constructed from native code (e.g. GtkBuilder):
130      * Construct the JS object from the constructor, then use the GObject
131      * that was associated in gjs_object_custom_init()
132      */
133     JSAutoRealm ar(cx, gjs_get_import_global(cx));
134 
135     JS::RootedObject constructor(
136         cx, gjs_lookup_object_constructor_from_info(cx, nullptr, type));
137     if (!constructor)
138         return nullptr;
139 
140     JSObject* object;
141     if (n_construct_properties) {
142         JS::RootedObject props_hash(cx, JS_NewPlainObject(cx));
143 
144         for (unsigned i = 0; i < n_construct_properties; i++)
145             if (!jsobj_set_gproperty(cx, props_hash,
146                                      construct_properties[i].value,
147                                      construct_properties[i].pspec))
148                 return nullptr;
149 
150         JS::RootedValueArray<1> args(cx);
151         args[0].set(JS::ObjectValue(*props_hash));
152         object = JS_New(cx, constructor, args);
153     } else {
154         object = JS_New(cx, constructor, JS::HandleValueArray::empty());
155     }
156 
157     if (!object)
158         return nullptr;
159 
160     auto* priv = ObjectBase::for_js_nocheck(object);
161     /* Should have been set in init_impl() and pushed into object_init_list,
162      * then popped from object_init_list in gjs_object_custom_init() */
163     g_assert(priv);
164     /* We only hold a toggle ref at this point, add back a ref that the
165      * native code can own.
166      */
167     return G_OBJECT(g_object_ref(priv->to_instance()->ptr()));
168 }
169 
gjs_object_set_gproperty(GObject * object,unsigned property_id,const GValue * value,GParamSpec * pspec)170 static void gjs_object_set_gproperty(GObject* object,
171                                      unsigned property_id [[maybe_unused]],
172                                      const GValue* value, GParamSpec* pspec) {
173     auto* priv = ObjectInstance::for_gobject(object);
174     JSContext* cx = current_js_context();
175 
176     JS::RootedObject js_obj(cx, priv->wrapper());
177     JSAutoRealm ar(cx, js_obj);
178 
179     if (!jsobj_set_gproperty(cx, js_obj, value, pspec))
180         gjs_log_exception_uncaught(cx);
181 }
182 
gjs_object_get_gproperty(GObject * object,unsigned property_id,GValue * value,GParamSpec * pspec)183 static void gjs_object_get_gproperty(GObject* object,
184                                      unsigned property_id [[maybe_unused]],
185                                      GValue* value, GParamSpec* pspec) {
186     auto* priv = ObjectInstance::for_gobject(object);
187     JSContext* cx = current_js_context();
188 
189     JS::RootedObject js_obj(cx, priv->wrapper());
190     JS::RootedValue jsvalue(cx);
191     JSAutoRealm ar(cx, js_obj);
192 
193     GjsAutoChar underscore_name = gjs_hyphen_to_underscore(pspec->name);
194     if (!JS_GetProperty(cx, js_obj, underscore_name, &jsvalue)) {
195         gjs_log_exception_uncaught(cx);
196         return;
197     }
198     if (!gjs_value_to_g_value(cx, jsvalue, value))
199         gjs_log_exception(cx);
200 }
201 
gjs_object_class_init(void * class_pointer,void *)202 static void gjs_object_class_init(void* class_pointer, void*) {
203     GObjectClass* klass = G_OBJECT_CLASS(class_pointer);
204     GType gtype = G_OBJECT_CLASS_TYPE(klass);
205 
206     klass->constructor = gjs_object_constructor;
207     klass->set_property = gjs_object_set_gproperty;
208     klass->get_property = gjs_object_get_gproperty;
209 
210     AutoParamArray properties;
211     if (!pop_class_init_properties(gtype, &properties))
212         return;
213 
214     unsigned i = 0;
215     for (GjsAutoParam& pspec : properties) {
216         g_param_spec_set_qdata(pspec, ObjectBase::custom_property_quark(),
217                                GINT_TO_POINTER(1));
218         g_object_class_install_property(klass, ++i, pspec);
219     }
220 }
221 
gjs_object_custom_init(GTypeInstance * instance,void * g_class)222 static void gjs_object_custom_init(GTypeInstance* instance,
223                                    void* g_class [[maybe_unused]]) {
224     JSContext* cx = current_js_context();
225     GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
226 
227     if (gjs->object_init_list().empty())
228         return;
229 
230     JS::RootedObject object(cx, gjs->object_init_list().back());
231     auto* priv_base = ObjectBase::for_js_nocheck(object);
232     g_assert(priv_base);  // Should have been set in init_impl()
233     ObjectInstance* priv = priv_base->to_instance();
234 
235     if (priv_base->gtype() != G_TYPE_FROM_INSTANCE(instance)) {
236         /* This is not the most derived instance_init function,
237            do nothing.
238          */
239         return;
240     }
241 
242     gjs->object_init_list().popBack();
243 
244     if (!priv->init_custom_class_from_gobject(cx, object, G_OBJECT(instance)))
245         gjs_log_exception_uncaught(cx);
246 }
247 
gjs_interface_init(void * g_iface,void *)248 static void gjs_interface_init(void* g_iface, void*) {
249     GType gtype = G_TYPE_FROM_INTERFACE(g_iface);
250 
251     AutoParamArray properties;
252     if (!pop_class_init_properties(gtype, &properties))
253         return;
254 
255     for (GjsAutoParam& pspec : properties) {
256         g_param_spec_set_qdata(pspec, ObjectBase::custom_property_quark(),
257                                GINT_TO_POINTER(1));
258         g_object_interface_install_property(g_iface, pspec);
259     }
260 }
261 
262 constexpr GTypeInfo gjs_gobject_class_info = {
263     0,  // class_size
264 
265     gjs_object_base_init,
266     gjs_object_base_finalize,
267 
268     gjs_object_class_init,
269     GClassFinalizeFunc(nullptr),
270     nullptr,  // class_data
271 
272     0,  // instance_size
273     0,  // n_preallocs
274     gjs_object_custom_init,
275 };
276 
277 constexpr GTypeInfo gjs_gobject_interface_info = {
278     sizeof(GTypeInterface),  // class_size
279 
280     GBaseInitFunc(nullptr),
281     GBaseFinalizeFunc(nullptr),
282 
283     gjs_interface_init,
284     GClassFinalizeFunc(nullptr),
285     nullptr,  // class_data
286 
287     0,        // instance_size
288     0,        // n_preallocs
289     nullptr,  // instance_init
290 };
291