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