[gjs/wip/ptomato/mozjs31prep: 5/8] jsapi-util: Rooting-safe gjs_parse_call_args()



commit 30e41fb75880c372f5e5f0b8d2f2b24348bec766
Author: Philip Chimento <philip chimento gmail com>
Date:   Sun Oct 16 22:11:32 2016 -0700

    jsapi-util: Rooting-safe gjs_parse_call_args()
    
    This removes the old unused gjs_parse_args(), and moves
    gjs_parse_call_args() into new files jsapi-util-args.cpp and
    jsapi-util-args.h.
    
    In order to ensure that JSObjects unpacked from gjs_parse_call_args()
    land in GC roots, we need to add some type safety to its variable
    arguments. Instead of accepting a JSObject** pointer for the "o" format
    character, it must accept a JS::MutableHandleObject.
    
    We can do this (and make the whole thing type safe as well) by using C++
    templates instead of C varargs. Now we can issue a compile-time error
    when an unknown type is passed in as a return location for an argument,
    in particular JSObject**.
    
    This also fixes some undefined behaviour: the old signature of
    gjs_parse_call_args() placed the varargs parameter right after the
    JS::CallArgs& parameter, and using a reference parameter in va_start() is
    undefined. Here's a good explanation of what can go wrong:
    http://stackoverflow.com/a/222288/172999
    
    https://bugzilla.gnome.org/show_bug.cgi?id=742249

 Makefile-test.am                  |    1 +
 Makefile.am                       |    2 +
 gi/object.cpp                     |   37 ++--
 gjs/byteArray.cpp                 |    5 +-
 gjs/coverage.cpp                  |    5 +-
 gjs/jsapi-util-args.cpp           |  212 +++++++++++++++++++
 gjs/jsapi-util-args.h             |  258 ++++++++++++++++++++++
 gjs/jsapi-util.cpp                |  267 -----------------------
 gjs/jsapi-util.h                  |   13 --
 modules/cairo-context.cpp         |  103 +++++-----
 modules/cairo-gradient.cpp        |   23 +-
 modules/cairo-image-surface.cpp   |   13 +-
 modules/cairo-linear-gradient.cpp |   11 +-
 modules/cairo-pdf-surface.cpp     |    9 +-
 modules/cairo-ps-surface.cpp      |    9 +-
 modules/cairo-radial-gradient.cpp |   15 +-
 modules/cairo-region.cpp          |   18 +-
 modules/cairo-solid-pattern.cpp   |   19 +-
 modules/cairo-surface-pattern.cpp |   15 +-
 modules/cairo-surface.cpp         |    5 +-
 modules/cairo-svg-surface.cpp     |    9 +-
 modules/system.cpp                |   19 +-
 test/gjs-test-call-args.cpp       |  423 +++++++++++++++++++++++++++++++++++++
 test/gjs-tests-add-funcs.h        |    2 +
 test/gjs-tests.cpp                |    1 +
 25 files changed, 1066 insertions(+), 428 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index bd6beb4..428fc8f 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -92,6 +92,7 @@ gjs_tests_LDADD =             \
 gjs_tests_SOURCES =                                    \
        test/gjs-tests.cpp                              \
        test/gjs-tests-add-funcs.h                      \
+       test/gjs-test-call-args.cpp                     \
        test/gjs-test-coverage.cpp                      \
        mock-js-resources.c                             \
        $(NULL)
diff --git a/Makefile.am b/Makefile.am
index b4c15c9..3303a30 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -82,6 +82,8 @@ libgjs_la_SOURCES =           \
        gjs/jsapi-private.h             \
        gjs/jsapi-util.cpp      \
        gjs/jsapi-dynamic-class.cpp \
+       gjs/jsapi-util-args.cpp         \
+       gjs/jsapi-util-args.h           \
        gjs/jsapi-util-error.cpp        \
        gjs/jsapi-util-string.cpp       \
        gjs/mem.cpp             \
diff --git a/gi/object.cpp b/gi/object.cpp
index 0fc1c9e..05c7a8a 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -30,6 +30,7 @@
 #include "object.h"
 #include "gtype.h"
 #include "interface.h"
+#include "gjs/jsapi-util-args.h"
 #include "arg.h"
 #include "repo.h"
 #include "gtype.h"
