summaryrefslogtreecommitdiff
path: root/gjs/jsapi-dynamic-class.cpp
blob: 3bf86aeabaac3af7fa130c1cc79fd20431cd7bae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2008 litl, LLC
// SPDX-FileCopyrightText: 2012 Giovanni Campagna <scampa.giovanni@gmail.com>

#include <config.h>

#include <string.h>  // for strlen

#include <glib.h>

#include <js/CallAndConstruct.h>
#include <js/CallArgs.h>  // for JSNative
#include <js/Class.h>
#include <js/ComparisonOperators.h>
#include <js/Object.h>              // for GetClass
#include <js/PropertyAndElement.h>  // for JS_DefineFunctions, JS_DefinePro...
#include <js/Realm.h>  // for GetRealmObjectPrototype
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
#include <jsapi.h>        // for JS_GetFunctionObject, JS_GetPrototype
#include <jsfriendapi.h>  // for GetFunctionNativeReserved, NewFun...
#include <jspubtd.h>      // for JSProto_TypeError

#include "gjs/atoms.h"
#include "gjs/context-private.h"
#include "gjs/jsapi-util.h"
#include "gjs/macros.h"

struct JSFunctionSpec;
struct JSPropertySpec;
namespace JS {
class HandleValueArray;
}

/* Reserved slots of JSNative accessor wrappers */
enum {
    DYNAMIC_PROPERTY_PRIVATE_SLOT,
};

bool gjs_init_class_dynamic(JSContext* context, JS::HandleObject in_object,
                            JS::HandleObject parent_proto, const char* ns_name,
                            const char* class_name, const JSClass* clasp,
                            JSNative constructor_native, unsigned nargs,
                            JSPropertySpec* proto_ps, JSFunctionSpec* proto_fs,
                            JSPropertySpec* static_ps,
                            JSFunctionSpec* static_fs,
                            JS::MutableHandleObject prototype,
                            JS::MutableHandleObject constructor) {
    /* Without a name, JS_NewObject fails */
    g_assert (clasp->name != NULL);

    /* gjs_init_class_dynamic only makes sense for instantiable classes,
       use JS_InitClass for static classes like Math */
    g_assert (constructor_native != NULL);

    /* Class initialization consists of five parts:
       - building a prototype
       - defining prototype properties and functions
       - building a constructor and defining it on the right object
       - defining constructor properties and functions
       - linking the constructor and the prototype, so that
         JS_NewObjectForConstructor can find it
    */

    if (parent_proto) {
        prototype.set(JS_NewObjectWithGivenProto(context, clasp, parent_proto));
    } else {
        /* JS_NewObject will use Object.prototype as the prototype if the
         * clasp's constructor is not a built-in class.
         */
        prototype.set(JS_NewObject(context, clasp));
    }
    if (!prototype)
        return false;

    if (proto_ps && !JS_DefineProperties(context, prototype, proto_ps))
        return false;
    if (proto_fs && !JS_DefineFunctions(context, prototype, proto_fs))
        return false;

    GjsAutoChar full_function_name =
        g_strdup_printf("%s_%s", ns_name, class_name);
    JSFunction* constructor_fun =
        JS_NewFunction(context, constructor_native, nargs, JSFUN_CONSTRUCTOR,
                       full_function_name);
    if (!constructor_fun)
        return false;

    constructor.set(JS_GetFunctionObject(constructor_fun));

    if (static_ps && !JS_DefineProperties(context, constructor, static_ps))
        return false;
    if (static_fs && !JS_DefineFunctions(context, constructor, static_fs))
        return false;

    if (!JS_LinkConstructorAndPrototype(context, constructor, prototype))
        return false;

    /* The constructor defined by JS_InitClass has no property attributes, but this
       is a more useful default for gjs */
    return JS_DefineProperty(context, in_object, class_name, constructor,
                             GJS_MODULE_PROP_FLAGS);
}

[[nodiscard]] static const char* format_dynamic_class_name(const char* name) {
    if (g_str_has_prefix(name, "_private_"))
        return name + strlen("_private_");
    else
        return name;
}

bool
gjs_typecheck_instance(JSContext       *context,
                       JS::HandleObject obj,
                       const JSClass   *static_clasp,
                       bool             throw_error)
{
    if (!JS_InstanceOf(context, obj, static_clasp, NULL)) {
        if (throw_error) {
            const JSClass* obj_class = JS::GetClass(obj);

            gjs_throw_custom(context, JSProto_TypeError, nullptr,
                             "Object %p is not a subclass of %s, it's a %s",
                             obj.get(), static_clasp->name,
                             format_dynamic_class_name(obj_class->name));
        }

        return false;
    }

    return true;
}

JSObject*
gjs_construct_object_dynamic(JSContext                  *context,
                             JS::HandleObject            proto,
                             const JS::HandleValueArray& args)
{
    const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
    JS::RootedObject constructor(context);

    if (!gjs_object_require_property(context, proto, "prototype",
                                     atoms.constructor(), &constructor))
        return NULL;

    JS::RootedValue v_constructor(context, JS::ObjectValue(*constructor));
    JS::RootedObject object(context);
    if (!JS::Construct(context, v_constructor, args, &object))
        return nullptr;

    return object;
}

GJS_JSAPI_RETURN_CONVENTION
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;

    return JS_DefineProperty(cx, proto, prop_name, getter_obj, setter_obj,
                             flags);
}

/**
 * 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);
}

/**
 * gjs_object_in_prototype_chain:
 * @cx:
 * @proto: The prototype which we are checking if @check_obj has in its chain
 * @check_obj: The object to check
 * @is_in_chain: (out): Whether @check_obj has @proto in its prototype chain
 *
 * Similar to JS_HasInstance() but takes into account abstract classes defined
 * with JS_InitClass(), which JS_HasInstance() does not. Abstract classes don't
 * have constructors, and JS_HasInstance() requires a constructor.
 *
 * Returns: false if an exception was thrown, true otherwise.
 */
bool gjs_object_in_prototype_chain(JSContext* cx, JS::HandleObject proto,
                                   JS::HandleObject check_obj,
                                   bool* is_in_chain) {
    JS::RootedObject object_prototype(cx, JS::GetRealmObjectPrototype(cx));
    if (!object_prototype)
        return false;

    JS::RootedObject proto_iter(cx);
    if (!JS_GetPrototype(cx, check_obj, &proto_iter))
        return false;
    while (proto_iter != object_prototype) {
        if (proto_iter == proto) {
            *is_in_chain = true;
            return true;
        }
        if (!JS_GetPrototype(cx, proto_iter, &proto_iter))
            return false;
    }
    *is_in_chain = false;
    return true;
}