/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* * Copyright (c) 2008 litl, LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include #include #include // for exit #include // for strcmp, memset, size_t #include #include #include #include #include #include #include #include #include #include #include #include #include // for JSPROP_PERMANENT #include #include // for GetRealmFunctionPrototype #include #include #include #include #include #include // for HandleValueArray, JS_GetElement #include "gi/arg-cache.h" #include "gi/arg-inl.h" #include "gi/arg.h" #include "gi/closure.h" #include "gi/function.h" #include "gi/gerror.h" #include "gi/object.h" #include "gi/utils-inl.h" #include "gjs/context-private.h" #include "gjs/context.h" #include "gjs/jsapi-class.h" #include "gjs/jsapi-util.h" #include "gjs/mem-private.h" #include "util/log.h" /* We use guint8 for arguments; functions can't * have more than this. */ #define GJS_ARG_INDEX_INVALID G_MAXUINT8 typedef struct { GICallableInfo* info; GjsArgumentCache* arguments; uint8_t js_in_argc; guint8 js_out_argc; GIFunctionInvoker invoker; } Function; extern struct JSClass gjs_function_class; /* Because we can't free the mmap'd data for a callback * while it's in use, this list keeps track of ones that * will be freed the next time we invoke a C function. */ static GSList *completed_trampolines = NULL; /* GjsCallbackTrampoline */ GJS_DEFINE_PRIV_FROM_JS(Function, gjs_function_class) void gjs_callback_trampoline_ref(GjsCallbackTrampoline *trampoline) { trampoline->ref_count++; } void gjs_callback_trampoline_unref(GjsCallbackTrampoline *trampoline) { /* Not MT-safe, like all the rest of GJS */ trampoline->ref_count--; if (trampoline->ref_count == 0) { g_clear_pointer(&trampoline->js_function, g_closure_unref); if (trampoline->info && trampoline->closure) g_callable_info_free_closure(trampoline->info, trampoline->closure); g_clear_pointer(&trampoline->info, g_base_info_unref); g_free (trampoline->param_types); g_free(trampoline); } } template static inline std::enable_if_t && std::is_signed_v> set_ffi_arg(void* result, GIArgument* value) { *static_cast(result) = gjs_arg_get(value); } template static inline std::enable_if_t || std::is_unsigned_v> set_ffi_arg(void* result, GIArgument* value) { *static_cast(result) = gjs_arg_get(value); } template static inline std::enable_if_t> set_ffi_arg( void* result, GIArgument* value) { *static_cast(result) = gjs_pointer_to_int(gjs_arg_get(value)); } static void set_return_ffi_arg_from_giargument (GITypeInfo *ret_type, void *result, GIArgument *return_value) { // Be consistent with gjs_value_to_g_argument() switch (g_type_info_get_tag(ret_type)) { case GI_TYPE_TAG_VOID: g_assert_not_reached(); case GI_TYPE_TAG_INT8: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT8: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT16: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT16: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT32: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT32: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_BOOLEAN: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UNICHAR: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT64: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INTERFACE: { GIBaseInfo* interface_info; GIInfoType interface_type; interface_info = g_type_info_get_interface(ret_type); interface_type = g_base_info_get_type(interface_info); if (interface_type == GI_INFO_TYPE_ENUM || interface_type == GI_INFO_TYPE_FLAGS) set_ffi_arg(result, return_value); else set_ffi_arg(result, return_value); g_base_info_unref(interface_info); } break; case GI_TYPE_TAG_UINT64: // Other primitive types need to squeeze into 64-bit ffi_arg too set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_FLOAT: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_DOUBLE: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_GTYPE: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UTF8: case GI_TYPE_TAG_FILENAME: set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_ARRAY: case GI_TYPE_TAG_GLIST: case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: default: set_ffi_arg(result, return_value); break; } } static void warn_about_illegal_js_callback(const GjsCallbackTrampoline *trampoline, const char *when, const char *reason) { g_critical("Attempting to run a JS callback %s. This is most likely caused " "by %s. Because it would crash the application, it has been " "blocked.", when, reason); if (trampoline->info) { const char *name = g_base_info_get_name(trampoline->info); g_critical("The offending callback was %s()%s.", name, trampoline->is_vfunc ? ", a vfunc" : ""); } return; } /* This is our main entry point for ffi_closure callbacks. * ffi_prep_closure is doing pure magic and replaces the original * function call with this one which gives us the ffi arguments, * a place to store the return value and our use data. * In other words, everything we need to call the JS function and * getting the return value back. */ static void gjs_callback_closure(ffi_cif* cif [[maybe_unused]], void* result, void** ffi_args, void* data) { JSContext *context; GjsCallbackTrampoline *trampoline; int i, n_args, n_jsargs, n_outargs, c_args_offset = 0; GITypeInfo ret_type; bool success = false; auto args = reinterpret_cast(ffi_args); trampoline = (GjsCallbackTrampoline *) data; g_assert(trampoline); gjs_callback_trampoline_ref(trampoline); if (G_UNLIKELY(!gjs_closure_is_valid(trampoline->js_function))) { warn_about_illegal_js_callback(trampoline, "during shutdown", "destroying a Clutter actor or GTK widget with ::destroy signal " "connected, or using the destroy(), dispose(), or remove() vfuncs"); gjs_dumpstack(); gjs_callback_trampoline_unref(trampoline); return; } context = gjs_closure_get_context(trampoline->js_function); GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); if (G_UNLIKELY(gjs->sweeping())) { warn_about_illegal_js_callback(trampoline, "during garbage collection", "destroying a Clutter actor or GTK widget with ::destroy signal " "connected, or using the destroy(), dispose(), or remove() vfuncs"); gjs_dumpstack(); gjs_callback_trampoline_unref(trampoline); return; } if (G_UNLIKELY(!gjs->is_owner_thread())) { warn_about_illegal_js_callback(trampoline, "on a different thread", "an API not intended to be used in JS"); gjs_callback_trampoline_unref(trampoline); return; } JSAutoRealm ar(context, JS_GetFunctionObject(gjs_closure_get_callable( trampoline->js_function))); bool can_throw_gerror = g_callable_info_can_throw_gerror(trampoline->info); n_args = g_callable_info_get_n_args(trampoline->info); g_assert(n_args >= 0); JS::RootedObject this_object(context); if (trampoline->is_vfunc) { GObject* gobj = G_OBJECT(gjs_arg_get(args[0])); if (gobj) { this_object = ObjectInstance::wrapper_from_gobject(context, gobj); if (!this_object) { gjs_log_exception(context); return; } } /* "this" is not included in the GI signature, but is in the C (and * FFI) signature */ c_args_offset = 1; } n_outargs = 0; JS::RootedValueVector jsargs(context); if (!jsargs.reserve(n_args)) g_error("Unable to reserve space for vector"); JS::RootedValue rval(context); g_callable_info_load_return_type(trampoline->info, &ret_type); bool ret_type_is_void = g_type_info_get_tag (&ret_type) == GI_TYPE_TAG_VOID; for (i = 0, n_jsargs = 0; i < n_args; i++) { GIArgInfo arg_info; GITypeInfo type_info; GjsParamType param_type; g_callable_info_load_arg(trampoline->info, i, &arg_info); g_arg_info_load_type(&arg_info, &type_info); /* Skip void * arguments */ if (g_type_info_get_tag(&type_info) == GI_TYPE_TAG_VOID) continue; if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT) { n_outargs++; continue; } if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_INOUT) n_outargs++; param_type = trampoline->param_types[i]; switch (param_type) { case PARAM_SKIPPED: continue; case PARAM_ARRAY: { gint array_length_pos = g_type_info_get_array_length(&type_info); GIArgInfo array_length_arg; GITypeInfo arg_type_info; JS::RootedValue length(context); g_callable_info_load_arg(trampoline->info, array_length_pos, &array_length_arg); g_arg_info_load_type(&array_length_arg, &arg_type_info); if (!gjs_value_from_g_argument(context, &length, &arg_type_info, args[array_length_pos + c_args_offset], true)) goto out; if (!jsargs.growBy(1)) g_error("Unable to grow vector"); if (!gjs_value_from_explicit_array(context, jsargs[n_jsargs++], &type_info, args[i + c_args_offset], length.toInt32())) goto out; break; } case PARAM_NORMAL: { if (!jsargs.growBy(1)) g_error("Unable to grow vector"); GIArgument* arg = args[i + c_args_offset]; if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_INOUT) arg = *reinterpret_cast(arg); if (!gjs_value_from_g_argument(context, jsargs[n_jsargs++], &type_info, arg, false)) goto out; break; } case PARAM_CALLBACK: /* Callbacks that accept another callback as a parameter are not * supported, see gjs_callback_trampoline_new() */ case PARAM_UNKNOWN: // PARAM_UNKNOWN is currently not ever set on a callback's args. default: g_assert_not_reached(); } } if (!gjs_closure_invoke(trampoline->js_function, this_object, jsargs, &rval, true)) goto out; if (n_outargs == 0 && ret_type_is_void) { /* void return value, no out args, nothing to do */ } else if (n_outargs == 0) { GIArgument argument; GITransfer transfer; transfer = g_callable_info_get_caller_owns (trampoline->info); /* non-void return value, no out args. Should * be a single return value. */ if (!gjs_value_to_g_argument(context, rval, &ret_type, "callback", GJS_ARGUMENT_RETURN_VALUE, transfer, true, &argument)) goto out; set_return_ffi_arg_from_giargument(&ret_type, result, &argument); } else if (n_outargs == 1 && ret_type_is_void) { /* void return value, one out args. Should * be a single return value. */ for (i = 0; i < n_args; i++) { GIArgInfo arg_info; g_callable_info_load_arg(trampoline->info, i, &arg_info); if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_IN) continue; if (!gjs_value_to_arg(context, rval, &arg_info, *reinterpret_cast(args[i + c_args_offset]))) goto out; break; } } else { bool is_array = rval.isObject(); if (!JS::IsArrayObject(context, rval, &is_array)) goto out; if (!is_array) { JSFunction* fn = gjs_closure_get_callable(trampoline->js_function); gjs_throw(context, "Function %s (%s.%s) returned unexpected value, " "expecting an Array", gjs_debug_string(JS_GetFunctionDisplayId(fn)).c_str(), g_base_info_get_namespace(trampoline->info), g_base_info_get_name(trampoline->info)); goto out; } JS::RootedValue elem(context); JS::RootedObject out_array(context, rval.toObjectOrNull()); gsize elem_idx = 0; /* more than one of a return value or an out argument. * Should be an array of output values. */ if (!ret_type_is_void) { GIArgument argument; GITransfer transfer = g_callable_info_get_caller_owns(trampoline->info); if (!JS_GetElement(context, out_array, elem_idx, &elem)) goto out; if (!gjs_value_to_g_argument(context, elem, &ret_type, "callback", GJS_ARGUMENT_RETURN_VALUE, transfer, true, &argument)) goto out; set_return_ffi_arg_from_giargument(&ret_type, result, &argument); elem_idx++; } for (i = 0; i < n_args; i++) { GIArgInfo arg_info; g_callable_info_load_arg(trampoline->info, i, &arg_info); if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_IN) continue; if (!JS_GetElement(context, out_array, elem_idx, &elem)) goto out; if (!gjs_value_to_arg(context, elem, &arg_info, *(GIArgument **)args[i + c_args_offset])) goto out; elem_idx++; } } success = true; out: if (!success) { if (!JS_IsExceptionPending(context)) { /* "Uncatchable" exception thrown, we have to exit. We may be in a * main loop, or maybe not, but there's no way to tell, so we have * to exit here instead of propagating the exception back to the * original calling JS code. */ uint8_t code; if (gjs->should_exit(&code)) exit(code); /* Some other uncatchable exception, e.g. out of memory */ JSFunction* fn = gjs_closure_get_callable(trampoline->js_function); g_error("Function %s (%s.%s) terminated with uncatchable exception", gjs_debug_string(JS_GetFunctionDisplayId(fn)).c_str(), g_base_info_get_namespace(trampoline->info), g_base_info_get_name(trampoline->info)); } /* Fill in the result with some hopefully neutral value */ if (!ret_type_is_void) { GIArgument argument = {}; g_callable_info_load_return_type(trampoline->info, &ret_type); gjs_gi_argument_init_default(&ret_type, &argument); set_return_ffi_arg_from_giargument(&ret_type, result, &argument); } /* If the callback has a GError** argument and invoking the closure * returned an error, try to make a GError from it */ if (can_throw_gerror && rval.isObject()) { JS::RootedObject exc_object(context, &rval.toObject()); GError *local_error = gjs_gerror_make_from_error(context, exc_object); if (local_error) { /* the GError ** pointer is the last argument, and is not * included in the n_args */ GIArgument *error_argument = args[n_args + c_args_offset]; auto* gerror = gjs_arg_get(error_argument); g_propagate_error(gerror, local_error); JS_ClearPendingException(context); /* don't log */ } } else if (!rval.isUndefined()) { JS_SetPendingException(context, rval); } gjs_log_exception_uncaught(context); } if (trampoline->scope == GI_SCOPE_TYPE_ASYNC) { completed_trampolines = g_slist_prepend(completed_trampolines, trampoline); } gjs_callback_trampoline_unref(trampoline); gjs->schedule_gc_if_needed(); } GjsCallbackTrampoline* gjs_callback_trampoline_new( JSContext* context, JS::HandleFunction function, GICallableInfo* callable_info, GIScopeType scope, bool has_scope_object, bool is_vfunc) { GjsCallbackTrampoline *trampoline; int n_args, i; g_assert(function); trampoline = g_new(GjsCallbackTrampoline, 1); new (trampoline) GjsCallbackTrampoline(); trampoline->ref_count = 1; trampoline->info = callable_info; g_base_info_ref((GIBaseInfo*)trampoline->info); /* Analyze param types and directions, similarly to init_cached_function_data */ n_args = g_callable_info_get_n_args(trampoline->info); trampoline->param_types = g_new0(GjsParamType, n_args); for (i = 0; i < n_args; i++) { GIDirection direction; GIArgInfo arg_info; GITypeInfo type_info; GITypeTag type_tag; if (trampoline->param_types[i] == PARAM_SKIPPED) continue; g_callable_info_load_arg(trampoline->info, i, &arg_info); g_arg_info_load_type(&arg_info, &type_info); direction = g_arg_info_get_direction(&arg_info); type_tag = g_type_info_get_tag(&type_info); if (direction != GI_DIRECTION_IN) { /* INOUT and OUT arguments are handled differently. */ continue; } if (type_tag == GI_TYPE_TAG_INTERFACE) { GIBaseInfo* interface_info; GIInfoType interface_type; interface_info = g_type_info_get_interface(&type_info); interface_type = g_base_info_get_type(interface_info); if (interface_type == GI_INFO_TYPE_CALLBACK) { gjs_throw(context, "%s %s accepts another callback as a parameter. This " "is not supported", is_vfunc ? "VFunc" : "Callback", g_base_info_get_name(callable_info)); g_base_info_unref(interface_info); gjs_callback_trampoline_unref(trampoline); return NULL; } g_base_info_unref(interface_info); } else if (type_tag == GI_TYPE_TAG_ARRAY) { if (g_type_info_get_array_type(&type_info) == GI_ARRAY_TYPE_C) { int array_length_pos = g_type_info_get_array_length(&type_info); if (array_length_pos >= 0 && array_length_pos < n_args) { GIArgInfo length_arg_info; g_callable_info_load_arg(trampoline->info, array_length_pos, &length_arg_info); if (g_arg_info_get_direction(&length_arg_info) != direction) { gjs_throw(context, "%s %s has an array with different-direction " "length argument. This is not supported", is_vfunc ? "VFunc" : "Callback", g_base_info_get_name(callable_info)); gjs_callback_trampoline_unref(trampoline); return NULL; } trampoline->param_types[array_length_pos] = PARAM_SKIPPED; trampoline->param_types[i] = PARAM_ARRAY; } } } } trampoline->closure = g_callable_info_prepare_closure(callable_info, &trampoline->cif, gjs_callback_closure, trampoline); // The rule is: // - notify callbacks in GObject methods are traced from the scope object // - async and call callbacks, and other notify callbacks, are rooted // - vfuncs are traced from the GObject prototype bool should_root = scope != GI_SCOPE_TYPE_NOTIFIED || !has_scope_object; trampoline->js_function = gjs_closure_new( context, function, g_base_info_get_name(callable_info), should_root); trampoline->scope = scope; trampoline->is_vfunc = is_vfunc; return trampoline; } /* Intended for error messages. Return value must be freed */ [[nodiscard]] static char* format_function_name(Function* function) { if (g_callable_info_is_method(function->info)) return g_strdup_printf( "method %s.%s.%s", g_base_info_get_namespace(function->info), g_base_info_get_name(g_base_info_get_container(function->info)), g_base_info_get_name(function->info)); return g_strdup_printf("function %s.%s", g_base_info_get_namespace(function->info), g_base_info_get_name(function->info)); } static void complete_async_calls(void) { if (completed_trampolines) { for (GSList *iter = completed_trampolines; iter; iter = iter->next) { auto trampoline = static_cast(iter->data); gjs_callback_trampoline_unref(trampoline); } g_slist_free(completed_trampolines); completed_trampolines = nullptr; } } static void* get_return_ffi_pointer_from_giargument( GjsArgumentCache* return_arg, GIFFIReturnValue* return_value) { // This should be the inverse of gi_type_info_extract_ffi_return_value(). if (return_arg->skip_out) return nullptr; // FIXME: Note that v_long and v_ulong don't have type-safe template // overloads yet, and I don't understand why they won't compile switch (g_type_info_get_tag(&return_arg->type_info)) { case GI_TYPE_TAG_INT8: case GI_TYPE_TAG_INT16: case GI_TYPE_TAG_INT32: return &return_value->v_long; case GI_TYPE_TAG_UINT8: case GI_TYPE_TAG_UINT16: case GI_TYPE_TAG_UINT32: case GI_TYPE_TAG_BOOLEAN: case GI_TYPE_TAG_UNICHAR: return &return_value->v_ulong; case GI_TYPE_TAG_INT64: return &gjs_arg_member(return_value); case GI_TYPE_TAG_UINT64: return &gjs_arg_member(return_value); case GI_TYPE_TAG_FLOAT: return &gjs_arg_member(return_value); case GI_TYPE_TAG_DOUBLE: return &gjs_arg_member(return_value); case GI_TYPE_TAG_INTERFACE: { GjsAutoBaseInfo info = g_type_info_get_interface(&return_arg->type_info); switch (g_base_info_get_type(info)) { case GI_INFO_TYPE_ENUM: case GI_INFO_TYPE_FLAGS: return &return_value->v_long; default: return &gjs_arg_member(return_value); } break; } default: return &gjs_arg_member(return_value); } } // This function can be called in two different ways. You can either use it to // create JavaScript objects by calling it without @r_value, or you can decide // to keep the return values in #GArgument format by providing a @r_value // argument. GJS_JSAPI_RETURN_CONVENTION static bool gjs_invoke_c_function(JSContext* context, Function* function, const JS::CallArgs& args, JS::HandleObject this_obj = nullptr, GIArgument* r_value = nullptr) { g_assert((args.isConstructing() || !this_obj) && "If not a constructor, then pass the 'this' object via CallArgs"); void* return_value_p; // will point inside the return GIArgument union GIFFIReturnValue return_value; int gi_argc, gi_arg_pos; bool can_throw_gerror; bool did_throw_gerror = false; GError *local_error = NULL; bool failed, postinvoke_release_failed; bool is_method; JS::RootedValueVector return_values(context); /* Because we can't free a closure while we're in it, we defer * freeing until the next time a C function is invoked. What * we should really do instead is queue it for a GC thread. */ complete_async_calls(); is_method = g_callable_info_is_method(function->info); can_throw_gerror = g_callable_info_can_throw_gerror(function->info); unsigned ffi_argc = function->invoker.cif.nargs; gi_argc = g_callable_info_get_n_args( (GICallableInfo*) function->info); if (gi_argc > GjsArgumentCache::MAX_ARGS) { GjsAutoChar name = format_function_name(function); gjs_throw(context, "Function %s has too many arguments", name.get()); return false; } // ffi_argc is the number of arguments that the underlying C function takes. // gi_argc is the number of arguments the GICallableInfo describes (which // does not include "this" or GError**). function->js_in_argc is the number // of arguments we expect the JS function to take (which does not include // PARAM_SKIPPED args). // args.length() is the number of arguments that were actually passed. if (args.length() > function->js_in_argc) { GjsAutoChar name = format_function_name(function); if (!JS::WarnUTF8(context, "Too many arguments to %s: expected %u, got %u", name.get(), function->js_in_argc, args.length())) return false; } else if (args.length() < function->js_in_argc) { GjsAutoChar name = format_function_name(function); args.reportMoreArgsNeeded(context, name, function->js_in_argc, args.length()); return false; } // These arrays hold argument pointers. // - state.in_cvalues: C values which are passed on input (in or inout) // - state.out_cvalues: C values which are returned as arguments (out or // inout) // - state.inout_original_cvalues: For the special case of (inout) args, we // need to keep track of the original values we passed into the function, // in case we need to free it. // - ffi_arg_pointers: For passing data to FFI, we need to create another // layer of indirection; this array is a pointer to an element in // state.in_cvalues or state.out_cvalues. // - return_value: The actual return value of the C function, i.e. not an // (out) param // // The 3 GIArgument arrays are indexed by the GI argument index, with the // following exceptions: // - [-1] is the return value (which can be nothing/garbage if the function // function returns void) // - [-2] is the instance parameter, if present // ffi_arg_pointers, on the other hand, represents the actual C arguments, // in the way ffi expects them. // // Use gi_arg_pos to index inside the GIArgument array. Use ffi_arg_pos to // index inside ffi_arg_pointers. GjsFunctionCallState state(context); if (is_method) { state.in_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; state.out_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; state.inout_original_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; } else { state.in_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; state.out_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; state.inout_original_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; } void** ffi_arg_pointers = g_newa(void*, ffi_argc); failed = false; unsigned ffi_arg_pos = 0; // index into ffi_arg_pointers unsigned js_arg_pos = 0; // index into args JS::RootedObject obj(context, this_obj); if (!args.isConstructing() && !args.computeThis(context, &obj)) return false; if (is_method) { GjsArgumentCache* cache = &function->arguments[-2]; GIArgument* in_value = &state.in_cvalues[-2]; JS::RootedValue in_js_value(context, JS::ObjectValue(*obj)); if (!cache->marshallers->in(context, cache, &state, in_value, in_js_value)) return false; ffi_arg_pointers[ffi_arg_pos] = in_value; ++ffi_arg_pos; // Callback lifetimes will be attached to the instance object if it is // a GObject or GInterface if (cache->contents.info) { GType gtype = g_registered_type_info_get_g_type(cache->contents.info); if (g_type_is_a(gtype, G_TYPE_OBJECT) || g_type_is_a(gtype, G_TYPE_INTERFACE)) state.instance_object = obj; } } unsigned processed_c_args = ffi_arg_pos; for (gi_arg_pos = 0; gi_arg_pos < gi_argc; gi_arg_pos++, ffi_arg_pos++) { GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; GIArgument* in_value = &state.in_cvalues[gi_arg_pos]; gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Marshalling argument '%s' in, %d/%d GI args, %u/%u " "C args, %u/%u JS args", cache->arg_name, gi_arg_pos, gi_argc, ffi_arg_pos, ffi_argc, js_arg_pos, args.length()); ffi_arg_pointers[ffi_arg_pos] = in_value; if (!cache->marshallers->in) { gjs_throw(context, "Error invoking %s.%s: impossible to determine what " "to pass to the '%s' argument. It may be that the " "function is unsupported, or there may be a bug in " "its annotations.", g_base_info_get_namespace(function->info), g_base_info_get_name(function->info), cache->arg_name); failed = true; break; } JS::RootedValue js_in_arg(context); if (js_arg_pos < args.length()) js_in_arg = args[js_arg_pos]; if (!cache->marshallers->in(context, cache, &state, in_value, js_in_arg)) { failed = true; break; } if (!cache->skip_in) js_arg_pos++; processed_c_args++; } // This pointer needs to exist on the stack across the ffi_call() call GError** errorp = &local_error; /* Did argument conversion fail? In that case, skip invocation and jump to release * processing. */ if (failed) { did_throw_gerror = false; goto release; } if (can_throw_gerror) { g_assert(ffi_arg_pos < ffi_argc && "GError** argument number mismatch"); ffi_arg_pointers[ffi_arg_pos] = &errorp; ffi_arg_pos++; /* don't update processed_c_args as we deal with local_error * separately */ } g_assert_cmpuint(ffi_arg_pos, ==, ffi_argc); g_assert_cmpuint(gi_arg_pos, ==, gi_argc); return_value_p = get_return_ffi_pointer_from_giargument( &function->arguments[-1], &return_value); ffi_call(&(function->invoker.cif), FFI_FN(function->invoker.native_address), return_value_p, ffi_arg_pointers); /* Return value and out arguments are valid only if invocation doesn't * return error. In arguments need to be released always. */ if (can_throw_gerror) { did_throw_gerror = local_error != NULL; } else { did_throw_gerror = false; } if (!r_value) args.rval().setUndefined(); if (!function->arguments[-1].skip_out) { gi_type_info_extract_ffi_return_value( &function->arguments[-1].type_info, &return_value, &state.out_cvalues[-1]); } // Process out arguments and return values. This loop is skipped if we fail // the type conversion above, or if did_throw_gerror is true. js_arg_pos = 0; for (gi_arg_pos = -1; gi_arg_pos < gi_argc; gi_arg_pos++) { GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; GIArgument* out_value = &state.out_cvalues[gi_arg_pos]; gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Marshalling argument '%s' out, %d/%d GI args", cache->arg_name, gi_arg_pos, gi_argc); JS::RootedValue js_out_arg(context); if (!r_value) { if (!cache->marshallers->out(context, cache, &state, out_value, &js_out_arg)) { failed = true; break; } } if (!cache->skip_out) { if (!r_value) { if (!return_values.append(js_out_arg)) { JS_ReportOutOfMemory(context); failed = true; break; } } js_arg_pos++; } } g_assert(failed || did_throw_gerror || js_arg_pos == function->js_out_argc); release: // If we failed before calling the function, or if the function threw an // exception, then any GI_TRANSFER_EVERYTHING or GI_TRANSFER_CONTAINER // in-parameters were not transferred. Treat them as GI_TRANSFER_NOTHING so // that they are freed. if (!failed && !did_throw_gerror) state.call_completed = true; // In this loop we use ffi_arg_pos just to ensure we don't release stuff // we haven't allocated yet, if we failed in type conversion above. // If we start from -1 (the return value), we need to process 1 more than // processed_c_args. // If we start from -2 (the instance parameter), we need to process 2 more ffi_arg_pos = is_method ? 1 : 0; unsigned ffi_arg_max = processed_c_args + (is_method ? 2 : 1); postinvoke_release_failed = false; for (gi_arg_pos = is_method ? -2 : -1; gi_arg_pos < gi_argc && ffi_arg_pos < ffi_arg_max; gi_arg_pos++, ffi_arg_pos++) { GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; GIArgument* in_value = &state.in_cvalues[gi_arg_pos]; GIArgument* out_value = &state.out_cvalues[gi_arg_pos]; gjs_debug_marshal( GJS_DEBUG_GFUNCTION, "Releasing argument '%s', %d/%d GI args, %u/%u C args", cache->arg_name, gi_arg_pos, gi_argc, ffi_arg_pos, processed_c_args); // Only process in or inout arguments if we failed, the rest is garbage if (failed && cache->skip_in) continue; // Save the return GIArgument if it was requested if (r_value && gi_arg_pos == -1) { *r_value = *out_value; continue; } if (!cache->marshallers->release(context, cache, &state, in_value, out_value)) { postinvoke_release_failed = true; // continue with the release even if we fail, to avoid leaks } } if (postinvoke_release_failed) failed = true; g_assert(ffi_arg_pos == processed_c_args + (is_method ? 2 : 1)); if (!r_value && function->js_out_argc > 0 && (!failed && !did_throw_gerror)) { // If we have one return value or out arg, return that item on its // own, otherwise return a JavaScript array with [return value, // out arg 1, out arg 2, ...] if (function->js_out_argc == 1) { args.rval().set(return_values[0]); } else { JSObject* array = JS::NewArrayObject(context, return_values); if (!array) { failed = true; } else { args.rval().setObject(*array); } } } if (!failed && did_throw_gerror) { return gjs_throw_gerror(context, local_error); } else if (failed) { return false; } else { return true; } } GJS_JSAPI_RETURN_CONVENTION static bool function_call(JSContext *context, unsigned js_argc, JS::Value *vp) { JS::CallArgs js_argv = JS::CallArgsFromVp(js_argc, vp); JS::RootedObject callee(context, &js_argv.callee()); Function *priv; priv = priv_from_js(context, callee); gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Call callee %p priv %p", callee.get(), priv); if (priv == NULL) return true; /* we are the prototype, or have the wrong class */ return gjs_invoke_c_function(context, priv, js_argv); } GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(function) /* Does not actually free storage for structure, just * reverses init_cached_function_data */ static void uninit_cached_function_data (Function *function) { if (function->arguments) { g_assert(function->info && "Don't know how to free cache without GI info"); // Careful! function->arguments is offset by one or two elements inside // the allocated space, so we have to free index -1 or -2. int start_index = g_callable_info_is_method(function->info) ? -2 : -1; int gi_argc = MIN(g_callable_info_get_n_args(function->info), function->js_in_argc + function->js_out_argc); for (int i = 0; i < gi_argc; i++) { int ix = start_index + i; if (!function->arguments[ix].marshallers) break; if (function->arguments[ix].marshallers->free) function->arguments[ix].marshallers->free( &function->arguments[ix]); } g_free(&function->arguments[start_index]); function->arguments = nullptr; } g_clear_pointer(&function->info, g_base_info_unref); g_function_invoker_destroy(&function->invoker); } static void function_finalize(JSFreeOp*, JSObject* obj) { Function *priv; priv = (Function *) JS_GetPrivate(obj); gjs_debug_lifecycle(GJS_DEBUG_GFUNCTION, "finalize, obj %p priv %p", obj, priv); if (priv == NULL) return; /* we are the prototype, not a real instance, so constructor never called */ uninit_cached_function_data(priv); GJS_DEC_COUNTER(function); g_free(priv); } GJS_JSAPI_RETURN_CONVENTION static bool get_num_arguments (JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, rec, to, Function, priv); rec.rval().setInt32(priv->js_in_argc); return true; } GJS_JSAPI_RETURN_CONVENTION static bool function_to_string (JSContext *context, guint argc, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, rec, to, Function, priv); int i, n_args, n_jsargs; GString *arg_names_str; gchar *arg_names; if (priv == NULL) { JSString* retval = JS_NewStringCopyZ(context, "function () {\n}"); if (!retval) return false; rec.rval().setString(retval); return true; } n_args = g_callable_info_get_n_args(priv->info); n_jsargs = 0; arg_names_str = g_string_new(""); for (i = 0; i < n_args; i++) { if (priv->arguments[i].skip_in) continue; if (n_jsargs > 0) g_string_append(arg_names_str, ", "); n_jsargs++; g_string_append(arg_names_str, priv->arguments[i].arg_name); } arg_names = g_string_free(arg_names_str, false); GjsAutoChar descr; if (g_base_info_get_type(priv->info) == GI_INFO_TYPE_FUNCTION) { descr = g_strdup_printf( "function %s(%s) {\n\t/* wrapper for native symbol %s(); */\n}", g_base_info_get_name(priv->info), arg_names, g_function_info_get_symbol(priv->info)); } else { descr = g_strdup_printf( "function %s(%s) {\n\t/* wrapper for native symbol */\n}", g_base_info_get_name(priv->info), arg_names); } g_free(arg_names); return gjs_string_from_utf8(context, descr, rec.rval()); } /* The bizarre thing about this vtable is that it applies to both * instances of the object, and to the prototype that instances of the * class have. */ static const struct JSClassOps gjs_function_class_ops = { nullptr, // addProperty nullptr, // deleteProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve function_finalize, function_call}; struct JSClass gjs_function_class = { "GIRepositoryFunction", /* means "new GIRepositoryFunction()" works */ JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE, &gjs_function_class_ops }; static JSPropertySpec gjs_function_proto_props[] = { JS_PSG("length", get_num_arguments, JSPROP_PERMANENT), JS_STRING_SYM_PS(toStringTag, "GIRepositoryFunction", JSPROP_READONLY), JS_PS_END}; /* The original Function.prototype.toString complains when given a GIRepository function as an argument */ static JSFunctionSpec gjs_function_proto_funcs[] = { JS_FN("toString", function_to_string, 0, 0), JS_FS_END }; static JSFunctionSpec *gjs_function_static_funcs = nullptr; GJS_JSAPI_RETURN_CONVENTION static bool init_cached_function_data (JSContext *context, Function *function, GType gtype, GICallableInfo *info) { guint8 i, n_args; GError *error = NULL; GIInfoType info_type; info_type = g_base_info_get_type((GIBaseInfo *)info); if (info_type == GI_INFO_TYPE_FUNCTION) { if (!g_function_info_prep_invoker((GIFunctionInfo *)info, &(function->invoker), &error)) { return gjs_throw_gerror(context, error); } } else if (info_type == GI_INFO_TYPE_VFUNC) { gpointer addr; addr = g_vfunc_info_get_address((GIVFuncInfo *)info, gtype, &error); if (error != NULL) { if (error->code != G_INVOKE_ERROR_SYMBOL_NOT_FOUND) return gjs_throw_gerror(context, error); gjs_throw(context, "Virtual function not implemented: %s", error->message); g_clear_error(&error); return false; } if (!g_function_invoker_new_for_address(addr, info, &(function->invoker), &error)) { return gjs_throw_gerror(context, error); } } bool is_method = g_callable_info_is_method(info); n_args = g_callable_info_get_n_args((GICallableInfo*) info); // arguments is one or two inside an array of n_args + 2, so // arguments[-1] is the return value (which can be skipped if void) // arguments[-2] is the instance parameter size_t offset = is_method ? 2 : 1; GjsArgumentCache* arguments = g_new0(GjsArgumentCache, n_args + offset) + offset; function->arguments = arguments; function->info = g_base_info_ref(info); function->js_in_argc = 0; function->js_out_argc = 0; if (is_method && !gjs_arg_cache_build_instance(context, &arguments[-2], info)) return false; bool inc_counter; if (!gjs_arg_cache_build_return(context, &arguments[-1], arguments, info, &inc_counter)) return false; function->js_out_argc = inc_counter ? 1 : 0; for (i = 0; i < n_args; i++) { GIDirection direction; GIArgInfo arg_info; if (arguments[i].skip_in || arguments[i].skip_out) continue; g_callable_info_load_arg((GICallableInfo*) info, i, &arg_info); direction = g_arg_info_get_direction(&arg_info); if (!gjs_arg_cache_build_arg(context, &arguments[i], arguments, i, direction, &arg_info, info, &inc_counter)) return false; if (inc_counter) { switch (direction) { case GI_DIRECTION_INOUT: function->js_out_argc++; [[fallthrough]]; case GI_DIRECTION_IN: function->js_in_argc++; break; case GI_DIRECTION_OUT: function->js_out_argc++; break; default: g_assert_not_reached(); } } } return true; } [[nodiscard]] static inline JSObject* gjs_builtin_function_get_proto( JSContext* cx) { return JS::GetRealmFunctionPrototype(cx); } GJS_DEFINE_PROTO_FUNCS_WITH_PARENT(function, builtin_function) GJS_JSAPI_RETURN_CONVENTION static JSObject* function_new(JSContext *context, GType gtype, GICallableInfo *info) { Function *priv; JS::RootedObject proto(context); if (!gjs_function_define_proto(context, nullptr, &proto)) return nullptr; JS::RootedObject function(context, JS_NewObjectWithGivenProto(context, &gjs_function_class, proto)); if (!function) { gjs_debug(GJS_DEBUG_GFUNCTION, "Failed to construct function"); return NULL; } priv = g_new0(Function, 1); GJS_INC_COUNTER(function); g_assert(priv_from_js(context, function) == NULL); JS_SetPrivate(function, priv); gjs_debug_lifecycle(GJS_DEBUG_GFUNCTION, "function constructor, obj %p priv %p", function.get(), priv); if (!init_cached_function_data(context, priv, gtype, (GICallableInfo *)info)) return NULL; return function; } GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_define_function(JSContext *context, JS::HandleObject in_object, GType gtype, GICallableInfo *info) { GIInfoType info_type; gchar *name; bool free_name; info_type = g_base_info_get_type((GIBaseInfo *)info); JS::RootedObject function(context, function_new(context, gtype, info)); if (!function) return NULL; if (info_type == GI_INFO_TYPE_FUNCTION) { name = (gchar *) g_base_info_get_name((GIBaseInfo*) info); free_name = false; } else if (info_type == GI_INFO_TYPE_VFUNC) { name = g_strdup_printf("vfunc_%s", g_base_info_get_name((GIBaseInfo*) info)); free_name = true; } else { g_assert_not_reached (); } if (!JS_DefineProperty(context, in_object, name, function, GJS_MODULE_PROP_FLAGS)) { gjs_debug(GJS_DEBUG_GFUNCTION, "Failed to define function"); function = NULL; } if (free_name) g_free(name); return function; } bool gjs_invoke_constructor_from_c(JSContext* context, GIFunctionInfo* info, JS::HandleObject obj, const JS::CallArgs& args, GIArgument* rvalue) { Function function; memset(&function, 0, sizeof(Function)); if (!init_cached_function_data(context, &function, 0, info)) return false; bool result = gjs_invoke_c_function(context, &function, args, obj, rvalue); uninit_cached_function_data(&function); return result; }