@@ -2234,8 +2235,7 @@ gjs_hook_up_vfunc(JSContext *cx,
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
     gchar *name;
-    JS::RootedObject object(cx);
-    JSObject *function;
+    JS::RootedObject object(cx), function(cx);
     ObjectInstance *priv;
     GType gtype, info_gtype;
     GIObjectInfo *info;
@@ -2243,11 +2243,10 @@ gjs_hook_up_vfunc(JSContext *cx,
     gpointer implementor_vtable;
     GIFieldInfo *field_info;
 
-    if (!gjs_parse_call_args(cx, "hook_up_vfunc",
-                        "oso", argv,
-                        "object", object.address(),
-                        "name", &name,
-                        "function", &function))
+    if (!gjs_parse_call_args(cx, "hook_up_vfunc", argv, "oso",
+                             "object", &object,
+                             "name", &name,
+                             "function", &function))
         return false;
 
     if (!do_base_typecheck(cx, object, true))
@@ -2492,9 +2491,9 @@ gjs_override_property(JSContext *cx,
     GParamSpec *new_pspec;
     GType gtype;
 
-    if (!gjs_parse_call_args(cx, "override_property", "so", args,
+    if (!gjs_parse_call_args(cx, "override_property", args, "so",
                              "name", &name,
-                             "type", type.address()))
+                             "type", &type))
         return false;
 
     if ((gtype = gjs_gtype_get_actual_gtype(cx, type)) == G_TYPE_INVALID) {
@@ -2749,7 +2748,8 @@ gjs_register_interface(JSContext *cx,
 {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
     char *name = NULL;
-    JSObject *constructor, *interfaces, *properties, *module;
+    JS::RootedObject interfaces(cx), properties(cx);
+    JSObject *constructor, *module;
     guint32 i, n_interfaces, n_properties;
     GType *iface_types;
     GType interface_type;
@@ -2769,7 +2769,7 @@ gjs_register_interface(JSContext *cx,
         NULL, /* instance_init */
     };
 
-    if (!gjs_parse_call_args(cx, "register_interface", "soo", args,
+    if (!gjs_parse_call_args(cx, "register_interface", args, "soo",
                              "name", &name,
                              "interfaces", &interfaces,
                              "properties", &properties))
@@ -2828,8 +2828,8 @@ gjs_register_type(JSContext *cx,
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
     gchar *name;
-    JS::RootedObject parent(cx);
-    JSObject *constructor, *interfaces, *properties, *module;
+    JS::RootedObject parent(cx), interfaces(cx), properties(cx);
+    JSObject *constructor, *module;
     GType instance_type, parent_type;
     GTypeQuery query;
     GTypeModule *type_module;
@@ -2854,12 +2854,11 @@ gjs_register_type(JSContext *cx,
 
     JS_BeginRequest(cx);
 
-    if (!gjs_parse_call_args(cx, "register_type",
-                        "osoo", argv,
-                        "parent", parent.address(),
-                        "name", &name,
-                        "interfaces", &interfaces,
-                        "properties", &properties))
+    if (!gjs_parse_call_args(cx, "register_type", argv, "osoo",
+                             "parent", &parent,
+                             "name", &name,
+                             "interfaces", &interfaces,
+                             "properties", &properties))
         goto out;
 
     if (!parent)
diff --git a/gjs/byteArray.cpp b/gjs/byteArray.cpp
index 204b5ef..6287b79 100644
--- a/gjs/byteArray.cpp
+++ b/gjs/byteArray.cpp
@@ -27,6 +27,7 @@
 #include "byteArray.h"
 #include "gi/boxed.h"
 #include "compat.h"
+#include "jsapi-util-args.h"
 #include <girepository.h>
 #include <util/log.h>
 
@@ -712,8 +713,8 @@ from_gbytes_func(JSContext *context,
     GBytes *gbytes;
     ByteArrayInstance *priv;
 
-    if (!gjs_parse_call_args(context, "overrides_gbytes_to_array", "o", argv,
-                             "bytes", bytes_obj.address()))
+    if (!gjs_parse_call_args(context, "overrides_gbytes_to_array", argv, "o",
+                             "bytes", &bytes_obj))
         return false;
 
     if (!gjs_typecheck_boxed(context, bytes_obj, NULL, G_TYPE_BYTES, true))
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
index a415a13..888a0aa 100644
--- a/gjs/coverage.cpp
+++ b/gjs/coverage.cpp
@@ -26,6 +26,7 @@
 #include "coverage.h"
 #include "coverage-internal.h"
 #include "importer.h"
+#include "jsapi-util-args.h"
 #include "util/error.h"
 
 struct _GjsCoveragePrivate {
@@ -1383,7 +1384,7 @@ get_filename_from_filename_as_js_string(JSContext    *context,
                                         JS::CallArgs &args) {
     char *filename = NULL;
 
-    if (!gjs_parse_call_args(context, "getFileContents", "s", args,
+    if (!gjs_parse_call_args(context, "getFileContents", args, "s",
                              "filename", &filename))
         return NULL;
 
@@ -1482,7 +1483,7 @@ coverage_get_file_contents(JSContext *context,
     JSString *script_jsstr;
     GError *error = NULL;
 
-    if (!gjs_parse_call_args(context, "getFileContents", "s", args,
+    if (!gjs_parse_call_args(context, "getFileContents", args, "s",
                              "filename", &filename))
         goto out;
 
diff --git a/gjs/jsapi-util-args.cpp b/gjs/jsapi-util-args.cpp
new file mode 100644
index 0000000..f4f8977
--- /dev/null
+++ b/gjs/jsapi-util-args.cpp
@@ -0,0 +1,212 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright © 2016 Endless Mobile, Inc.
+ *
+ * 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.
+ *
+ * Authored by: Philip Chimento <philip endlessm com>
+ */
+
+#include <glib.h>
+
+#include "compat.h"
+#include "jsapi-util-args.h"
+
+/* This is the non-templated part of jsapi-util-args.h */
+
+/* This preserves the previous behaviour of gjs_parse_args(), but maybe we want
+ * to use JS::ToBoolean instead? */
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       bool           *ref)
+{
+    if (c != 'b')
+        throw g_strdup_printf("Wrong type for %c, got bool*", c);
+    if (!value.isBoolean())
+        throw g_strdup("Not a boolean");
+    if (nullable)
+        throw g_strdup("Invalid format string combination ?b");
+    *ref = value.toBoolean();
+}
+
+/* This preserves the previous behaviour of gjs_parse_args(), but maybe we want
+ * to box primitive types instead of throwing? */
+void
+assign(JSContext              *cx,
+       const char              c,
+       bool                    nullable,
+       JS::HandleValue         value,
+       JS::MutableHandleObject ref)
+{
+    if (c != 'o')
+        throw g_strdup_printf("Wrong type for %c, got JS::MutableHandleObject", c);
+    if (nullable && value.isNull()) {
+        ref.set(NULL);
+        return;
+    }
+    if (!value.isObject())
+        throw g_strdup("Not an object");
+    ref.set(&value.toObject());
+}
+
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       char          **ref)
+{
+    if (nullable && (c == 's' || c == 'F') && value.isNull()) {
+        *ref = NULL;
+        return;
+    }
+    if (c == 's') {
+        if (!gjs_string_to_utf8(cx, value, ref))
+            throw g_strdup("Couldn't convert to string");
+    } else if (c == 'F') {
+        if (!gjs_string_to_filename(cx, value, ref))
+            throw g_strdup("Couldn't convert to filename");
+    } else {
+        throw g_strdup_printf("Wrong type for %c, got char**", c);
+    }
+}
+
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       int32_t        *ref)
+{
+    if (c != 'i')
+        throw g_strdup_printf("Wrong type for %c, got int32_t*", c);
+    if (nullable)
+        throw g_strdup("Invalid format string combination ?i");
+    if (!JS::ToInt32(cx, value, ref))
+        throw g_strdup("Couldn't convert to integer");
+}
+
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       uint32_t       *ref)
+{
+    double num;
+
+    if (c != 'u')
+        throw g_strdup_printf("Wrong type for %c, got uint32_t*", c);
+    if (nullable)
+        throw g_strdup("Invalid format string combination ?u");
+    if (!value.isNumber() || !JS::ToNumber(cx, value, &num))
+        throw g_strdup("Couldn't convert to unsigned integer");
+    if (num > G_MAXUINT32 || num < 0)
+        throw g_strdup_printf("Value %f is out of range", num);
+    *ref = num;
+}
+
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       int64_t        *ref)
+{
+    if (c != 't')
+        throw g_strdup_printf("Wrong type for %c, got int64_t*", c);
+    if (nullable)
+        throw g_strdup("Invalid format string combination ?t");
+    if (!JS::ToInt64(cx, value, ref))
+        throw g_strdup("Couldn't convert to 64-bit integer");
+}
+
+void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       double         *ref)
+{
+    if (c != 'f')
+        throw g_strdup_printf("Wrong type for %c, got double*", c);
+    if (nullable)
+        throw g_strdup("Invalid format string combination ?f");
+    if (!JS::ToNumber(cx, value, ref))
+        throw g_strdup("Couldn't convert to double");
+}
+
+bool
+check_nullable(const char*& fchar,
+               const char*& fmt_string)
+{
+    if (*fchar != '?')
+        return false;
+
+    fchar++;
+    fmt_string++;
+    g_assert(((void) "Invalid format string, parameter required after '?'",
+              *fchar != '\0'));
+    return true;
+}
+
+void
+free_if_necessary(JS::MutableHandleObject param_ref)
+{
+    /* This is not exactly right, since before we consumed a JS::ObjectValue
+     * there may have been something different inside the handle. But it has
+     * already been clobbered at this point anyhow */
+    param_ref.set(NULL);
+}
+
+void
+free_if_necessary(char **param_ref)
+{
+    g_free(*param_ref);
+}
+
+/* Empty-args version of the template in jsapi-util-args.h */
+bool
+gjs_parse_call_args(JSContext    *cx,
+                    const char   *function_name,
+                    JS::CallArgs& args,
+                    const char   *format)
+{
+    bool ignore_trailing_args = false;
+
+    if (*format == '!') {
+        ignore_trailing_args = true;
+        format++;
+    }
+
+    g_assert(((void) "Wrong number of parameters passed to gjs_parse_call_args()",
+              *format == '\0'));
+
+    if (!ignore_trailing_args && args.length() > 0) {
+        JSAutoRequest ar(cx);
+        gjs_throw(cx, "Error invoking %s: Expected 0 arguments, got %d",
+                  function_name, args.length());
+        return false;
+    }
+
+    return true;
+}
diff --git a/gjs/jsapi-util-args.h b/gjs/jsapi-util-args.h
new file mode 100644
index 0000000..9cbc355
--- /dev/null
+++ b/gjs/jsapi-util-args.h
@@ -0,0 +1,258 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright © 2016 Endless Mobile, Inc.
+ *
+ * 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.
+ *
+ * Authored by: Philip Chimento <philip endlessm com>
+ */
+
+#include <type_traits>
+
+#include <glib.h>
+
+#include "compat.h"
+
+bool check_nullable(const char*& fchar, const char*& fmt_string);
+
+void assign(JSContext *, const char, bool, JS::HandleValue, bool *);
+void assign(JSContext *, const char, bool, JS::HandleValue, JS::MutableHandleObject);
+void assign(JSContext *, const char, bool, JS::HandleValue, char **);
+void assign(JSContext *, const char, bool, JS::HandleValue, int32_t *);
+void assign(JSContext *, const char, bool, JS::HandleValue, uint32_t *);
+void assign(JSContext *, const char, bool, JS::HandleValue, int64_t *);
+void assign(JSContext *, const char, bool, JS::HandleValue, double *);
+
+/* Special case: treat pointer-to-enum as pointer-to-int, but use enable_if to
+ * prevent instantiation for any other types besides pointer-to-enum */
+template<typename T,
+         typename std::enable_if<std::is_enum<T>::value, int>::type = 0>
+static void
+assign(JSContext      *cx,
+       const char      c,
+       bool            nullable,
+       JS::HandleValue value,
+       T              *ref)
+{
+    /* Sadly, we cannot use std::underlying_type<T> here; the underlying type of
+     * an enum is implementation-defined, so it would not be clear what letter
+     * to use in the format string. For the same reason, we can only support
+     * enum types that are the same width as int.
+     * Additionally, it would be nice to be able to check whether the resulting
+     * value was in range for the enum, but that is not possible (yet?) */
+    static_assert(sizeof(T) == sizeof(int),
+                  "Short or wide enum types not supported");
+    assign(cx, c, nullable, value, (int *)ref);
+}
+
+/* Force JS::RootedObject * to be converted to JS::MutableHandleObject,
+ * see overload in jsapi-util-args.cpp */
+template<typename T,
+         typename std::enable_if<!std::is_same<T, JS::RootedObject *>::value, int>::type = 0>
+static void
+free_if_necessary(T param_ref) {}
+
+void free_if_necessary(JS::MutableHandleObject);
+void free_if_necessary(char **);
+
+template<typename T>
+static bool
+parse_call_args_helper(JSContext    *cx,
+                       const char   *function_name,
+                       JS::CallArgs& args,
+                       bool          ignore_trailing_args,
+                       const char*&  fmt_required,
+                       const char*&  fmt_optional,
+                       unsigned      param_ix,
+                       const char   *param_name,
+                       T             param_ref)
+{
+    bool nullable = false;
+    const char *fchar = fmt_required;
+
+    g_return_val_if_fail (param_name != NULL, false);
+
+    if (*fchar != '\0') {
+        nullable = check_nullable(fchar, fmt_required);
+        fmt_required++;
+    } else {
+        /* No more args passed in JS, only optional formats left */
+        if (args.length() <= param_ix)
+            return true;
+
+        fchar = fmt_optional;
+        g_assert(((void) "Wrong number of parameters passed to gjs_parse_call_args()",
+                  *fchar != '\0'));
+        nullable = check_nullable(fchar, fmt_optional);
+        fmt_optional++;
+    }
+
+    try {
+        /* COMPAT: JS::CallArgs::operator[] will yield Handle in mozjs31 */
+        assign(cx, *fchar, nullable, args.handleOrUndefinedAt(param_ix), param_ref);
+    } catch (char *message) {
+        /* Our error messages are going to be more useful than whatever was
+         * thrown by the various conversion functions */
+        JS_ClearPendingException(cx);
+        gjs_throw(cx, "Error invoking %s, at argument %d (%s): %s",
+                  function_name, param_ix, param_name, message);
+        g_free(message);
+        return false;
+    }
+
+    return true;
+}
+
+template<typename T, typename... Args>
+static bool
+parse_call_args_helper(JSContext    *cx,
+                       const char   *function_name,
+                       JS::CallArgs& args,
+                       bool          ignore_trailing_args,
+                       const char*&  fmt_required,
+                       const char*&  fmt_optional,
+                       unsigned      param_ix,
+                       const char   *param_name,
+                       T             param_ref,
+                       Args       ...params)
+{
+    bool retval;
+
+    if (!parse_call_args_helper(cx, function_name, args, ignore_trailing_args,
+                                fmt_required, fmt_optional, param_ix,
+                                param_name, param_ref))
+        return false;
+
+    retval = parse_call_args_helper(cx, function_name, args,
+                                    ignore_trailing_args,
+                                    fmt_required, fmt_optional, ++param_ix,
+                                    params...);
+
+    /* We still own the strings in the error case, free any we converted */
+    if (!retval)
+        free_if_necessary(param_ref);
+    return retval;
+}
+
+bool gjs_parse_call_args(JSContext    *cx,
+                         const char   *function_name,
+                         JS::CallArgs& args,
+                         const char   *format);
+
+/**
+ * gjs_parse_call_args:
+ * @context:
+ * @function_name: The name of the function being called
+ * @format: Printf-like format specifier containing the expected arguments
+ * @args: #JS::CallArgs from #JSNative function
+ * @params: for each character in @format, a pair of const char * which is the
+ * name of the argument, and a location to store the value. The type of
+ * location argument depends on the format character, as described below.
+ *
+ * This function is inspired by Python's PyArg_ParseTuple for those
+ * familiar with it.  It takes a format specifier which gives the
+ * types of the expected arguments, and a list of argument names and
+ * value location pairs.  The currently accepted format specifiers are:
+ *
+ * b: A boolean (pass a bool *)
+ * s: A string, converted into UTF-8 (pass a char **)
+ * F: A string, converted into "filename encoding" (i.e. active locale) (pass
+ *   a char **)
+ * i: A number, will be converted to a 32-bit int (pass an int32_t * or a
+ *   pointer to an enum type)
+ * u: A number, converted into a 32-bit unsigned int (pass a uint32_t *)
+ * t: A 64-bit number, converted into a 64-bit int (pass an int64_t *)
+ * f: A number, will be converted into a double (pass a double *)
+ * o: A JavaScript object (pass a JS::MutableHandleObject)
+ *
+ * If the first character in the format string is a '!', then JS is allowed
+ * to pass extra arguments that are ignored, to the function.
+ *
+ * The '|' character introduces optional arguments.  All format specifiers
+ * after a '|' when not specified, do not cause any changes in the C
+ * value location.
+ *
+ * A prefix character '?' in front of 's', 'F', or 'o' means that the next value
+ * may be null. For 's' or 'F' a null pointer is returned, for 'o' the handle is
+ * set to null.
+ */
+template<typename... Args>
+static bool
+gjs_parse_call_args(JSContext    *cx,
+                    const char   *function_name,
+                    JS::CallArgs& args,
+                    const char   *format,
+                    Args       ...params)
+{
+    const char *fmt_iter, *fmt_required, *fmt_optional;
+    unsigned n_required = 0, n_total = 0;
+    bool ignore_trailing_args = false, retval;
+    char **parts;
+
+    if (*format == '!') {
+        ignore_trailing_args = true;
+        format++;
+    }
+
+    for (fmt_iter = format; *fmt_iter; fmt_iter++) {
+        switch (*fmt_iter) {
+        case '|':
+            n_required = n_total;
+            continue;
+        case '?':
+            continue;
+        default:
+            n_total++;
+        }
+    }
+
+    if (n_required == 0)
+        n_required = n_total;
+
+    g_assert(((void) "Wrong number of parameters passed to gjs_parse_call_args()",
+              sizeof...(Args) / 2 == n_total));
+
+    JSAutoRequest ar(cx);
+
+    /* COMPAT: In future, use args.requireAtLeast() */
+    if (args.length() < n_required ||
+        (args.length() > n_total && !ignore_trailing_args)) {
+        if (n_required == n_total) {
+            gjs_throw(cx, "Error invoking %s: Expected %d arguments, got %d",
+                      function_name, n_required, args.length());
+        } else {
+            gjs_throw(cx,
+                      "Error invoking %s: Expected minimum %d arguments (and %d optional), got %d",
+                      function_name, n_required, n_total - n_required,
+                      args.length());
+        }
+        return false;
+    }
+
+    parts = g_strsplit(format, "|", 2);
+    fmt_required = parts[0];
+    fmt_optional = parts[1];  /* may be NULL */
+
+    retval = parse_call_args_helper(cx, function_name, args,
+                                    ignore_trailing_args, fmt_required,
+                                    fmt_optional, 0, params...);
+
+    g_strfreev(parts);
+    return retval;
+}
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index bb26009..0cf09b3 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -814,273 +814,6 @@ gjs_get_type_name(JS::Value value)
     }
 }
 
-static bool
-gjs_parse_args_valist (JSContext  *context,
-                       const char *function_name,
-                       const char *format,
-                       unsigned    argc,
-                       JS::Value  *argv,
-                       va_list     args)
-{
-    guint i;
-    const char *fmt_iter;
-    guint n_unwind = 0;
-#define MAX_UNWIND_STRINGS 16
-    gpointer unwind_strings[MAX_UNWIND_STRINGS];
-    bool ignore_trailing_args = false;
-    guint n_required = 0;
-    guint n_total = 0;
-    guint consumed_args;
-
-    JS_BeginRequest(context);
-
-    if (*format == '!') {
-        ignore_trailing_args = true;
-        format++;
-    }
-
-    for (fmt_iter = format; *fmt_iter; fmt_iter++) {
-        switch (*fmt_iter) {
-        case '|':
-            n_required = n_total;
-            continue;
-        case '?':
-            continue;
-        default:
-            break;
-        }
-
-        n_total++;
-    }
-
-    if (n_required == 0)
-        n_required = n_total;
-
-    if (argc < n_required || (argc > n_total && !ignore_trailing_args)) {
-        if (n_required == n_total) {
-            gjs_throw(context, "Error invoking %s: Expected %d arguments, got %d", function_name,
-                      n_required, argc);
-        } else {
-            gjs_throw(context, "Error invoking %s: Expected minimum %d arguments (and %d optional), got %d", 
function_name,
-                      n_required, n_total - n_required, argc);
-        }
-        goto error_unwind;
-    }
-
-    /* We have 3 iteration variables here.
-     * @i: The current integer position in fmt_args
-     * @fmt_iter: A pointer to the character in fmt_args
-     * @consumed_args: How many arguments we've taken from argv
-     *
-     * consumed_args can currently be different from 'i' because of the '|' character.
-     */
-    for (i = 0, consumed_args = 0, fmt_iter = format; *fmt_iter; fmt_iter++, i++) {
-        const char *argname;
-        gpointer arg_location;
-        JS::Value js_value;
-        const char *arg_error_message = NULL;
-
-        if (*fmt_iter == '|')
-            continue;
-
-        if (consumed_args == argc) {
-            break;
-        }
-
-        argname = va_arg (args, char *);
-        arg_location = va_arg (args, gpointer);
-
-        g_return_val_if_fail (argname != NULL, false);
-        g_return_val_if_fail (arg_location != NULL, false);
-
-        js_value = argv[consumed_args];
-
-        if (*fmt_iter == '?') {
-            fmt_iter++;
-
-            if (js_value.isNull()) {
-                gpointer *arg = (gpointer*) arg_location;
-                *arg = NULL;
-                goto got_value;
-            }
-        }
-
-        switch (*fmt_iter) {
-        case 'b': {
-            if (!js_value.isBoolean()) {
-                arg_error_message = "Not a boolean";
-            } else {
-                bool *arg = (bool *) arg_location;
-                *arg = js_value.toBoolean();
-            }
-        }
-            break;
-        case 'o': {
-            if (!js_value.isObject()) {
-                arg_error_message = "Not an object";
-            } else {
-                JSObject **arg = (JSObject**) arg_location;
-                *arg = &js_value.toObject();
-            }
-        }
-            break;
-        case 's': {
-            char **arg = (char**) arg_location;
-
-            if (gjs_string_to_utf8 (context, js_value, arg)) {
-                unwind_strings[n_unwind++] = *arg;
-                g_assert(n_unwind < MAX_UNWIND_STRINGS);
-            } else {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to string";
-            }
-        }
-            break;
-        case 'F': {
-            char **arg = (char**) arg_location;
-
-            if (gjs_string_to_filename (context, js_value, arg)) {
-                unwind_strings[n_unwind++] = *arg;
-                g_assert(n_unwind < MAX_UNWIND_STRINGS);
-            } else {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to filename";
-            }
-        }
-            break;
-        case 'i': {
-            if (!JS::ToInt32(context, js_value, (int32_t *) arg_location)) {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to integer";
-            }
-        }
-            break;
-        case 'u': {
-            gdouble num;
-            if (!js_value.isNumber() || !JS::ToNumber(context, js_value, &num)) {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to unsigned integer";
-            } else if (num > G_MAXUINT32 || num < 0) {
-                arg_error_message = "Value is out of range";
-            } else {
-                *((guint32*) arg_location) = num;
-            }
-        }
-            break;
-        case 't': {
-            if (!JS::ToInt64(context, js_value, (int64_t *) arg_location)) {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to 64-bit integer";
-            }
-        }
-            break;
-        case 'f': {
-            double num;
-            if (!JS::ToNumber(context, js_value, &num)) {
-                /* Our error message is going to be more useful */
-                JS_ClearPendingException(context);
-                arg_error_message = "Couldn't convert to double";
-            } else {
-                *((double*) arg_location) = num;
-            }
-        }
-            break;
-        default:
-            g_assert_not_reached ();
-        }
-
-    got_value:
-        if (arg_error_message != NULL) {
-            gjs_throw(context, "Error invoking %s, at argument %d (%s): %s", function_name,
-                      consumed_args+1, argname, arg_error_message);
-            goto error_unwind;
-        }
-
-        consumed_args++;
-    }
-
-    JS_EndRequest(context);
-    return true;
-
- error_unwind:
-    /* We still own the strings in the error case, free any we converted */
-    for (i = 0; i < n_unwind; i++) {
-        g_free (unwind_strings[i]);
-    }
-    JS_EndRequest(context);
-    return false;
-}
-
-/**
- * gjs_parse_args:
- * @context:
- * @function_name: The name of the function being called
- * @format: Printf-like format specifier containing the expected arguments
- * @argc: Number of JavaScript arguments
- * @argv: JavaScript argument array
- * @Varargs: for each character in @format, a pair of a char * which is the name
- * of the argument, and a pointer to a location to store the value. The type of
- * value stored depends on the format character, as described below.
- *
- * This function is inspired by Python's PyArg_ParseTuple for those
- * familiar with it.  It takes a format specifier which gives the
- * types of the expected arguments, and a list of argument names and
- * value location pairs.  The currently accepted format specifiers are:
- *
- * b: A boolean
- * s: A string, converted into UTF-8
- * F: A string, converted into "filename encoding" (i.e. active locale)
- * i: A number, will be converted to a C "gint32"
- * u: A number, converted into a C "guint32"
- * t: A 64-bit number, converted into a C "gint64"
- * o: A JavaScript object, as a "JSObject *"
- *
- * If the first character in the format string is a '!', then JS is allowed
- * to pass extra arguments that are ignored, to the function.
- *
- * The '|' character introduces optional arguments.  All format specifiers
- * after a '|' when not specified, do not cause any changes in the C
- * value location.
- *
- * A prefix character '?' means that the next value may be null, in
- * which case the C value %NULL is returned.
- */
-bool
-gjs_parse_args (JSContext  *context,
-                const char *function_name,
-                const char *format,
-                unsigned    argc,
-                JS::Value  *argv,
-                ...)
-{
-    va_list args;
-    bool ret;
-    va_start (args, argv);
-    ret = gjs_parse_args_valist (context, function_name, format, argc, argv, args);
-    va_end (args);
-    return ret;
-}
-
-bool
-gjs_parse_call_args (JSContext    *context,
-                     const char   *function_name,
-                     const char   *format,
-                     JS::CallArgs &call_args,
-                     ...)
-{
-    va_list args;
-    bool ret;
-    va_start (args, call_args);
-    ret = gjs_parse_args_valist (context, function_name, format, call_args.length(), call_args.array(), 
args);
-    va_end (args);
-    return ret;
-}
-
 #ifdef __linux__
 static void
 _linux_get_self_process_size (gulong *vm_size,
diff --git a/gjs/jsapi-util.h b/gjs/jsapi-util.h
index 17eaf36..1c697d7 100644
--- a/gjs/jsapi-util.h
+++ b/gjs/jsapi-util.h
@@ -444,19 +444,6 @@ bool        gjs_unichar_from_string          (JSContext       *context,
 
 const char* gjs_get_type_name                (JS::Value        value);
 
-bool        gjs_parse_args                   (JSContext  *context,
-                                              const char *function_name,
-                                              const char *format,
-                                              unsigned   argc,
-                                              JS::Value  *argv,
-                                              ...);
-
-bool        gjs_parse_call_args              (JSContext    *context,
-                                              const char   *function_name,
-                                              const char   *format,
-                                              JS::CallArgs &args,
-                                              ...);
-
 /* Functions intended for more "internal" use */
 
 void gjs_maybe_gc (JSContext *context);
diff --git a/modules/cairo-context.cpp b/modules/cairo-context.cpp
index b553c8f..0309783 100644
--- a/modules/cairo-context.cpp
+++ b/modules/cairo-context.cpp
@@ -24,6 +24,7 @@
 
 #include "gjs/compat.h"
 #include "gi/foreign.h"
+#include "gjs/jsapi-util-args.h"
 
 #include <cairo.h>
 #include <cairo-gobject.h>
@@ -73,8 +74,8 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END
 #define _GJS_CAIRO_CONTEXT_DEFINE_FUNC2FFAFF(method, cfunc, n1, n2)        \
 _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     double arg1, arg2;                                                     \
-    if (!gjs_parse_call_args(context, #method, "ff", argv,                 \
-                        #n1, &arg1, #n2, &arg2))                           \
+    if (!gjs_parse_call_args(context, #method, argv, "ff",                 \
+                             #n1, &arg1, #n2, &arg2))                      \
         return false;                                                      \
     cfunc(cr, &arg1, &arg2);                                               \
     if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) {                        \
@@ -141,8 +142,8 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END
 #define _GJS_CAIRO_CONTEXT_DEFINE_FUNC1(method, cfunc, fmt, t1, n1)        \
 _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t1 arg1;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1))                                       \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1))                                  \
         return false;                                                      \
     cfunc(cr, arg1);                                                       \
     argv.rval().setUndefined();                                            \
@@ -152,8 +153,8 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END
 _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t1 arg1;                                                               \
     t2 arg2;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1, #n2, &arg2))                           \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2))                      \
         return false;                                                      \
     cfunc(cr, arg1, arg2);                                                 \
     argv.rval().setUndefined();                                            \
@@ -164,8 +165,8 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t1 arg1;                                                               \
     t2 arg2;                                                               \
     cairo_bool_t ret;                                                      \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                 \
-                        #n1, &arg1, #n2, &arg2))                           \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2))                      \
         return false;                                                      \
     ret = cfunc(cr, arg1, arg2);                                           \
     argv.rval().setBoolean(ret);                                           \
@@ -176,8 +177,8 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t1 arg1;                                                               \
     t2 arg2;                                                               \
     t3 arg3;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1, #n2, &arg2, #n3, &arg3))               \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2, #n3, &arg3))          \
         return false;                                                      \
     cfunc(cr, arg1, arg2, arg3);                                           \
     argv.rval().setUndefined();                                            \
@@ -189,8 +190,9 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t2 arg2;                                                               \
     t3 arg3;                                                               \
     t4 arg4;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1, #n2, &arg2, #n3, &arg3, #n4, &arg4))   \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2,                       \
+                             #n3, &arg3, #n4, &arg4))                      \
         return false;                                                      \
     cfunc(cr, arg1, arg2, arg3, arg4);                                     \
 _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END
@@ -202,9 +204,9 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t3 arg3;                                                               \
     t4 arg4;                                                               \
     t5 arg5;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1, #n2, &arg2, #n3, &arg3,                \
-                        #n4, &arg4, #n5, &arg5))                           \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2, #n3, &arg3,           \
+                             #n4, &arg4, #n5, &arg5))                      \
         return false;                                                      \
     cfunc(cr, arg1, arg2, arg3, arg4, arg5);                               \
     argv.rval().setUndefined();                                            \
@@ -218,9 +220,9 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method)                               \
     t4 arg4;                                                               \
     t5 arg5;                                                               \
     t6 arg6;                                                               \
-    if (!gjs_parse_call_args(context, #method, fmt, argv,                  \
-                        #n1, &arg1, #n2, &arg2, #n3, &arg3,                \
-                        #n4, &arg4, #n5, &arg5, #n6, &arg6))               \
+    if (!gjs_parse_call_args(context, #method, argv, fmt,                  \
+                             #n1, &arg1, #n2, &arg2, #n3, &arg3,           \
+                             #n4, &arg4, #n5, &arg5, #n6, &arg6))          \
         return false;                                                      \
     cfunc(cr, arg1, arg2, arg3, arg4, arg5, arg6);                         \
     argv.rval().setUndefined();                                            \
@@ -256,14 +258,14 @@ _gjs_cairo_context_construct_internal(JSContext       *context,
 GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_context)
 {
     GJS_NATIVE_CONSTRUCTOR_VARIABLES(cairo_context)
-    JSObject *surface_wrapper;
     cairo_surface_t *surface;
     cairo_t *cr;
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_context);
 
-    if (!gjs_parse_call_args(context, "Context", "o", argv,
-                        "surface", &surface_wrapper))
+    JS::RootedObject surface_wrapper(context);
+    if (!gjs_parse_call_args(context, "Context", argv, "o",
+                             "surface", &surface_wrapper))
         return false;
 
     surface = gjs_cairo_surface_get_surface(context, surface_wrapper);
@@ -406,12 +408,12 @@ appendPath_func(JSContext *context,
                 JS::Value *vp)
 {
     GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv);
-    JSObject *path_wrapper;
+    JS::RootedObject path_wrapper(context);
     cairo_path_t *path;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "path", "o", argv,
-                        "path", &path_wrapper))
+    if (!gjs_parse_call_args(context, "path", argv, "o",
+                             "path", &path_wrapper))
         return false;
 
     path = gjs_cairo_path_get_path(context, path_wrapper);
@@ -434,7 +436,7 @@ copyPath_func(JSContext *context,
     cairo_path_t *path;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "", "", argv))
+    if (!gjs_parse_call_args(context, "", argv, ""))
         return false;
 
     path = cairo_copy_path(cr);
@@ -451,7 +453,7 @@ copyPathFlat_func(JSContext *context,
     cairo_path_t *path;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "", "", argv))
+    if (!gjs_parse_call_args(context, "", argv, ""))
         return false;
 
     path = cairo_copy_path_flat(cr);
@@ -465,12 +467,12 @@ mask_func(JSContext *context,
           JS::Value *vp)
 {
     GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv);
-    JSObject *pattern_wrapper;
+    JS::RootedObject pattern_wrapper(context);
     cairo_pattern_t *pattern;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "mask", "o", argv,
-                        "pattern", &pattern_wrapper))
+    if (!gjs_parse_call_args(context, "mask", argv, "o",
+                             "pattern", &pattern_wrapper))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, pattern_wrapper);
@@ -494,15 +496,15 @@ maskSurface_func(JSContext *context,
                  JS::Value *vp)
 {
     GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv);
-    JSObject *surface_wrapper;
+    JS::RootedObject surface_wrapper(context);
     double x, y;
     cairo_surface_t *surface;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "maskSurface", "off", argv,
-                        "surface", &surface_wrapper,
-                        "x", &x,
-                        "y", &y))
+    if (!gjs_parse_call_args(context, "maskSurface", argv, "off",
+                             "surface", &surface_wrapper,
+                             "x", &x,
+                             "y", &y))
         return false;
 
     surface = gjs_cairo_surface_get_surface(context, surface_wrapper);
@@ -534,8 +536,9 @@ setDash_func(JSContext *context,
     guint len;
     GArray *dashes_c = NULL;
 
-    if (!gjs_parse_call_args(context, "setDash", "of", argv,
-                        "dashes", dashes.address(), "offset", &offset))
+    if (!gjs_parse_call_args(context, "setDash", argv, "of",
+                             "dashes", &dashes,
+                             "offset", &offset))
         return false;
 
 
@@ -586,12 +589,12 @@ setSource_func(JSContext *context,
                JS::Value *vp)
 {
     GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv);
-    JSObject *pattern_wrapper;
+    JS::RootedObject pattern_wrapper(context);
     cairo_pattern_t *pattern;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "setSource", "o", argv,
-                        "pattern", &pattern_wrapper))
+    if (!gjs_parse_call_args(context, "setSource", argv, "o",
+                             "pattern", &pattern_wrapper))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, pattern_wrapper);
@@ -616,15 +619,15 @@ setSourceSurface_func(JSContext *context,
                       JS::Value *vp)
 {
     GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv);
-    JSObject *surface_wrapper;
+    JS::RootedObject surface_wrapper(context);
     double x, y;
     cairo_surface_t *surface;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "setSourceSurface", "off", argv,
-                        "surface", &surface_wrapper,
-                        "x", &x,
-                        "y", &y))
+    if (!gjs_parse_call_args(context, "setSourceSurface", argv, "off",
+                             "surface", &surface_wrapper,
+                             "x", &x,
+                             "y", &y))
         return false;
 
     surface = gjs_cairo_surface_get_surface(context, surface_wrapper);
@@ -652,8 +655,8 @@ showText_func(JSContext *context,
     char *utf8;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "showText", "s", argv,
-                        "utf8", &utf8))
+    if (!gjs_parse_call_args(context, "showText", argv, "s",
+                             "utf8", &utf8))
         return false;
 
     cairo_show_text(cr, utf8);
@@ -678,10 +681,10 @@ selectFontFace_func(JSContext *context,
     cairo_font_weight_t weight;
     cairo_t *cr = priv ? priv->cr : NULL;
 
-    if (!gjs_parse_call_args(context, "selectFontFace", "sii", argv,
-                        "family", &family,
-                        "slang", &slant,
-                        "weight", &weight))
+    if (!gjs_parse_call_args(context, "selectFontFace", argv, "sii",
+                             "family", &family,
+                             "slang", &slant,
+                             "weight", &weight))
         return false;
 
     cairo_select_font_face(cr, family, slant, weight);
diff --git a/modules/cairo-gradient.cpp b/modules/cairo-gradient.cpp
index 29a84b8..7431068 100644
--- a/modules/cairo-gradient.cpp
+++ b/modules/cairo-gradient.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -51,11 +52,11 @@ addColorStopRGB_func(JSContext *context,
     double offset, red, green, blue;
     cairo_pattern_t *pattern;
 
-    if (!gjs_parse_call_args(context, "addColorStopRGB", "ffff", argv,
-                        "offset", &offset,
-                        "red", &red,
-                        "green", &green,
-                        "blue", &blue))
+    if (!gjs_parse_call_args(context, "addColorStopRGB", argv, "ffff",
+                             "offset", &offset,
+                             "red", &red,
+                             "green", &green,
+                             "blue", &blue))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, obj);
@@ -78,12 +79,12 @@ addColorStopRGBA_func(JSContext *context,
     double offset, red, green, blue, alpha;
     cairo_pattern_t *pattern;
 
-    if (!gjs_parse_call_args(context, "addColorStopRGBA", "fffff", argv,
-                        "offset", &offset,
-                        "red", &red,
-                        "green", &green,
-                        "blue", &blue,
-                        "alpha", &alpha))
+    if (!gjs_parse_call_args(context, "addColorStopRGBA", argv, "fffff",
+                             "offset", &offset,
+                             "red", &red,
+                             "green", &green,
+                             "blue", &blue,
+                             "alpha", &alpha))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, obj);
diff --git a/modules/cairo-image-surface.cpp b/modules/cairo-image-surface.cpp
index 3dec670..fbaf1b9 100644
--- a/modules/cairo-image-surface.cpp
+++ b/modules/cairo-image-surface.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -37,10 +38,10 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_image_surface)
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_image_surface);
 
     // create_for_data optional parameter
-    if (!gjs_parse_call_args(context, "ImageSurface", "iii", argv,
-                        "format", &format,
-                        "width", &width,
-                        "height", &height))
+    if (!gjs_parse_call_args(context, "ImageSurface", argv, "iii",
+                             "format", &format,
+                             "width", &width,
+                             "height", &height))
         return false;
 
     surface = cairo_image_surface_create((cairo_format_t) format, width, height);
@@ -76,8 +77,8 @@ createFromPNG_func(JSContext *context,
     char *filename;
     cairo_surface_t *surface;
 
-    if (!gjs_parse_call_args(context, "createFromPNG", "s", argv,
-                        "filename", &filename))
+    if (!gjs_parse_call_args(context, "createFromPNG", argv, "s",
+                             "filename", &filename))
         return false;
 
     surface = cairo_image_surface_create_from_png(filename);
diff --git a/modules/cairo-linear-gradient.cpp b/modules/cairo-linear-gradient.cpp
index 6571779..53e3a02 100644
--- a/modules/cairo-linear-gradient.cpp
+++ b/modules/cairo-linear-gradient.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -36,11 +37,11 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_linear_gradient)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_linear_gradient);
 
-    if (!gjs_parse_call_args(context, "LinearGradient", "ffff", argv,
-                        "x0", &x0,
-                        "y0", &y0,
-                        "x1", &x1,
-                        "y1", &y1))
+    if (!gjs_parse_call_args(context, "LinearGradient", argv, "ffff",
+                             "x0", &x0,
+                             "y0", &y0,
+                             "x1", &x1,
+                             "y1", &y1))
         return false;
 
     pattern = cairo_pattern_create_linear(x0, y0, x1, y1);
diff --git a/modules/cairo-pdf-surface.cpp b/modules/cairo-pdf-surface.cpp
index 2812208..0a37f56 100644
--- a/modules/cairo-pdf-surface.cpp
+++ b/modules/cairo-pdf-surface.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -40,10 +41,10 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_pdf_surface)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_pdf_surface);
 
-    if (!gjs_parse_call_args(context, "PDFSurface", "sff", argv,
-                        "filename", &filename,
-                        "width", &width,
-                        "height", &height))
+    if (!gjs_parse_call_args(context, "PDFSurface", argv, "Fff",
+                             "filename", &filename,
+                             "width", &width,
+                             "height", &height))
         return false;
 
     surface = cairo_pdf_surface_create(filename, width, height);
diff --git a/modules/cairo-ps-surface.cpp b/modules/cairo-ps-surface.cpp
index 63c60e3..71cdd3e 100644
--- a/modules/cairo-ps-surface.cpp
+++ b/modules/cairo-ps-surface.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -40,10 +41,10 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_ps_surface)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_ps_surface);
 
-    if (!gjs_parse_call_args(context, "PSSurface", "sff", argv,
-                        "filename", &filename,
-                        "width", &width,
-                        "height", &height))
+    if (!gjs_parse_call_args(context, "PSSurface", argv, "Fff",
+                             "filename", &filename,
+                             "width", &width,
+                             "height", &height))
         return false;
 
     surface = cairo_ps_surface_create(filename, width, height);
diff --git a/modules/cairo-radial-gradient.cpp b/modules/cairo-radial-gradient.cpp
index 23052f3..6e4b6d4 100644
--- a/modules/cairo-radial-gradient.cpp
+++ b/modules/cairo-radial-gradient.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -36,13 +37,13 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_radial_gradient)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_radial_gradient);
 
-    if (!gjs_parse_call_args(context, "RadialGradient", "ffffff", argv,
-                        "cx0", &cx0,
-                        "cy0", &cy0,
-                        "radius0", &radius0,
-                        "cx1", &cx1,
-                        "cy1", &cy1,
-                        "radius1", &radius1))
+    if (!gjs_parse_call_args(context, "RadialGradient", argv, "ffffff",
+                             "cx0", &cx0,
+                             "cy0", &cy0,
+                             "radius0", &radius0,
+                             "cx1", &cx1,
+                             "cy1", &cy1,
+                             "radius1", &radius1))
         return false;
 
     pattern = cairo_pattern_create_radial(cx0, cy0, radius0, cx1, cy1, radius1);
diff --git a/modules/cairo-region.cpp b/modules/cairo-region.cpp
index 9324c93..ecee331 100644
--- a/modules/cairo-region.cpp
+++ b/modules/cairo-region.cpp
@@ -24,6 +24,7 @@
 
 #include "gjs/compat.h"
 #include "gi/foreign.h"
+#include "gjs/jsapi-util-args.h"
 
 #include <cairo.h>
 #include <cairo-gobject.h>
@@ -69,8 +70,8 @@ fill_rectangle(JSContext *context, JSObject *obj,
         PRELUDE;                                                \
         JS::RootedObject other_obj(context);                    \
         cairo_region_t *other_region;                           \
-        if (!gjs_parse_call_args(context, #method, "o", argv,   \
-                                 "other_region", other_obj.address())) \
+        if (!gjs_parse_call_args(context, #method, argv, "o",   \
+                                 "other_region", &other_obj))   \
             return false;                                       \
                                                                 \
         other_region = get_region(context, other_obj);          \
@@ -87,10 +88,10 @@ fill_rectangle(JSContext *context, JSObject *obj,
                             JS::Value *vp)                      \
     {                                                           \
         PRELUDE;                                                \
-        JSObject *rect_obj;                                     \
+        JS::RootedObject rect_obj(context);                     \
         cairo_rectangle_int_t rect;                             \
-        if (!gjs_parse_call_args(context, #method, "o", argv,   \
-                            "rect", &rect_obj))                 \
+        if (!gjs_parse_call_args(context, #method, argv, "o",   \
+                                 "rect", &rect_obj))            \
             return false;                                       \
                                                                 \
         if (!fill_rectangle(context, rect_obj, &rect))          \
@@ -170,7 +171,7 @@ num_rectangles_func(JSContext *context,
     PRELUDE;
     int n_rects;
 
-    if (!gjs_parse_call_args(context, "num_rectangles", "", argv))
+    if (!gjs_parse_call_args(context, "num_rectangles", argv, ""))
         return false;
 
     n_rects = cairo_region_num_rectangles(this_region);
@@ -188,7 +189,8 @@ get_rectangle_func(JSContext *context,
     JSObject *rect_obj;
     cairo_rectangle_int_t rect;
 
-    if (!gjs_parse_call_args(context, "get_rectangle", "i", argv, "rect", &i))
+    if (!gjs_parse_call_args(context, "get_rectangle", argv, "i",
+                             "rect", &i))
         return false;
 
     cairo_region_get_rectangle(this_region, i, &rect);
@@ -242,7 +244,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_region)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_region);
 
-    if (!gjs_parse_call_args(context, "Region", "", argv))
+    if (!gjs_parse_call_args(context, "Region", argv, ""))
         return false;
 
     region = cairo_region_create();
diff --git a/modules/cairo-solid-pattern.cpp b/modules/cairo-solid-pattern.cpp
index e9ca032..f4a3e17 100644
--- a/modules/cairo-solid-pattern.cpp
+++ b/modules/cairo-solid-pattern.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -49,10 +50,10 @@ createRGB_func(JSContext *context,
     cairo_pattern_t *pattern;
     JSObject *pattern_wrapper;
 
-    if (!gjs_parse_call_args(context, "createRGB", "fff", argv,
-                        "red", &red,
-                        "green", &green,
-                        "blue", &blue))
+    if (!gjs_parse_call_args(context, "createRGB", argv, "fff",
+                             "red", &red,
+                             "green", &green,
+                             "blue", &blue))
         return false;
 
     pattern = cairo_pattern_create_rgb(red, green, blue);
@@ -77,11 +78,11 @@ createRGBA_func(JSContext *context,
     cairo_pattern_t *pattern;
     JSObject *pattern_wrapper;
 
-    if (!gjs_parse_call_args(context, "createRGBA", "ffff", argv,
-                        "red", &red,
-                        "green", &green,
-                        "blue", &blue,
-                        "alpha", &alpha))
+    if (!gjs_parse_call_args(context, "createRGBA", argv, "ffff",
+                             "red", &red,
+                             "green", &green,
+                             "blue", &blue,
+                             "alpha", &alpha))
         return false;
 
     pattern = cairo_pattern_create_rgba(red, green, blue, alpha);
diff --git a/modules/cairo-surface-pattern.cpp b/modules/cairo-surface-pattern.cpp
index 7f56d86..271cb80 100644
--- a/modules/cairo-surface-pattern.cpp
+++ b/modules/cairo-surface-pattern.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -31,14 +32,14 @@ GJS_DEFINE_PROTO("CairoSurfacePattern", cairo_surface_pattern, JSCLASS_BACKGROUN
 GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_surface_pattern)
 {
     GJS_NATIVE_CONSTRUCTOR_VARIABLES(cairo_surface_pattern)
-    JSObject *surface_wrapper;
     cairo_surface_t *surface;
     cairo_pattern_t *pattern;
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_surface_pattern);
 
-    if (!gjs_parse_call_args(context, "SurfacePattern", "o", argv,
-                        "surface", &surface_wrapper))
+    JS::RootedObject surface_wrapper(context);
+    if (!gjs_parse_call_args(context, "SurfacePattern", argv, "o",
+                             "surface", &surface_wrapper))
         return false;
 
     surface = gjs_cairo_surface_get_surface(context, surface_wrapper);
@@ -82,8 +83,8 @@ setExtend_func(JSContext *context,
     cairo_extend_t extend;
     cairo_pattern_t *pattern;
 
-    if (!gjs_parse_call_args(context, "setExtend", "i", argv,
-                        "extend", &extend))
+    if (!gjs_parse_call_args(context, "setExtend", argv, "i",
+                             "extend", &extend))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, obj);
@@ -130,8 +131,8 @@ setFilter_func(JSContext *context,
     cairo_filter_t filter;
     cairo_pattern_t *pattern;
 
-    if (!gjs_parse_call_args(context, "setFilter", "i", argv,
-                        "filter", &filter))
+    if (!gjs_parse_call_args(context, "setFilter", argv, "i",
+                             "filter", &filter))
         return false;
 
     pattern = gjs_cairo_pattern_get_pattern(context, obj);
diff --git a/modules/cairo-surface.cpp b/modules/cairo-surface.cpp
index df28824..e563595 100644
--- a/modules/cairo-surface.cpp
+++ b/modules/cairo-surface.cpp
@@ -24,6 +24,7 @@
 
 #include "gjs/compat.h"
 #include "gi/foreign.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include <cairo-gobject.h>
 #include "cairo-private.h"
@@ -65,8 +66,8 @@ writeToPNG_func(JSContext *context,
     char *filename;
     cairo_surface_t *surface;
 
-    if (!gjs_parse_call_args(context, "writeToPNG", "s", argv,
-                        "filename", &filename))
+    if (!gjs_parse_call_args(context, "writeToPNG", argv, "F",
+                             "filename", &filename))
         return false;
 
     surface = gjs_cairo_surface_get_surface(context, obj);
diff --git a/modules/cairo-svg-surface.cpp b/modules/cairo-svg-surface.cpp
index 483cf2d..e601c70 100644
--- a/modules/cairo-svg-surface.cpp
+++ b/modules/cairo-svg-surface.cpp
@@ -23,6 +23,7 @@
 #include <config.h>
 
 #include "gjs/compat.h"
+#include "gjs/jsapi-util-args.h"
 #include <cairo.h>
 #include "cairo-private.h"
 
@@ -40,10 +41,10 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_svg_surface)
 
     GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_svg_surface);
 
-    if (!gjs_parse_call_args(context, "SVGSurface", "sff", argv,
-                        "filename", &filename,
-                        "width", &width,
-                        "height", &height))
+    if (!gjs_parse_call_args(context, "SVGSurface", argv, "Fff",
+                             "filename", &filename,
+                             "width", &width,
+                             "height", &height))
         return false;
 
     surface = cairo_svg_surface_create(filename, width, height);
diff --git a/modules/system.cpp b/modules/system.cpp
index 612088f..5d03cc1 100644
--- a/modules/system.cpp
+++ b/modules/system.cpp
@@ -31,6 +31,7 @@
 #include <gjs/context.h>
 
 #include "gi/object.h"
+#include "gjs/jsapi-util-args.h"
 #include "system.h"
 
 static JSBool
@@ -39,14 +40,15 @@ gjs_address_of(JSContext *context,
                JS::Value *vp)
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
-    JSObject *target_obj;
+    JS::RootedObject target_obj(context);
     bool ret;
     char *pointer_string;
 
-    if (!gjs_parse_call_args(context, "addressOf", "o", argv, "object", &target_obj))
+    if (!gjs_parse_call_args(context, "addressOf", argv, "o",
+                             "object", &target_obj))
         return false;
 
-    pointer_string = g_strdup_printf("%p", target_obj);
+    pointer_string = g_strdup_printf("%p", target_obj.get());
 
     ret = gjs_string_from_utf8(context, pointer_string, -1, argv.rval());
 
@@ -63,8 +65,8 @@ gjs_refcount(JSContext *context,
     JS::RootedObject target_obj(context);
     GObject *obj;
 
-    if (!gjs_parse_call_args(context, "refcount", "o", argv,
-                             "object", target_obj.address()))
+    if (!gjs_parse_call_args(context, "refcount", argv, "o",
+                             "object", &target_obj))
         return false;
 
     if (!gjs_typecheck_object(context, target_obj, G_TYPE_OBJECT, true))
@@ -84,7 +86,7 @@ gjs_breakpoint(JSContext *context,
                JS::Value *vp)
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
-    if (!gjs_parse_call_args(context, "breakpoint", "", argv))
+    if (!gjs_parse_call_args(context, "breakpoint", argv, ""))
         return false;
     G_BREAKPOINT();
     argv.rval().setUndefined();
@@ -97,7 +99,7 @@ gjs_gc(JSContext *context,
        JS::Value *vp)
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
-    if (!gjs_parse_call_args(context, "gc", "", argv))
+    if (!gjs_parse_call_args(context, "gc", argv, ""))
         return false;
     JS_GC(JS_GetRuntime(context));
     argv.rval().setUndefined();
@@ -111,7 +113,8 @@ gjs_exit(JSContext *context,
 {
     JS::CallArgs argv = JS::CallArgsFromVp (argc, vp);
     gint32 ecode;
-    if (!gjs_parse_call_args(context, "exit", "i", argv, "ecode", &ecode))
+    if (!gjs_parse_call_args(context, "exit", argv, "i",
+                             "ecode", &ecode))
         return false;
     exit(ecode);
     return true;
diff --git a/test/gjs-test-call-args.cpp b/test/gjs-test-call-args.cpp
new file mode 100644
index 0000000..ea1ffe6
--- /dev/null
+++ b/test/gjs-test-call-args.cpp
@@ -0,0 +1,423 @@
+#include <string.h>
+
+#include <glib.h>
+
+#include "gjs/compat.h"
+#include "gjs/context.h"
+#include "gjs/jsapi-util-args.h"
+
+#define assert_match(str, pattern)                                            \
+    G_STMT_START {                                                            \
+        const char *__s1 = (str), *__s2 = (pattern);                          \
+        if (!g_pattern_match_simple(__s2, __s1)) {                            \
+            g_printerr("**\nExpected \"%s\" to match \"%s\"\n", __s1, __s2);  \
+            g_assert_not_reached();                                           \
+        }                                                                     \
+    } G_STMT_END
+
+typedef struct {
+    GjsContext *gjs_context;
+    JSContext *cx;
+    JSCompartment *compartment;
+    char *message;  /* Thrown exception message */
+} GjsUnitTestFixture;
+
+typedef enum _test_enum {
+    ZERO,
+    ONE,
+    TWO,
+    THREE
+} test_enum_t;
+
+typedef enum _test_signed_enum {
+    MINUS_THREE = -3,
+    MINUS_TWO,
+    MINUS_ONE
+} test_signed_enum_t;
+
+#define JSNATIVE_TEST_FUNC_BEGIN(name)                      \
+    static JSBool                                           \
+    name(JSContext *cx,                                     \
+         unsigned   argc,                                   \
+         JS::Value *vp)                                     \
+    {                                                       \
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);   \
+        bool retval;
+
+#define JSNATIVE_TEST_FUNC_END           \
+        if (retval)                      \
+            args.rval().setUndefined();  \
+        return retval;                   \
+    }
+
+JSNATIVE_TEST_FUNC_BEGIN(no_args)
+    retval = gjs_parse_call_args(cx, "noArgs", args, "");
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(no_args_ignore_trailing)
+    retval = gjs_parse_call_args(cx, "noArgsIgnoreTrailing", args, "!");
+JSNATIVE_TEST_FUNC_END
+
+#define JSNATIVE_NO_ASSERT_TYPE_TEST_FUNC(type, fmt)                      \
+    JSNATIVE_TEST_FUNC_BEGIN(type##_arg_no_assert)                        \
+        type val;                                                         \
+        retval = gjs_parse_call_args(cx, #type "ArgNoAssert", args, fmt,  \
+                                     "val", &val);                        \
+    JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_NO_ASSERT_TYPE_TEST_FUNC(bool, "b");
+JSNATIVE_NO_ASSERT_TYPE_TEST_FUNC(int, "i");
+
+#undef JSNATIVE_NO_ASSERT_TYPE_TEST_FUNC
+
+JSNATIVE_TEST_FUNC_BEGIN(object_arg_no_assert)
+    JS::RootedObject val(cx);
+    retval = gjs_parse_call_args(cx, "objectArgNoAssert", args, "o",
+                                 "val", &val);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(optional_int_args_no_assert)
+    int val1, val2;
+    retval = gjs_parse_call_args(cx, "optionalIntArgsNoAssert", args, "i|i",
+                                 "val1", &val1,
+                                 "val2", &val2);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(args_ignore_trailing)
+    int val;
+    retval = gjs_parse_call_args(cx, "argsIgnoreTrailing", args, "!i",
+                                 "val", &val);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(one_of_each_type)
+    bool boolval;
+    char *strval, *fileval;
+    int intval;
+    unsigned uintval;
+    int64_t int64val;
+    double dblval;
+    JS::RootedObject objval(cx);
+    retval = gjs_parse_call_args(cx, "oneOfEachType", args, "bsFiutfo",
+                                 "bool", &boolval,
+                                 "str", &strval,
+                                 "file", &fileval,
+                                 "int", &intval,
+                                 "uint", &uintval,
+                                 "int64", &int64val,
+                                 "dbl", &dblval,
+                                 "obj", &objval);
+    g_assert_cmpint(boolval, ==, true);
+    g_assert_cmpstr(strval, ==, "foo");
+    g_assert_cmpstr(fileval, ==, "foo");
+    g_assert_cmpint(intval, ==, 1);
+    g_assert_cmpint(uintval, ==, 1);
+    g_assert_cmpint(int64val, ==, 1);
+    g_assert_cmpfloat(dblval, ==, 1.0);
+    g_assert_nonnull(objval);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(optional_args_all)
+    bool val1, val2, val3;
+    retval = gjs_parse_call_args(cx, "optionalArgsAll", args, "b|bb",
+                                 "val1", &val1,
+                                 "val2", &val2,
+                                 "val3", &val3);
+    g_assert_cmpint(val1, ==, true);
+    g_assert_cmpint(val2, ==, true);
+    g_assert_cmpint(val3, ==, true);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(optional_args_only_required)
+    bool val1 = false, val2 = false, val3 = false;
+    retval = gjs_parse_call_args(cx, "optionalArgsOnlyRequired", args, "b|bb",
+                                 "val1", &val1,
+                                 "val2", &val2,
+                                 "val3", &val3);
+    g_assert_cmpint(val1, ==, true);
+    g_assert_cmpint(val2, ==, false);
+    g_assert_cmpint(val3, ==, false);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(unsigned_enum_arg)
+    test_enum_t val;
+    retval = gjs_parse_call_args(cx, "unsignedEnumArg", args, "i",
+                                 "enum_param", &val);
+    g_assert_cmpint(val, ==, ONE);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(signed_enum_arg)
+    test_signed_enum_t val;
+    retval = gjs_parse_call_args(cx, "signedEnumArg", args, "i",
+                                 "enum_param", &val);
+    g_assert_cmpint(val, ==, MINUS_ONE);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(one_of_each_nullable_type)
+    char *strval, *fileval;
+    JS::RootedObject objval(cx);
+    retval = gjs_parse_call_args(cx, "oneOfEachNullableType", args, "?s?F?o",
+                                 "strval", &strval,
+                                 "fileval", &fileval,
+                                 "objval", &objval);
+    g_assert_null(strval);
+    g_assert_null(fileval);
+    g_assert_null(objval);
+JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_TEST_FUNC_BEGIN(unwind_free_test)
+    char *strval, *fileval;
+    int intval;
+    unsigned uval;
+    JS::RootedObject objval(cx);
+    retval = gjs_parse_call_args(cx, "unwindFreeTest", args, "sFoiu",
+                                 "strval", &strval,
+                                 "fileval", &fileval,
+                                 "objval", &objval,
+                                 "intval", &intval,
+                                 "error", &uval);
+    g_assert_null(objval);
+    /* Sadly, we cannot assert that strval and fileval have been freed */
+JSNATIVE_TEST_FUNC_END
+
+#define JSNATIVE_BAD_NULLABLE_TEST_FUNC(type, fmt)                 \
+    JSNATIVE_TEST_FUNC_BEGIN(type##_invalid_nullable)              \
+        type val;                                                  \
+        retval = gjs_parse_call_args(cx, #type "InvalidNullable",  \
+                                     args, "?" fmt,                \
+                                     "val", &val);                 \
+    JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_BAD_NULLABLE_TEST_FUNC(bool, "b");
+JSNATIVE_BAD_NULLABLE_TEST_FUNC(int, "i");
+JSNATIVE_BAD_NULLABLE_TEST_FUNC(unsigned, "u");
+JSNATIVE_BAD_NULLABLE_TEST_FUNC(int64_t, "t");
+JSNATIVE_BAD_NULLABLE_TEST_FUNC(double, "f");
+
+#undef JSNATIVE_BAD_NULLABLE_TEST_FUNC
+
+#define JSNATIVE_BAD_TYPE_TEST_FUNC(type, ch)                            \
+    JSNATIVE_TEST_FUNC_BEGIN(type##_invalid_type)                        \
+        type val;                                                        \
+        retval = gjs_parse_call_args(cx, #type "InvalidType", args, ch,  \
+                                     "val", &val);                       \
+    JSNATIVE_TEST_FUNC_END
+
+JSNATIVE_BAD_TYPE_TEST_FUNC(bool, "i");
+JSNATIVE_BAD_TYPE_TEST_FUNC(int, "u");
+JSNATIVE_BAD_TYPE_TEST_FUNC(unsigned, "t");
+JSNATIVE_BAD_TYPE_TEST_FUNC(int64_t, "f");
+JSNATIVE_BAD_TYPE_TEST_FUNC(double, "b");
+typedef char *charptr;
+JSNATIVE_BAD_TYPE_TEST_FUNC(charptr, "i");
+
+#undef JSNATIVE_BAD_TYPE_TEST_FUNC
+
+JSNATIVE_TEST_FUNC_BEGIN(object_invalid_type)
+    JS::RootedObject val(cx);
+    retval = gjs_parse_call_args(cx, "objectInvalidType", args, "i",
+                                 "val", &val);
+JSNATIVE_TEST_FUNC_END
+
+static JSFunctionSpec native_test_funcs[] = {
+    JS_FS("noArgs", no_args, 0, 0),
+    JS_FS("noArgsIgnoreTrailing", no_args_ignore_trailing, 0, 0),
+    JS_FS("boolArgNoAssert", bool_arg_no_assert, 0, 0),
+    JS_FS("intArgNoAssert", int_arg_no_assert, 0, 0),
+    JS_FS("objectArgNoAssert", object_arg_no_assert, 0, 0),
+    JS_FS("optionalIntArgsNoAssert", optional_int_args_no_assert, 0, 0),
+    JS_FS("argsIgnoreTrailing", args_ignore_trailing, 0, 0),
+    JS_FS("oneOfEachType", one_of_each_type, 0, 0),
+    JS_FS("optionalArgsAll", optional_args_all, 0, 0),
+    JS_FS("optionalArgsOnlyRequired", optional_args_only_required, 0, 0),
+    JS_FS("unsignedEnumArg", unsigned_enum_arg, 0, 0),
+    JS_FS("signedEnumArg", signed_enum_arg, 0, 0),
+    JS_FS("oneOfEachNullableType", one_of_each_nullable_type, 0, 0),
+    JS_FS("unwindFreeTest", unwind_free_test, 0, 0),
+    JS_FS("boolInvalidNullable", bool_invalid_nullable, 0, 0),
+    JS_FS("intInvalidNullable", int_invalid_nullable, 0, 0),
+    JS_FS("unsignedInvalidNullable", unsigned_invalid_nullable, 0, 0),
+    JS_FS("int64_tInvalidNullable", int64_t_invalid_nullable, 0, 0),
+    JS_FS("doubleInvalidNullable", double_invalid_nullable, 0, 0),
+    JS_FS("boolInvalidType", bool_invalid_type, 0, 0),
+    JS_FS("intInvalidType", int_invalid_type, 0, 0),
+    JS_FS("unsignedInvalidType", unsigned_invalid_type, 0, 0),
+    JS_FS("int64_tInvalidType", int64_t_invalid_type, 0, 0),
+    JS_FS("doubleInvalidType", double_invalid_type, 0, 0),
+    JS_FS("charptrInvalidType", charptr_invalid_type, 0, 0),
+    JS_FS("objectInvalidType", object_invalid_type, 0, 0),
+    JS_FS_END
+};
+
+static void
+unit_test_error_reporter(JSContext     *cx,
+                         const char    *message,
+                         JSErrorReport *report)
+{
+    GjsContext *gjs_context = gjs_context_get_current();
+    GjsUnitTestFixture *fx =
+        (GjsUnitTestFixture *) g_object_get_data(G_OBJECT(gjs_context),
+                                                 "test fixture");
+    g_free(fx->message);
+    fx->message = g_strdup(message);
+}
+
+static void
+setup(GjsUnitTestFixture *fx,
+      gconstpointer       unused)
+{
+    fx->gjs_context = gjs_context_new();
+    fx->cx = (JSContext *) gjs_context_get_native_context(fx->gjs_context);
+
+    /* This is for shoving private data into the error reporter callback */
+    g_object_set_data(G_OBJECT(fx->gjs_context), "test fixture", fx);
+    JS_SetErrorReporter(fx->cx, unit_test_error_reporter);
+
+    JS_BeginRequest(fx->cx);
+
+    JS::RootedObject global(fx->cx, gjs_get_global_object(fx->cx));
+    fx->compartment = JS_EnterCompartment(fx->cx, global);
+
+    bool success = JS_DefineFunctions(fx->cx, global, native_test_funcs);
+    g_assert_true(success);
+}
+
+static void
+teardown(GjsUnitTestFixture *fx,
+         gconstpointer       unused)
+{
+    JS_LeaveCompartment(fx->cx, fx->compartment);
+    JS_EndRequest(fx->cx);
+
+    g_object_unref(fx->gjs_context);
+
+    if (fx->message != NULL)
+        g_printerr("**\n%s\n", fx->message);
+    g_free(fx->message);
+}
+
+static void
+run_code(GjsUnitTestFixture *fx,
+         gconstpointer       code)
+{
+    const char *script = (const char *) code;
+
+    JS::CompileOptions options(fx->cx, JSVERSION_UNKNOWN);
+    options.setFileAndLine("unit test", 1);
+
+    JS::RootedObject global(fx->cx, gjs_get_global_object(fx->cx));
+    bool ok = JS::Evaluate(fx->cx, global, options, script, strlen(script), NULL);
+    JS_ReportPendingException(fx->cx);
+
+    g_assert_null(fx->message);
+    g_assert_true(ok);
+}
+
+static void
+run_code_expect_exception(GjsUnitTestFixture *fx,
+                          gconstpointer       code)
+{
+    const char *script = (const char *) code;
+
+    JS::CompileOptions options(fx->cx, JSVERSION_UNKNOWN);
+    options.setFileAndLine("unit test", 1);
+
+    JS::RootedObject global(fx->cx, gjs_get_global_object(fx->cx));
+    bool ok = JS::Evaluate(fx->cx, global, options, script, strlen(script), NULL);
+    g_assert_false(ok);
+    g_assert_true(JS_IsExceptionPending(fx->cx));
+    JS_ReportPendingException(fx->cx);
+    g_assert_nonnull(fx->message);
+
+    /* Cheap way to shove an expected exception message into the data argument */
+    const char *expected_msg = strstr((const char *) code, "//");
+    if (expected_msg != NULL) {
+        expected_msg += 2;
+        assert_match(fx->message, expected_msg);
+    }
+}
+
+void
+gjs_test_add_tests_for_parse_call_args(void)
+{
+#define ADD_CALL_ARGS_TEST_BASE(path, code, f) \
+    g_test_add("/callargs/" path, GjsUnitTestFixture, code, setup, f, teardown)
+#define ADD_CALL_ARGS_TEST(path, code) \
+    ADD_CALL_ARGS_TEST_BASE(path, code, run_code)
+#define ADD_CALL_ARGS_TEST_XFAIL(path, code) \
+    ADD_CALL_ARGS_TEST_BASE(path, code, run_code_expect_exception)
+
+    ADD_CALL_ARGS_TEST("no-args-works", "noArgs()");
+    ADD_CALL_ARGS_TEST_XFAIL("no-args-fails-on-extra-args",
+                             "noArgs(1, 2, 3)//*Expected 0 arguments, got 3");
+    ADD_CALL_ARGS_TEST("no-args-ignores-trailing",
+                       "noArgsIgnoreTrailing(1, 2, 3)");
+    ADD_CALL_ARGS_TEST_XFAIL("too-many-args-fails",
+                             "intArgNoAssert(1, 2)"
+                             "//*Expected 1 arguments, got 2");
+    ADD_CALL_ARGS_TEST_XFAIL("too-many-args-fails-when-more-than-optional",
+                             "optionalIntArgsNoAssert(1, 2, 3)"
+                             "//*Expected minimum 1 arguments (and 1 optional), got 3");
+    ADD_CALL_ARGS_TEST_XFAIL("too-few-args-fails",
+                             "intArgNoAssert()//*Expected 1 arguments, got 0");
+    ADD_CALL_ARGS_TEST_XFAIL("too-few-args-fails-with-optional",
+                             "optionalIntArgsNoAssert()"
+                             "//*Expected minimum 1 arguments (and 1 optional), got 0");
+    ADD_CALL_ARGS_TEST("args-ignores-trailing", "argsIgnoreTrailing(1, 2, 3)");
+    ADD_CALL_ARGS_TEST("one-of-each-type-works",
+                       "oneOfEachType(true, 'foo', 'foo', 1, 1, 1, 1, {})");
+    ADD_CALL_ARGS_TEST("optional-args-work-when-passing-all-args",
+                       "optionalArgsAll(true, true, true)");
+    ADD_CALL_ARGS_TEST("optional-args-work-when-passing-only-required-args",
+                       "optionalArgsOnlyRequired(true)");
+    ADD_CALL_ARGS_TEST("enum-types-work", "unsignedEnumArg(1)");
+    ADD_CALL_ARGS_TEST("signed-enum-types-work", "signedEnumArg(-1)");
+    ADD_CALL_ARGS_TEST("one-of-each-nullable-type-works",
+                       "oneOfEachNullableType(null, null, null)");
+    ADD_CALL_ARGS_TEST_XFAIL("allocated-args-are-freed-on-error",
+                             "unwindFreeTest('', '', {}, 1, -1)"
+                             "//*Value * is out of range");
+    ADD_CALL_ARGS_TEST_XFAIL("nullable-bool-is-invalid",
+                             "boolInvalidNullable(true)"
+                             "//*Invalid format string combination ?b");
+    ADD_CALL_ARGS_TEST_XFAIL("nullable-int-is-invalid",
+                             "intInvalidNullable(1)"
+                             "//*Invalid format string combination ?i");
+    ADD_CALL_ARGS_TEST_XFAIL("nullable-unsigned-is-invalid",
+                             "unsignedInvalidNullable(1)"
+                             "//*Invalid format string combination ?u");
+    ADD_CALL_ARGS_TEST_XFAIL("nullable-int64-is-invalid",
+                             "int64_tInvalidNullable(1)"
+                             "//*Invalid format string combination ?t");
+    ADD_CALL_ARGS_TEST_XFAIL("nullable-double-is-invalid",
+                             "doubleInvalidNullable(1)"
+                             "//*Invalid format string combination ?f");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-bool-type",
+                             "boolInvalidType(1)"
+                             "//*Wrong type for i, got bool?");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-int-type",
+                             "intInvalidType(1)"
+                             "//*Wrong type for u, got int32_t?");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-unsigned-type",
+                             "unsignedInvalidType(1)"
+                             "//*Wrong type for t, got uint32_t?");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-int64-type",
+                             "int64_tInvalidType(1)"
+                             "//*Wrong type for f, got int64_t?");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-double-type",
+                             "doubleInvalidType(false)"
+                             "//*Wrong type for b, got double?");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-string-type",
+                             "charptrInvalidType(1)"
+                             "//*Wrong type for i, got char??");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-object-type",
+                             "objectInvalidType(1)"
+                             "//*Wrong type for i, got JS::MutableHandleObject");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-boolean",
+                             "boolArgNoAssert({})//*Not a boolean");
+    ADD_CALL_ARGS_TEST_XFAIL("invalid-object",
+                             "objectArgNoAssert(3)//*Not an object");
+
+#undef ADD_CALL_ARGS_TEST_XFAIL
+#undef ADD_CALL_ARGS_TEST
+#undef ADD_CALL_ARGS_TEST_BASE
+}
diff --git a/test/gjs-tests-add-funcs.h b/test/gjs-tests-add-funcs.h
index a7414a6..23ae846 100644
--- a/test/gjs-tests-add-funcs.h
+++ b/test/gjs-tests-add-funcs.h
@@ -22,4 +22,6 @@
 
 void gjs_test_add_tests_for_coverage ();
 
+void gjs_test_add_tests_for_parse_call_args(void);
+
 #endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index a2141da..be5bd81 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -286,6 +286,7 @@ main(int    argc,
     g_test_add_func("/util/glib/strv/concat/pointers", gjstest_test_func_util_glib_strv_concat_pointers);
 
     gjs_test_add_tests_for_coverage ();
+    gjs_test_add_tests_for_parse_call_args();
 
     g_test_run();
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]