diff --git a/COPYING b/COPYING index f5350bd4..747bc946 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,7 @@ Copyright (c) 2008 litl, LLC +This project is dual-licensed as MIT and LGPLv2+. + 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 diff --git a/Makefile-test.am b/Makefile-test.am index c816c138..b0d3eed9 100644 --- a/Makefile-test.am +++ b/Makefile-test.am @@ -30,25 +30,6 @@ mock-js-resources.h: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_re mock-js-resources.c: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_resource_files) $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) --generate --c-name mock_js_resources $< -mock_js_invalidation_resources_dir = $(top_srcdir)/test/gjs-test-coverage/cache_invalidation -mock_js_invalidation_before_resources_files = \ - $(mock_js_invalidation_resources_dir)/before/resource.js \ - $(mock_js_invalidation_resources_dir)/before/mock-js-resource-cache-before.gresource.xml \ - $(NULL) -mock_js_invalidation_after_resources_files = \ - $(mock_js_invalidation_resources_dir)/after/resource.js \ - $(mock_js_invalidation_resources_dir)/after/mock-js-resource-cache-after.gresource.xml \ - $(NULL) -mock_js_invalidation_resources_files = \ - $(mock_js_invalidation_before_resources_files) \ - $(mock_js_invalidation_after_resources_files) \ - $(NULL) - -mock-cache-invalidation-before.gresource: $(mock_js_invalidation_before_resources_files) - $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(mock_js_invalidation_resources_dir)/before $(mock_js_invalidation_resources_dir)/before/mock-js-resource-cache-before.gresource.xml -mock-cache-invalidation-after.gresource: $(mock_js_invalidation_after_resources_files) - $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(mock_js_invalidation_resources_dir)/after $(mock_js_invalidation_resources_dir)/after/mock-js-resource-cache-after.gresource.xml - jsunit_resources_files := $(shell glib-compile-resources --sourcedir=$(srcdir)/installed-tests/js --generate-dependencies $(srcdir)/installed-tests/js/jsunit.gresources.xml) jsunit-resources.h: $(srcdir)/installed-tests/js/jsunit.gresources.xml $(jsunit_resources_files) $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir)/installed-tests/js --sourcedir=$(builddir) --generate --c-name jsunit_resources $< @@ -59,15 +40,11 @@ BUILT_SOURCES += mock-js-resources.c jsunit-resources.h jsunit-resources.c EXTRA_DIST += \ $(mock_js_resources_files) \ - $(mock_js_invalidation_resources_files) \ $(srcdir)/test/mock-js-resources.gresource.xml \ - $(srcdir)/test/gjs-test-coverage/loadedJSFromResource.js \ $(jsunit_resources_files) \ $(NULL) CLEANFILES += \ - mock-cache-invalidation-before.gresource \ - mock-cache-invalidation-after.gresource \ mock-js-resources.c \ jsunit-resources.c \ jsunit-resources.h \ @@ -105,11 +82,6 @@ gjs_tests_gtester_SOURCES = \ mock-js-resources.c \ $(NULL) -gjs_tests_gtester_DEPENDENCIES = \ - mock-cache-invalidation-before.gresource \ - mock-cache-invalidation-after.gresource \ - $(NULL) - minijasmine_SOURCES = \ installed-tests/minijasmine.cpp \ jsunit-resources.c \ @@ -219,7 +191,6 @@ CLEANFILES += $(TEST_INTROSPECTION_GIRS) $(TEST_INTROSPECTION_TYPELIBS) common_jstests_files = \ installed-tests/js/testself.js \ installed-tests/js/testByteArray.js \ - installed-tests/js/testCoverage.js \ installed-tests/js/testExceptions.js \ installed-tests/js/testEverythingBasic.js \ installed-tests/js/testEverythingEncapsulated.js \ @@ -253,9 +224,10 @@ jasmine_tests += installed-tests/js/testGDBus.js endif if ENABLE_GTK -jasmine_tests += \ - installed-tests/js/testGtk.js \ - installed-tests/js/testLegacyGtk.js \ +jasmine_tests += \ + installed-tests/js/testGtk.js \ + installed-tests/js/testGObjectDestructionAccess.js \ + installed-tests/js/testLegacyGtk.js \ $(NULL) endif @@ -263,14 +235,15 @@ if ENABLE_CAIRO jasmine_tests += installed-tests/js/testCairo.js endif -EXTRA_DIST += \ - $(common_jstests_files) \ - installed-tests/js/testCairo.js \ - installed-tests/js/testGtk.js \ - installed-tests/js/testGDBus.js \ - installed-tests/js/testLegacyGtk.js \ - installed-tests/extra/gjs.supp \ - installed-tests/extra/lsan.supp \ +EXTRA_DIST += \ + $(common_jstests_files) \ + installed-tests/js/testCairo.js \ + installed-tests/js/testGtk.js \ + installed-tests/js/testGDBus.js \ + installed-tests/js/testGObjectDestructionAccess.js \ + installed-tests/js/testLegacyGtk.js \ + installed-tests/extra/gjs.supp \ + installed-tests/extra/lsan.supp \ $(NULL) ### TEST EXECUTION ##################################################### @@ -307,6 +280,7 @@ AM_TESTS_ENVIRONMENT = \ export G_FILENAME_ENCODING=latin1; \ export LSAN_OPTIONS="suppressions=$(abs_top_srcdir)/installed-tests/extra/lsan.supp"; \ export NO_AT_BRIDGE=1; \ + export LC_ALL=C.UTF-8; \ $(COVERAGE_TESTS_ENVIRONMENT) \ $(XVFB_START) \ $(DBUS_SESSION_COMMAND) \ diff --git a/Makefile.am b/Makefile.am index e2541175..1048a30a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -79,6 +79,11 @@ endif # reasons libcjs_la_SOURCES = $(gjs_srcs) +if ENABLE_PROFILER +libcjs_la_SOURCES += $(gjs_sysprof_srcs) +libcjs_la_LIBADD += $(LIB_TIMER_TIME) +endif + # Also, these files used to be a separate library libgjs_private_source_files = $(gjs_private_srcs) @@ -214,6 +219,8 @@ CPPCHECK=cppcheck ### cppcheck static code analysis # cppcheck: - $(CPPCHECK) --enable=warning,performance,portability,information,missingInclude --force -q $(top_srcdir) -I $(top_builddir) + $(CPPCHECK) --inline-suppr \ + --enable=warning,performance,portability,information,missingInclude \ + --force -q $(top_srcdir) -I $(top_builddir) -include $(top_srcdir)/git.mk diff --git a/NEWS b/NEWS index f787da16..43feb1fa 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,231 @@ +Version 1.52.1 +-------------- + +- This version has more changes than would normally be expected from a stable + version. The intention of 1.52.1 is to deliver a version that runs cleaner + under performance tools, in time for the upcoming GNOME Shell performance + hackfest. We also wanted to deliver a stable CI pipeline before branching + GNOME 3.28 off of master. + +- Claudio André's work on the CI pipeline deserves a spotlight. We now have + test jobs that run linters, sanitizers, Valgrind, and more; the tests are + run on up-to-date Docker images; and the reliability errors that were plaguing + the test runs are solved. + +- In addition to System.dumpHeap(), you can now dump a heap from a running + Javascript program by starting it with the environment variable + GJS_DEBUG_HEAP_OUTPUT=some_name, and sending it SIGUSR1. + +- heapgraph.py is a tool in the repository (not installed in distributions) for + analyzing and graphing heap dumps, to aid with tracking down memory leaks. + +- The linter CI jobs will compare your branch against GNOME/gjs@master, and fail + if your branch added any new linter errors. There may be false positives, and + the rules configuration is not perfect. If that's the case on your merge + request, you can skip the appropriate linter job by adding the text + "[skip (linter)]" in your commit message: e.g., "[skip cpplint]". + +- We welcomed first merge requests from several new contributors for this + release. + +- Closed bugs and merge requests: + + * Crash when resolving promises if exception is pending [#18, !95, Philip + Chimento] + * gjs_byte_array_get_proto(JSContext*): assertion failed: (((void) "gjs_" + "byte_array" "_define_proto() must be called before " "gjs_" "byte_array" + "_get_proto()", !v_proto.isUndefined())) [#39, !92, Philip Chimento] + * Tools for examining heap graph [#116, !61, !118, Andy Holmes, Tommi + Komulainen, Philip Chimento] + * Run analysis tools to prepare for release [#120, !88, Philip Chimento] + * Add support for passing flags to Gio.DBusProxy in makeProxyWrapper [#122, + !81, Florian Müllner] + * Cannot instantiate Cairo.Context [#126, !91, Philip Chimento] + * GISCAN GjsPrivate-1.0.gir fails [#128, !90, Philip Chimento] + * Invalid read of g_object_finalized flag [#129, !117, Philip Chimento] + * Fix race condition in coverage file test [#130, !99, Philip Chimento] + * Linter jobs should only fail if new lint errors were added [#133, !94, + Philip Chimento] + * Disable all tests that depends on X if there is no XServer [#135, !109, + Claudio André] + * Pick a different C++ linter [#137, !102, Philip Chimento] + * Create a CI test that builds using autotools only [!74, Claudio André] + * CI: enable ASAN [!89, Claudio André] + * CI: disable static analysis jobs using the commit message [!93, Claudio + André] + * profiler: Don't assume layout of struct sigaction [!96, James Cowgill] + * Valgrind [!98, Claudio André] + * Robustness of CI [!103, Claudio André] + * CI: make a separate job for installed tests [!106, Claudio André] + * Corrected Markdown format and added links to JHBuild in setup guide for GJS + [!111, Avi Zajac] + * Update tweener.js -- 48 eslint errors fixed [!112, Karen Medina] + * Various maintenance [!100, !104, !105, !107, !110, !113, !116, Claudio + André, Philip Chimento] + +Version 1.52.0 +-------------- + +- No changes from 1.51.92 except for the continuous integration configuration. + +- Closed bugs and merge requests: + + * Various CI improvements [!84, !85, !86, !87, Claudio André] + +Version 1.51.92 +--------------- + +- Closed bugs and merge requests: + + * abort if we are called back in a non-main thread [#75, !72, Philip Chimento] + * 3.27.91 build failure on debian/Ubuntu [#122, !73, Tim Lunn] + * Analyze project code quality with Code Climate inside CI [#10, !77, Claudio + André] + * Various CI improvements [!75, !76, !79, !80, !82, !83, Claudio André] + +Version 1.51.91 +--------------- + +- Promises now resolve with a higher priority, so asynchronous code should be + faster. [Jason Hicks] + +- Closed bugs and merge requests: + + * New build 'warnings' [#117, !62, !63, Claudio André, Philip Chimento] + * Various CI maintenance [!64, !65, !66, Claudio André, Philip Chimento] + * profiler: Don't include alloca.h when disabled [!69, Ting-Wei Lan] + * GNOME crash with fatal error "Finalizing proxy for an object that's + scheduled to be unrooted: Gio.Subprocess" in gjs [#26, !70, Philip Chimento] + +Version 1.51.90 +--------------- + +- Note that all the old Bugzilla bug reports have been migrated over to GitLab. + +- GJS now, once again, includes a profiler, which outputs files that can be + read with sysprof. To use it, simply run your program with the environment + variable GJS_ENABLE_PROFILER=1 set. If your program is a JS script that is + executed with the interpreter, you can also pass --profile to the + interpreter. See "gjs --help" for more info. + +- New API: For programs that want more control over when to start and stop + profiling, there is new API for GjsContext. When you create your GjsContext + there are two construct-only properties available, "profiler-enabled" and + "profiler-sigusr2". If you set profiler-sigusr2 to TRUE, then the profiler + can be started and stopped while the program is running by sending SIGUSR2 to + the process. You can also use gjs_context_get_profiler(), + gjs_profiler_set_filename(), gjs_profiler_start(), and gjs_profiler_stop() + for more explicit control. + +- New API: GObject.signal_connect(), GObject.signal_disconnect(), and + GObject.signal_emit_by_name() are now available in case a GObject-derived + class has conflicting connect(), disconnect() or emit() methods. + +- Closed bugs and merge requests: + + * Handle 0-valued GType gracefully [#11, !10, Philip Chimento] + * Profiler [#31, !37, Christian Hergert, Philip Chimento] + * Various maintenance [!40, !59, Philip Chimento, Giovanni Campagna] + * Rename GObject.Object.connect/disconnect? [#65, !47, Giovanni Campagna] + * Better debugging output for uncatchable exceptions [!39, Simon McVittie] + * Update Docker images and various CI maintenance [!54, !56, !57, !58, + Claudio André] + * Install GJS suppression file for Valgrind [#2, !55, Philip Chimento] + +Version 1.50.4 +-------------- + +- Closed bugs and merge requests: + + * Gnome Shell crash with places-status extension when you plug an USB device + [#33, !38, Philip Chimento] + +Version 1.50.3 +-------------- + +- GJS will now log a warning when a GObject is accessed in Javascript code + after the underlying object has been freed in C. (This used to work most of + the time, but crash unpredictably.) We now prevent this situation which, is + usually caused by a memory management bug in the underlying C library. + +- Closed bugs and merge requests: + + * Add checks for GObjects that have been finalized [#21, #23, !25, !28, !33, + Marco Trevisan] + * Test "Cairo context has methods when created from a C function" fails [#27, + !35, Valentín Barros] + * Various fixes from the master branch for rare crashes [Philip Chimento] + +Version 1.51.4 +-------------- + +- We welcomed code and documentation from several new contributors in this + release! + +- GJS will now log a warning when a GObject is accessed in Javascript code + after the underlying object has been freed in C. (This used to work most of + the time, but crash unpredictably.) We now prevent this situation which, is + usually caused by a memory management bug in the underlying C library. + +- APIs exposed through GObject Introspection that use the GdkAtom type are now + usable from Javascript. Previously these did not work. On the Javascript side, + a GdkAtom translates to a string, so there is no Gdk.Atom type that you can + access. The special atom GDK_NONE translates to null in Javascript, and there + is also no Gdk.NONE constant. + +- The GitLab CI tasks have continued to gradually become more and more + sophisticated. + +- Closed bugs and merge requests: + + * Add checks for GObjects that have been finalized [#21, #23, !22, !27, Marco + Trevisan] + * Fail static analyzer if new warnings are found [!24, Claudio André] + * Run code coverage on GitLab [!20, Claudio André] + * Amend gtk.js and add gtk-application.js with suggestion [!32, Andy Holmes] + * Improve GdkAtom support that is blocking clipboard APIs [#14, !29, makepost] + * Test "Cairo context has methods when created from a C function" fails [#27, + !35, Valentín Barros] + * Various CI improvements [#6, !26, !34, Claudio André] + * Various maintenance [!23, !36, Philip Chimento] + +Version 1.51.3 +-------------- + +- This release was made from an earlier state of master, before a breaking + change was merged, while we decide whether to revert that change or not. + +- Closed bugs and merge requests: + + * CI improvements on GitLab [!14, !15, !19, Claudio André] + * Fix CI build on Ubuntu [#16, !18, !21, Claudio André, Philip Chimento] + +Version 1.51.2 +-------------- + +- Version 1.51.1 was skipped. + +- The home of GJS is now at GNOME's GitLab instance: + https://gitlab.gnome.org/GNOME/gjs + From now on we'll be taking GitLab merge requests instead of Bugzilla + patches. If you want to report a bug, please report it at GitLab. + +- Closed bugs and merge requests: + + * Allow throwing GErrors from JS virtual functions [#682701, Giovanni + Campagna] + * [RFC] bootstrap system [#777724, Jasper St. Pierre, Philip Chimento] + * Fix code coverage (and refactor it to take advantage of mozjs52 features) + [#788166, !1, !3, Philip Chimento] + * Various maintenance [!2, Philip Chimento] + * Get GitLab CI working and various improvements [#6, !7, !9, !11, !13, + Claudio André] + * Add build status badge to README [!8, Claudio André] + * Use Docker images for CI [!12, Claudio André] + +- Some changes in progress to improve garbage collection when signals are + disconnected. See bug #679688 for more information [Giovanni Campagna] + Version 1.50.2 -------------- diff --git a/cjs/byteArray.cpp b/cjs/byteArray.cpp index 4c32e987..930850fc 100644 --- a/cjs/byteArray.cpp +++ b/cjs/byteArray.cpp @@ -408,7 +408,7 @@ to_string_func(JSContext *context, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, argv, to, ByteArrayInstance, priv); - GjsAutoJSChar encoding(context); + GjsAutoJSChar encoding; bool encoding_is_utf8; gchar *data; @@ -418,7 +418,9 @@ to_string_func(JSContext *context, byte_array_ensure_array(priv); if (argc >= 1 && argv[0].isString()) { - if (!gjs_string_to_utf8(context, argv[0], &encoding)) + JS::RootedString str(context, argv[0].toString()); + encoding = JS_EncodeStringToUTF8(context, str); + if (!encoding) return false; /* maybe we should be smarter about utf8 synonyms here. @@ -440,8 +442,7 @@ to_string_func(JSContext *context, /* optimization, avoids iconv overhead and runs * libmozjs hardwired utf8-to-utf16 */ - return gjs_string_from_utf8(context, data, priv->array->len, - argv.rval()); + return gjs_string_from_utf8_n(context, data, priv->array->len, argv.rval()); } else { bool ok = false; gsize bytes_written; @@ -530,7 +531,7 @@ from_string_func(JSContext *context, { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); ByteArrayInstance *priv; - GjsAutoJSChar encoding(context); + GjsAutoJSChar encoding; bool encoding_is_utf8; JS::RootedObject obj(context, byte_array_new(context)); @@ -551,7 +552,9 @@ from_string_func(JSContext *context, } if (argc > 1 && argv[1].isString()) { - if (!gjs_string_to_utf8(context, argv[1], &encoding)) + JS::RootedString str(context, argv[1].toString()); + encoding = JS_EncodeStringToUTF8(context, str); + if (!encoding) return false; /* maybe we should be smarter about utf8 synonyms here. @@ -567,10 +570,9 @@ from_string_func(JSContext *context, /* optimization? avoids iconv overhead and runs * libmozjs hardwired utf16-to-utf8. */ - GjsAutoJSChar utf8(context); - if (!gjs_string_to_utf8(context, - argv[0], - &utf8)) + JS::RootedString str(context, argv[0].toString()); + GjsAutoJSChar utf8 = JS_EncodeStringToUTF8(context, str); + if (!utf8) return false; g_byte_array_set_size(priv->array, 0); diff --git a/cjs/console.cpp b/cjs/console.cpp index d3cfcac1..917e71da 100644 --- a/cjs/console.cpp +++ b/cjs/console.cpp @@ -33,15 +33,27 @@ static char **include_path = NULL; static char **coverage_prefixes = NULL; static char *coverage_output_path = NULL; +static char *profile_output_path = nullptr; static char *command = NULL; static gboolean print_version = false; +static gboolean print_js_version = false; +static bool enable_profiler = false; +static gboolean parse_profile_arg(const char *, const char *, void *, GError **); + +/* Keep in sync with entries in check_script_args_for_stray_gjs_args() */ static GOptionEntry entries[] = { { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, "Print GJS version and exit" }, + { "jsversion", 0, 0, G_OPTION_ARG_NONE, &print_js_version, + "Print version of the JS engine and exit" }, { "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" }, { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &coverage_prefixes, "Add the prefix PREFIX to the list of files to generate coverage info for", "PREFIX" }, { "coverage-output", 0, 0, G_OPTION_ARG_STRING, &coverage_output_path, "Write coverage output to a directory DIR. This option is mandatory when using --coverage-path", "DIR", }, { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, &include_path, "Add the directory DIR to the list of directories to search for js files.", "DIR" }, + { "profile", 0, G_OPTION_FLAG_OPTIONAL_ARG | G_OPTION_FLAG_FILENAME, + G_OPTION_ARG_CALLBACK, reinterpret_cast(&parse_profile_arg), + "Enable the profiler and write output to FILE (default: gjs-$PID.syscap)", + "FILE" }, { NULL } }; @@ -84,6 +96,32 @@ strcatv(char **strv1, return retval; } +static gboolean +parse_profile_arg(const char *option_name, + const char *value, + void *data, + GError **error_out) +{ + enable_profiler = true; + g_free(profile_output_path); + if (value) + profile_output_path = g_strdup(value); + return true; +} + +static gboolean +check_stray_profile_arg(const char *option_name, + const char *value, + void *data, + GError **error_out) +{ + g_warning("You used the --profile option after the script on the GJS " + "command line. Support for this will be removed in a future " + "version. Place the option before the script or use the " + "GJS_ENABLE_PROFILER environment variable."); + return parse_profile_arg(option_name, value, data, error_out); +} + static void check_script_args_for_stray_gjs_args(int argc, char * const *argv) @@ -92,10 +130,13 @@ check_script_args_for_stray_gjs_args(int argc, char **new_coverage_prefixes = NULL; char *new_coverage_output_path = NULL; char **new_include_paths = NULL; + /* Keep in sync with entries[] at the top */ static GOptionEntry script_check_entries[] = { { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &new_coverage_prefixes }, { "coverage-output", 0, 0, G_OPTION_ARG_STRING, &new_coverage_output_path }, { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, &new_include_paths }, + { "profile", 0, G_OPTION_FLAG_OPTIONAL_ARG | G_OPTION_FLAG_FILENAME, + G_OPTION_ARG_CALLBACK, reinterpret_cast(&check_stray_profile_arg) }, { NULL } }; char **argv_copy = g_new(char *, argc + 2); @@ -165,6 +206,7 @@ main(int argc, char **argv) char * const *script_argv; const char *env_coverage_output_path; const char *env_coverage_prefixes; + bool interactive_mode = false; setlocale(LC_ALL, ""); @@ -205,6 +247,7 @@ main(int argc, char **argv) coverage_output_path = NULL; command = NULL; print_version = false; + print_js_version = false; g_option_context_set_ignore_unknown_options(context, false); g_option_context_set_help_enabled(context, true); if (!g_option_context_parse_strv(context, &gjs_argv, &error)) @@ -217,6 +260,11 @@ main(int argc, char **argv) exit(0); } + if (print_js_version) { + g_print("%s\n", gjs_get_js_version()); + exit(0); + } + gjs_argc = g_strv_length(gjs_argv); if (command != NULL) { script = command; @@ -228,6 +276,7 @@ main(int argc, char **argv) len = strlen(script); filename = ""; program_name = gjs_argv[0]; + interactive_mode = true; } else { /* All unprocessed options should be in script_argv */ g_assert(gjs_argc == 2); @@ -243,9 +292,16 @@ main(int argc, char **argv) /* This should be removed after a suitable time has passed */ check_script_args_for_stray_gjs_args(script_argc, script_argv); + if (interactive_mode && enable_profiler) { + g_message("Profiler disabled in interactive mode."); + enable_profiler = false; + g_unsetenv("GJS_ENABLE_PROFILER"); /* ignore env var in eval() */ + } + js_context = (GjsContext*) g_object_new(GJS_TYPE_CONTEXT, "search-path", include_path, "program-name", program_name, + "profiler-enabled", enable_profiler, NULL); env_coverage_output_path = g_getenv("GJS_COVERAGE_OUTPUT"); @@ -270,6 +326,11 @@ main(int argc, char **argv) g_object_unref(output); } + if (enable_profiler && profile_output_path) { + GjsProfiler *profiler = gjs_context_get_profiler(js_context); + gjs_profiler_set_filename(profiler, profile_output_path); + } + /* prepare command line arguments */ if (!gjs_context_define_string_array(js_context, "ARGV", script_argc, (const char **) script_argv, @@ -297,6 +358,7 @@ main(int argc, char **argv) gjs_coverage_write_statistics(coverage); g_free(coverage_output_path); + g_free(profile_output_path); g_strfreev(coverage_prefixes); if (coverage) g_object_unref(coverage); diff --git a/cjs/context-private.h b/cjs/context-private.h index 6dbe6690..49c0cf9e 100644 --- a/cjs/context-private.h +++ b/cjs/context-private.h @@ -36,6 +36,8 @@ bool _gjs_context_destroying (GjsContext *js_context); void _gjs_context_schedule_gc_if_needed (GjsContext *js_context); +void _gjs_context_schedule_gc(GjsContext *js_context); + void _gjs_context_exit(GjsContext *js_context, uint8_t exit_code); diff --git a/cjs/context.cpp b/cjs/context.cpp index 7937b608..f44a8dea 100644 --- a/cjs/context.cpp +++ b/cjs/context.cpp @@ -23,6 +23,10 @@ #include +#include +#include +#include + #include #include @@ -32,10 +36,11 @@ #include "engine.h" #include "global.h" #include "importer.h" -#include "jsapi-private.h" #include "jsapi-util.h" #include "jsapi-wrapper.h" +#include "mem.h" #include "native.h" +#include "profiler-private.h" #include "byteArray.h" #include "gi/object.h" #include "gi/repo.h" @@ -85,6 +90,7 @@ struct _GjsContext { uint8_t exit_code; guint auto_gc_id; + bool force_gc; std::array const_strings; @@ -93,6 +99,10 @@ struct _GjsContext { bool draining_job_queue; std::unordered_map unhandled_rejection_stacks; + + GjsProfiler *profiler; + bool should_profile : 1; + bool should_listen_sigusr2 : 1; }; /* Keep this consistent with GjsConstString */ @@ -112,17 +122,93 @@ struct _GjsContextClass { GObjectClass parent; }; +/* Temporary workaround for https://bugzilla.gnome.org/show_bug.cgi?id=793175 */ +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic push") +_Pragma("GCC diagnostic ignored \"-Wcast-function-type\"") +#endif G_DEFINE_TYPE(GjsContext, gjs_context, G_TYPE_OBJECT); +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic pop") +#endif enum { PROP_0, PROP_SEARCH_PATH, PROP_PROGRAM_NAME, + PROP_PROFILER_ENABLED, + PROP_PROFILER_SIGUSR2, }; static GMutex contexts_lock; static GList *all_contexts = NULL; +static GjsAutoChar dump_heap_output; +static unsigned dump_heap_idle_id = 0; + +static void +gjs_context_dump_heaps(void) +{ + static unsigned counter = 0; + + gjs_memory_report("signal handler", false); + + /* dump to sequential files to allow easier comparisons */ + GjsAutoChar filename = g_strdup_printf("%s.%jd.%u", dump_heap_output.get(), + intmax_t(getpid()), counter); + ++counter; + + FILE *fp = fopen(filename, "w"); + if (!fp) + return; + + for (GList *l = all_contexts; l; l = g_list_next(l)) { + auto js_context = static_cast(l->data); + js::DumpHeap(js_context->context, fp, js::IgnoreNurseryObjects); + } + + fclose(fp); +} + +static gboolean +dump_heap_idle(gpointer user_data) +{ + dump_heap_idle_id = 0; + + gjs_context_dump_heaps(); + + return false; +} + +static void +dump_heap_signal_handler(int signum) +{ + if (dump_heap_idle_id == 0) + dump_heap_idle_id = g_idle_add_full(G_PRIORITY_HIGH_IDLE, + dump_heap_idle, nullptr, nullptr); +} + +static void +setup_dump_heap(void) +{ + static bool dump_heap_initialized = false; + if (!dump_heap_initialized) { + dump_heap_initialized = true; + + /* install signal handler only if environment variable is set */ + const char *heap_output = g_getenv("GJS_DEBUG_HEAP_OUTPUT"); + if (heap_output) { + struct sigaction sa; + + dump_heap_output = g_strdup(heap_output); + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = dump_heap_signal_handler; + sigaction(SIGUSR1, &sa, nullptr); + } + } +} + static void gjs_context_init(GjsContext *js_context) { @@ -164,7 +250,40 @@ gjs_context_class_init(GjsContextClass *klass) pspec); g_param_spec_unref(pspec); - /* For CjsPrivate */ + /** + * GjsContext:profiler-enabled: + * + * Set this property to profile any JS code run by this context. By + * default, the profiler is started and stopped when you call + * gjs_context_eval(). + * + * The value of this property is superseded by the GJS_ENABLE_PROFILER + * environment variable. + * + * You may only have one context with the profiler enabled at a time. + */ + pspec = g_param_spec_boolean("profiler-enabled", "Profiler enabled", + "Whether to profile JS code run by this context", + FALSE, + GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property(object_class, PROP_PROFILER_ENABLED, pspec); + g_param_spec_unref(pspec); + + /** + * GjsContext:profiler-sigusr2: + * + * Set this property to install a SIGUSR2 signal handler that starts and + * stops the profiler. This property also implies that + * #GjsContext:profiler-enabled is set. + */ + pspec = g_param_spec_boolean("profiler-sigusr2", "Profiler SIGUSR2", + "Whether to activate the profiler on SIGUSR2", + FALSE, + GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property(object_class, PROP_PROFILER_SIGUSR2, pspec); + g_param_spec_unref(pspec); + + /* For GjsPrivate */ { #ifdef G_OS_WIN32 extern HMODULE gjs_dll; @@ -210,19 +329,34 @@ warn_about_unhandled_promise_rejections(GjsContext *gjs_context) static void gjs_context_dispose(GObject *object) { + gjs_debug(GJS_DEBUG_CONTEXT, "JS shutdown sequence"); + GjsContext *js_context; js_context = GJS_CONTEXT(object); - /* Run dispose notifications first, so that anything releasing + /* Profiler must be stopped and freed before context is shut down */ + gjs_debug(GJS_DEBUG_CONTEXT, "Stopping profiler"); + if (js_context->profiler) + g_clear_pointer(&js_context->profiler, _gjs_profiler_free); + + /* Stop accepting entries in the toggle queue before running dispose + * notifications, which causes all GjsMaybeOwned instances to unroot. + * We don't want any objects to toggle down after that. */ + gjs_debug(GJS_DEBUG_CONTEXT, "Shutting down toggle queue"); + gjs_object_clear_toggles(); + gjs_object_shutdown_toggle_queue(); + + /* Run dispose notifications next, so that anything releasing * references in response to this can still get garbage collected */ + gjs_debug(GJS_DEBUG_CONTEXT, + "Notifying reference holders of GjsContext dispose"); G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object); if (js_context->context != NULL) { gjs_debug(GJS_DEBUG_CONTEXT, - "Destroying JS context"); - + "Checking unhandled promise rejections"); warn_about_unhandled_promise_rejections(js_context); JS_BeginRequest(js_context->context); @@ -231,34 +365,42 @@ gjs_context_dispose(GObject *object) * that we may not have the JS_GetPrivate() to access the * context */ + gjs_debug(GJS_DEBUG_CONTEXT, "Final triggered GC"); JS_GC(js_context->context); JS_EndRequest(js_context->context); + gjs_debug(GJS_DEBUG_CONTEXT, "Destroying JS context"); js_context->destroying = true; /* Now, release all native objects, to avoid recursion between * the JS teardown and the C teardown. The JSObject proxies * still exist, but point to NULL. */ + gjs_debug(GJS_DEBUG_CONTEXT, "Releasing all native objects"); gjs_object_prepare_shutdown(); + gjs_debug(GJS_DEBUG_CONTEXT, "Disabling auto GC"); if (js_context->auto_gc_id > 0) { g_source_remove (js_context->auto_gc_id); js_context->auto_gc_id = 0; } + gjs_debug(GJS_DEBUG_CONTEXT, "Ending trace on global object"); JS_RemoveExtraGCRootsTracer(js_context->context, gjs_context_tracer, js_context); js_context->global = NULL; + gjs_debug(GJS_DEBUG_CONTEXT, "Unrooting atoms"); for (auto& root : js_context->const_strings) delete root; + gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources"); delete js_context->job_queue; /* Tear down JS */ JS_DestroyContext(js_context->context); js_context->context = NULL; + gjs_debug(GJS_DEBUG_CONTEXT, "JS context destroyed"); } } @@ -307,6 +449,21 @@ gjs_context_constructed(GObject *object) g_error("Failed to create javascript context"); js_context->context = cx; + const char *env_profiler = g_getenv("GJS_ENABLE_PROFILER"); + if (env_profiler || js_context->should_listen_sigusr2) + js_context->should_profile = true; + + if (js_context->should_profile) { + js_context->profiler = _gjs_profiler_new(js_context); + + if (!js_context->profiler) { + js_context->should_profile = false; + } else { + if (js_context->should_listen_sigusr2) + _gjs_profiler_setup_signals(js_context->profiler, js_context); + } + } + new (&js_context->unhandled_rejection_stacks) std::unordered_map; new (&js_context->const_strings) std::array; for (i = 0; i < GJS_STRING_LAST; i++) { @@ -342,16 +499,25 @@ gjs_context_constructed(GObject *object) gjs_set_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS, JS::ObjectValue(*importer)); - if (!gjs_define_global_properties(cx, global)) { + if (!gjs_define_global_properties(cx, global, "default")) { gjs_log_exception(cx); g_error("Failed to define properties on global object"); } + /* Pre-import the byteArray module. We depend on this module for some of + * our GObject introspection marshalling, so the ByteArray prototype + * defined in it needs to be always available. */ + gjs_import_native_module(cx, importer, "byteArray"); + JS_EndRequest(cx); g_mutex_lock (&contexts_lock); all_contexts = g_list_prepend(all_contexts, object); g_mutex_unlock (&contexts_lock); + + setup_dump_heap(); + + g_object_weak_ref(object, gjs_object_context_dispose_notify, nullptr); } static void @@ -391,6 +557,12 @@ gjs_context_set_property (GObject *object, case PROP_PROGRAM_NAME: js_context->program_name = g_value_dup_string(value); break; + case PROP_PROFILER_ENABLED: + js_context->should_profile = g_value_get_boolean(value); + break; + case PROP_PROFILER_SIGUSR2: + js_context->should_listen_sigusr2 = g_value_get_boolean(value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -423,13 +595,24 @@ trigger_gc_if_needed (gpointer user_data) { GjsContext *js_context = GJS_CONTEXT(user_data); js_context->auto_gc_id = 0; - gjs_gc_if_needed(js_context->context); + + if (js_context->force_gc) + JS_GC(js_context->context); + else + gjs_gc_if_needed(js_context->context); + + js_context->force_gc = false; + return G_SOURCE_REMOVE; } -void -_gjs_context_schedule_gc_if_needed (GjsContext *js_context) + +static void +_gjs_context_schedule_gc_internal(GjsContext *js_context, + bool force_gc) { + js_context->force_gc |= force_gc; + if (js_context->auto_gc_id > 0) return; @@ -438,6 +621,18 @@ _gjs_context_schedule_gc_if_needed (GjsContext *js_context) js_context, NULL); } +void +_gjs_context_schedule_gc(GjsContext *js_context) +{ + _gjs_context_schedule_gc_internal(js_context, true); +} + +void +_gjs_context_schedule_gc_if_needed(GjsContext *js_context) +{ + _gjs_context_schedule_gc_internal(js_context, false); +} + void _gjs_context_exit(GjsContext *js_context, uint8_t exit_code) @@ -508,10 +703,9 @@ _gjs_context_enqueue_job(GjsContext *gjs_context, if (!gjs_context->job_queue->append(job)) return false; if (!gjs_context->idle_drain_handler) - /*Modified for CJS, Promises shouldn't take 200ms to resolve. When several - promises are queued up, this artifically inflates resolve time. */ gjs_context->idle_drain_handler = - g_idle_add_full(0, drain_job_queue_idle_handler, gjs_context, NULL); + g_idle_add_full(G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, + gjs_context, nullptr); return true; } @@ -572,6 +766,10 @@ _gjs_context_run_jobs(GjsContext *gjs_context) * System.exit() works in the interactive shell and when * exiting the interpreter. */ if (!JS_IsExceptionPending(cx)) { + /* System.exit() is an uncatchable exception, but does not + * indicate a bug. Log everything else. */ + if (!_gjs_context_should_exit(gjs_context, nullptr)) + g_critical("Promise callback terminated with uncatchable exception"); retval = false; continue; } @@ -693,11 +891,19 @@ gjs_context_eval(GjsContext *js_context, { bool ret = false; + bool auto_profile = js_context->should_profile; + if (auto_profile && (_gjs_profiler_is_running(js_context->profiler) || + js_context->should_listen_sigusr2)) + auto_profile = false; + JSAutoCompartment ac(js_context->context, js_context->global); JSAutoRequest ar(js_context->context); g_object_ref(G_OBJECT(js_context)); + if (auto_profile) + gjs_profiler_start(js_context->profiler); + JS::RootedValue retval(js_context->context); bool ok = gjs_eval_with_scope(js_context->context, nullptr, script, script_len, filename, &retval); @@ -705,7 +911,13 @@ gjs_context_eval(GjsContext *js_context, /* The promise job queue should be drained even on error, to finish * outstanding async tasks before the context is torn down. Drain after * uncaught exceptions have been reported since draining runs callbacks. */ - ok = _gjs_context_run_jobs(js_context) && ok; + { + JS::AutoSaveExceptionState saved_exc(js_context->context); + ok = _gjs_context_run_jobs(js_context) && ok; + } + + if (auto_profile) + gjs_profiler_stop(js_context->profiler); if (!ok) { uint8_t code; @@ -718,11 +930,18 @@ gjs_context_eval(GjsContext *js_context, goto out; /* Don't log anything */ } + if (!JS_IsExceptionPending(js_context->context)) { + g_critical("Script %s terminated with an uncatchable exception", + filename); + g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, + "Script %s terminated with an uncatchable exception", + filename); + } else { + g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, + "Script %s threw an exception", filename); + } + gjs_log_exception(js_context->context); - g_set_error(error, - GJS_ERROR, - GJS_ERROR_FAILED, - "JS_EvaluateScript() failed"); /* No exit code from script, but we don't want to exit(0) */ *exit_status_p = 1; goto out; @@ -844,4 +1063,32 @@ gjs_get_import_global(JSContext *context) { GjsContext *gjs_context = (GjsContext *) JS_GetContextPrivate(context); return gjs_context->global; +} + +/** + * gjs_context_get_profiler: + * @self: the #GjsContext + * + * Returns the profiler's internal instance of #GjsProfiler for you to + * customize, or %NULL if profiling is not enabled on this #GjsContext. + * + * Returns: (transfer none) (nullable): a #GjsProfiler + */ +GjsProfiler * +gjs_context_get_profiler(GjsContext *self) +{ + return self->profiler; +} + +/** + * gjs_get_js_version: + * + * Returns the underlying version of the JS engine. + * + * Returns: a string + */ +const char * +gjs_get_js_version(void) +{ + return JS_GetImplementationVersion(); } \ No newline at end of file diff --git a/cjs/context.h b/cjs/context.h index 83dda017..129c59a6 100644 --- a/cjs/context.h +++ b/cjs/context.h @@ -25,13 +25,14 @@ #define __GJS_CONTEXT_H__ #if !defined (__GJS_GJS_H__) && !defined (GJS_COMPILATION) -#error "Only can be included directly." +#error "Only can be included directly." #endif #include #include #include +#include G_BEGIN_DECLS @@ -91,9 +92,19 @@ void gjs_context_maybe_gc (GjsContext *context); GJS_EXPORT void gjs_context_gc (GjsContext *context); +GJS_EXPORT +GjsProfiler *gjs_context_get_profiler(GjsContext *self); + +GJS_EXPORT +bool gjs_profiler_chain_signal(GjsContext *context, + siginfo_t *info); + GJS_EXPORT void gjs_dumpstack (void); +GJS_EXPORT +const char *gjs_get_js_version(void); + G_END_DECLS -#endif /* __GJS_CONTEXT_H__ */ +#endif /* __GJS_CONTEXT_H__ */ \ No newline at end of file diff --git a/cjs/coverage-internal.h b/cjs/coverage-internal.h deleted file mode 100644 index d26679d0..00000000 --- a/cjs/coverage-internal.h +++ /dev/null @@ -1,68 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright © 2015 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: Sam Spilsbury - */ - -#ifndef _GJS_COVERAGE_INTERNAL_H -#define _GJS_COVERAGE_INTERNAL_H - -#include - -#include "jsapi-util.h" -#include "coverage.h" - -G_BEGIN_DECLS - -GjsCoverage *gjs_coverage_new_internal_with_cache(const char * const *coverage_prefixes, - GjsContext *context, - GFile *output_dir, - GFile *cache_path); - -GjsCoverage *gjs_coverage_new_internal_without_cache(const char * const *prefixes, - GjsContext *cx, - GFile *output_dir); - -GBytes * gjs_serialize_statistics(GjsCoverage *coverage); - -JSString * gjs_deserialize_cache_to_object(GjsCoverage *coverage, - GBytes *cache_bytes); - -bool gjs_run_script_in_coverage_compartment(GjsCoverage *coverage, - const char *script); -bool gjs_inject_value_into_coverage_compartment(GjsCoverage *coverage, - JS::HandleValue value, - const char *property); - -bool gjs_get_file_mtime(GFile *file, - GTimeVal *mtime); - -char *gjs_get_file_checksum(GFile *file); - -bool gjs_write_cache_file(GFile *file, - GBytes *cache_bytes); - -extern const char *GJS_COVERAGE_CACHE_FILE_NAME; - -G_END_DECLS - -#endif diff --git a/cjs/coverage.cpp b/cjs/coverage.cpp index e0ea7ec7..355e4f04 100644 --- a/cjs/coverage.cpp +++ b/cjs/coverage.cpp @@ -29,7 +29,6 @@ #include #include "coverage.h" -#include "coverage-internal.h" #include "global.h" #include "importer.h" #include "jsapi-util-args.h" @@ -42,17 +41,21 @@ struct _GjsCoverage { typedef struct { gchar **prefixes; GjsContext *context; - JS::Heap coverage_statistics; + JS::Heap compartment; GFile *output_dir; - GFile *cache; - /* tells whether priv->cache == NULL means no cache, or not specified */ - bool cache_specified; } GjsCoveragePrivate; +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic push") +_Pragma("GCC diagnostic ignored \"-Wcast-function-type\"") +#endif G_DEFINE_TYPE_WITH_PRIVATE(GjsCoverage, gjs_coverage, G_TYPE_OBJECT) +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic pop") +#endif enum { PROP_0, @@ -65,23 +68,6 @@ enum { static GParamSpec *properties[PROP_N] = { NULL, }; -typedef struct _GjsCoverageBranchExit { - unsigned int line; - unsigned int hit_count; -} GjsCoverageBranchExit; - -typedef struct _GjsCoverageBranch { - GArray *exits; - unsigned int point; - bool hit; -} GjsCoverageBranch; - -typedef struct _GjsCoverageFunction { - char *key; - unsigned int line_number; - unsigned int hit_count; -} GjsCoverageFunction; - static char * get_file_identifier(GFile *source_file) { char *path = g_file_get_path(source_file); @@ -90,241 +76,31 @@ get_file_identifier(GFile *source_file) { return path; } -static void +static bool write_source_file_header(GOutputStream *stream, - GFile *source_file) -{ - char *path = get_file_identifier(source_file); - g_output_stream_printf(stream, NULL, NULL, NULL, "SF:%s\n", path); - g_free(path); -} - -typedef struct _FunctionHitCountData { - GOutputStream *stream; - unsigned int *n_functions_found; - unsigned int *n_functions_hit; -} FunctionHitCountData; - -static void -write_function_hit_count(GOutputStream *stream, - const char *function_name, - unsigned int hit_count, - unsigned int *n_functions_found, - unsigned int *n_functions_hit) -{ - (*n_functions_found)++; - - if (hit_count > 0) - (*n_functions_hit)++; - - g_output_stream_printf(stream, NULL, NULL, NULL, "FNDA:%d,%s\n", hit_count, function_name); -} - -static void -write_functions_hit_counts(GOutputStream *stream, - GArray *functions, - unsigned int *n_functions_found, - unsigned int *n_functions_hit) -{ - unsigned int i = 0; - - for (; i < functions->len; ++i) { - GjsCoverageFunction *function = &(g_array_index(functions, GjsCoverageFunction, i)); - write_function_hit_count(stream, - function->key, - function->hit_count, - n_functions_found, - n_functions_hit); - } -} - -static void -write_function_foreach_func(gpointer value, - gpointer user_data) -{ - GOutputStream *stream = (GOutputStream *) user_data; - GjsCoverageFunction *function = (GjsCoverageFunction *) value; - - g_output_stream_printf(stream, NULL, NULL, NULL, "FN:%d,%s\n", function->line_number, function->key); -} - -static void -for_each_element_in_array(GArray *array, - GFunc func, - gpointer user_data) -{ - const gsize element_size = g_array_get_element_size(array); - unsigned int i; - char *current_array_pointer = (char *) array->data; - - for (i = 0; i < array->len; ++i, current_array_pointer += element_size) - (*func)(current_array_pointer, user_data); -} - -static void -write_functions(GOutputStream *data_stream, - GArray *functions) -{ - for_each_element_in_array(functions, write_function_foreach_func, data_stream); -} - -static void -write_function_coverage(GOutputStream *data_stream, - unsigned int n_found_functions, - unsigned int n_hit_functions) -{ - g_output_stream_printf(data_stream, NULL, NULL, NULL, "FNF:%d\n", n_found_functions); - g_output_stream_printf(data_stream, NULL, NULL, NULL, "FNH:%d\n", n_hit_functions); -} - -typedef struct _WriteAlternativeData { - unsigned int *n_branch_alternatives_found; - unsigned int *n_branch_alternatives_hit; - GOutputStream *output_stream; - gpointer *all_alternatives; - bool branch_point_was_hit; -} WriteAlternativeData; - -typedef struct _WriteBranchInfoData { - unsigned int *n_branch_exits_found; - unsigned int *n_branch_exits_hit; - GOutputStream *output_stream; -} WriteBranchInfoData; - -static void -write_individual_branch(gpointer branch_ptr, - gpointer user_data) -{ - GjsCoverageBranch *branch = (GjsCoverageBranch *) branch_ptr; - WriteBranchInfoData *data = (WriteBranchInfoData *) user_data; - - /* This line is not a branch, don't write anything */ - if (!branch->point) - return; - - unsigned int i = 0; - for (; i < branch->exits->len; ++i) { - GjsCoverageBranchExit *exit = &(g_array_index(branch->exits, GjsCoverageBranchExit, i)); - unsigned int alternative_counter = exit->hit_count; - unsigned int branch_point = branch->point; - char *hit_count_string = NULL; - - if (!branch->hit) - hit_count_string = g_strdup_printf("-"); - else - hit_count_string = g_strdup_printf("%d", alternative_counter); - - g_output_stream_printf(data->output_stream, NULL, NULL, NULL, "BRDA:%d,0,%d,%s\n", - branch_point, i, hit_count_string); - g_free(hit_count_string); - - ++(*data->n_branch_exits_found); - - if (alternative_counter > 0) - ++(*data->n_branch_exits_hit); - } -} - -static void -write_branch_coverage(GOutputStream *stream, - GArray *branches, - unsigned int *n_branch_exits_found, - unsigned int *n_branch_exits_hit) - -{ - /* Write individual branches and pass-out the totals */ - WriteBranchInfoData data = { - n_branch_exits_found, - n_branch_exits_hit, - stream - }; - - for_each_element_in_array(branches, - write_individual_branch, - &data); -} - -static void -write_branch_totals(GOutputStream *stream, - unsigned int n_branch_exits_found, - unsigned int n_branch_exits_hit) -{ - g_output_stream_printf(stream, NULL, NULL, NULL, "BRF:%d\n", n_branch_exits_found); - g_output_stream_printf(stream, NULL, NULL, NULL, "BRH:%d\n", n_branch_exits_hit); -} - -static void -write_line_coverage(GOutputStream *stream, - GArray *stats, - unsigned int *lines_hit_count, - unsigned int *executable_lines_count) -{ - unsigned int i = 0; - for (i = 0; i < stats->len; ++i) { - int hit_count_for_line = g_array_index(stats, int, i); - - if (hit_count_for_line == -1) - continue; - - g_output_stream_printf(stream, NULL, NULL, NULL, "DA:%d,%d\n", i, hit_count_for_line); - - if (hit_count_for_line > 0) - ++(*lines_hit_count); - - ++(*executable_lines_count); - } -} - -static void -write_line_totals(GOutputStream *stream, - unsigned int lines_hit_count, - unsigned int executable_lines_count) + GFile *source_file, + GError **error) { - g_output_stream_printf(stream, NULL, NULL, NULL, "LH:%d\n", lines_hit_count); - g_output_stream_printf(stream, NULL, NULL, NULL, "LF:%d\n", executable_lines_count); + GjsAutoChar path = get_file_identifier(source_file); + return g_output_stream_printf(stream, NULL, NULL, error, "SF:%s\n", path.get()); } -static void -write_end_of_record(GOutputStream *stream) -{ - g_output_stream_printf(stream, NULL, NULL, NULL, "end_of_record\n"); -} - -static void -copy_source_file_to_coverage_output(GFile *source_file, - GFile *destination_file) +static bool +copy_source_file_to_coverage_output(GFile *source_file, + GFile *destination_file, + GError **error) { - GError *error = NULL; - /* We need to recursively make the directory we * want to copy to, as g_file_copy doesn't do that */ GjsAutoUnref destination_dir = g_file_get_parent(destination_file); - if (!g_file_make_directory_with_parents(destination_dir, NULL, &error)) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_EXISTS)) - goto fail; - g_clear_error(&error); - } - - if (!g_file_copy(source_file, - destination_file, - G_FILE_COPY_OVERWRITE, - NULL, - NULL, - NULL, - &error)) { - goto fail; + if (!g_file_make_directory_with_parents(destination_dir, NULL, error)) { + if (!g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + return false; + g_clear_error(error); } - return; - -fail: - char *source_uri = get_file_identifier(source_file); - char *dest_uri = get_file_identifier(destination_file); - g_critical("Failed to copy source file %s to destination %s: %s\n", - source_uri, dest_uri, error->message); - g_free(source_uri); - g_free(dest_uri); - g_clear_error(&error); + return g_file_copy(source_file, destination_file, G_FILE_COPY_OVERWRITE, + nullptr, nullptr, nullptr, error); } /* This function will strip a URI scheme and return @@ -398,837 +174,141 @@ find_diverging_child_components(GFile *child, return stripped_uri; } -typedef bool (*ConvertAndInsertJSVal) (GArray *array, - JSContext *context, - JS::HandleValue element); - static bool -get_array_from_js_value(JSContext *context, - JS::HandleValue value, - size_t array_element_size, - GDestroyNotify element_clear_func, - ConvertAndInsertJSVal inserter, - GArray **out_array) -{ - g_return_val_if_fail(out_array != NULL, false); - g_return_val_if_fail(*out_array == NULL, false); - - bool is_array; - if (!JS_IsArrayObject(context, value, &is_array)) - return false; - if (!is_array) { - g_critical("Returned object from is not an array"); - return false; - } - - /* We're not preallocating any space here at the moment until - * we have some profiling data that suggests a good size to - * preallocate to. */ - GArray *c_side_array = g_array_new(true, true, array_element_size); - uint32_t js_array_len; - JS::RootedObject js_array(context, &value.toObject()); - - if (element_clear_func) - g_array_set_clear_func(c_side_array, element_clear_func); - - if (!JS_GetArrayLength(context, js_array, &js_array_len)) { - g_array_unref(c_side_array); - return false; - } - - JS::RootedValue element(context); - for (uint32_t i = 0; i < js_array_len; ++i) { - if (!JS_GetElement(context, js_array, i, &element) || - !inserter(c_side_array, context, element)) { - g_array_unref(c_side_array); - return false; - } - } - - *out_array = c_side_array; - - return true; -} - -static bool -convert_and_insert_unsigned_int(GArray *array, - JSContext *context, - JS::HandleValue element) -{ - if (!element.isInt32() && !element.isUndefined() && !element.isNull()) { - g_critical("Array element is not an integer or undefined or null"); - return false; - } - - if (element.isInt32()) { - unsigned int element_integer = element.toInt32(); - g_array_append_val(array, element_integer); - } else { - int not_executable = -1; - g_array_append_val(array, not_executable); - } - - return true; -} - -static GArray * -get_executed_lines_for(JSContext *context, - JS::HandleObject coverage_statistics, - JS::HandleValue filename_value) -{ - GArray *array = NULL; - JS::RootedValue rval(context); - JS::AutoValueArray<1> args(context); - args[0].set(filename_value); - - if (!JS_CallFunctionName(context, coverage_statistics, "getExecutedLinesFor", - args, &rval)) { - gjs_log_exception(context); - return NULL; - } - - if (!get_array_from_js_value(context, rval, sizeof(unsigned int), NULL, - convert_and_insert_unsigned_int, &array)) { - gjs_log_exception(context); - return NULL; - } - - return array; -} - -static void -init_covered_function(GjsCoverageFunction *function, - const char *key, - unsigned int line_number, - unsigned int hit_count) -{ - function->key = g_strdup(key); - function->line_number = line_number; - function->hit_count = hit_count; -} - -static void -clear_coverage_function(gpointer info_location) -{ - GjsCoverageFunction *info = (GjsCoverageFunction *) info_location; - g_free(info->key); -} - -static bool -get_hit_count_and_line_data(JSContext *cx, - JS::HandleObject obj, - const char *description, - int32_t *hit_count, - int32_t *line) -{ - JS::RootedId hit_count_name(cx, gjs_intern_string_to_id(cx, "hitCount")); - if (!gjs_object_require_property(cx, obj, description, - hit_count_name, hit_count)) - return false; - - JS::RootedId line_number_name(cx, gjs_intern_string_to_id(cx, "line")); - return gjs_object_require_property(cx, obj, description, - line_number_name, line); -} - -static bool -convert_and_insert_function_decl(GArray *array, - JSContext *context, - JS::HandleValue element) -{ - if (!element.isObject()) { - gjs_throw(context, "Function element is not an object"); - return false; - } - - JS::RootedObject object(context, &element.toObject()); - JS::RootedValue function_name_property_value(context); - - if (!gjs_object_require_property(context, object, NULL, GJS_STRING_NAME, - &function_name_property_value)) - return false; - - GjsAutoJSChar utf8_string(context); - - if (function_name_property_value.isString()) { - if (!gjs_string_to_utf8(context, - function_name_property_value, - &utf8_string)) { - gjs_throw(context, "Failed to convert function_name to string"); - return false; - } - } else if (!function_name_property_value.isNull()) { - gjs_throw(context, "Unexpected type for function_name"); - return false; - } - - int32_t hit_count; - int32_t line_number; - if (!get_hit_count_and_line_data(context, object, "function element", - &hit_count, &line_number)) - return false; - - GjsCoverageFunction info; - init_covered_function(&info, - utf8_string, - line_number, - hit_count); - - g_array_append_val(array, info); - - return true; -} - -static GArray * -get_functions_for(JSContext *context, - JS::HandleObject coverage_statistics, - JS::HandleValue filename_value) -{ - GArray *array = NULL; - JS::RootedValue rval(context); - JS::AutoValueArray<1> args(context); - args[0].set(filename_value); - - if (!JS_CallFunctionName(context, coverage_statistics, "getFunctionsFor", - args, &rval)) { - gjs_log_exception(context); - return NULL; - } - - if (!get_array_from_js_value(context, rval, sizeof(GjsCoverageFunction), - clear_coverage_function, convert_and_insert_function_decl, &array)) { - gjs_log_exception(context); - return NULL; - } - - return array; -} - -static void -init_covered_branch(GjsCoverageBranch *branch, - unsigned int point, - bool was_hit, - GArray *exits) -{ - branch->point = point; - branch->hit = !!was_hit; - branch->exits = exits; -} - -static void -clear_coverage_branch(gpointer branch_location) -{ - GjsCoverageBranch *branch = (GjsCoverageBranch *) branch_location; - g_array_unref(branch->exits); -} - -static bool -convert_and_insert_branch_exit(GArray *array, - JSContext *context, - JS::HandleValue element) -{ - if (!element.isObject()) { - gjs_throw(context, "Branch exit array element is not an object"); - return false; - } - - JS::RootedObject object(context, &element.toObject()); - - int32_t hit_count; - int32_t line; - if (!get_hit_count_and_line_data(context, object, "branch exit array element", - &hit_count, &line)) - return false; - - GjsCoverageBranchExit exit = { - (unsigned int) line, - (unsigned int) hit_count - }; - - g_array_append_val(array, exit); - - return true; -} - -static bool -convert_and_insert_branch_info(GArray *array, - JSContext *context, - JS::HandleValue element) -{ - if (!element.isObject() && !element.isUndefined()) { - gjs_throw(context, "Branch array element is not an object or undefined"); - return false; - } - - if (element.isObject()) { - JS::RootedObject object(context, &element.toObject()); - - int32_t branch_point; - JS::RootedId point_name(context, gjs_intern_string_to_id(context, "point")); - - if (!gjs_object_require_property(context, object, - "branch array element", - point_name, &branch_point)) - return false; - - bool was_hit; - JS::RootedId hit_name(context, gjs_intern_string_to_id(context, "hit")); - - if (!gjs_object_require_property(context, object, - "branch array element", - hit_name, &was_hit)) - return false; - - JS::RootedValue branch_exits_value(context); - GArray *branch_exits_array = NULL; - - if (!JS_GetProperty(context, object, "exits", &branch_exits_value) || - !branch_exits_value.isObject()) { - gjs_throw(context, "Failed to get exits property from element"); - return false; - } - - if (!get_array_from_js_value(context, - branch_exits_value, - sizeof(GjsCoverageBranchExit), - NULL, - convert_and_insert_branch_exit, - &branch_exits_array)) { - /* Already logged the exception, no need to do anything here */ - return false; - } - - GjsCoverageBranch branch; - init_covered_branch(&branch, - branch_point, - was_hit, - branch_exits_array); - - g_array_append_val(array, branch); - } - - return true; -} - -static GArray * -get_branches_for(JSContext *context, - JS::HandleObject coverage_statistics, - JS::HandleValue filename_value) -{ - GArray *array = NULL; - JS::AutoValueArray<1> args(context); - args[0].set(filename_value); - JS::RootedValue rval(context); - - if (!JS_CallFunctionName(context, coverage_statistics, "getBranchesFor", - args, &rval)) { - gjs_log_exception(context); - return NULL; - } - - if (!get_array_from_js_value(context, rval, sizeof(GjsCoverageBranch), - clear_coverage_branch, - convert_and_insert_branch_info, &array)) { - gjs_log_exception(context); - return NULL; - } - - return array; -} - -typedef struct _GjsCoverageFileStatistics { - char *filename; - GArray *lines; - GArray *functions; - GArray *branches; -} GjsCoverageFileStatistics; - -static bool -fetch_coverage_file_statistics_from_js(JSContext *context, - JS::HandleObject coverage_statistics, - const char *filename, - GjsCoverageFileStatistics *statistics) -{ - JSAutoCompartment compartment(context, coverage_statistics); - JSAutoRequest ar(context); - - JSString *filename_jsstr = JS_NewStringCopyZ(context, filename); - JS::RootedValue filename_jsval(context, JS::StringValue(filename_jsstr)); - - GArray *lines = get_executed_lines_for(context, coverage_statistics, filename_jsval); - GArray *functions = get_functions_for(context, coverage_statistics, filename_jsval); - GArray *branches = get_branches_for(context, coverage_statistics, filename_jsval); - - if (!lines || !functions || !branches) - { - g_clear_pointer(&lines, g_array_unref); - g_clear_pointer(&functions, g_array_unref); - g_clear_pointer(&branches, g_array_unref); - return false; - } - - statistics->filename = g_strdup(filename); - statistics->lines = lines; - statistics->functions = functions; - statistics->branches = branches; - - return true; -} - -static void -gjs_coverage_statistics_file_statistics_clear(gpointer data) -{ - GjsCoverageFileStatistics *statistics = (GjsCoverageFileStatistics *) data; - g_free(statistics->filename); - g_array_unref(statistics->lines); - g_array_unref(statistics->functions); - g_array_unref(statistics->branches); -} - -static void -print_statistics_for_file(GjsCoverageFileStatistics *file_statistics, - GFile *output_dir, - GOutputStream *ostream) -{ - /* The source file could be a resource, so we must use - * g_file_new_for_commandline_arg() to disambiguate between URIs and - * filesystem paths. */ - GFile *source = g_file_new_for_commandline_arg(file_statistics->filename); - - char *diverged_paths = find_diverging_child_components(source, output_dir); - GFile *dest = g_file_resolve_relative_path(output_dir, diverged_paths); - - copy_source_file_to_coverage_output(source, dest); - g_object_unref(source); - - write_source_file_header(ostream, dest); - g_object_unref(dest); - - write_functions(ostream, file_statistics->functions); - - unsigned int functions_hit_count = 0; - unsigned int functions_found_count = 0; - - write_functions_hit_counts(ostream, - file_statistics->functions, - &functions_found_count, - &functions_hit_count); - write_function_coverage(ostream, - functions_found_count, - functions_hit_count); - - unsigned int branches_hit_count = 0; - unsigned int branches_found_count = 0; - - write_branch_coverage(ostream, - file_statistics->branches, - &branches_found_count, - &branches_hit_count); - write_branch_totals(ostream, - branches_found_count, - branches_hit_count); - - unsigned int lines_hit_count = 0; - unsigned int executable_lines_count = 0; - - write_line_coverage(ostream, - file_statistics->lines, - &lines_hit_count, - &executable_lines_count); - write_line_totals(ostream, - lines_hit_count, - executable_lines_count); - write_end_of_record(ostream); - - g_free(diverged_paths); -} - -static char ** -get_covered_files(GjsCoverage *coverage) +filename_has_coverage_prefixes(GjsCoverage *self, const char *filename) { - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoRequest ar(context); - JSAutoCompartment ac(context, priv->coverage_statistics); - JS::RootedObject rooted_priv(context, priv->coverage_statistics); - JS::RootedValue rval(context); + auto priv = static_cast(gjs_coverage_get_instance_private(self)); - char **files = NULL; - uint32_t n_files; - - if (!JS_CallFunctionName(context, rooted_priv, "getCoveredFiles", - JS::HandleValueArray::empty(), &rval)) { - gjs_log_exception(context); - return NULL; + for (const char * const *prefix = priv->prefixes; *prefix; prefix++) { + if (g_str_has_prefix(filename, *prefix)) + return true; } - - if (!rval.isObject()) - return NULL; - - JS::RootedObject files_obj(context, &rval.toObject()); - if (!JS_GetArrayLength(context, files_obj, &n_files)) - return NULL; - - files = g_new0(char *, n_files + 1); - JS::RootedValue element(context); - for (uint32_t i = 0; i < n_files; i++) { - GjsAutoJSChar file(context); - if (!JS_GetElement(context, files_obj, i, &element)) - goto error; - - if (!gjs_string_to_utf8(context, element, &file)) - goto error; - - files[i] = file.copy(); - } - - files[n_files] = NULL; - return files; - - error: - g_strfreev(files); - return NULL; -} - -bool -gjs_get_file_mtime(GFile *file, - GTimeVal *mtime) -{ - GError *error = NULL; - GFileInfo *info = g_file_query_info(file, - "time::modified,time::modified-usec", - G_FILE_QUERY_INFO_NONE, - NULL, - &error); - - if (!info) { - char *path = get_file_identifier(file); - g_warning("Failed to get modification time of %s, " - "falling back to checksum method for caching. Reason was: %s", - path, error->message); - g_clear_object(&info); - return false; - } - - g_file_info_get_modification_time(info, mtime); - g_clear_object(&info); - - /* For some URI types, eg, resources, the operation getting - * the mtime might succeed, but by default zero is returned. - * - * Check if that is the case for both tv_sec and tv_usec and if - * so return false. */ - return !(mtime->tv_sec == 0 && mtime->tv_usec == 0); + return false; } -static GBytes * -read_all_bytes_from_file(GFile *file) +static inline bool +write_line(GOutputStream *out, + const char *line, + GError **error) { - /* We have to use g_file_query_exists here since - * g_file_test(path, G_FILE_TEST_EXISTS) is implemented in terms - * of access(), which doesn't work with resource paths. */ - if (!g_file_query_exists(file, NULL)) - return NULL; - - gsize len = 0; - gchar *data = NULL; - - GError *error = NULL; - - if (!g_file_load_contents(file, - NULL, - &data, - &len, - NULL, - &error)) { - char *path = get_file_identifier(file); - g_critical("Unable to read bytes from: %s, reason was: %s\n", - path, error->message); - g_clear_error(&error); - g_free(path); - return NULL; - } - - return g_bytes_new_take(data, len); + return g_output_stream_printf(out, nullptr, nullptr, error, "%s\n", line); } -gchar * -gjs_get_file_checksum(GFile *file) +static GjsAutoUnref +write_statistics_internal(GjsCoverage *coverage, + JSContext *cx, + GError **error) { - GBytes *data = read_all_bytes_from_file(file); - - if (!data) - return NULL; - - gchar *checksum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA512, data); + using AutoCChar = std::unique_ptr; + using AutoStrv = std::unique_ptr; - g_bytes_unref(data); - return checksum; -} - -GBytes * -gjs_serialize_statistics(GjsCoverage *coverage) -{ GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - - JSAutoRequest ar(js_context); - JSAutoCompartment ac(js_context, priv->coverage_statistics); - JS::RootedObject rooted_priv(js_context, priv->coverage_statistics); - JS::RootedValue string_value_return(js_context); - - if (!JS_CallFunctionName(js_context, rooted_priv, "stringify", - JS::HandleValueArray::empty(), - &string_value_return)) { - gjs_log_exception(js_context); - return NULL; - } - if (!string_value_return.isString()) - return NULL; - - /* Free'd by g_bytes_new_take */ - GjsAutoJSChar statistics_as_json_string(js_context); - - if (!gjs_string_to_utf8(js_context, - string_value_return.get(), - &statistics_as_json_string)) { - gjs_log_exception(js_context); - return NULL; + /* Create output directory if it doesn't exist */ + if (!g_file_make_directory_with_parents(priv->output_dir, nullptr, error)) { + if (!g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + return nullptr; + g_clear_error(error); } - int json_string_len = strlen(statistics_as_json_string); - auto json_bytes = - reinterpret_cast(statistics_as_json_string.copy()); - - return g_bytes_new_take(json_bytes, - json_string_len); -} - -static JSString * -gjs_deserialize_cache_to_object_for_compartment(JSContext *context, - JS::HandleObject global_object, - GBytes *cache_data) -{ - JSAutoRequest ar(context); - JSAutoCompartment ac(context, - JS_GetGlobalForObject(context, - global_object)); - - gsize len = 0; - auto string = static_cast(g_bytes_get_data(cache_data, &len)); - - return JS_NewStringCopyN(context, string, len); -} - -JSString * -gjs_deserialize_cache_to_object(GjsCoverage *coverage, - GBytes *cache_data) -{ - /* Deserialize into an object with the following structure: - * - * object = { - * 'filename': { - * contents: (file contents), - * nLines: (number of lines in file), - * lines: Number[nLines + 1], - * branches: Array for n_branches of { - * point: branch_point, - * exits: Number[nLines + 1] - * }, - * functions: Array for n_functions of { - * key: function_name,r - * line: line - * } - * } - */ - - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoRequest ar(context); - JSAutoCompartment ac(context, priv->coverage_statistics); - JS::RootedObject global_object(context, - JS_GetGlobalForObject(context, priv->coverage_statistics)); - return gjs_deserialize_cache_to_object_for_compartment(context, global_object, cache_data); -} - -static GArray * -gjs_fetch_statistics_from_js(GjsCoverage *coverage, - gchar **coverage_files) -{ - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - - GArray *file_statistics_array = g_array_new(false, - false, - sizeof(GjsCoverageFileStatistics)); - g_array_set_clear_func(file_statistics_array, - gjs_coverage_statistics_file_statistics_clear); - - JS::RootedObject rooted_coverage_statistics(js_context, - priv->coverage_statistics); - - char **file_iter = coverage_files; - while (*file_iter) { - GjsCoverageFileStatistics statistics; - if (fetch_coverage_file_statistics_from_js(js_context, - rooted_coverage_statistics, - *file_iter, - &statistics)) - g_array_append_val(file_statistics_array, statistics); - else - g_warning("Couldn't fetch statistics for %s", *file_iter); - - ++file_iter; - } + GFile *output_file = g_file_get_child(priv->output_dir, "coverage.lcov"); - return file_statistics_array; -} + size_t lcov_length; + AutoCChar lcov(js::GetCodeCoverageSummary(cx, &lcov_length), free); -bool -gjs_write_cache_file(GFile *file, - GBytes *cache) -{ - gsize cache_len = 0; - char *cache_data = (char *) g_bytes_get_data(cache, &cache_len); - GError *error = NULL; - - if (!g_file_replace_contents(file, - cache_data, - cache_len, - NULL, - false, - G_FILE_CREATE_NONE, - NULL, - NULL, - &error)) { - char *path = get_file_identifier(file); - g_warning("Failed to write all bytes to %s, reason was: %s\n", - path, error->message); - g_warning("Will remove this file to prevent inconsistent cache " - "reads next time."); - g_clear_error(&error); - if (!g_file_delete(file, NULL, &error)) { - g_assert(error != NULL); - g_critical("Deleting %s failed because %s! You will need to " - "delete it manually before running the coverage " - "mode again.", path, error->message); - g_clear_error(&error); + GjsAutoUnref ostream = + G_OUTPUT_STREAM(g_file_append_to(output_file, + G_FILE_CREATE_NONE, + NULL, + error)); + if (!ostream) + return nullptr; + + AutoStrv lcov_lines(g_strsplit(lcov.get(), "\n", -1), g_strfreev); + GjsAutoChar test_name; + bool ignoring_file = false; + + for (const char * const *iter = lcov_lines.get(); *iter; iter++) { + if (ignoring_file) { + if (strcmp(*iter, "end_of_record") == 0) + ignoring_file = false; + continue; } - g_free(path); - return false; - } + if (g_str_has_prefix(*iter, "TN:")) { + /* Don't write the test name if the next line shows we are + * ignoring the source file */ + test_name = *iter; + continue; + } else if (g_str_has_prefix(*iter, "SF:")) { + const char *filename = *iter + 3; + if (!filename_has_coverage_prefixes(coverage, filename)) { + ignoring_file = true; + continue; + } - return true; -} + /* Now we can write the test name before writing the source file */ + if (!write_line(ostream, test_name.get(), error)) + return nullptr; + + /* The source file could be a resource, so we must use + * g_file_new_for_commandline_arg() to disambiguate between URIs and + * filesystem paths. */ + GjsAutoUnref source_file = g_file_new_for_commandline_arg(filename); + GjsAutoChar diverged_paths = + find_diverging_child_components(source_file, priv->output_dir); + GjsAutoUnref destination_file = + g_file_resolve_relative_path(priv->output_dir, diverged_paths); + if (!copy_source_file_to_coverage_output(source_file, destination_file, error)) + return nullptr; + + /* Rewrite the source file path to be relative to the output + * dir so that genhtml will find it */ + if (!write_source_file_header(ostream, destination_file, error)) + return nullptr; + continue; + } -static bool -coverage_statistics_has_stale_cache(GjsCoverage *coverage) -{ - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - - JSAutoRequest ar(js_context); - JSAutoCompartment ac(js_context, priv->coverage_statistics); - JS::RootedObject rooted_priv(js_context, priv->coverage_statistics); - JS::RootedValue stale_cache_value(js_context); - if (!JS_CallFunctionName(js_context, rooted_priv, "staleCache", - JS::HandleValueArray::empty(), - &stale_cache_value)) { - gjs_log_exception(js_context); - g_error("Failed to call into javascript to get stale cache value. This is a bug"); + if (!write_line(ostream, *iter, error)) + return nullptr; } - return stale_cache_value.toBoolean(); + return output_file; } -static unsigned int _suppressed_coverage_messages_count = 0; - /** * gjs_coverage_write_statistics: * @coverage: A #GjsCoverage - * @output_directory: A directory to write coverage information to. Scripts - * which were provided as part of the coverage-paths construction property will be written - * out to output_directory, in the same directory structure relative to the source dir where - * the tests were run. + * @output_directory: A directory to write coverage information to. * - * This function takes all available statistics and writes them out to either the file provided - * or to files of the pattern (filename).info in the same directory as the scanned files. It will - * provide coverage data for all files ending with ".js" in the coverage directories, even if they - * were never actually executed. + * Scripts which were provided as part of the #GjsCoverage:prefixes + * construction property will be written out to @output_directory, in the same + * directory structure relative to the source dir where the tests were run. + * + * This function takes all available statistics and writes them out to either + * the file provided or to files of the pattern (filename).info in the same + * directory as the scanned files. It will provide coverage data for all files + * ending with ".js" in the coverage directories. */ void gjs_coverage_write_statistics(GjsCoverage *coverage) { - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - GError *error = NULL; - - JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoCompartment compartment(context, priv->coverage_statistics); - JSAutoRequest ar(context); + auto priv = static_cast(gjs_coverage_get_instance_private(coverage)); + GError *error = nullptr; - /* Create output directory if it doesn't exist */ - if (!g_file_make_directory_with_parents(priv->output_dir, NULL, &error)) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { - g_critical("Could not create coverage output: %s", error->message); - g_clear_error(&error); - return; - } - g_clear_error(&error); - } + auto cx = static_cast(gjs_context_get_native_context(priv->context)); + JSAutoCompartment ac(cx, gjs_get_import_global(cx)); + JSAutoRequest ar(cx); - GFile *output_file = g_file_get_child(priv->output_dir, "coverage.lcov"); - - GOutputStream *ostream = - G_OUTPUT_STREAM(g_file_append_to(output_file, - G_FILE_CREATE_NONE, - NULL, - &error)); - - char **executed_coverage_files = get_covered_files(coverage); - GArray *file_statistics_array = gjs_fetch_statistics_from_js(coverage, - executed_coverage_files); - - for (size_t i = 0; i < file_statistics_array->len; ++i) - { - GjsCoverageFileStatistics *statistics = &(g_array_index(file_statistics_array, GjsCoverageFileStatistics, i)); - - /* Only print statistics if the file was actually executed */ - for (char **iter = executed_coverage_files; *iter; ++iter) { - if (g_strcmp0(*iter, statistics->filename) == 0) { - print_statistics_for_file(statistics, priv->output_dir, ostream); - - /* Inner loop */ - break; - } - } - } - - g_strfreev(executed_coverage_files); - - const bool has_cache_path = priv->cache != NULL; - const bool cache_is_stale = coverage_statistics_has_stale_cache(coverage); - - if (has_cache_path && cache_is_stale) { - GBytes *cache_data = gjs_serialize_statistics(coverage); - gjs_write_cache_file(priv->cache, cache_data); - g_bytes_unref(cache_data); - } - - char *output_file_path = g_file_get_path(priv->output_dir); - g_message("Wrote coverage statistics to %s", output_file_path); - if (_suppressed_coverage_messages_count) { - g_message("There were %i suppressed message(s) when collecting " - "coverage, set GJS_SHOW_COVERAGE_MESSAGES to see them.", - _suppressed_coverage_messages_count); - _suppressed_coverage_messages_count = 0; + GjsAutoUnref output_file = write_statistics_internal(coverage, cx, &error); + if (!output_file) { + g_critical("Error writing coverage data: %s", error->message); + g_error_free(error); + return; } - g_free(output_file_path); - g_array_unref(file_statistics_array); - g_object_unref(ostream); - g_object_unref(output_file); + GjsAutoChar output_file_path = g_file_get_path(output_file); + g_message("Wrote coverage statistics to %s", output_file_path.get()); } static void @@ -1236,375 +316,43 @@ gjs_coverage_init(GjsCoverage *self) { } -static bool -gjs_context_eval_file_in_compartment(GjsContext *context, - const char *filename, - JS::HandleObject compartment_object, - GError **error) -{ - char *script = NULL; - gsize script_len = 0; - - GFile *file = g_file_new_for_commandline_arg(filename); - - if (!g_file_load_contents(file, - NULL, - &script, - &script_len, - NULL, - error)) { - g_object_unref(file); - return false; - } - - g_object_unref(file); - - int start_line_number = 1; - const char *stripped_script = gjs_strip_unix_shebang(script, &script_len, - &start_line_number); - - JSContext *js_context = (JSContext *) gjs_context_get_native_context(context); - - JSAutoCompartment compartment(js_context, compartment_object); - - JS::CompileOptions options(js_context); - options.setUTF8(true) - .setFileAndLine(filename, start_line_number) - .setSourceIsLazy(true); - JS::RootedScript compiled_script(js_context); - if (!JS::Compile(js_context, options, stripped_script, script_len, - &compiled_script)) - return false; - - JS::RootedValue dummy_rval(js_context); - if (!JS::CloneAndExecuteScript(js_context, compiled_script, &dummy_rval)) { - g_free(script); - gjs_log_exception(js_context); - g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, "Failed to evaluate %s", filename); - return false; - } - - g_free(script); - - return true; -} - -static bool -coverage_log(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GjsAutoJSChar s(context); - JSExceptionState *exc_state; - - if (argc != 1) { - gjs_throw(context, "Must pass a single argument to log()"); - return false; - } - - JSAutoRequest ar(context); - - if (!g_getenv("GJS_SHOW_COVERAGE_MESSAGES")) { - _suppressed_coverage_messages_count++; - argv.rval().setUndefined(); - return true; - } - - /* JS::ToString might throw, in which case we will only log that the value - * could not be converted to string */ - exc_state = JS_SaveExceptionState(context); - JS::RootedString jstr(context, JS::ToString(context, argv[0])); - if (jstr) - argv[0].setString(jstr); // GC root - JS_RestoreExceptionState(context, exc_state); - - if (!jstr) { - g_message("JS LOG: "); - return true; - } - - if (!gjs_string_to_utf8(context, JS::StringValue(jstr), &s)) { - return false; - } - - g_message("JS COVERAGE MESSAGE: %s", s.get()); - - argv.rval().setUndefined(); - return true; -} - -static GFile * -get_file_from_call_args_filename(JSContext *context, - JS::CallArgs &args) { - GjsAutoJSChar filename(context); - - if (!gjs_parse_call_args(context, "getFileContents", args, "s", - "filename", &filename)) - return NULL; - - /* path could be a resource, so use g_file_new_for_commandline_arg. */ - return g_file_new_for_commandline_arg(filename); -} - -static bool -coverage_get_file_modification_time(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GTimeVal mtime; - bool ret = false; - GFile *file = get_file_from_call_args_filename(context, args); - - if (!file) - return false; - - if (gjs_get_file_mtime(file, &mtime)) { - JS::AutoValueArray<2> mtime_values_array(context); - mtime_values_array[0].setInt32(mtime.tv_sec); - mtime_values_array[1].setInt32(mtime.tv_usec); - JS::RootedObject array_obj(context, - JS_NewArrayObject(context, mtime_values_array)); - if (!array_obj) - goto out; - args.rval().setObject(*array_obj); - } else { - args.rval().setNull(); - } - - ret = true; - -out: - g_object_unref(file); - return ret; -} - -static bool -coverage_get_file_checksum(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GFile *file = get_file_from_call_args_filename(context, args); - - if (!file) - return false; - - char *checksum = gjs_get_file_checksum(file); - - if (!checksum) { - char *filename = get_file_identifier(file); - gjs_throw(context, "Failed to read %s and get its checksum", filename); - g_free(filename); - g_object_unref(file); - return false; - } - - args.rval().setString(JS_NewStringCopyZ(context, checksum)); - - g_object_unref(file); - g_free(checksum); - return true; -} - -static bool -coverage_get_file_contents(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - bool ret = false; - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GFile *file = NULL; - char *script = NULL; - gsize script_len; - GError *error = NULL; - - file = get_file_from_call_args_filename(context, args); - if (!file) - return false; - - if (!g_file_load_contents(file, - NULL, - &script, - &script_len, - NULL, - &error)) { - char *filename = get_file_identifier(file); - gjs_throw(context, "Failed to load contents for filename %s: %s", filename, error->message); - g_free(filename); - goto out; - } - - args.rval().setString(JS_NewStringCopyN(context, script, script_len)); - ret = true; - - out: - g_clear_error(&error); - g_object_unref(file); - g_free(script); - return ret; -} - -static JSFunctionSpec coverage_funcs[] = { - JS_FS("log", coverage_log, 1, GJS_MODULE_PROP_FLAGS), - JS_FS("getFileContents", coverage_get_file_contents, 1, GJS_MODULE_PROP_FLAGS), - JS_FS("getFileModificationTime", coverage_get_file_modification_time, 1, GJS_MODULE_PROP_FLAGS), - JS_FS("getFileChecksum", coverage_get_file_checksum, 1, GJS_MODULE_PROP_FLAGS), - JS_FS_END -}; - static void -coverage_statistics_tracer(JSTracer *trc, void *data) +coverage_tracer(JSTracer *trc, void *data) { GjsCoverage *coverage = (GjsCoverage *) data; GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JS::TraceEdge(trc, &priv->coverage_statistics, - "coverage_statistics"); -} - -/* This function is mainly used in the tests in order to fiddle with - * the internals of the coverage statisics collector on the coverage - * compartment side */ -bool -gjs_run_script_in_coverage_compartment(GjsCoverage *coverage, - const char *script) -{ - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoCompartment ac(js_context, priv->coverage_statistics); - JSAutoRequest ar(js_context); - - JS::CompileOptions options(js_context); - options.setUTF8(true); - - JS::RootedValue rval(js_context); - if (!JS::Evaluate(js_context, options, script, strlen(script), &rval)) { - gjs_log_exception(js_context); - g_warning("Failed to evaluate "); - return false; - } - - return true; -} - -bool -gjs_inject_value_into_coverage_compartment(GjsCoverage *coverage, - JS::HandleValue value, - const char *property) -{ - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoRequest ar(js_context); - JSAutoCompartment ac(js_context, priv->coverage_statistics); - - JS::RootedObject coverage_global_scope(js_context, - JS_GetGlobalForObject(js_context, priv->coverage_statistics)); - - if (!JS_SetProperty(js_context, coverage_global_scope, property, - value)) { - g_warning("Failed to set property %s to requested value", property); - return false; - } - - return true; + JS::TraceEdge(trc, &priv->compartment, "Coverage compartment"); } static bool bootstrap_coverage(GjsCoverage *coverage) { - static const char *coverage_script = "resource:///org/cinnamon/cjs/modules/coverage.js"; GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - GBytes *cache_bytes = NULL; - GError *error = NULL; JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context); JSAutoRequest ar(context); JSObject *debuggee = gjs_get_import_global(context); - JS::CompartmentOptions options; - options.behaviors().setVersion(JSVERSION_LATEST); JS::RootedObject debugger_compartment(context, gjs_create_global_object(context)); { JSAutoCompartment compartment(context, debugger_compartment); JS::RootedObject debuggeeWrapper(context, debuggee); - if (!JS_WrapObject(context, &debuggeeWrapper)) { - gjs_throw(context, "Failed to wrap debugeee"); + if (!JS_WrapObject(context, &debuggeeWrapper)) return false; - } JS::RootedValue debuggeeWrapperValue(context, JS::ObjectValue(*debuggeeWrapper)); if (!JS_SetProperty(context, debugger_compartment, "debuggee", - debuggeeWrapperValue)) { - gjs_throw(context, "Failed to set debuggee property"); - return false; - } - - if (!gjs_define_global_properties(context, debugger_compartment)) { - gjs_throw(context, "Failed to define global properties on debugger " - "compartment"); + debuggeeWrapperValue) || + !gjs_define_global_properties(context, debugger_compartment, + "coverage")) return false; - } - - if (!JS_DefineFunctions(context, debugger_compartment, &coverage_funcs[0])) - g_error("Failed to init coverage"); - - if (!gjs_context_eval_file_in_compartment(priv->context, - coverage_script, - debugger_compartment, - &error)) - g_error("Failed to eval coverage script: %s\n", error->message); - - JS::RootedObject coverage_statistics_constructor(context); - JS::RootedId coverage_statistics_name(context, - gjs_intern_string_to_id(context, "CoverageStatistics")); - if (!gjs_object_require_property(context, debugger_compartment, - "debugger compartment", - coverage_statistics_name, - &coverage_statistics_constructor)) - return false; - - /* Create value for holding the cache. This will be undefined if - * the cache does not exist, otherwise it will be an object set - * to the value of the cache */ - JS::RootedValue cache_value(context); - - if (priv->cache) - cache_bytes = read_all_bytes_from_file(priv->cache); - - if (cache_bytes) { - JSString *cache_object = gjs_deserialize_cache_to_object_for_compartment(context, - debugger_compartment, - cache_bytes); - cache_value.setString(cache_object); - g_bytes_unref(cache_bytes); - } - - /* Now create the array to pass the desired prefixes over */ - JSObject *prefixes = gjs_build_string_array(context, -1, priv->prefixes); - - JS::AutoValueArray<3> coverage_statistics_constructor_args(context); - coverage_statistics_constructor_args[0].setObject(*prefixes); - coverage_statistics_constructor_args[1].set(cache_value); - coverage_statistics_constructor_args[2] - .setBoolean(g_getenv("GJS_DEBUG_COVERAGE_EXECUTED_LINES")); - - JSObject *coverage_statistics = JS_New(context, - coverage_statistics_constructor, - coverage_statistics_constructor_args); - - if (!coverage_statistics) { - gjs_throw(context, "Failed to create coverage_statitiscs object"); - return false; - } /* Add a tracer, as suggested by jdm on #jsapi */ - JS_AddExtraGCRootsTracer(context, coverage_statistics_tracer, coverage); + JS_AddExtraGCRootsTracer(context, coverage_tracer, coverage); - priv->coverage_statistics = coverage_statistics; + priv->compartment = debugger_compartment; } return true; @@ -1617,12 +365,7 @@ gjs_coverage_constructed(GObject *object) GjsCoverage *coverage = GJS_COVERAGE(object); GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - new (&priv->coverage_statistics) JS::Heap(); - - if (!priv->cache_specified) { - g_message("Cache path was not given, picking default one"); - priv->cache = g_file_new_for_path(".internal-gjs-coverage-cache"); - } + new (&priv->compartment) JS::Heap(); if (!bootstrap_coverage(coverage)) { JSContext *context = static_cast(gjs_context_get_native_context(priv->context)); @@ -1648,9 +391,6 @@ gjs_coverage_set_property(GObject *object, priv->context = GJS_CONTEXT(g_value_dup_object(value)); break; case PROP_CACHE: - priv->cache_specified = true; - /* g_value_dup_object() adds a reference if not NULL */ - priv->cache = static_cast(g_value_dup_object(value)); break; case PROP_OUTPUT_DIRECTORY: priv->output_dir = G_FILE(g_value_dup_object(value)); @@ -1661,33 +401,6 @@ gjs_coverage_set_property(GObject *object, } } -static void -gjs_clear_js_side_statistics_from_coverage_object(GjsCoverage *coverage) -{ - GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - - if (priv->coverage_statistics) { - /* Remove tracer before disposing the context */ - JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoRequest ar(js_context); - JSAutoCompartment ac(js_context, priv->coverage_statistics); - JS::RootedObject rooted_priv(js_context, priv->coverage_statistics); - JS::RootedValue rval(js_context); - if (!JS_CallFunctionName(js_context, rooted_priv, "deactivate", - JS::HandleValueArray::empty(), &rval)) { - gjs_log_exception(js_context); - g_error("Failed to deactivate debugger - this is a fatal error"); - } - - /* Remove GC roots trace after we've decomissioned the object - * and no longer need it to be traced here. */ - JS_RemoveExtraGCRootsTracer(js_context, coverage_statistics_tracer, - coverage); - - priv->coverage_statistics = NULL; - } -} - static void gjs_coverage_dispose(GObject *object) { @@ -1696,7 +409,10 @@ gjs_coverage_dispose(GObject *object) /* Decomission objects inside of the JSContext before * disposing of the context */ - gjs_clear_js_side_statistics_from_coverage_object(coverage); + auto cx = static_cast(gjs_context_get_native_context(priv->context)); + JS_RemoveExtraGCRootsTracer(cx, coverage_tracer, coverage); + priv->compartment = nullptr; + g_clear_object(&priv->context); G_OBJECT_CLASS(gjs_coverage_parent_class)->dispose(object); @@ -1710,8 +426,7 @@ gjs_coverage_finalize (GObject *object) g_strfreev(priv->prefixes); g_clear_object(&priv->output_dir); - g_clear_object(&priv->cache); - priv->coverage_statistics.~Heap(); + priv->compartment.~Heap(); G_OBJECT_CLASS(gjs_coverage_parent_class)->finalize(object); } @@ -1737,10 +452,10 @@ gjs_coverage_class_init (GjsCoverageClass *klass) GJS_TYPE_CONTEXT, (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); properties[PROP_CACHE] = g_param_spec_object("cache", - "Cache", - "File containing a cache to preload ASTs from", + "Deprecated property", + "Has no effect", G_TYPE_FILE, - (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); + (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_DEPRECATED)); properties[PROP_OUTPUT_DIRECTORY] = g_param_spec_object("output-directory", "Output directory", "Directory handle at which to output coverage statistics", @@ -1760,9 +475,8 @@ gjs_coverage_class_init (GjsCoverageClass *klass) * @output_dir: A #GFile handle to a directory in which to write coverage * information * - * Creates a new #GjsCoverage object, using a cache in a temporary file to - * pre-fill the AST information for the specified scripts in @prefixes, so long - * as the data in the cache has the same mtime as those scripts. + * Creates a new #GjsCoverage object that collects coverage information for + * any scripts run in @context. * * Scripts which were provided as part of @prefixes will be written out to * @output_dir, in the same directory structure relative to the source dir where @@ -1783,29 +497,4 @@ gjs_coverage_new (const char * const *prefixes, NULL)); return coverage; -} - -GjsCoverage * -gjs_coverage_new_internal_with_cache(const char * const *coverage_prefixes, - GjsContext *context, - GFile *output_dir, - GFile *cache) -{ - GjsCoverage *coverage = - GJS_COVERAGE(g_object_new(GJS_TYPE_COVERAGE, - "prefixes", coverage_prefixes, - "context", context, - "cache", cache, - "output-directory", output_dir, - NULL)); - - return coverage; -} - -GjsCoverage * -gjs_coverage_new_internal_without_cache(const char * const *prefixes, - GjsContext *cx, - GFile *output_dir) -{ - return gjs_coverage_new_internal_with_cache(prefixes, cx, output_dir, NULL); -} +} \ No newline at end of file diff --git a/cjs/engine.cpp b/cjs/engine.cpp index a6d5f48c..67911eef 100644 --- a/cjs/engine.cpp +++ b/cjs/engine.cpp @@ -48,24 +48,12 @@ gjs_locale_to_upper_case (JSContext *context, JS::HandleString src, JS::MutableHandleValue retval) { - bool success = false; - GjsAutoJSChar utf8(context); - char *upper_case_utf8 = NULL; - - if (!gjs_string_to_utf8(context, JS::StringValue(src), &utf8)) - goto out; - - upper_case_utf8 = g_utf8_strup (utf8, -1); - - if (!gjs_string_from_utf8(context, upper_case_utf8, -1, retval)) - goto out; - - success = true; - -out: - g_free(upper_case_utf8); + GjsAutoJSChar utf8 = JS_EncodeStringToUTF8(context, src); + if (!utf8) + return false; - return success; + GjsAutoChar upper_case_utf8 = g_utf8_strup(utf8, -1); + return gjs_string_from_utf8(context, upper_case_utf8, retval); } static bool @@ -73,24 +61,12 @@ gjs_locale_to_lower_case (JSContext *context, JS::HandleString src, JS::MutableHandleValue retval) { - bool success = false; - GjsAutoJSChar utf8(context); - char *lower_case_utf8 = NULL; - - if (!gjs_string_to_utf8(context, JS::StringValue(src), &utf8)) - goto out; - - lower_case_utf8 = g_utf8_strdown (utf8, -1); - - if (!gjs_string_from_utf8(context, lower_case_utf8, -1, retval)) - goto out; - - success = true; - -out: - g_free(lower_case_utf8); + GjsAutoJSChar utf8 = JS_EncodeStringToUTF8(context, src); + if (!utf8) + return false; - return success; + GjsAutoChar lower_case_utf8 = g_utf8_strdown(utf8, -1); + return gjs_string_from_utf8(context, lower_case_utf8, retval); } static bool @@ -99,10 +75,12 @@ gjs_locale_compare (JSContext *context, JS::HandleString src_2, JS::MutableHandleValue retval) { - GjsAutoJSChar utf8_1(context), utf8_2(context); + GjsAutoJSChar utf8_1 = JS_EncodeStringToUTF8(context, src_1); + if (!utf8_1) + return false; - if (!gjs_string_to_utf8(context, JS::StringValue(src_1), &utf8_1) || - !gjs_string_to_utf8(context, JS::StringValue(src_2), &utf8_2)) + GjsAutoJSChar utf8_2 = JS_EncodeStringToUTF8(context, src_2); + if (!utf8_2) return false; retval.setInt32(g_utf8_collate(utf8_1, utf8_2)); @@ -115,11 +93,9 @@ gjs_locale_to_unicode (JSContext *context, const char *src, JS::MutableHandleValue retval) { - bool success; - char *utf8; GError *error = NULL; - utf8 = g_locale_to_utf8(src, -1, NULL, NULL, &error); + GjsAutoChar utf8 = g_locale_to_utf8(src, -1, NULL, NULL, &error); if (!utf8) { gjs_throw(context, "Failed to convert locale string to UTF8: %s", @@ -128,10 +104,7 @@ gjs_locale_to_unicode (JSContext *context, return false; } - success = gjs_string_from_utf8(context, utf8, -1, retval); - g_free (utf8); - - return success; + return gjs_string_from_utf8(context, utf8, retval); } static JSLocaleCallbacks gjs_locale_callbacks = diff --git a/cjs/gjs.h b/cjs/gjs.h index 7c3bcd7a..aae48c2b 100644 --- a/cjs/gjs.h +++ b/cjs/gjs.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #endif /* __GJS_GJS_H__ */ diff --git a/cjs/global.cpp b/cjs/global.cpp index 7d41273a..e725311f 100644 --- a/cjs/global.cpp +++ b/cjs/global.cpp @@ -30,6 +30,41 @@ #include "jsapi-util.h" #include "jsapi-wrapper.h" +static bool +run_bootstrap(JSContext *cx, + const char *bootstrap_script, + JS::HandleObject global) +{ + GjsAutoChar path = g_strdup_printf("/org/cinnamon/cjs/modules/_bootstrap/%s.js", + bootstrap_script); + GError *error = nullptr; + std::unique_ptr script_bytes( + g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error), + g_bytes_unref); + if (!script_bytes) { + gjs_throw_g_error(cx, error); + return false; + } + + JSAutoCompartment ac(cx, global); + + GjsAutoChar uri = g_strconcat("resource://", path.get(), nullptr); + JS::CompileOptions options(cx); + options.setUTF8(true) + .setFileAndLine(uri, 1) + .setSourceIsLazy(true); + + JS::RootedScript compiled_script(cx); + size_t script_len; + auto script = static_cast(g_bytes_get_data(script_bytes.get(), + &script_len)); + if (!JS::Compile(cx, options, script, script_len, &compiled_script)) + return false; + + JS::RootedValue ignored(cx); + return JS::CloneAndExecuteScript(cx, compiled_script, &ignored); +} + static bool gjs_log(JSContext *cx, unsigned argc, @@ -55,8 +90,8 @@ gjs_log(JSContext *cx, return true; } - GjsAutoJSChar s(cx); - if (!gjs_string_to_utf8(cx, JS::StringValue(jstr), &s)) + GjsAutoJSChar s = JS_EncodeStringToUTF8(cx, jstr); + if (!s) return false; g_message("JS LOG: %s", s.get()); @@ -114,8 +149,8 @@ gjs_print_parse_args(JSContext *cx, exc_state.restore(); if (jstr) { - GjsAutoJSChar s(cx); - if (!gjs_string_to_utf8(cx, JS::StringValue(jstr), &s)) { + GjsAutoJSChar s = JS_EncodeStringToUTF8(cx, jstr); + if (!s) { g_string_free(str, true); return false; } @@ -225,7 +260,8 @@ class GjsGlobal { static bool define_properties(JSContext *cx, - JS::HandleObject global) + JS::HandleObject global, + const char *bootstrap_script) { if (!JS_DefineProperty(cx, global, "window", global, JSPROP_READONLY | JSPROP_PERMANENT) || @@ -240,9 +276,17 @@ class GjsGlobal { /* Wrapping is a no-op if the importer is already in the same * compartment. */ - return JS_WrapObject(cx, &root_importer) && - gjs_object_define_property(cx, global, GJS_STRING_IMPORTS, - root_importer, GJS_MODULE_PROP_FLAGS); + if (!JS_WrapObject(cx, &root_importer) || + !gjs_object_define_property(cx, global, GJS_STRING_IMPORTS, + root_importer, GJS_MODULE_PROP_FLAGS)) + return false; + + if (bootstrap_script) { + if (!run_bootstrap(cx, bootstrap_script, global)) + return false; + } + + return true; } }; @@ -265,8 +309,13 @@ gjs_create_global_object(JSContext *cx) * gjs_define_global_properties: * @cx: a #JSContext * @global: a JS global object that has not yet been passed to this function + * @bootstrap_script: (nullable): name of a bootstrap script (found at + * resource://org/gnome/gjs/modules/_bootstrap/@bootstrap_script) or %NULL for + * none * - * Defines properties on the global object such as 'window' and 'imports'. + * Defines properties on the global object such as 'window' and 'imports', and + * runs a bootstrap JS script on the global object to define any properties + * that can be defined from JS. * This function completes the initialization of a new global object, but it * is separate from gjs_create_global_object() because all globals share the * same root importer. @@ -283,9 +332,10 @@ gjs_create_global_object(JSContext *cx) */ bool gjs_define_global_properties(JSContext *cx, - JS::HandleObject global) + JS::HandleObject global, + const char *bootstrap_script) { - return GjsGlobal::define_properties(cx, global); + return GjsGlobal::define_properties(cx, global, bootstrap_script); } void diff --git a/cjs/global.h b/cjs/global.h index 31056d6f..ae99b151 100644 --- a/cjs/global.h +++ b/cjs/global.h @@ -58,7 +58,8 @@ typedef enum { JSObject *gjs_create_global_object(JSContext *cx); bool gjs_define_global_properties(JSContext *cx, - JS::HandleObject global); + JS::HandleObject global, + const char *bootstrap_script); JS::Value gjs_get_global_slot(JSContext *cx, GjsGlobalSlot slot); diff --git a/cjs/importer.cpp b/cjs/importer.cpp index b4f28f70..df7f2af5 100644 --- a/cjs/importer.cpp +++ b/cjs/importer.cpp @@ -23,8 +23,9 @@ #include -#include -#include +#include + +#include #include "importer.h" #include "jsapi-class.h" @@ -32,8 +33,8 @@ #include "mem.h" #include "module.h" #include "native.h" - -#include +#include "util/glib.h" +#include "util/log.h" #ifdef G_OS_WIN32 #define WIN32_LEAN_AND_MEAN @@ -64,7 +65,7 @@ static const JSClass gjs_importer_class = *js::Jsvalify(&gjs_importer_real_class GJS_DEFINE_PRIV_FROM_JS(Importer, gjs_importer_class) static JSObject *gjs_define_importer(JSContext *, JS::HandleObject, - const char *, const char **, bool); + const char *, const char * const *, bool); static bool importer_to_string(JSContext *cx, @@ -79,12 +80,12 @@ importer_to_string(JSContext *cx, &module_path)) return false; - GjsAutoJSChar path(cx); GjsAutoChar output; if (module_path.isNull()) { output = g_strdup_printf("[%s root]", klass->name); } else { + GjsAutoJSChar path; if (!gjs_string_to_utf8(cx, module_path, &path)) return false; output = g_strdup_printf("[%s %s]", klass->name, path.get()); @@ -145,7 +146,7 @@ define_meta_properties(JSContext *context, if (parent_module_path.isNull()) { module_path_buf = g_strdup(module_name); } else { - GjsAutoJSChar parent_path(context); + GjsAutoJSChar parent_path; if (!gjs_string_to_utf8(context, parent_module_path, &parent_path)) return false; module_path_buf = g_strdup_printf("%s.%s", parent_path.get(), module_name); @@ -179,10 +180,10 @@ define_meta_properties(JSContext *context, } static bool -import_directory(JSContext *context, - JS::HandleObject obj, - const char *name, - const char **full_paths) +import_directory(JSContext *context, + JS::HandleObject obj, + const char *name, + const char * const *full_paths) { JSObject *importer; @@ -261,33 +262,29 @@ cancel_import(JSContext *context, } } -static bool -import_native_file(JSContext *context, - JS::HandleObject obj, - const char *name) +/* + * gjs_import_native_module: + * @cx: the #JSContext + * @importer: the root importer + * @name: Name under which the module was registered with + * gjs_register_native_module() + * + * Imports a builtin native-code module so that it is available to JS code as + * `imports[name]`. + * + * Returns: true on success, false if an exception was thrown. + */ +bool +gjs_import_native_module(JSContext *cx, + JS::HandleObject importer, + const char *name) { - JS::RootedObject module_obj(context); - gjs_debug(GJS_DEBUG_IMPORTER, "Importing '%s'", name); - if (!gjs_import_native_module(context, name, &module_obj)) - return false; - - if (!define_meta_properties(context, module_obj, NULL, name, obj)) - return false; - - if (JS_IsExceptionPending(context)) { - /* I am not sure whether this can happen, but if it does we want to trap it. - */ - gjs_debug(GJS_DEBUG_IMPORTER, - "Module '%s' reported an exception but gjs_import_native_module() returned true", - name); - return false; - } - - JS::RootedValue v_module(context, JS::ObjectValue(*module_obj)); - return JS_DefineProperty(context, obj, name, v_module, - GJS_MODULE_PROP_FLAGS); + JS::RootedObject module(cx); + return gjs_load_native_module(cx, name, &module) && + define_meta_properties(cx, module, nullptr, name, importer) && + JS_DefineProperty(cx, importer, name, module, GJS_MODULE_PROP_FLAGS); } static bool @@ -456,14 +453,10 @@ do_import(JSContext *context, JS::HandleId id, const char *name) { - char *filename; - char *full_path; JS::RootedObject search_path(context); guint32 search_path_len; guint32 i; - bool result, exists, is_array; - GPtrArray *directories; - GFile *gfile; + bool exists, is_array; if (!gjs_object_require_property(context, obj, "importer", GJS_STRING_SEARCH_PATH, &search_path)) @@ -481,33 +474,28 @@ do_import(JSContext *context, return false; } - result = false; - - filename = g_strdup_printf("%s.js", name); - full_path = NULL; - directories = NULL; - + GjsAutoChar filename = g_strdup_printf("%s.js", name); + std::vector directories; JS::RootedValue elem(context); + JS::RootedString str(context); /* First try importing an internal module like byteArray */ - if (priv->is_root && - gjs_is_registered_native_module(context, obj, name) && - import_native_file(context, obj, name)) { + if (priv->is_root && gjs_is_registered_native_module(context, obj, name)) { + if (!gjs_import_native_module(context, obj, name)) + return false; + gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'", name); - result = true; - goto out; + return true; } for (i = 0; i < search_path_len; ++i) { - GjsAutoJSChar dirname(context); - elem.setUndefined(); if (!JS_GetElement(context, search_path, i, &elem)) { /* this means there was an exception, while elem.isUndefined() * means no element found */ - goto out; + return false; } if (elem.isUndefined()) @@ -515,56 +503,47 @@ do_import(JSContext *context, if (!elem.isString()) { gjs_throw(context, "importer searchPath contains non-string"); - goto out; + return false; } - if (!gjs_string_to_utf8(context, elem, &dirname)) - goto out; /* Error message already set */ + str = elem.toString(); + GjsAutoJSChar dirname = JS_EncodeStringToUTF8(context, str); + if (!dirname) + return false; /* Ignore empty path elements */ if (dirname[0] == '\0') continue; /* Try importing __init__.js and loading the symbol from it */ - import_symbol_from_init_js(context, obj, dirname, name, &result); - if (result) - goto out; + bool found = false; + if (!import_symbol_from_init_js(context, obj, dirname, name, &found)) + return false; + if (found) + return true; /* Second try importing a directory (a sub-importer) */ - if (full_path) - g_free(full_path); - full_path = g_build_filename(dirname, name, - NULL); - gfile = g_file_new_for_commandline_arg(full_path); + GjsAutoChar full_path = g_build_filename(dirname, name, nullptr); + GjsAutoUnref gfile = g_file_new_for_commandline_arg(full_path); if (g_file_query_file_type(gfile, (GFileQueryInfoFlags) 0, NULL) == G_FILE_TYPE_DIRECTORY) { gjs_debug(GJS_DEBUG_IMPORTER, "Adding directory '%s' to child importer '%s'", - full_path, name); - if (directories == NULL) { - directories = g_ptr_array_new(); - } - g_ptr_array_add(directories, full_path); - /* don't free it twice - pass ownership to ptr array */ - full_path = NULL; + full_path.get(), name); + directories.push_back(std::move(full_path)); } - g_object_unref(gfile); - /* If we just added to directories, we know we don't need to * check for a file. If we added to directories on an earlier * iteration, we want to ignore any files later in the * path. So, always skip the rest of the loop block if we have * directories. */ - if (directories != NULL) { + if (!directories.empty()) continue; - } /* Third, if it's not a directory, try importing a file */ - g_free(full_path); - full_path = g_build_filename(dirname, filename, - NULL); + full_path = g_build_filename(dirname, filename.get(), nullptr); gfile = g_file_new_for_commandline_arg(full_path); exists = g_file_query_exists(gfile, NULL); @@ -572,65 +551,44 @@ do_import(JSContext *context, gjs_debug(GJS_DEBUG_IMPORTER, "JS import '%s' not found in %s", name, dirname.get()); - - g_object_unref(gfile); continue; } if (import_file_on_module(context, obj, id, name, gfile)) { gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'", name); - result = true; + return true; } - g_object_unref(gfile); - /* Don't keep searching path if we fail to load the file for * reasons other than it doesn't exist... i.e. broken files * block searching for nonbroken ones */ - goto out; + return false; } - if (directories != NULL) { + if (!directories.empty()) { /* NULL-terminate the char** */ - g_ptr_array_add(directories, NULL); - - if (import_directory(context, obj, name, - (const char**) directories->pdata)) { - gjs_debug(GJS_DEBUG_IMPORTER, - "successfully imported directory '%s'", name); - result = true; - } - } - - out: - if (directories != NULL) { - char **str_array; + const char **full_paths = g_new0(const char *, directories.size() + 1); + for (size_t ix = 0; ix < directories.size(); ix++) + full_paths[ix] = directories[ix].get(); - /* NULL-terminate the char** - * (maybe for a second time, but doesn't matter) - */ - g_ptr_array_add(directories, NULL); - - str_array = (char**) directories->pdata; - g_ptr_array_free(directories, false); - g_strfreev(str_array); - } - - g_free(full_path); - g_free(filename); + bool result = import_directory(context, obj, name, full_paths); + g_free(full_paths); + if (!result) + return false; - if (!result && - !JS_IsExceptionPending(context)) { - /* If no exception occurred, the problem is just that we got to the - * end of the path. Be sure an exception is set. - */ - gjs_throw_custom(context, "Error", "ImportError", - "No JS module '%s' found in search path", name); + gjs_debug(GJS_DEBUG_IMPORTER, + "successfully imported directory '%s'", name); + return true; } - return result; + /* If no exception occurred, the problem is just that we got to the + * end of the path. Be sure an exception is set. */ + g_assert(!JS_IsExceptionPending(context)); + gjs_throw_custom(context, JSProto_Error, "ImportError", + "No JS module '%s' found in search path", name); + return false; } /* Note that in a for ... in loop, this will be called first on the object, @@ -672,8 +630,8 @@ importer_enumerate(JSContext *context, } JS::RootedValue elem(context); + JS::RootedString str(context); for (i = 0; i < search_path_len; ++i) { - GjsAutoJSChar dirname(context); char *init_path; elem.setUndefined(); @@ -692,8 +650,10 @@ importer_enumerate(JSContext *context, return false; } - if (!gjs_string_to_utf8(context, elem, &dirname)) - return false; /* Error message already set */ + str = elem.toString(); + GjsAutoJSChar dirname = JS_EncodeStringToUTF8(context, str); + if (!dirname) + return false; init_path = g_build_filename(dirname, MODULE_INIT_FILENAME, NULL); @@ -704,7 +664,7 @@ importer_enumerate(JSContext *context, /* new_for_commandline_arg handles resource:/// paths */ GjsAutoUnref dir = g_file_new_for_commandline_arg(dirname); GjsAutoUnref direnum = - g_file_enumerate_children(dir, G_FILE_ATTRIBUTE_STANDARD_TYPE, + g_file_enumerate_children(dir, "standard::name,standard::type", G_FILE_QUERY_INFO_NONE, NULL, NULL); while (true) { @@ -740,11 +700,8 @@ importer_enumerate(JSContext *context, return true; } -/* - * The *objp out parameter, on success, should be null to indicate that id - * was not resolved; and non-null, referring to obj or one of its prototypes, - * if id was resolved. - */ +/* The *resolved out parameter, on success, should be false to indicate that id + * was not resolved; and true if id was resolved. */ static bool importer_resolve(JSContext *context, JS::HandleObject obj, @@ -753,7 +710,11 @@ importer_resolve(JSContext *context, { Importer *priv; jsid module_init_name; - GjsAutoJSChar name(context); + + if (!JSID_IS_STRING(id)) { + *resolved = false; + return true; + } module_init_name = gjs_context_get_const_string(context, GJS_STRING_MODULE_INIT); if (id == module_init_name) { @@ -761,21 +722,20 @@ importer_resolve(JSContext *context, return true; } - if (!gjs_get_string_id(context, id, &name)) - return false; - /* let Object.prototype resolve these */ - if (strcmp(name, "valueOf") == 0 || - strcmp(name, "toString") == 0 || - strcmp(name, "__iterator__") == 0) { + JSFlatString *str = JSID_TO_FLAT_STRING(id); + if (JS_FlatStringEqualsAscii(str, "valueOf") || + JS_FlatStringEqualsAscii(str, "toString") || + JS_FlatStringEqualsAscii(str, "__iterator__")) { *resolved = false; return true; } + priv = priv_from_js(context, obj); gjs_debug_jsprop(GJS_DEBUG_IMPORTER, - "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (!priv) { /* we are the prototype, or have the wrong class */ *resolved = false; @@ -783,6 +743,13 @@ importer_resolve(JSContext *context, } JSAutoRequest ar(context); + + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; + } + if (!do_import(context, obj, priv, id, name)) return false; @@ -985,11 +952,11 @@ gjs_create_importer(JSContext *context, } static JSObject * -gjs_define_importer(JSContext *context, - JS::HandleObject in_object, - const char *importer_name, - const char **initial_search_path, - bool add_standard_search_path) +gjs_define_importer(JSContext *context, + JS::HandleObject in_object, + const char *importer_name, + const char * const *initial_search_path, + bool add_standard_search_path) { JS::RootedObject importer(context, diff --git a/cjs/importer.h b/cjs/importer.h index 4006ee61..7e7419f0 100644 --- a/cjs/importer.h +++ b/cjs/importer.h @@ -33,6 +33,10 @@ G_BEGIN_DECLS JSObject *gjs_create_root_importer(JSContext *cx, const char * const *search_path); +bool gjs_import_native_module(JSContext *cx, + JS::HandleObject importer, + const char *name); + G_END_DECLS #endif /* __GJS_IMPORTER_H__ */ diff --git a/cjs/jsapi-dynamic-class.cpp b/cjs/jsapi-dynamic-class.cpp index a660c192..cb343495 100644 --- a/cjs/jsapi-dynamic-class.cpp +++ b/cjs/jsapi-dynamic-class.cpp @@ -31,7 +31,6 @@ #include "jsapi-class.h" #include "jsapi-util.h" #include "jsapi-wrapper.h" -#include "jsapi-private.h" #include #include @@ -121,19 +120,20 @@ gjs_init_class_dynamic(JSContext *context, } else { /* Have to fake it with JSPROP_RESOLVING, otherwise it will trigger * the resolve hook */ - if (!JS_DefineProperty(context, constructor, "prototype", prototype, - JSPROP_PERMANENT | JSPROP_READONLY | JSPROP_RESOLVING, - JS_STUBGETTER, JS_STUBSETTER)) + if (!gjs_object_define_property(context, constructor, + GJS_STRING_PROTOTYPE, prototype, + JSPROP_PERMANENT | JSPROP_READONLY | JSPROP_RESOLVING)) goto out; - if (!JS_DefineProperty(context, prototype, "constructor", constructor, - JSPROP_RESOLVING, JS_STUBGETTER, JS_STUBSETTER)) + if (!gjs_object_define_property(context, prototype, + GJS_STRING_CONSTRUCTOR, constructor, + JSPROP_RESOLVING)) goto out; } /* The constructor defined by JS_InitClass has no property attributes, but this is a more useful default for gjs */ if (!JS_DefineProperty(context, in_object, class_name, constructor, - GJS_MODULE_PROP_FLAGS, JS_STUBGETTER, JS_STUBSETTER)) + GJS_MODULE_PROP_FLAGS)) goto out; res = true; @@ -166,7 +166,7 @@ gjs_typecheck_instance(JSContext *context, if (throw_error) { const JSClass *obj_class = JS_GetClass(obj); - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object %p is not a subclass of %s, it's a %s", obj.get(), static_clasp->name, format_dynamic_class_name(obj_class->name)); diff --git a/cjs/jsapi-private.cpp b/cjs/jsapi-private.cpp deleted file mode 100644 index f6584ef0..00000000 --- a/cjs/jsapi-private.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright (c) 2010 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 -#include - -#include "jsapi-util.h" -#include "jsapi-private.h" -#include "jsapi-wrapper.h" - -void -gjs_warning_reporter(JSContext *context, - JSErrorReport *report) -{ - const char *warning; - GLogLevelFlags level; - - g_assert(report); - - if (gjs_environment_variable_is_set("GJS_ABORT_ON_OOM") && - report->flags == JSREPORT_ERROR && - report->errorNumber == 137) { - /* 137, JSMSG_OUT_OF_MEMORY */ - g_error("GJS ran out of memory at %s: %i.", - report->filename, - report->lineno); - } - - if ((report->flags & JSREPORT_WARNING) != 0) { - warning = "WARNING"; - level = G_LOG_LEVEL_MESSAGE; - - /* suppress bogus warnings. See mozilla/js/src/js.msg */ - if (report->errorNumber == 162) { - /* 162, JSMSG_UNDEFINED_PROP: warns every time a lazy property - * is resolved, since the property starts out - * undefined. When this is a real bug it should usually - * fail somewhere else anyhow. - */ - return; - } - } else { - warning = "REPORTED"; - level = G_LOG_LEVEL_WARNING; - } - - g_log(G_LOG_DOMAIN, level, "JS %s: [%s %d]: %s", warning, report->filename, - report->lineno, report->message().c_str()); -} diff --git a/cjs/jsapi-util-args.h b/cjs/jsapi-util-args.h index 76003d79..e723919f 100644 --- a/cjs/jsapi-util-args.h +++ b/cjs/jsapi-util-args.h @@ -96,7 +96,7 @@ assign(JSContext *cx, if (c != 's') throw g_strdup_printf("Wrong type for %c, got GjsAutoJSChar*", c); if (nullable && value.isNull()) { - ref->reset(cx, nullptr); + ref->reset(); return; } if (!gjs_string_to_utf8(cx, value, ref)) diff --git a/cjs/jsapi-util-error.cpp b/cjs/jsapi-util-error.cpp index 0af708d5..f4f67145 100644 --- a/cjs/jsapi-util-error.cpp +++ b/cjs/jsapi-util-error.cpp @@ -26,6 +26,7 @@ #include "jsapi-util.h" #include "jsapi-wrapper.h" #include "gi/gerror.h" +#include "util/misc.h" #include @@ -44,7 +45,7 @@ static void G_GNUC_PRINTF(4, 0) gjs_throw_valist(JSContext *context, - const char *error_class, + JSProtoKey error_kind, const char *error_name, const char *format, va_list args) @@ -82,19 +83,15 @@ gjs_throw_valist(JSContext *context, JS::AutoValueArray<1> error_args(context); result = false; - if (!gjs_string_from_utf8(context, s, -1, error_args[0])) { + if (!gjs_string_from_utf8(context, s, error_args[0])) { JS_ReportErrorUTF8(context, "Failed to copy exception string"); goto out; } - if (!JS_GetProperty(context, global, error_class, &v_constructor) || - !v_constructor.isObject()) { - JS_ReportErrorUTF8(context, "??? Missing Error constructor in global object?"); + if (!JS_GetClassObject(context, error_kind, &constructor)) goto out; - } /* throw new Error(message) */ - constructor = &v_constructor.toObject(); new_exc = JS_New(context, constructor, error_args); if (!new_exc) @@ -102,7 +99,7 @@ gjs_throw_valist(JSContext *context, if (error_name != NULL) { JS::RootedValue name_value(context); - if (!gjs_string_from_utf8(context, error_name, -1, &name_value) || + if (!gjs_string_from_utf8(context, error_name, &name_value) || !gjs_object_set_property(context, new_exc, GJS_STRING_NAME, name_value)) goto out; @@ -141,7 +138,7 @@ gjs_throw(JSContext *context, va_list args; va_start(args, format); - gjs_throw_valist(context, "Error", NULL, format, args); + gjs_throw_valist(context, JSProto_Error, nullptr, format, args); va_end(args); } @@ -151,16 +148,21 @@ gjs_throw(JSContext *context, * error. */ void -gjs_throw_custom(JSContext *context, - const char *error_class, - const char *error_name, - const char *format, +gjs_throw_custom(JSContext *cx, + JSProtoKey kind, + const char *error_name, + const char *format, ...) { va_list args; + g_return_if_fail(kind == JSProto_Error || kind == JSProto_InternalError || + kind == JSProto_EvalError || kind == JSProto_RangeError || + kind == JSProto_ReferenceError || kind == JSProto_SyntaxError || + kind == JSProto_TypeError || kind == JSProto_URIError || + kind == JSProto_StopIteration); va_start(args, format); - gjs_throw_valist(context, error_class, error_name, format, args); + gjs_throw_valist(cx, kind, error_name, format, args); va_end(args); } @@ -220,9 +222,9 @@ gjs_format_stack_trace(JSContext *cx, JS::AutoSaveExceptionState saved_exc(cx); JS::RootedString stack_trace(cx); - GjsAutoJSChar stack_utf8(cx); + GjsAutoJSChar stack_utf8; if (JS::BuildStackString(cx, saved_frame, &stack_trace, 2)) - stack_utf8.reset(cx, JS_EncodeStringToUTF8(cx, stack_trace)); + stack_utf8 = JS_EncodeStringToUTF8(cx, stack_trace); saved_exc.restore(); @@ -231,3 +233,43 @@ gjs_format_stack_trace(JSContext *cx, return g_filename_from_utf8(stack_utf8, -1, nullptr, nullptr, nullptr); } + +void +gjs_warning_reporter(JSContext *context, + JSErrorReport *report) +{ + const char *warning; + GLogLevelFlags level; + + g_assert(report); + + if (gjs_environment_variable_is_set("GJS_ABORT_ON_OOM") && + report->flags == JSREPORT_ERROR && + report->errorNumber == 137) { + /* 137, JSMSG_OUT_OF_MEMORY */ + g_error("GJS ran out of memory at %s: %i.", + report->filename, + report->lineno); + } + + if ((report->flags & JSREPORT_WARNING) != 0) { + warning = "WARNING"; + level = G_LOG_LEVEL_MESSAGE; + + /* suppress bogus warnings. See mozilla/js/src/js.msg */ + if (report->errorNumber == 162) { + /* 162, JSMSG_UNDEFINED_PROP: warns every time a lazy property + * is resolved, since the property starts out + * undefined. When this is a real bug it should usually + * fail somewhere else anyhow. + */ + return; + } + } else { + warning = "REPORTED"; + level = G_LOG_LEVEL_WARNING; + } + + g_log(G_LOG_DOMAIN, level, "JS %s: [%s %d]: %s", warning, report->filename, + report->lineno, report->message().c_str()); +} diff --git a/cjs/jsapi-util-root.h b/cjs/jsapi-util-root.h index 1237057f..f78ec45f 100644 --- a/cjs/jsapi-util-root.h +++ b/cjs/jsapi-util-root.h @@ -219,6 +219,7 @@ class GjsMaybeOwned { return m_root->get() == nullptr; return m_heap.unbarrieredGet() == nullptr; } + inline bool operator!=(std::nullptr_t) const { return !(*this == nullptr); } /* You can get a Handle if the thing is rooted, so that you can use this * wrapper with stack rooting. However, you must not do this if the @@ -247,10 +248,12 @@ class GjsMaybeOwned { m_data = data; m_root = new JS::PersistentRooted(m_cx, thing); - auto gjs_cx = static_cast(JS_GetContextPrivate(m_cx)); - g_assert(GJS_IS_CONTEXT(gjs_cx)); - g_object_weak_ref(G_OBJECT(gjs_cx), on_context_destroy, this); - m_has_weakref = true; + if (notify) { + auto gjs_cx = static_cast(JS_GetContextPrivate(m_cx)); + g_assert(GJS_IS_CONTEXT(gjs_cx)); + g_object_weak_ref(G_OBJECT(gjs_cx), on_context_destroy, this); + m_has_weakref = true; + } } /* You can only assign directly to the GjsMaybeOwned wrapper in the diff --git a/cjs/jsapi-util-string.cpp b/cjs/jsapi-util-string.cpp index 41929b55..03e8c4fd 100644 --- a/cjs/jsapi-util-string.cpp +++ b/cjs/jsapi-util-string.cpp @@ -24,65 +24,53 @@ #include #include +#include +#include +#include #include #include "jsapi-util.h" #include "jsapi-wrapper.h" +/** + * gjs_string_to_utf8: + * @cx: JSContext + * @value: a JS::Value containing a string + * @utf8_string_p: return location for a unique JS chars pointer + * + * Converts the JSString in @value to UTF-8 and puts it in @utf8_string_p. + * + * This function is a convenience wrapper around JS_EncodeStringToUTF8() that + * typechecks the JS::Value and throws an exception if it's the wrong type. + * Don't use this function if you already have a JS::RootedString, or if you + * know the value already holds a string; use JS_EncodeStringToUTF8() instead. + */ bool -gjs_string_to_utf8 (JSContext *context, - const JS::Value value, - GjsAutoJSChar *utf8_string_p) +gjs_string_to_utf8(JSContext *cx, + const JS::Value value, + GjsAutoJSChar *utf8_string_p) { - JS_BeginRequest(context); + JSAutoRequest ar(cx); if (!value.isString()) { - gjs_throw(context, - "Value is not a string, cannot convert to UTF-8"); - JS_EndRequest(context); + gjs_throw(cx, "Value is not a string, cannot convert to UTF-8"); return false; } - JS::RootedString str(context, value.toString()); - utf8_string_p->reset(context, JS_EncodeStringToUTF8(context, str)); - - JS_EndRequest(context); - - return true; + JS::RootedString str(cx, value.toString()); + utf8_string_p->reset(JS_EncodeStringToUTF8(cx, str)); + return !!*utf8_string_p; } bool gjs_string_from_utf8(JSContext *context, const char *utf8_string, - ssize_t n_bytes, JS::MutableHandleValue value_p) { - char16_t *u16_string; - glong u16_string_length; - GError *error; - - /* intentionally using n_bytes even though glib api suggests n_chars; with - * n_chars (from g_utf8_strlen()) the result appears truncated - */ - - error = NULL; - u16_string = - reinterpret_cast(g_utf8_to_utf16(utf8_string, n_bytes, NULL, - &u16_string_length, &error)); - if (!u16_string) { - gjs_throw(context, - "Failed to convert UTF-8 string to " - "JS string: %s", - error->message); - g_error_free(error); - return false; - } - JS_BeginRequest(context); - /* Avoid a copy - assumes that g_malloc == js_malloc == malloc */ - JS::RootedString str(context, - JS_NewUCString(context, u16_string, u16_string_length)); + JS::ConstUTF8CharsZ chars(utf8_string, strlen(utf8_string)); + JS::RootedString str(context, JS_NewStringCopyUTF8Z(context, chars)); if (str) value_p.setString(str); @@ -90,13 +78,29 @@ gjs_string_from_utf8(JSContext *context, return str != nullptr; } +bool +gjs_string_from_utf8_n(JSContext *cx, + const char *utf8_chars, + size_t len, + JS::MutableHandleValue out) +{ + JSAutoRequest ar(cx); + + JS::UTF8Chars chars(utf8_chars, len); + JS::RootedString str(cx, JS_NewStringCopyUTF8N(cx, chars)); + if (str) + out.setString(str); + + return !!str; +} + bool gjs_string_to_filename(JSContext *context, const JS::Value filename_val, GjsAutoChar *filename_string) { GError *error; - GjsAutoJSChar tmp(context); + GjsAutoJSChar tmp; /* gjs_string_to_filename verifies that filename_val is a string */ @@ -123,27 +127,20 @@ gjs_string_from_filename(JSContext *context, { gsize written; GError *error; - gchar *utf8_string; error = NULL; - utf8_string = g_filename_to_utf8(filename_string, n_bytes, NULL, - &written, &error); + GjsAutoChar utf8_string = g_filename_to_utf8(filename_string, n_bytes, + nullptr, &written, &error); if (error) { gjs_throw(context, "Could not convert UTF-8 string '%s' to a filename: '%s'", filename_string, error->message); g_error_free(error); - g_free(utf8_string); return false; } - if (!gjs_string_from_utf8(context, utf8_string, written, value_p)) - return false; - - g_free(utf8_string); - - return true; + return gjs_string_from_utf8_n(context, utf8_string, written, value_p); } /* Converts a JSString's array of Latin-1 chars to an array of a wider integer @@ -178,40 +175,31 @@ from_latin1(JSContext *cx, /** * gjs_string_get_char16_data: * @context: js context - * @value: a JS::Value + * @str: a rooted JSString * @data_p: address to return allocated data buffer * @len_p: address to return length of data (number of 16-bit characters) * - * Get the binary data (as a sequence of 16-bit characters) in the JSString - * contained in @value. - * Throws a JS exception if value is not a string. + * Get the binary data (as a sequence of 16-bit characters) in @str. * * Returns: false if exception thrown **/ bool gjs_string_get_char16_data(JSContext *context, - JS::Value value, + JS::HandleString str, char16_t **data_p, size_t *len_p) { JSAutoRequest ar(context); - if (!value.isString()) { - gjs_throw(context, - "Value is not a string, can't return binary data from it"); - return false; - } - - if (JS_StringHasLatin1Chars(value.toString())) - return from_latin1(context, value.toString(), data_p, len_p); + if (JS_StringHasLatin1Chars(str)) + return from_latin1(context, str, data_p, len_p); /* From this point on, crash if a GC is triggered while we are using * the string's chars */ JS::AutoCheckCannotGC nogc; const char16_t *js_data = - JS_GetTwoByteStringCharsAndLength(context, nogc, - value.toString(), len_p); + JS_GetTwoByteStringCharsAndLength(context, nogc, str, len_p); if (js_data == NULL) return false; @@ -224,33 +212,27 @@ gjs_string_get_char16_data(JSContext *context, /** * gjs_string_to_ucs4: * @cx: a #JSContext - * @value: JS::Value containing a string + * @str: rooted JSString * @ucs4_string_p: return location for a #gunichar array * @len_p: return location for @ucs4_string_p length * * Returns: true on success, false otherwise in which case a JS error is thrown */ bool -gjs_string_to_ucs4(JSContext *cx, - JS::HandleValue value, - gunichar **ucs4_string_p, - size_t *len_p) +gjs_string_to_ucs4(JSContext *cx, + JS::HandleString str, + gunichar **ucs4_string_p, + size_t *len_p) { if (ucs4_string_p == NULL) return true; - if (!value.isString()) { - gjs_throw(cx, "Value is not a string, cannot convert to UCS-4"); - return false; - } - JSAutoRequest ar(cx); - JS::RootedString str(cx, value.toString()); size_t len; GError *error = NULL; if (JS_StringHasLatin1Chars(str)) - return from_latin1(cx, value.toString(), ucs4_string_p, len_p); + return from_latin1(cx, str, ucs4_string_p, len_p); /* From this point on, crash if a GC is triggered while we are using * the string's chars */ @@ -344,7 +326,9 @@ gjs_get_string_id (JSContext *context, return false; if (id_val.isString()) { - return gjs_string_to_utf8(context, id_val, name_p); + JS::RootedString str(context, id_val.toString()); + name_p->reset(JS_EncodeStringToUTF8(context, str)); + return !!*name_p; } else { return false; } @@ -367,7 +351,7 @@ gjs_unichar_from_string (JSContext *context, JS::Value value, gunichar *result) { - GjsAutoJSChar utf8_str(context); + GjsAutoJSChar utf8_str; if (gjs_string_to_utf8(context, value, &utf8_str)) { *result = g_utf8_get_char(utf8_str); return true; @@ -381,6 +365,142 @@ gjs_intern_string_to_id(JSContext *cx, { JSAutoRequest ar(cx); JS::RootedString str(cx, JS_AtomizeAndPinString(cx, string)); - JS::RootedId id(cx, INTERNED_STRING_TO_JSID(cx, str)); - return id; + return INTERNED_STRING_TO_JSID(cx, str); +} + +static std::string +gjs_debug_flat_string(JSFlatString *fstr) +{ + JSLinearString *str = js::FlatStringToLinearString(fstr); + size_t len = js::GetLinearStringLength(str); + + JS::AutoCheckCannotGC nogc; + if (js::LinearStringHasLatin1Chars(str)) { + const JS::Latin1Char *chars = js::GetLatin1LinearStringChars(nogc, str); + return std::string(reinterpret_cast(chars), len); + } + + std::ostringstream out; + const char16_t *chars = js::GetTwoByteLinearStringChars(nogc, str); + for (size_t ix = 0; ix < len; ix++) { + char16_t c = chars[ix]; + if (c == '\n') + out << "\\n"; + else if (c == '\t') + out << "\\t"; + else if (c >= 32 && c < 127) + out << c; + else if (c <= 255) + out << "\\x" << std::setfill('0') << std::setw(2) << unsigned(c); + else + out << "\\x" << std::setfill('0') << std::setw(4) << unsigned(c); + } + return out.str(); +} + +std::string +gjs_debug_string(JSString *str) +{ + if (!JS_StringIsFlat(str)) { + std::ostringstream out("'; + return out.str(); + } + return gjs_debug_flat_string(JS_ASSERT_STRING_IS_FLAT(str)); +} + +std::string +gjs_debug_symbol(JS::Symbol * const sym) +{ + /* This is OK because JS::GetSymbolCode() and JS::GetSymbolDescription() + * can't cause a garbage collection */ + JS::HandleSymbol handle = JS::HandleSymbol::fromMarkedLocation(&sym); + JS::SymbolCode code = JS::GetSymbolCode(handle); + JSString *descr = JS::GetSymbolDescription(handle); + + if (size_t(code) < JS::WellKnownSymbolLimit) + return gjs_debug_string(descr); + + std::ostringstream out; + if (code == JS::SymbolCode::InSymbolRegistry) { + out << "Symbol.for("; + if (descr) + out << gjs_debug_string(descr); + else + out << "undefined"; + out << ")"; + return out.str(); + } + if (code == JS::SymbolCode::UniqueSymbol) { + if (descr) + out << "Symbol(" << gjs_debug_string(descr) << ")"; + else + out << ""; + return out.str(); + } + + out << ""; + return out.str(); +} + +std::string +gjs_debug_object(JSObject * const obj) +{ + std::ostringstream out; + const JSClass* clasp = JS_GetClass(obj); + out << "name << " at " << obj << '>'; + return out.str(); +} + +std::string +gjs_debug_value(JS::Value v) +{ + std::ostringstream out; + if (v.isNull()) + return "null"; + if (v.isUndefined()) + return "undefined"; + if (v.isInt32()) { + out << v.toInt32(); + return out.str(); + } + if (v.isDouble()) { + out << v.toDouble(); + return out.str(); + } + if (v.isString()) { + out << gjs_debug_string(v.toString()); + return out.str(); + } + if (v.isSymbol()) { + out << gjs_debug_symbol(v.toSymbol()); + return out.str(); + } + if (v.isObject() && js::IsFunctionObject(&v.toObject())) { + JSFunction* fun = JS_GetObjectFunction(&v.toObject()); + JSString *display_name = JS_GetFunctionDisplayId(fun); + if (display_name) + out << "'; + return out.str(); + } + if (v.isObject()) { + out << gjs_debug_object(&v.toObject()); + return out.str(); + } + if (v.isBoolean()) + return (v.toBoolean() ? "true" : "false"); + if (v.isMagic()) + return ""; + return "unexpected value"; +} + +std::string +gjs_debug_id(jsid id) +{ + if (JSID_IS_STRING(id)) + return gjs_debug_flat_string(JSID_TO_FLAT_STRING(id)); + return gjs_debug_value(js::IdToValue(id)); } diff --git a/cjs/jsapi-util.cpp b/cjs/jsapi-util.cpp index 0dfe12f0..ce66146c 100644 --- a/cjs/jsapi-util.cpp +++ b/cjs/jsapi-util.cpp @@ -24,6 +24,11 @@ #include +#include +#include +#include "jsapi-wrapper.h" +#include + #include #include #include @@ -31,9 +36,7 @@ #include "jsapi-class.h" #include "jsapi-util.h" -#include "jsapi-wrapper.h" #include "context-private.h" -#include "jsapi-private.h" #include #include @@ -83,13 +86,11 @@ gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleValue value, - unsigned flags, - JSNative getter, - JSNative setter) + unsigned flags) { return JS_DefinePropertyById(cx, obj, gjs_context_get_const_string(cx, property_name), - value, flags, getter, setter); + value, flags); } bool @@ -97,13 +98,11 @@ gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleObject value, - unsigned flags, - JSNative getter, - JSNative setter) + unsigned flags) { return JS_DefinePropertyById(cx, obj, gjs_context_get_const_string(cx, property_name), - value, flags, getter, setter); + value, flags); } bool @@ -111,13 +110,11 @@ gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleString value, - unsigned flags, - JSNative getter, - JSNative setter) + unsigned flags) { return JS_DefinePropertyById(cx, obj, gjs_context_get_const_string(cx, property_name), - value, flags, getter, setter); + value, flags); } bool @@ -125,13 +122,11 @@ gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, uint32_t value, - unsigned flags, - JSNative getter, - JSNative setter) + unsigned flags) { return JS_DefinePropertyById(cx, obj, gjs_context_get_const_string(cx, property_name), - value, flags, getter, setter); + value, flags); } static void @@ -144,15 +139,12 @@ throw_property_lookup_error(JSContext *cx, /* remember gjs_throw() is a no-op if JS_GetProperty() * already set an exception */ - GjsAutoJSChar name(cx); - gjs_get_string_id(cx, property_name, &name); - if (description) - gjs_throw(cx, "No property '%s' in %s (or %s)", name.get(), description, - reason); + gjs_throw(cx, "No property '%s' in %s (or %s)", + gjs_debug_id(property_name).c_str(), description, reason); else - gjs_throw(cx, "No property '%s' in object %p (or %s)", name.get(), - obj.address(), reason); + gjs_throw(cx, "No property '%s' in object %p (or %s)", + gjs_debug_id(property_name).c_str(), obj.get(), reason); } /* Returns whether the object had the property; if the object did @@ -319,8 +311,9 @@ gjs_build_string_array(JSContext *context, g_error("Unable to reserve memory for vector"); for (i = 0; i < array_length; ++i) { + JS::ConstUTF8CharsZ chars(array_values[i], strlen(array_values[i])); JS::RootedValue element(context, - JS::StringValue(JS_NewStringCopyZ(context, array_values[i]))); + JS::StringValue(JS_NewStringCopyUTF8Z(context, chars))); if (!elems.append(element)) g_error("Unable to append to vector"); } @@ -360,22 +353,21 @@ gjs_define_string_array(JSContext *context, * */ static char * -gjs_string_readable (JSContext *context, - JSString *string) +gjs_string_readable(JSContext *context, + JS::HandleString string) { GString *buf = g_string_new(""); - GjsAutoJSChar chars(context); JS_BeginRequest(context); g_string_append_c(buf, '"'); - if (!gjs_string_to_utf8(context, JS::StringValue(string), &chars)) { - /* I'm not sure this code will actually ever be reached, since - * JS_EncodeStringToUTF8(), called internally by - * gjs_string_to_utf8(), seems to happily output non-valid UTF-8 - * bytes. However, let's leave this in, since SpiderMonkey may - * decide to do this in the future. */ + GjsAutoJSChar chars = JS_EncodeStringToUTF8(context, string); + if (!chars) { + /* I'm not sure this code will actually ever be reached except in the + * case of OOM, since JS_EncodeStringToUTF8() seems to happily output + * non-valid UTF-8 bytes. However, let's leave this in, since + * SpiderMonkey may decide to do validation in the future. */ /* Find out size of buffer to allocate, not counting 0-terminator */ size_t len = JS_PutEscapedString(context, NULL, 0, string, '"'); @@ -450,7 +442,8 @@ gjs_value_debug_string(JSContext *context, /* Special case debug strings for strings */ if (value.isString()) { - return gjs_string_readable(context, value.toString()); + JS::RootedString str(context, value.toString()); + return gjs_string_readable(context, str); } JS_BeginRequest(context); @@ -496,10 +489,11 @@ static char * utf8_exception_from_non_gerror_value(JSContext *cx, JS::HandleValue exc) { - GjsAutoJSChar utf8_exception(cx); JS::RootedString exc_str(cx, JS::ToString(cx, exc)); - if (exc_str) - gjs_string_to_utf8(cx, JS::StringValue(exc_str), &utf8_exception); + if (!exc_str) + return nullptr; + + GjsAutoJSChar utf8_exception = JS_EncodeStringToUTF8(cx, exc_str); return utf8_exception.copy(); } @@ -509,7 +503,6 @@ gjs_log_exception_full(JSContext *context, JS::HandleString message) { char *utf8_exception; - GjsAutoJSChar utf8_message(context); bool is_syntax; JS_BeginRequest(context); @@ -527,22 +520,17 @@ gjs_log_exception_full(JSContext *context, g_quark_to_string(gerror->domain), gerror->message); } else { - JS::RootedValue js_name(context); - GjsAutoJSChar utf8_name(context); - - if (gjs_object_get_property(context, exc_obj, - GJS_STRING_NAME, &js_name) && - js_name.isString() && - gjs_string_to_utf8(context, js_name, &utf8_name)) { - is_syntax = strcmp("SyntaxError", utf8_name) == 0; - } + const JSClass *syntax_error = + js::Jsvalify(js::ProtoKeyToClass(JSProto_SyntaxError)); + is_syntax = JS_InstanceOf(context, exc_obj, syntax_error, nullptr); utf8_exception = utf8_exception_from_non_gerror_value(context, exc); } } + GjsAutoJSChar utf8_message; if (message) - gjs_string_to_utf8(context, JS::StringValue(message), &utf8_message); + utf8_message = JS_EncodeStringToUTF8(context, message); /* We log syntax errors differently, because the stack for those includes only the referencing module, but we want to print out the filename and @@ -552,17 +540,19 @@ gjs_log_exception_full(JSContext *context, if (is_syntax) { JS::RootedValue js_lineNumber(context), js_fileName(context); unsigned lineNumber; - GjsAutoJSChar utf8_filename(context); gjs_object_get_property(context, exc_obj, GJS_STRING_LINE_NUMBER, &js_lineNumber); gjs_object_get_property(context, exc_obj, GJS_STRING_FILENAME, &js_fileName); - if (js_fileName.isString()) - gjs_string_to_utf8(context, js_fileName, &utf8_filename); - else - utf8_filename.reset(context, JS_strdup(context, "unknown")); + GjsAutoJSChar utf8_filename; + if (js_fileName.isString()) { + JS::RootedString str(context, js_fileName.toString()); + utf8_filename = JS_EncodeStringToUTF8(context, str); + } + if (!utf8_filename) + utf8_filename = JS_strdup(context, "unknown"); lineNumber = js_lineNumber.toInt32(); @@ -575,25 +565,24 @@ gjs_log_exception_full(JSContext *context, } } else { - GjsAutoJSChar utf8_stack(context); + GjsAutoJSChar utf8_stack; JS::RootedValue stack(context); - bool have_utf8_stack = false; if (exc.isObject() && gjs_object_get_property(context, exc_obj, GJS_STRING_STACK, &stack) && stack.isString()) { - gjs_string_to_utf8(context, stack, &utf8_stack); - have_utf8_stack = true; + JS::RootedString str(context, stack.toString()); + utf8_stack = JS_EncodeStringToUTF8(context, str); } if (message) { - if (have_utf8_stack) + if (utf8_stack) g_warning("JS ERROR: %s: %s\n%s", utf8_message.get(), utf8_exception, utf8_stack.get()); else g_warning("JS ERROR: %s: %s", utf8_message.get(), utf8_exception); } else { - if (have_utf8_stack) + if (utf8_stack) g_warning("JS ERROR: %s\n%s", utf8_exception, utf8_stack.get()); else g_warning("JS ERROR: %s", utf8_exception); @@ -650,29 +639,6 @@ gjs_call_function_value(JSContext *context, return result; } -/* get a debug string for type tag in JS::Value */ -const char* -gjs_get_type_name(JS::Value value) -{ - if (value.isNull()) { - return "null"; - } else if (value.isUndefined()) { - return "undefined"; - } else if (value.isInt32()) { - return "integer"; - } else if (value.isDouble()) { - return "double"; - } else if (value.isBoolean()) { - return "boolean"; - } else if (value.isString()) { - return "string"; - } else if (value.isObject()) { - return "object"; - } else { - return ""; - } -} - #ifdef __linux__ static void _linux_get_self_process_size (gulong *vm_size, @@ -741,7 +707,7 @@ gjs_gc_if_needed (JSContext *context) */ if (rss_size > linux_rss_trigger) { linux_rss_trigger = (gulong) MIN(G_MAXULONG, rss_size * 1.25); - JS_GC(context); + JS::GCForReason(context, GC_SHRINK, JS::gcreason::Reason::API); } else if (rss_size < (0.75 * linux_rss_trigger)) { /* If we've shrunk by 75%, lower the trigger */ linux_rss_trigger = (rss_size * 1.25); @@ -861,19 +827,19 @@ gjs_eval_with_scope(JSContext *context, eval_obj = JS_NewPlainObject(context); JS::CompileOptions options(context); - options.setUTF8(true) - .setFileAndLine(filename, start_line_number) + options.setFileAndLine(filename, start_line_number) .setSourceIsLazy(true); - JS::RootedScript compiled_script(context); - if (!JS::Compile(context, options, script, real_len, &compiled_script)) - return false; + std::wstring_convert, char16_t> convert; + std::u16string utf16_string = convert.from_bytes(script); + JS::SourceBufferHolder buf(utf16_string.c_str(), utf16_string.size(), + JS::SourceBufferHolder::NoOwnership); JS::AutoObjectVector scope_chain(context); if (!scope_chain.append(eval_obj)) g_error("Unable to append to vector"); - if (!JS_ExecuteScript(context, scope_chain, compiled_script, retval)) + if (!JS::Evaluate(context, scope_chain, options, buf, retval)) return false; gjs_schedule_gc_if_needed(context); diff --git a/cjs/jsapi-util.h b/cjs/jsapi-util.h index cb90a2f3..2474f3b8 100644 --- a/cjs/jsapi-util.h +++ b/cjs/jsapi-util.h @@ -25,6 +25,7 @@ #define __GJS_JSAPI_UTIL_H__ #include +#include #include #include @@ -65,58 +66,27 @@ class GjsAutoUnref : public std::unique_ptr { } }; -class GjsJSFreeArgs { -private: - JSContext *m_cx; - -public: - explicit GjsJSFreeArgs(JSContext *cx) : m_cx(cx) - {} - +struct GjsJSFreeArgs { void operator() (char *str) { - JS_free(m_cx, str); - } - - JSContext* get_context() { - return m_cx; - } - - void set_context(JSContext *cx) { - m_cx = cx; + JS_free(nullptr, str); } }; -class GjsAutoJSChar { -private: - std::unique_ptr m_ptr; - +class GjsAutoJSChar : public std::unique_ptr { public: - GjsAutoJSChar(JSContext *cx, char *str = nullptr) - : m_ptr (str, GjsJSFreeArgs(cx)) { - g_assert(cx != nullptr); - } + GjsAutoJSChar(char *str = nullptr) : unique_ptr(str, GjsJSFreeArgs()) { } operator const char*() { - return m_ptr.get(); + return get(); } - const char* get() { - return m_ptr.get(); + void operator=(char *str) { + reset(str); } char* copy() { /* Strings acquired by this should be g_free()'ed */ - return g_strdup(m_ptr.get()); - } - - char* js_copy() { - /* Strings acquired by this should be JS_free()'ed */ - return JS_strdup(m_ptr.get_deleter().get_context(), m_ptr.get()); - } - - void reset(JSContext *cx, char *str) { - m_ptr.get_deleter().set_context(cx); - m_ptr.reset(str); + return g_strdup(get()); } }; @@ -137,7 +107,6 @@ typedef struct GjsRootedArray GjsRootedArray; /* Flags that should be set on properties exported from native code modules. * Basically set these on API, but do NOT set them on data. * - * READONLY: forbid setting prop to another value * PERMANENT: forbid deleting the prop * ENUMERATE: allows copyProperties to work among other reasons to have it */ @@ -180,7 +149,7 @@ void gjs_throw (JSContext *context, const char *format, ...) G_GNUC_PRINTF (2, 3); void gjs_throw_custom (JSContext *context, - const char *error_class, + JSProtoKey error_kind, const char *error_name, const char *format, ...) G_GNUC_PRINTF (4, 5); @@ -212,8 +181,11 @@ bool gjs_string_to_utf8 (JSContext *context, GjsAutoJSChar *utf8_string_p); bool gjs_string_from_utf8(JSContext *context, const char *utf8_string, - ssize_t n_bytes, JS::MutableHandleValue value_p); +bool gjs_string_from_utf8_n(JSContext *cx, + const char *utf8_chars, + size_t len, + JS::MutableHandleValue out); bool gjs_string_to_filename(JSContext *cx, const JS::Value string_val, @@ -224,15 +196,15 @@ bool gjs_string_from_filename(JSContext *context, ssize_t n_bytes, JS::MutableHandleValue value_p); -bool gjs_string_get_char16_data(JSContext *context, - JS::Value value, - char16_t **data_p, - size_t *len_p); +bool gjs_string_get_char16_data(JSContext *cx, + JS::HandleString str, + char16_t **data_p, + size_t *len_p); -bool gjs_string_to_ucs4(JSContext *cx, - JS::HandleValue value, - gunichar **ucs4_string_p, - size_t *len_p); +bool gjs_string_to_ucs4(JSContext *cx, + JS::HandleString value, + gunichar **ucs4_string_p, + size_t *len_p); bool gjs_string_from_ucs4(JSContext *cx, const gunichar *ucs4_string, ssize_t n_chars, @@ -248,11 +220,11 @@ bool gjs_unichar_from_string (JSContext *context, JS::Value string, gunichar *result); -const char* gjs_get_type_name (JS::Value value); - /* Functions intended for more "internal" use */ void gjs_maybe_gc (JSContext *context); +void gjs_schedule_gc_if_needed(JSContext *cx); +void gjs_gc_if_needed(JSContext *cx); bool gjs_eval_with_scope(JSContext *context, JS::HandleObject object, @@ -324,33 +296,25 @@ bool gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleValue value, - unsigned flags, - JSNative getter = nullptr, - JSNative setter = nullptr); + unsigned flags); bool gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleObject value, - unsigned flags, - JSNative getter = nullptr, - JSNative setter = nullptr); + unsigned flags); bool gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, JS::HandleString value, - unsigned flags, - JSNative getter = nullptr, - JSNative setter = nullptr); + unsigned flags); bool gjs_object_define_property(JSContext *cx, JS::HandleObject obj, GjsConstString property_name, uint32_t value, - unsigned flags, - JSNative getter = nullptr, - JSNative setter = nullptr); + unsigned flags); JS::HandleId gjs_context_get_const_string(JSContext *cx, GjsConstString string); @@ -420,4 +384,10 @@ bool gjs_object_require_converted_property(JSContext *cx, value); } +std::string gjs_debug_string(JSString *str); +std::string gjs_debug_symbol(JS::Symbol * const sym); +std::string gjs_debug_object(JSObject *obj); +std::string gjs_debug_value(JS::Value v); +std::string gjs_debug_id(jsid id); + #endif /* __GJS_JSAPI_UTIL_H__ */ diff --git a/cjs/mem.cpp b/cjs/mem.cpp index 1ed251b9..3fff516d 100644 --- a/cjs/mem.cpp +++ b/cjs/mem.cpp @@ -35,38 +35,32 @@ GJS_DEFINE_COUNTER(everything) GJS_DEFINE_COUNTER(boxed) -GJS_DEFINE_COUNTER(gerror) GJS_DEFINE_COUNTER(closure) -GJS_DEFINE_COUNTER(database) GJS_DEFINE_COUNTER(function) GJS_DEFINE_COUNTER(fundamental) +GJS_DEFINE_COUNTER(gerror) GJS_DEFINE_COUNTER(importer) +GJS_DEFINE_COUNTER(interface) GJS_DEFINE_COUNTER(ns) GJS_DEFINE_COUNTER(object) GJS_DEFINE_COUNTER(param) GJS_DEFINE_COUNTER(repo) -GJS_DEFINE_COUNTER(resultset) -GJS_DEFINE_COUNTER(weakhash) -GJS_DEFINE_COUNTER(interface) #define GJS_LIST_COUNTER(name) \ & gjs_counter_ ## name static GjsMemCounter* counters[] = { GJS_LIST_COUNTER(boxed), - GJS_LIST_COUNTER(gerror), GJS_LIST_COUNTER(closure), - GJS_LIST_COUNTER(database), GJS_LIST_COUNTER(function), GJS_LIST_COUNTER(fundamental), + GJS_LIST_COUNTER(gerror), GJS_LIST_COUNTER(importer), + GJS_LIST_COUNTER(interface), GJS_LIST_COUNTER(ns), GJS_LIST_COUNTER(object), GJS_LIST_COUNTER(param), GJS_LIST_COUNTER(repo), - GJS_LIST_COUNTER(resultset), - GJS_LIST_COUNTER(weakhash), - GJS_LIST_COUNTER(interface) }; void diff --git a/cjs/mem.h b/cjs/mem.h index eac576db..c85ff54f 100644 --- a/cjs/mem.h +++ b/cjs/mem.h @@ -41,19 +41,16 @@ typedef struct { GJS_DECLARE_COUNTER(everything) GJS_DECLARE_COUNTER(boxed) -GJS_DECLARE_COUNTER(gerror) GJS_DECLARE_COUNTER(closure) -GJS_DECLARE_COUNTER(database) GJS_DECLARE_COUNTER(function) GJS_DECLARE_COUNTER(fundamental) +GJS_DECLARE_COUNTER(gerror) GJS_DECLARE_COUNTER(importer) +GJS_DECLARE_COUNTER(interface) GJS_DECLARE_COUNTER(ns) GJS_DECLARE_COUNTER(object) GJS_DECLARE_COUNTER(param) GJS_DECLARE_COUNTER(repo) -GJS_DECLARE_COUNTER(resultset) -GJS_DECLARE_COUNTER(weakhash) -GJS_DECLARE_COUNTER(interface) #define GJS_INC_COUNTER(name) \ do { \ diff --git a/cjs/module.cpp b/cjs/module.cpp index b476848d..474bba40 100644 --- a/cjs/module.cpp +++ b/cjs/module.cpp @@ -21,9 +21,11 @@ * IN THE SOFTWARE. */ +#include +#include + #include -#include "jsapi-private.h" #include "jsapi-util.h" #include "jsapi-wrapper.h" #include "module.h" @@ -87,20 +89,20 @@ class GjsModule { int line_number) { JS::CompileOptions options(cx); - options.setUTF8(true) - .setFileAndLine(filename, line_number) + options.setFileAndLine(filename, line_number) .setSourceIsLazy(true); - JS::RootedScript compiled_script(cx); - if (!JS::Compile(cx, options, script, script_len, &compiled_script)) - return false; + std::wstring_convert, char16_t> convert; + std::u16string utf16_string = convert.from_bytes(script); + JS::SourceBufferHolder buf(utf16_string.c_str(), utf16_string.size(), + JS::SourceBufferHolder::NoOwnership); JS::AutoObjectVector scope_chain(cx); if (!scope_chain.append(module)) g_error("Unable to append to vector"); JS::RootedValue ignored_retval(cx); - if (!JS_ExecuteScript(cx, scope_chain, compiled_script, &ignored_retval)) + if (!JS::Evaluate(cx, scope_chain, options, buf, &ignored_retval)) return false; gjs_schedule_gc_if_needed(cx); @@ -161,17 +163,14 @@ class GjsModule { * be supported according to ES6. For compatibility with earlier GJS, * we treat it as if it were a real property, but warn about it. */ - GjsAutoJSChar prop_name(cx); - if (!gjs_get_string_id(cx, id, &prop_name)) - return false; - g_warning("Some code accessed the property '%s' on the module '%s'. " "That property was defined with 'let' or 'const' inside the " "module. This was previously supported, but is not correct " "according to the ES6 standard. Any symbols to be exported " "from a module must be defined with 'var'. The property " "access will work as previously for the time being, but " - "please fix your code anyway.", prop_name.get(), m_name); + "please fix your code anyway.", + gjs_debug_id(id).c_str(), m_name); JS::Rooted desc(cx); return JS_GetPropertyDescriptorById(cx, lexical, id, &desc) && diff --git a/cjs/native.cpp b/cjs/native.cpp index 18bf38c5..c7f6dcb9 100644 --- a/cjs/native.cpp +++ b/cjs/native.cpp @@ -75,16 +75,20 @@ gjs_is_registered_native_module(JSContext *context, } /** - * gjs_import_native_module: - * @context: - * @module_obj: + * gjs_load_native_module: + * @context: the #JSContext + * @name: Name under which the module was registered with + * gjs_register_native_module() + * @module_out: Return location for a #JSObject + * + * Loads a builtin native-code module called @name into @module_out. * - * Return a native module that's been preloaded. + * Returns: true on success, false if an exception was thrown. */ bool -gjs_import_native_module(JSContext *context, - const char *name, - JS::MutableHandleObject module_out) +gjs_load_native_module(JSContext *context, + const char *name, + JS::MutableHandleObject module_out) { GjsDefineModuleFunc func; diff --git a/cjs/native.h b/cjs/native.h index 0fd0d1a3..8443d2e3 100644 --- a/cjs/native.h +++ b/cjs/native.h @@ -42,10 +42,10 @@ bool gjs_is_registered_native_module(JSContext *context, JSObject *parent, const char *name); -/* called by importer.c to load a statically linked native module */ -bool gjs_import_native_module (JSContext *context, - const char *name, - JS::MutableHandleObject module_out); +/* called by importer.cpp to load a statically linked native module */ +bool gjs_load_native_module(JSContext *cx, + const char *name, + JS::MutableHandleObject module_out); G_END_DECLS diff --git a/cjs/jsapi-private.h b/cjs/profiler-private.h similarity index 68% rename from cjs/jsapi-private.h rename to cjs/profiler-private.h index 5acedcab..98bb7224 100644 --- a/cjs/jsapi-private.h +++ b/cjs/profiler-private.h @@ -1,6 +1,6 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* - * Copyright (c) 2010 litl, LLC + * Copyright (c) 2018 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 @@ -21,23 +21,21 @@ * IN THE SOFTWARE. */ -/* This file wraps C++ stuff from the spidermonkey private API so we - * can use it from our other C files. This file should be included by - * jsapi-util.c only. "Public" API from this jsapi-private.c should be - * declared in jsapi-util.h - */ - -#ifndef __GJS_JSAPI_PRIVATE_H__ -#define __GJS_JSAPI_PRIVATE_H__ +#ifndef GJS_PROFILER_PRIVATE_H +#define GJS_PROFILER_PRIVATE_H -#include -#include "cjs/jsapi-util.h" +#include "context.h" +#include "profiler.h" G_BEGIN_DECLS -void gjs_schedule_gc_if_needed (JSContext *context); -void gjs_gc_if_needed (JSContext *context); +GjsProfiler *_gjs_profiler_new(GjsContext *context); +void _gjs_profiler_free(GjsProfiler *self); + +bool _gjs_profiler_is_running(GjsProfiler *self); + +void _gjs_profiler_setup_signals(GjsProfiler *self, GjsContext *context); G_END_DECLS -#endif /* __GJS_JSAPI_PRIVATE_H__ */ +#endif /* GJS_PROFILER_PRIVATE_H */ diff --git a/cjs/profiler.cpp b/cjs/profiler.cpp new file mode 100644 index 00000000..1b6817d1 --- /dev/null +++ b/cjs/profiler.cpp @@ -0,0 +1,619 @@ +/* profiler.cpp + * + * Copyright (C) 2016 Christian Hergert + * + * 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 +#include +#include +#include +#include +#include + +#include + +#include "jsapi-wrapper.h" +#include + +#include "context.h" +#include "jsapi-util.h" +#include "profiler-private.h" +#ifdef ENABLE_PROFILER +# include +# include "util/sp-capture-writer.h" +#endif + +/* + * This is mostly non-exciting code wrapping the builtin Profiler in + * mozjs. In particular, the profiler consumer is required to "bring your + * own sampler". We do the very non-surprising thing of using POSIX + * timers to deliver SIGPROF to the thread containing the JSContext. + * + * However, we do use a Linux'ism that allows us to deliver the signal + * to only a single thread. Doing this in a generic fashion would + * require thread-registration so that we can mask SIGPROF from all + * threads execpt the JS thread. The gecko engine uses tgkill() to do + * this with a secondary thread instead of using POSIX timers. We could + * do this too, but it would still be Linux-only. + * + * Another option might be to use pthread_kill() and a secondary thread + * to perform the notification. + * + * From within the signal handler, we process the current stack as + * delivered to us from the JSContext. Any pointer data that comes from + * the runtime has to be copied, so we keep our own dedup'd string + * pointers for JavaScript file/line information. Non-JS instruction + * pointers are just fine, as they can be resolved by parsing the ELF for + * the file mapped on disk containing that address. + * + * As much of this code has to run from signal handlers, it is very + * important that we don't use anything that can malloc() or lock, or + * deadlocks are very likely. Most of GjsProfilerCapture is signal-safe. + */ + +#define SAMPLES_PER_SEC G_GUINT64_CONSTANT(1000) +#define NSEC_PER_SEC G_GUINT64_CONSTANT(1000000000) + +G_DEFINE_POINTER_TYPE(GjsProfiler, gjs_profiler) + +struct _GjsProfiler { +#ifdef ENABLE_PROFILER + /* The stack for the JSContext profiler to use for current stack + * information while executing. We will look into this during our + * SIGPROF handler. + */ + js::ProfileEntry stack[1024]; + + /* The context being profiled */ + JSContext *cx; + + /* Buffers and writes our sampled stacks */ + SpCaptureWriter *capture; +#endif /* ENABLE_PROFILER */ + + /* The filename to write to */ + char *filename; + +#ifdef ENABLE_PROFILER + /* Our POSIX timer to wakeup SIGPROF */ + timer_t timer; + + /* The depth of @stack. This value may be larger than the + * number of elements in stack, and so you MUST ensure you + * don't walk past the end of stack[] when iterating. + */ + uint32_t stack_depth; + + /* Cached copy of our pid */ + GPid pid; + + /* GLib signal handler ID for SIGUSR2 */ + unsigned sigusr2_id; +#endif /* ENABLE_PROFILER */ + + /* If we are currently sampling */ + unsigned running : 1; +}; + +static GjsContext *profiling_context; + +#ifdef ENABLE_PROFILER +/* + * gjs_profiler_extract_maps: + * + * This function will write the mapped section information to the + * capture file so that the callgraph builder can generate symbols + * from the stack addresses provided. + * + * Returns: %TRUE if successful; otherwise %FALSE and the profile + * should abort. + */ +static bool +gjs_profiler_extract_maps(GjsProfiler *self) +{ + using AutoStrv = std::unique_ptr; + + int64_t now = g_get_monotonic_time() * 1000L; + + g_assert(((void) "Profiler must be set up before extracting maps", self)); + + GjsAutoChar path = g_strdup_printf("/proc/%jd/maps", intmax_t(self->pid)); + + char *content_tmp; + size_t len; + if (!g_file_get_contents(path, &content_tmp, &len, nullptr)) + return false; + GjsAutoChar content = content_tmp; + + AutoStrv lines(g_strsplit(content, "\n", 0), g_strfreev); + + for (size_t ix = 0; lines.get()[ix]; ix++) { + char file[256]; + unsigned long start; + unsigned long end; + unsigned long offset; + unsigned long inode; + + file[sizeof file - 1] = '\0'; + + int r = sscanf(lines.get()[ix], "%lx-%lx %*15s %lx %*x:%*x %lu %255s", + &start, &end, &offset, &inode, file); + if (r != 5) + continue; + + if (strcmp("[vdso]", file) == 0) { + offset = 0; + inode = 0; + } + + if (!sp_capture_writer_add_map(self->capture, now, -1, self->pid, start, + end, offset, inode, file)) + return false; + } + + return true; +} +#endif /* ENABLE_PROFILER */ + +/* + * _gjs_profiler_new: + * @context: The #GjsContext to profile + * + * This creates a new profiler for the #JSContext. It is important that + * this instance is freed with _gjs_profiler_free() before the context is + * destroyed. + * + * Call gjs_profiler_start() to enable the profiler, and gjs_profiler_stop() + * when you have finished. + * + * The profiler works by enabling the JS profiler in spidermonkey so that + * sample information is available. A POSIX timer is used to signal SIGPROF + * to the process on a regular interval to collect the most recent profile + * sample and stash it away. It is a programming error to mask SIGPROF from + * the thread controlling the JS context. + * + * If another #GjsContext already has a profiler, or @context already has one, + * then returns %NULL instead. + * + * Returns: (transfer full) (nullable): A newly allocated #GjsProfiler + */ +GjsProfiler * +_gjs_profiler_new(GjsContext *context) +{ + g_return_val_if_fail(context, nullptr); + + if (profiling_context == context) { + g_critical("You can only create one profiler at a time."); + return nullptr; + } + + if (profiling_context) { + g_message("Not going to profile GjsContext %p; you can only profile " + "one context at a time.", context); + return nullptr; + } + + GjsProfiler *self = g_new0(GjsProfiler, 1); + +#ifdef ENABLE_PROFILER + self->cx = static_cast(gjs_context_get_native_context(context)); + self->pid = getpid(); +#endif + + profiling_context = context; + + return self; +} + +/* + * _gjs_profiler_free: + * @self: A #GjsProfiler + * + * Frees a profiler instance and cleans up any allocated data. + * + * If the profiler is running, it will be stopped. This may result in blocking + * to write the contents of the buffer to the underlying file-descriptor. + */ +void +_gjs_profiler_free(GjsProfiler *self) +{ + if (!self) + return; + + if (self->running) + gjs_profiler_stop(self); + + profiling_context = nullptr; + + g_clear_pointer(&self->filename, g_free); +#ifdef ENABLE_PROFILER + g_clear_pointer(&self->capture, sp_capture_writer_unref); +#endif + g_free(self); +} + +/* + * _gjs_profiler_is_running: + * @self: A #GjsProfiler + * + * Checks if the profiler is currently running. This means that the JS + * profiler is enabled and POSIX signal timers are registered. + * + * Returns: %TRUE if the profiler is active. + */ +bool +_gjs_profiler_is_running(GjsProfiler *self) +{ + g_return_val_if_fail(self, false); + + return self->running; +} + +#ifdef ENABLE_PROFILER + +/* Run from a signal handler */ +static inline unsigned +gjs_profiler_get_stack_size(GjsProfiler *self) +{ + g_assert(((void) "Profiler must be set up before getting stack size", self)); + + /* + * Note that stack_depth could be larger than the number of + * items we have in our stack space. We must protect ourselves + * against overflowing by discarding anything after that depth + * of the stack. + */ + return std::min(self->stack_depth, uint32_t(G_N_ELEMENTS(self->stack))); +} + +static void +gjs_profiler_sigprof(int signum, + siginfo_t *info, + void *unused) +{ + GjsProfiler *self = gjs_context_get_profiler(profiling_context); + + g_assert(((void) "SIGPROF handler called with invalid signal info", info)); + g_assert(((void) "SIGPROF handler called with other signal", + info->si_signo == SIGPROF)); + + /* + * NOTE: + * + * This is the SIGPROF signal handler. Everything done in this thread + * needs to be things that are safe to do in a signal handler. One thing + * that is not okay to do, is *malloc*. + */ + + if (!self || info->si_code != SI_TIMER) + return; + + size_t depth = gjs_profiler_get_stack_size(self); + if (depth == 0) + return; + + static_assert(G_N_ELEMENTS(self->stack) < G_MAXUSHORT, + "Number of elements in profiler stack should be expressible" + "in an unsigned short"); + + int64_t now = g_get_monotonic_time() * 1000L; + + /* NOTE: cppcheck warns that alloca() is not recommended since it can + * easily overflow the stack; however, dynamic allocation is not an option + * here since we are in a signal handler. + * Another option would be to always allocate G_N_ELEMENTS(self->stack), + * but that is by definition at least as large of an allocation and + * therefore is more likely to overflow. + */ + // cppcheck-suppress allocaCalled + SpCaptureAddress *addrs = static_cast(alloca(sizeof *addrs * depth)); + + for (size_t ix = 0; ix < depth; ix++) { + js::ProfileEntry& entry = self->stack[ix]; + const char *label = entry.label(); + size_t flipped = depth - 1 - ix; + + /* + * SPSProfiler will put "js::RunScript" on the stack, but it has + * a stack address of "this", which is not terribly useful since + * everything will show up as [stack] when building callgraphs. + */ + if (label) + addrs[flipped] = sp_capture_writer_add_jitmap(self->capture, label); + else + addrs[flipped] = SpCaptureAddress(entry.stackAddress()); + } + + if (!sp_capture_writer_add_sample(self->capture, now, -1, self->pid, addrs, depth)) + gjs_profiler_stop(self); +} + +#endif /* ENABLE_PROFILER */ + +/** + * gjs_profiler_start: + * @self: A #GjsProfiler + * + * As expected, this starts the GjsProfiler. + * + * This will enable the underlying JS profiler and register a POSIX timer to + * deliver SIGPROF on the configured sampling frequency. + * + * To reduce sampling overhead, #GjsProfiler stashes information about the + * profile to be calculated once the profiler has been disabled. Calling + * gjs_profiler_stop() will result in that delayed work to be completed. + * + * You should call gjs_profiler_stop() when the profiler is no longer needed. + */ +void +gjs_profiler_start(GjsProfiler *self) +{ + g_return_if_fail(self); + if (self->running) + return; + +#ifdef ENABLE_PROFILER + + g_return_if_fail(!self->capture); + + struct sigaction sa = { 0 }; + struct sigevent sev = { 0 }; + struct itimerspec its = { 0 }; + struct itimerspec old_its; + + GjsAutoChar path = g_strdup(self->filename); + if (!path) + path = g_strdup_printf("gjs-%jd.syscap", intmax_t(self->pid)); + + self->capture = sp_capture_writer_new(path, 0); + + if (!self->capture) { + g_warning("Failed to open profile capture"); + return; + } + + if (!gjs_profiler_extract_maps(self)) { + g_warning("Failed to extract proc maps"); + g_clear_pointer(&self->capture, sp_capture_writer_unref); + return; + } + + self->stack_depth = 0; + + /* Setup our signal handler for SIGPROF delivery */ + sa.sa_flags = SA_RESTART | SA_SIGINFO; + sa.sa_sigaction = gjs_profiler_sigprof; + sigemptyset(&sa.sa_mask); + + if (sigaction(SIGPROF, &sa, nullptr) == -1) { + g_warning("Failed to register sigaction handler: %s", g_strerror(errno)); + g_clear_pointer(&self->capture, sp_capture_writer_unref); + return; + } + + /* + * Create our SIGPROF timer + * + * We want to receive a SIGPROF signal on the JS thread using our + * configured sampling frequency. Instead of allowing any thread to be + * notified, we set the _tid value to ensure that only our thread gets + * delivery of the signal. This feature is generally just for + * threading implementations, but it works for us as well and ensures + * that the thread is blocked while we capture the stack. + */ + sev.sigev_notify = SIGEV_THREAD_ID; + sev.sigev_signo = SIGPROF; + sev._sigev_un._tid = syscall(__NR_gettid); + + if (timer_create(CLOCK_MONOTONIC, &sev, &self->timer) == -1) { + g_warning("Failed to create profiler timer: %s", g_strerror(errno)); + g_clear_pointer(&self->capture, sp_capture_writer_unref); + return; + } + + /* Calculate sampling interval */ + its.it_interval.tv_sec = 0; + its.it_interval.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC; + its.it_value.tv_sec = 0; + its.it_value.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC; + + /* Now start this timer */ + if (timer_settime(self->timer, 0, &its, &old_its) != 0) { + g_warning("Failed to enable profiler timer: %s", g_strerror(errno)); + timer_delete(self->timer); + g_clear_pointer(&self->capture, sp_capture_writer_unref); + return; + } + + self->running = true; + + /* Notify the JS runtime of where to put stack info */ + js::SetContextProfilingStack(self->cx, self->stack, &self->stack_depth, + G_N_ELEMENTS(self->stack)); + + /* Start recording stack info */ + js::EnableContextProfilingStack(self->cx, true); + + g_message("Profiler started"); + +#else /* !ENABLE_PROFILER */ + + self->running = true; + g_message("Profiler is disabled. Recompile with --enable-profiler to use."); + +#endif /* ENABLE_PROFILER */ +} + +/** + * gjs_profiler_stop: + * @self: A #GjsProfiler + * + * Stops a currently running #GjsProfiler. If the profiler is not running, + * this function will do nothing. + * + * Some work may be delayed until the end of the capture. Such delayed work + * includes flushing the resulting samples and file location information to + * disk. + * + * This may block while writing to disk. Generally, the writes are delivered + * to a tmpfs device, and are therefore negligible. + */ +void +gjs_profiler_stop(GjsProfiler *self) +{ + /* Note: can be called from a signal handler */ + + g_assert(self); + + if (!self->running) + return; + +#ifdef ENABLE_PROFILER + + struct itimerspec its = { 0 }; + timer_settime(self->timer, 0, &its, nullptr); + timer_delete(self->timer); + + js::EnableContextProfilingStack(self->cx, false); + js::SetContextProfilingStack(self->cx, nullptr, nullptr, 0); + + sp_capture_writer_flush(self->capture); + + g_clear_pointer(&self->capture, sp_capture_writer_unref); + + self->stack_depth = 0; + g_message("Profiler stopped"); + +#endif /* ENABLE_PROFILER */ + + self->running = false; +} + +#ifdef ENABLE_PROFILER + +static gboolean +gjs_profiler_sigusr2(void *data) +{ + auto context = static_cast(data); + GjsProfiler *current_profiler = gjs_context_get_profiler(context); + + if (current_profiler) { + if (_gjs_profiler_is_running(current_profiler)) + gjs_profiler_stop(current_profiler); + else + gjs_profiler_start(current_profiler); + } + + return G_SOURCE_CONTINUE; +} + +#endif /* ENABLE_PROFILER */ + +/* + * _gjs_profiler_setup_signals: + * @context: a #GjsContext with a profiler attached + * + * If you want to simply allow profiling of your process with minimal + * fuss, simply call gjs_profiler_setup_signals(). This will allow + * enabling and disabling the profiler with SIGUSR2. You must call + * this from main() immediately when your program starts and must not + * block SIGUSR2 from your signal mask. + * + * If this is not sufficient, use gjs_profiler_chain_signal() from your + * own signal handler to pass the signal to a GjsProfiler. + */ +void +_gjs_profiler_setup_signals(GjsProfiler *self, + GjsContext *context) +{ + g_return_if_fail(context == profiling_context); + +#ifdef ENABLE_PROFILER + + if (self->sigusr2_id != 0) + return; + + self->sigusr2_id = g_unix_signal_add(SIGUSR2, gjs_profiler_sigusr2, context); + +#else /* !ENABLE_PROFILER */ + + g_message("Profiler is disabled. Not setting up signals."); + +#endif /* ENABLE_PROFILER */ +} + +/** + * gjs_profiler_chain_signal: + * @context: a #GjsContext with a profiler attached + * @info: #siginfo_t passed in to signal handler + * + * Use this to pass a signal info caught by another signal handler to a + * GjsProfiler. This might be needed if you have your own complex signal + * handling system for which GjsProfiler cannot simply add a SIGUSR2 handler. + * + * This function should only be called from the JS thread. + * + * Returns: %TRUE if the signal was handled. + */ +bool +gjs_profiler_chain_signal(GjsContext *context, + siginfo_t *info) +{ +#ifdef ENABLE_PROFILER + + if (info) { + if (info->si_signo == SIGPROF) { + gjs_profiler_sigprof(SIGPROF, info, nullptr); + return true; + } + + if (info->si_signo == SIGUSR2) { + gjs_profiler_sigusr2(context); + return true; + } + } + +#endif /* ENABLE_PROFILER */ + + return false; +} + +/** + * gjs_profiler_set_filename: + * @self: A #GjsProfiler + * @filename: string containing a filename + * + * Set the file to which profiling data is written when the @self is stopped. + * By default, this is `gjs-$PID.syscap` in the current directory. + */ +void +gjs_profiler_set_filename(GjsProfiler *self, + const char *filename) +{ + g_return_if_fail(self); + g_return_if_fail(!self->running); + + g_free(self->filename); + self->filename = g_strdup(filename); +} diff --git a/util/hash-x32.h b/cjs/profiler.h similarity index 62% rename from util/hash-x32.h rename to cjs/profiler.h index 6ab672d8..498487ec 100644 --- a/util/hash-x32.h +++ b/cjs/profiler.h @@ -1,6 +1,6 @@ -/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright (c) 2013 Red Hat, Inc. +/* profiler.h + * + * Copyright (C) 2016 Christian Hergert * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -20,23 +20,33 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ -#ifndef __GJS_UTIL_HASH_X32_H__ -#define __GJS_UTIL_HASH_X32_H__ + +#ifndef GJS_PROFILER_H +#define GJS_PROFILER_H #include +#include + G_BEGIN_DECLS -/* Hash table that operates on gsize; on every architecture except x32, - * sizeof(gsize) == sizeof(gpointer), and so we can just use it as a - * hash key directly. But on x32, we have to fall back to malloc(). - */ +#define GJS_TYPE_PROFILER (gjs_profiler_get_type()) + +typedef struct _GjsProfiler GjsProfiler; + +GJS_EXPORT +GType gjs_profiler_get_type(void); + +GJS_EXPORT +void gjs_profiler_set_filename(GjsProfiler *self, + const char *filename); + +GJS_EXPORT +void gjs_profiler_start(GjsProfiler *self); -GHashTable *gjs_hash_table_new_for_gsize (GDestroyNotify value_destroy); -void gjs_hash_table_for_gsize_insert (GHashTable *table, gsize key, gpointer value); -void gjs_hash_table_for_gsize_remove (GHashTable *table, gsize key); -gpointer gjs_hash_table_for_gsize_lookup (GHashTable *table, gsize key); +GJS_EXPORT +void gjs_profiler_stop(GjsProfiler *self); G_END_DECLS -#endif +#endif /* GJS_PROFILER_H */ diff --git a/configure.ac b/configure.ac index 55db6768..bdbe099a 100644 --- a/configure.ac +++ b/configure.ac @@ -31,6 +31,7 @@ PKG_PROG_PKG_CONFIG PKG_INSTALLDIR AC_LANG([C++]) +AC_USE_SYSTEM_EXTENSIONS AC_PROG_CXX AX_CXX_COMPILE_STDCXX_11 @@ -107,6 +108,18 @@ AS_IF([test x$have_gtk = xyes], [ ], [AS_IF([test "x$with_gtk" = "xyes"], [AC_MSG_ERROR([GTK requested but not found])])]) +# Some Linux APIs required for profiler +AC_ARG_ENABLE([profiler], + [AS_HELP_STRING([--disable-profiler], [Don't build profiler])]) +AS_IF([test x$enable_profiler != xno], [ + gl_TIMER_TIME + AS_IF([test x$ac_cv_func_timer_settime = xno], + [AC_MSG_ERROR([The profiler is currently only supported on Linux. +Configure with --disable-profiler to skip it on other platforms.])]) + AC_DEFINE([ENABLE_PROFILER], [1], [Define if the profiler should be built.]) +]) +AM_CONDITIONAL([ENABLE_PROFILER], [test x$enable_profiler != xno]) + PKG_CHECK_VAR([GI_DATADIR], [gobject-introspection-1.0], [gidatadir]) AC_SUBST([CJS_PACKAGE_REQUIRES]) @@ -309,6 +322,7 @@ AC_MSG_RESULT([ readline: ${ac_cv_header_readline_readline_h} dtrace: ${enable_dtrace:-no} systemtap: ${enable_systemtap:-no} + Profiler: ${enable_profiler:-yes} Run tests under: ${TEST_MSG} Code coverage: ${enable_code_coverage} ]) diff --git a/examples/gtk-application.js b/examples/gtk-application.js new file mode 100644 index 00000000..65084c11 --- /dev/null +++ b/examples/gtk-application.js @@ -0,0 +1,131 @@ +// See the note about Application.run() at the bottom of the script +const System = imports.system; + +// Include this in case both GTK3 and GTK4 installed, otherwise an exception +// will be thrown +imports.gi.versions.Gtk = "3.0"; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +// An example GtkApplication with a few bells and whistles, see also: +// https://wiki.gnome.org/HowDoI/GtkApplication +var ExampleApplication = GObject.registerClass({ + Properties: { + "exampleprop": GObject.ParamSpec.string( + "exampleprop", // property name + "ExampleProperty", // nickname + "An example read write property", // description + GObject.ParamFlags.READWRITE, // read/write/construct... + "" // implement defaults manually + ) + }, + Signals: { "examplesig": { param_types: [ GObject.TYPE_INT ] } }, +}, class ExampleApplication extends Gtk.Application { + _init() { + super._init({ + application_id: "org.gnome.gjs.ExampleApplication", + flags: Gio.ApplicationFlags.FLAGS_NONE + }); + } + + // Example property getter/setter + get exampleprop() { + if (typeof this._exampleprop === "undefined") { + return "a default value"; + } + + return this._exampleprop; + } + + set exampleprop(value) { + this._exampleprop = value; + + // notify() has to be called, if you want it + this.notify("exampleprop"); + } + + // Example signal emission + emit_examplesig(number) { + this.emit("examplesig", number); + } + + vfunc_startup() { + super.vfunc_startup(); + + // An example GAction, see: https://wiki.gnome.org/HowDoI/GAction + let exampleAction = new Gio.SimpleAction({ + name: "exampleAction", + parameter_type: new GLib.VariantType("s") + }); + + exampleAction.connect("activate", (action, param) => { + param = param.deep_unpack().toString(); + + if (param === "exampleParameter") { + log("Yes!"); + } + }); + + this.add_action(exampleAction); + } + + vfunc_activate() { + super.vfunc_activate(); + + this.hold(); + + // Example ApplicationWindow + let window = new Gtk.ApplicationWindow({ + application: this, + title: "Example Application Window", + default_width: 300, + default_height: 200 + }); + + let label = new Gtk.Label({ label: this.exampleprop }); + window.add(label); + + window.connect("delete-event", () => { + this.quit(); + }); + + window.show_all(); + + // Example GNotification, see: https://developer.gnome.org/GNotification/ + let notif = new Gio.Notification(); + notif.set_title("Example Notification"); + notif.set_body("Example Body"); + notif.set_icon( + new Gio.ThemedIcon({ name: "dialog-information-symbolic" }) + ); + + // A default action for when the body of the notification is clicked + notif.set_default_action("app.exampleAction('exampleParameter')"); + + // A button for the notification + notif.add_button( + "Button Text", + "app.exampleAction('exampleParameter')" + ); + + // This won't actually be shown, since an application needs a .desktop + // file with a base name matching the application id + this.send_notification("example-notification", notif); + + // Withdraw + this.withdraw_notification("example-notification"); + } +}); + +// The proper way to run a Gtk.Application or Gio.Application is take ARGV and +// prepend the program name to it, and pass that to run() +let app = new ExampleApplication(); +app.run([System.programInvocationName].concat(ARGV)); + +// Or a one-liner... +// (new ExampleApplication()).run([System.programInvocationName].concat(ARGV)); + diff --git a/examples/gtk.js b/examples/gtk.js index 2357c401..ec3a29df 100644 --- a/examples/gtk.js +++ b/examples/gtk.js @@ -1,73 +1,75 @@ -imports.gi.versions.Gtk = '3.0'; +// Include this in case both GTK3 and GTK4 installed, otherwise an exception +// will be thrown +imports.gi.versions.Gtk = "3.0"; const Gtk = imports.gi.Gtk; -// This is a callback function. The data arguments are ignored -// in this example. More on callbacks below. -function hello(widget) { - log("Hello World"); -} +// Initialize Gtk before you start calling anything from the import +Gtk.init(null); +// Construct a top-level window +let window = new Gtk.Window ({ + type: Gtk.WindowType.TOPLEVEL, + title: "A default title", + default_width: 300, + default_height: 250, + // A decent example of how constants are mapped: + // 'Gtk' and 'WindowPosition' from the enum name GtkWindowPosition, + // 'CENTER' from the enum's constant GTK_WIN_POS_CENTER + window_position: Gtk.WindowPosition.CENTER +}); + +// Object properties can also be set or changed after construction, unless they +// are marked construct-only. +window.title = "Hello World!"; + +// This is a callback function function onDeleteEvent(widget, event) { - // If you return false in the "delete_event" signal handler, - // GTK will emit the "destroy" signal. Returning true means - // you don't want the window to be destroyed. - // This is useful for popping up 'are you sure you want to quit?' - // type dialogs. - log("delete event occurred"); - - // Change false to true and the main window will not be destroyed - // with a "delete_event". + log("delete-event emitted"); + // If you return false in the "delete_event" signal handler, Gtk will emit + // the "destroy" signal. + // + // Returning true gives you a chance to pop up 'are you sure you want to + // quit?' type dialogs. return false; } -function onDestroy(widget) { - log("destroy signal occurred"); +// When the window is given the "delete_event" signal (this is given by the +// window manager, usually by the "close" option, or on the titlebar), we ask +// it to call the onDeleteEvent() function as defined above. +window.connect("delete-event", onDeleteEvent); + +// GJS will warn when calling a C function with unexpected arguments... +// +// window.connect("destroy", Gtk.main_quit); +// +// ...so use arrow functions for inline callbacks with arguments to adjust +window.connect("destroy", () => { Gtk.main_quit(); -} - -Gtk.init(null); - -// create a new window -let win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL }); - -// When the window is given the "delete_event" signal (this is given -// by the window manager, usually by the "close" option, or on the -// titlebar), we ask it to call the onDeleteEvent () function -// as defined above. -win.connect("delete-event", onDeleteEvent); - -// Here we connect the "destroy" event to a signal handler. -// This event occurs when we call gtk_widget_destroy() on the window, -// or if we return false in the "onDeleteEvent" callback. -win.connect("destroy", onDestroy); - -// Sets the border width of the window. -win.set_border_width(10); - -// Creates a new button with the label "Hello World". -let button = new Gtk.Button({ label: "Hello World" }); - -// When the button receives the "clicked" signal, it will call the -// function hello(). The hello() function is defined above. -button.connect("clicked", hello); - -// This will cause the window to be destroyed by calling -// gtk_widget_destroy(window) when "clicked". Again, the destroy -// signal could come from here, or the window manager. -button.connect("clicked", function() { - win.destroy(); - }); - -// This packs the button into the window (a GTK container). -win.add(button); - -// The final step is to display this newly created widget. -button.show(); - -// and the window -win.show(); - -// All gtk applications must have a Gtk.main(). Control ends here -// and waits for an event to occur (like a key press or mouse event). +}); + +// Create a button to close the window +let button = new Gtk.Button({ + label: "Close the Window", + // Set visible to 'true' if you don't want to call button.show() later + visible: true, + // Another example of constant mapping: + // 'Gtk' and 'Align' are taken from the GtkAlign enum, + // 'CENTER' from the constant GTK_ALIGN_CENTER + valign: Gtk.Align.CENTER, + halign: Gtk.Align.CENTER +}); + +// Connect to the 'clicked' signal, using another way to call an arrow function +button.connect("clicked", () => window.destroy()); + +// Add the button to the window +window.add(button); + +// Show the window +window.show(); + +// All gtk applications must have a Gtk.main(). Control will end here and wait +// for an event to occur (like a key press or mouse event). The main loop will +// run until Gtk.main_quit is called. Gtk.main(); diff --git a/gi/arg.cpp b/gi/arg.cpp index 27c7036e..5da7ca15 100644 --- a/gi/arg.cpp +++ b/gi/arg.cpp @@ -428,13 +428,14 @@ value_to_ghashtable_key(JSContext *cx, } case GI_TYPE_TAG_UTF8: { - GjsAutoJSChar cstr(cx); - JS::RootedValue str_val(cx, value); - if (!str_val.isString()) { - JS::RootedString str(cx, JS::ToString(cx, str_val)); - str_val.setString(str); - } - if (!gjs_string_to_utf8(cx, str_val, &cstr)) + JS::RootedString str(cx); + if (!value.isString()) + str = JS::ToString(cx, value); + else + str = value.toString(); + + GjsAutoJSChar cstr = JS_EncodeStringToUTF8(cx, str); + if (!cstr) return false; *pointer_out = cstr.copy(); break; @@ -592,7 +593,7 @@ gjs_array_from_strv(JSContext *context, if (!elems.growBy(1)) g_error("Unable to grow vector"); - if (!gjs_string_from_utf8(context, strv[i], -1, elems[i])) + if (!gjs_string_from_utf8(context, strv[i], elems[i])) return false; } @@ -619,8 +620,6 @@ gjs_array_to_strv(JSContext *context, result = g_new0(char *, length+1); for (i = 0; i < length; ++i) { - GjsAutoJSChar tmp_result(context); - elem = JS::UndefinedValue(); if (!JS_GetElement(context, array, i, &elem)) { g_free(result); @@ -630,12 +629,7 @@ gjs_array_to_strv(JSContext *context, return false; } - if (!elem.isString()) { - gjs_throw(context, - "Invalid element in string array"); - g_strfreev(result); - return false; - } + GjsAutoJSChar tmp_result; if (!gjs_string_to_utf8(context, elem, &tmp_result)) { g_strfreev(result); return false; @@ -649,11 +643,11 @@ gjs_array_to_strv(JSContext *context, } static bool -gjs_string_to_intarray(JSContext *context, - JS::Value string_val, - GITypeInfo *param_info, - void **arr_p, - gsize *length) +gjs_string_to_intarray(JSContext *context, + JS::HandleString str, + GITypeInfo *param_info, + void **arr_p, + size_t *length) { GITypeTag element_type; char16_t *result16; @@ -661,9 +655,8 @@ gjs_string_to_intarray(JSContext *context, element_type = g_type_info_get_tag(param_info); if (element_type == GI_TYPE_TAG_INT8 || element_type == GI_TYPE_TAG_UINT8) { - GjsAutoJSChar result(context); - - if (!gjs_string_to_utf8(context, string_val, &result)) + GjsAutoJSChar result = JS_EncodeStringToUTF8(context, str); + if (!result) return false; *length = strlen(result); *arr_p = result.copy(); @@ -671,8 +664,7 @@ gjs_string_to_intarray(JSContext *context, } if (element_type == GI_TYPE_TAG_INT16 || element_type == GI_TYPE_TAG_UINT16) { - if (!gjs_string_get_char16_data(context, string_val, - &result16, length)) + if (!gjs_string_get_char16_data(context, str, &result16, length)) return false; *arr_p = result16; return true; @@ -680,8 +672,7 @@ gjs_string_to_intarray(JSContext *context, if (element_type == GI_TYPE_TAG_UNICHAR) { gunichar *result_ucs4; - JS::RootedValue root(context, string_val); - if (!gjs_string_to_ucs4(context, root, &result_ucs4, length)) + if (!gjs_string_to_ucs4(context, str, &result_ucs4, length)) return false; *arr_p = result_ucs4; return true; @@ -1253,7 +1244,7 @@ throw_invalid_argument(JSContext *context, gjs_throw(context, "Expected type %s for %s but got type '%s'", type_tag_to_human_string(arginfo), - display_name, gjs_get_type_name(value)); + display_name, JS::InformalValueTypeName(value)); g_free(display_name); } @@ -1286,8 +1277,8 @@ gjs_array_to_explicit_array_internal(JSContext *context, *length_p = 0; } else if (value.isString()) { /* Allow strings as int8/uint8/int16/uint16 arrays */ - if (!gjs_string_to_intarray(context, value, param_info, - contents, length_p)) + JS::RootedString str(context, value.toString()); + if (!gjs_string_to_intarray(context, str, param_info, contents, length_p)) goto out; } else { JS::RootedObject array_obj(context, &value.toObject()); @@ -1323,6 +1314,38 @@ gjs_array_to_explicit_array_internal(JSContext *context, return ret; } +static bool +is_gdk_atom(GIBaseInfo *info) +{ + return (strcmp("Atom", g_base_info_get_name(info)) == 0 && + strcmp("Gdk", g_base_info_get_namespace(info)) == 0); +} + +static void +intern_gdk_atom(const char *name, + GArgument *ret) +{ + GIRepository *repo = g_irepository_get_default(); + GIFunctionInfo *atom_intern_fun = + g_irepository_find_by_name(repo, "Gdk", "atom_intern"); + + GIArgument atom_intern_args[2]; + + /* Can only store char * in GIArgument. First argument to gdk_atom_intern + * is const char *, string isn't modified. */ + atom_intern_args[0].v_string = const_cast(name); + + atom_intern_args[1].v_boolean = false; + + g_function_info_invoke(atom_intern_fun, + atom_intern_args, 2, + nullptr, 0, + ret, + nullptr); + + g_base_info_unref(atom_intern_fun); +} + bool gjs_value_to_g_argument(JSContext *context, JS::HandleValue value, @@ -1493,8 +1516,9 @@ gjs_value_to_g_argument(JSContext *context, if (value.isNull()) { arg->v_pointer = NULL; } else if (value.isString()) { - GjsAutoJSChar utf8_str(context); - if (gjs_string_to_utf8(context, value, &utf8_str)) + JS::RootedString str(context, value.toString()); + GjsAutoJSChar utf8_str = JS_EncodeStringToUTF8(context, str); + if (utf8_str) arg->v_pointer = utf8_str.copy(); else wrong = true; @@ -1587,6 +1611,24 @@ gjs_value_to_g_argument(JSContext *context, arg->v_pointer = NULL; wrong = true; } + } else if (is_gdk_atom(interface_info)) { + if (!value.isNull() && !value.isString()) { + wrong = true; + report_type_mismatch = true; + } else if (value.isNull()) { + intern_gdk_atom("NONE", arg); + } else { + JS::RootedString str(context, value.toString()); + GjsAutoJSChar atom_name = JS_EncodeStringToUTF8(context, str); + + if (!atom_name) { + wrong = true; + g_base_info_unref(interface_info); + break; + } + + intern_gdk_atom(atom_name, arg); + } } else if (expect_object != value.isObjectOrNull()) { wrong = true; report_type_mismatch = true; @@ -1754,7 +1796,7 @@ gjs_value_to_g_argument(JSContext *context, if (arg->v_pointer == NULL) { gjs_debug(GJS_DEBUG_GFUNCTION, "conversion of JSObject %p type %s to type %s failed", - &value.toObject(), gjs_get_type_name(value), + &value.toObject(), JS::InformalValueTypeName(value), g_base_info_get_name ((GIBaseInfo *)interface_info)); /* gjs_throw should have been called already */ @@ -1794,7 +1836,7 @@ gjs_value_to_g_argument(JSContext *context, } else { gjs_debug(GJS_DEBUG_GFUNCTION, "JSObject type '%s' is neither null nor an object", - gjs_get_type_name(value)); + JS::InformalValueTypeName(value)); wrong = true; report_type_mismatch = true; } @@ -2569,8 +2611,8 @@ gjs_object_from_g_hash (JSContext *context, if (!keystr) return false; - GjsAutoJSChar keyutf8(context); - if (!gjs_string_to_utf8(context, JS::StringValue(keystr), &keyutf8)) + GjsAutoJSChar keyutf8 = JS_EncodeStringToUTF8(context, keystr); + if (!keyutf8) return false; if (!gjs_value_from_g_argument(context, &valjs, @@ -2662,11 +2704,18 @@ gjs_value_from_g_argument (JSContext *context, break; case GI_TYPE_TAG_GTYPE: - { - JSObject *obj; - obj = gjs_gtype_create_gtype_wrapper(context, arg->v_ssize); - value_p.setObjectOrNull(obj); - } + { + GType gtype = arg->v_ssize; + if (gtype == 0) + return true; /* value_p is set to JS null */ + + JS::RootedObject obj(context, gjs_gtype_create_gtype_wrapper(context, gtype)); + if (!obj) + return false; + + value_p.setObject(*obj); + return true; + } break; case GI_TYPE_TAG_UNICHAR: @@ -2676,7 +2725,8 @@ gjs_value_from_g_argument (JSContext *context, /* Preserve the bidirectional mapping between 0 and "" */ if (arg->v_uint32 == 0) { - return gjs_string_from_utf8 (context, "", 0, value_p); + value_p.set(JS_GetEmptyStringValue(context)); + return true; } else if (!g_unichar_validate (arg->v_uint32)) { gjs_throw(context, "Invalid unicode codepoint %" G_GUINT32_FORMAT, @@ -2684,7 +2734,7 @@ gjs_value_from_g_argument (JSContext *context, return false; } else { bytes = g_unichar_to_utf8 (arg->v_uint32, utf8); - return gjs_string_from_utf8 (context, (char*)utf8, bytes, value_p); + return gjs_string_from_utf8_n(context, utf8, bytes, value_p); } } @@ -2698,9 +2748,9 @@ gjs_value_from_g_argument (JSContext *context, return true; } case GI_TYPE_TAG_UTF8: - if (arg->v_pointer) - return gjs_string_from_utf8(context, (const char *) arg->v_pointer, -1, value_p); - else { + if (arg->v_pointer) { + return gjs_string_from_utf8(context, reinterpret_cast(arg->v_pointer), value_p); + } else { /* For NULL we'll return JS::NullValue(), which is already set * in *value_p */ @@ -2820,6 +2870,32 @@ gjs_value_from_g_argument (JSContext *context, } if (interface_type == GI_INFO_TYPE_STRUCT || interface_type == GI_INFO_TYPE_BOXED) { + if (is_gdk_atom(interface_info)) { + GIFunctionInfo *atom_name_fun = g_struct_info_find_method(interface_info, "name"); + GIArgument atom_name_ret; + + g_function_info_invoke(atom_name_fun, + arg, 1, + nullptr, 0, + &atom_name_ret, + nullptr); + + g_base_info_unref(atom_name_fun); + g_base_info_unref(interface_info); + + if (strcmp("NONE", atom_name_ret.v_string) == 0) { + g_free(atom_name_ret.v_string); + value = JS::NullValue(); + + return true; + } + + bool atom_name_ok = gjs_string_from_utf8(context, atom_name_ret.v_string, value_p); + g_free(atom_name_ret.v_string); + + return atom_name_ok; + } + JSObject *obj; GjsBoxedCreationFlags flags; diff --git a/gi/boxed.cpp b/gi/boxed.cpp index 8053ec1b..6d14d42e 100644 --- a/gi/boxed.cpp +++ b/gi/boxed.cpp @@ -110,78 +110,71 @@ gjs_define_static_methods(JSContext *context, return true; } -/* - * The *objp out parameter, on success, should be null to indicate that id - * was not resolved; and non-null, referring to obj or one of its prototypes, - * if id was resolved. - */ +/* The *resolved out parameter, on success, should be false to indicate that id + * was not resolved; and true if id was resolved. */ static bool boxed_resolve(JSContext *context, JS::HandleObject obj, JS::HandleId id, bool *resolved) { - Boxed *priv; - GjsAutoJSChar name(context); - - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; - } - - priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GBOXED, "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + Boxed *priv = priv_from_js(context, obj); + gjs_debug_jsprop(GJS_DEBUG_GBOXED, "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), + priv); if (priv == nullptr) return false; /* wrong class */ - if (priv->gboxed == NULL) { - /* We are the prototype, so look for methods and other class properties */ - GIFunctionInfo *method_info; - - method_info = g_struct_info_find_method((GIStructInfo*) priv->info, - name); + if (priv->gboxed) { + /* We are an instance, not a prototype, so look for + * per-instance props that we want to define on the + * JSObject. Generally we do not want to cache these in JS, we + * want to always pull them from the C object, or JS would not + * see any changes made from C. So we use the get/set prop + * hooks, not this resolve hook. + */ + *resolved = false; + return true; + } - if (method_info != NULL) { - const char *method_name; + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; + } + /* We are the prototype, so look for methods and other class properties */ + GIFunctionInfo *method_info = g_struct_info_find_method(priv->info, name); + if (!method_info) { + *resolved = false; + return true; + } #if GJS_VERBOSE_ENABLE_GI_USAGE - _gjs_log_info_usage((GIBaseInfo*) method_info); + _gjs_log_info_usage(method_info); #endif - if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) { - method_name = g_base_info_get_name( (GIBaseInfo*) method_info); - - gjs_debug(GJS_DEBUG_GBOXED, - "Defining method %s in prototype for %s.%s", - method_name, - g_base_info_get_namespace( (GIBaseInfo*) priv->info), - g_base_info_get_name( (GIBaseInfo*) priv->info)); - - /* obj is the Boxed prototype */ - if (gjs_define_function(context, obj, priv->gtype, - (GICallableInfo *)method_info) == NULL) { - g_base_info_unref( (GIBaseInfo*) method_info); - return false; - } - *resolved = true; - } else { - *resolved = false; - } + if (g_function_info_get_flags(method_info) & GI_FUNCTION_IS_METHOD) { + const char *method_name = g_base_info_get_name(method_info); + gjs_debug(GJS_DEBUG_GBOXED, + "Defining method %s in prototype for %s.%s", + method_name, + g_base_info_get_namespace( (GIBaseInfo*) priv->info), + g_base_info_get_name( (GIBaseInfo*) priv->info)); + + /* obj is the Boxed prototype */ + if (!gjs_define_function(context, obj, priv->gtype, method_info)) { g_base_info_unref( (GIBaseInfo*) method_info); + return false; } + + *resolved = true; } else { - /* We are an instance, not a prototype, so look for - * per-instance props that we want to define on the - * JSObject. Generally we do not want to cache these in JS, we - * want to always pull them from the C object, or JS would not - * see any changes made from C. So we use the get/set prop - * hooks, not this resolve hook. - */ *resolved = false; } + + g_base_info_unref(method_info); return true; } @@ -275,10 +268,9 @@ boxed_init_from_props(JSContext *context, priv->field_map = get_field_map(priv->info); JS::RootedValue value(context); - JS::RootedId prop_id(context); for (ix = 0, length = ids.length(); ix < length; ix++) { GIFieldInfo *field_info; - GjsAutoJSChar name(context); + GjsAutoJSChar name; if (!gjs_get_string_id(context, ids[ix], &name)) return false; @@ -292,9 +284,9 @@ boxed_init_from_props(JSContext *context, /* ids[ix] is reachable because props is rooted, but require_property * doesn't know that */ - prop_id = ids[ix]; if (!gjs_object_require_property(context, props, "property list", - prop_id, &value)) + JS::HandleId::fromMarkedLocation(ids[ix].address()), + &value)) return false; if (!boxed_set_field_from_value(context, priv, field_info, value)) @@ -373,15 +365,14 @@ boxed_new(JSContext *context, } else if (priv->can_allocate_directly) { boxed_new_direct(priv); } else if (priv->default_constructor >= 0) { - bool retval; - - /* for simplicity, we simply delegate all the work to the actual JS constructor - function (which we retrieve from the JS constructor, that is, Namespace.BoxedType, - or object.constructor, given that object was created with the right prototype */ - JS::RootedId default_constructor_name(context, priv->default_constructor_name); - retval = boxed_invoke_constructor(context, obj, - default_constructor_name, args); - return retval; + /* for simplicity, we simply delegate all the work to the actual JS + * constructor function (which we retrieve from the JS constructor, + * that is, Namespace.BoxedType, or object.constructor, given that + * object was created with the right prototype. The ID is traced from + * the object, so it's OK to create a handle from it. */ + return boxed_invoke_constructor(context, obj, + JS::HandleId::fromMarkedLocation(priv->default_constructor_name.address()), + args); } else { gjs_throw(context, "Unable to construct struct type %s since it has no default constructor and cannot be allocated directly", g_base_info_get_name((GIBaseInfo*) priv->info)); @@ -834,23 +825,17 @@ define_boxed_class_fields(JSContext *cx, * error message. If we omitted fields or defined them read-only * we'd: * - * - Storing a new property for a non-accessible field + * - Store a new property for a non-accessible field * - Silently do nothing when writing a read-only field * * Which is pretty confusing if the only reason a field isn't * writable is language binding or memory-management restrictions. * * We just go ahead and define the fields immediately for the - * class; doing it lazily in boxed_new_resolve() would be possible + * class; doing it lazily in boxed_resolve() would be possible * as well if doing it ahead of time caused to much start-up * memory overhead. */ - if (n_fields > 256) { - g_warning("Only defining the first 256 fields in boxed type '%s'", - g_base_info_get_name ((GIBaseInfo *)priv->info)); - n_fields = 256; - } - for (i = 0; i < n_fields; i++) { GIFieldInfo *field = g_struct_info_get_field (priv->info, i); const char *field_name = g_base_info_get_name ((GIBaseInfo *)field); @@ -1284,7 +1269,7 @@ gjs_typecheck_boxed(JSContext *context, if (priv->gboxed == NULL) { if (throw_error) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is %s.%s.prototype, not an object instance - cannot convert to a boxed instance", g_base_info_get_namespace( (GIBaseInfo*) priv->info), g_base_info_get_name( (GIBaseInfo*) priv->info)); @@ -1302,14 +1287,14 @@ gjs_typecheck_boxed(JSContext *context, if (!result && throw_error) { if (expected_info != NULL) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s.%s", g_base_info_get_namespace((GIBaseInfo*) priv->info), g_base_info_get_name((GIBaseInfo*) priv->info), g_base_info_get_namespace((GIBaseInfo*) expected_info), g_base_info_get_name((GIBaseInfo*) expected_info)); } else { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s", g_base_info_get_namespace((GIBaseInfo*) priv->info), g_base_info_get_name((GIBaseInfo*) priv->info), diff --git a/gi/closure.cpp b/gi/closure.cpp index 1df01938..cf2996a3 100644 --- a/gi/closure.cpp +++ b/gi/closure.cpp @@ -185,10 +185,12 @@ closure_finalize(gpointer data, self->~Closure(); } -void +bool gjs_closure_invoke(GClosure *closure, + JS::HandleObject this_obj, const JS::HandleValueArray& args, - JS::MutableHandleValue retval) + JS::MutableHandleValue retval, + bool return_exception) { Closure *c; JSContext *context; @@ -198,11 +200,11 @@ gjs_closure_invoke(GClosure *closure, if (c->obj == nullptr) { /* We were destroyed; become a no-op */ c->context = NULL; - return; + return false; } context = c->context; - JS_BeginRequest(context); + JSAutoRequest ar(context); JSAutoCompartment ac(context, c->obj); if (JS_IsExceptionPending(context)) { @@ -212,18 +214,26 @@ gjs_closure_invoke(GClosure *closure, } JS::RootedValue v_closure(context, JS::ObjectValue(*c->obj)); - if (!gjs_call_function_value(context, - /* "this" object; null is some kind of default presumably */ - nullptr, - v_closure, args, retval)) { + if (!gjs_call_function_value(context, this_obj, v_closure, args, retval)) { /* Exception thrown... */ gjs_debug_closure("Closure invocation failed (exception should " "have been thrown) closure %p callable %p", closure, c->obj.get()); - if (!gjs_log_exception(context)) + /* If an exception has been thrown, log it, unless the caller + * explicitly wants to handle it manually (for example to turn it + * into a GError), in which case it replaces the return value + * (which is not valid anyway) */ + if (JS_IsExceptionPending(context)) { + if (return_exception) + JS_GetPendingException(context, retval); + else + gjs_log_exception(context); + } else { + retval.setUndefined(); gjs_debug_closure("Closure invocation failed but no exception was set?" "closure %p", closure); - goto out; + } + return false; } if (gjs_log_exception(context)) { @@ -232,9 +242,7 @@ gjs_closure_invoke(GClosure *closure, } JS_MaybeGC(context); - - out: - JS_EndRequest(context); + return true; } bool diff --git a/gi/closure.h b/gi/closure.h index 60202c69..c2fbea6a 100644 --- a/gi/closure.h +++ b/gi/closure.h @@ -36,9 +36,11 @@ GClosure* gjs_closure_new (JSContext *context, const char *description, bool root_function); -void gjs_closure_invoke(GClosure *closure, +bool gjs_closure_invoke(GClosure *closure, + JS::HandleObject this_obj, const JS::HandleValueArray& args, - JS::MutableHandleValue retval); + JS::MutableHandleValue retval, + bool return_exception); JSContext* gjs_closure_get_context (GClosure *closure); bool gjs_closure_is_valid (GClosure *closure); diff --git a/gi/foreign.cpp b/gi/foreign.cpp index 449ff4d1..3d2f2c59 100644 --- a/gi/foreign.cpp +++ b/gi/foreign.cpp @@ -61,8 +61,6 @@ gjs_foreign_load_foreign_module(JSContext *context, int i; for (i = 0; foreign_modules[i].gi_namespace; ++i) { - int code; - GError *error = NULL; char *script; if (strcmp(gi_namespace, foreign_modules[i].gi_namespace) != 0) @@ -74,12 +72,11 @@ gjs_foreign_load_foreign_module(JSContext *context, // FIXME: Find a way to check if a module is imported // and only execute this statement if isn't script = g_strdup_printf("imports.%s;", gi_namespace); - if (!gjs_context_eval((GjsContext*) JS_GetContextPrivate(context), script, strlen(script), - "", &code, - &error)) { - g_printerr("ERROR: %s\n", error->message); + JS::RootedValue retval(context); + if (!gjs_eval_with_scope(context, nullptr, script, strlen(script), + "", &retval)) { + g_critical("ERROR importing foreign module %s\n", gi_namespace); g_free(script); - g_error_free(error); return false; } g_free(script); diff --git a/gi/function.cpp b/gi/function.cpp index 0d0d7520..d0584a11 100644 --- a/gi/function.cpp +++ b/gi/function.cpp @@ -35,7 +35,6 @@ #include "param.h" #include "cjs/context-private.h" #include "cjs/jsapi-class.h" -#include "cjs/jsapi-private.h" #include "cjs/jsapi-wrapper.h" #include "cjs/mem.h" @@ -84,10 +83,10 @@ gjs_callback_trampoline_unref(GjsCallbackTrampoline *trampoline) trampoline->ref_count--; if (trampoline->ref_count == 0) { + g_closure_unref(trampoline->js_function); g_callable_info_free_closure(trampoline->info, trampoline->closure); g_base_info_unref( (GIBaseInfo*) trampoline->info); g_free (trampoline->param_types); - trampoline->~GjsCallbackTrampoline(); g_slice_free(GjsCallbackTrampoline, trampoline); } } @@ -159,6 +158,23 @@ set_return_ffi_arg_from_giargument (GITypeInfo *ret_type, } } +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" : ""); + } + gjs_dumpstack(); + 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, @@ -169,47 +185,65 @@ set_return_ffi_arg_from_giargument (GITypeInfo *ret_type, static void gjs_callback_closure(ffi_cif *cif, void *result, - void **args, + void **ffi_args, void *data) { JSContext *context; - JSObject *func_obj; GjsCallbackTrampoline *trampoline; - int i, n_args, n_jsargs, n_outargs; + int i, n_args, n_jsargs, n_outargs, c_args_offset = 0; GITypeInfo ret_type; bool success = false; bool ret_type_is_void; + auto args = reinterpret_cast(ffi_args); trampoline = (GjsCallbackTrampoline *) data; g_assert(trampoline); gjs_callback_trampoline_ref(trampoline); - context = trampoline->context; + 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_callback_trampoline_unref(trampoline); + return; + } + + context = gjs_closure_get_context(trampoline->js_function); if (G_UNLIKELY(_gjs_context_is_sweeping(context))) { - g_critical("Attempting to call back into JSAPI during the sweeping phase of GC. " - "This is most likely caused by not destroying a Clutter actor or Gtk+ " - "widget with ::destroy signals connected, but can also be caused by " - "using the destroy(), dispose(), or remove() vfuncs. " - "Because it would crash the application, it has been " - "blocked and the JS callback not invoked."); - if (trampoline->info) { - const char *name = g_base_info_get_name(static_cast(trampoline->info)); - g_critical("The offending callback was %s()%s.", name, - trampoline->is_vfunc ? ", a vfunc" : ""); - } - gjs_dumpstack(); + 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_callback_trampoline_unref(trampoline); + return; + } + + auto gjs_cx = static_cast(JS_GetContextPrivate(context)); + if (G_UNLIKELY (!_gjs_context_get_is_owner_thread(gjs_cx))) { + 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; } JS_BeginRequest(context); - func_obj = &trampoline->js_function.get().toObject(); - JSAutoCompartment ac(context, func_obj); + JSAutoCompartment ac(context, + 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) { + auto this_gobject = static_cast(args[0]->v_pointer); + this_object = gjs_object_from_g_object(context, this_gobject); + + /* "this" is not included in the GI signature, but is in the C (and + * FFI) signature */ + c_args_offset = 1; + } + n_outargs = 0; JS::AutoValueVector jsargs(context); @@ -217,8 +251,6 @@ gjs_callback_closure(ffi_cif *cif, g_error("Unable to reserve space for vector"); JS::RootedValue rval(context); - JS::RootedValue rooted_function(context, trampoline->js_function); - JS::RootedObject this_object(context); for (i = 0, n_jsargs = 0; i < n_args; i++) { GIArgInfo arg_info; @@ -253,16 +285,18 @@ gjs_callback_closure(ffi_cif *cif, 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, - (GArgument *) args[array_length_pos], true)) + 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, (GArgument*) args[i], length.toInt32())) + &type_info, + args[i + c_args_offset], + length.toInt32())) goto out; break; } @@ -272,7 +306,8 @@ gjs_callback_closure(ffi_cif *cif, if (!gjs_value_from_g_argument(context, jsargs[n_jsargs++], &type_info, - (GArgument *) args[i], false)) + args[i + c_args_offset], + false)) goto out; break; case PARAM_CALLBACK: @@ -281,22 +316,11 @@ gjs_callback_closure(ffi_cif *cif, default: g_assert_not_reached(); } - - if (trampoline->is_vfunc && i == 0) { - g_assert(n_jsargs > 0); - this_object = jsargs[0].toObjectOrNull(); - jsargs.popBack(); - n_jsargs--; - } } - if (!JS_CallFunctionValue(context, - this_object, - rooted_function, - jsargs, - &rval)) { + if (!gjs_closure_invoke(trampoline->js_function, this_object, jsargs, &rval, + true)) goto out; - } g_callable_info_load_return_type(trampoline->info, &ret_type); ret_type_is_void = g_type_info_get_tag (&ret_type) == GI_TYPE_TAG_VOID; @@ -341,7 +365,7 @@ gjs_callback_closure(ffi_cif *cif, GJS_ARGUMENT_ARGUMENT, GI_TRANSFER_NOTHING, true, - *(GArgument **)args[i])) + *(GIArgument **)args[i + c_args_offset])) goto out; break; @@ -394,7 +418,7 @@ gjs_callback_closure(ffi_cif *cif, GJS_ARGUMENT_ARGUMENT, GI_TRANSFER_NOTHING, true, - *(GArgument **)args[i])) + *(GIArgument **)args[i + c_args_offset])) goto out; elem_idx++; @@ -416,14 +440,32 @@ gjs_callback_closure(ffi_cif *cif, exit(code); /* Some other uncatchable exception, e.g. out of memory */ - exit(1); + g_error("Function %s terminated with uncatchable exception", + g_base_info_get_name(trampoline->info)); } - gjs_log_exception (context); - /* Fill in the result with some hopefully neutral value */ g_callable_info_load_return_type(trampoline->info, &ret_type); gjs_g_argument_init_default (context, &ret_type, (GArgument *) result); + + /* 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 = static_cast(error_argument->v_pointer); + g_propagate_error(gerror, local_error); + JS_ClearPendingException(context); /* don't log */ + } + } else if (!rval.isUndefined()) { + JS_SetPendingException(context, rval); + } + gjs_log_exception(context); } if (trampoline->scope == GI_SCOPE_TYPE_ASYNC) { @@ -449,11 +491,12 @@ gjs_destroy_notify_callback(gpointer data) } GjsCallbackTrampoline* -gjs_callback_trampoline_new(JSContext *context, - JS::HandleValue function, - GICallableInfo *callable_info, - GIScopeType scope, - bool is_vfunc) +gjs_callback_trampoline_new(JSContext *context, + JS::HandleValue function, + GICallableInfo *callable_info, + GIScopeType scope, + JS::HandleObject scope_object, + bool is_vfunc) { GjsCallbackTrampoline *trampoline; int n_args, i; @@ -467,13 +510,21 @@ gjs_callback_trampoline_new(JSContext *context, trampoline = g_slice_new(GjsCallbackTrampoline); new (trampoline) GjsCallbackTrampoline(); trampoline->ref_count = 1; - trampoline->context = context; trampoline->info = callable_info; g_base_info_ref((GIBaseInfo*)trampoline->info); - if (is_vfunc) - trampoline->js_function = function; - else - trampoline->js_function.root(context, function); + + /* The rule is: + * - async and call callbacks are rooted + * - callbacks in GObjects methods are traced from the object + * (and same for vfuncs, which are associated with a GObject prototype) + */ + bool should_root = scope != GI_SCOPE_TYPE_NOTIFIED || !scope_object; + trampoline->js_function = gjs_closure_new(context, &function.toObject(), + g_base_info_get_name(callable_info), + should_root); + if (!should_root && scope_object) + gjs_object_associate_closure(context, scope_object, + trampoline->js_function); /* Analyze param types and directions, similarly to init_cached_function_data */ n_args = g_callable_info_get_n_args(trampoline->info); @@ -569,13 +620,16 @@ static bool gjs_fill_method_instance(JSContext *context, JS::HandleObject obj, Function *function, - GIArgument *out_arg) + GIArgument *out_arg, + bool& is_gobject) { GIBaseInfo *container = g_base_info_get_container((GIBaseInfo *) function->info); GIInfoType type = g_base_info_get_type(container); GType gtype = g_registered_type_info_get_g_type ((GIRegisteredTypeInfo *)container); GITransfer transfer = g_callable_info_get_instance_ownership_transfer (function->info); + is_gobject = false; + if (type == GI_INFO_TYPE_STRUCT || type == GI_INFO_TYPE_BOXED) { /* GError must be special cased */ if (g_type_is_a(gtype, G_TYPE_ERROR)) { @@ -638,6 +692,7 @@ gjs_fill_method_instance(JSContext *context, if (!gjs_typecheck_object(context, obj, gtype, true)) return false; out_arg->v_pointer = gjs_g_object_from_object(context, obj); + is_gobject = true; if (transfer == GI_TRANSFER_EVERYTHING) g_object_ref (out_arg->v_pointer); } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { @@ -651,6 +706,7 @@ gjs_fill_method_instance(JSContext *context, if (!gjs_typecheck_object(context, obj, gtype, true)) return false; out_arg->v_pointer = gjs_g_object_from_object(context, obj); + is_gobject = true; if (transfer == GI_TRANSFER_EVERYTHING) g_object_ref (out_arg->v_pointer); } else { @@ -667,7 +723,7 @@ gjs_fill_method_instance(JSContext *context, if (transfer == GI_TRANSFER_EVERYTHING) gjs_fundamental_ref (context, out_arg->v_pointer); } else { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "%s.%s is not an object instance neither a fundamental instance of a supported type", g_base_info_get_namespace(container), g_base_info_get_name(container)); @@ -697,6 +753,19 @@ format_function_name(Function *function, g_base_info_get_name(baseinfo)); } +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; + } +} + /* * This function can be called in 2 different ways. You can either use * it to create javascript objects by providing a @js_rval argument or @@ -740,24 +809,17 @@ gjs_invoke_c_function(JSContext *context, bool failed, postinvoke_release_failed; bool is_method; + bool is_object_method = false; GITypeInfo return_info; GITypeTag return_tag; JS::AutoValueVector return_values(context); guint8 next_rval = 0; /* index into return_values */ - GSList *iter; /* 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. */ - if (completed_trampolines) { - for (iter = completed_trampolines; iter; iter = iter->next) { - GjsCallbackTrampoline *trampoline = (GjsCallbackTrampoline *) iter->data; - gjs_callback_trampoline_unref(trampoline); - } - g_slist_free(completed_trampolines); - completed_trampolines = NULL; - } + complete_async_calls(); is_method = g_callable_info_is_method(function->info); can_throw_gerror = g_callable_info_can_throw_gerror(function->info); @@ -772,7 +834,7 @@ gjs_invoke_c_function(JSContext *context, * arguments we expect the JS function to take (which does not * include PARAM_SKIPPED args). * - * @js_argc is the number of arguments that were actually passed. + * @args.length() is the number of arguments that were actually passed. */ if (args.length() > function->expected_js_argc) { GjsAutoChar name = format_function_name(function, is_method); @@ -800,8 +862,8 @@ gjs_invoke_c_function(JSContext *context, js_arg_pos = 0; /* index into argv */ if (is_method) { - if (!gjs_fill_method_instance(context, obj, - function, &in_arg_cvalues[0])) + if (!gjs_fill_method_instance(context, obj, function, + &in_arg_cvalues[0], is_object_method)) return false; ffi_arg_pointers[0] = &in_arg_cvalues[0]; ++c_arg_pos; @@ -890,7 +952,7 @@ gjs_invoke_c_function(JSContext *context, g_base_info_get_namespace( (GIBaseInfo*) function->info), g_base_info_get_name( (GIBaseInfo*) function->info), g_base_info_get_name( (GIBaseInfo*) &arg_info), - gjs_get_type_name(current_arg)); + JS::InformalValueTypeName(current_arg)); failed = true; break; } @@ -900,6 +962,7 @@ gjs_invoke_c_function(JSContext *context, current_arg, callable_info, scope, + is_object_method ? obj : nullptr, false); closure = trampoline->closure; g_base_info_unref(callable_info); @@ -1479,7 +1542,7 @@ function_to_string (JSContext *context, g_free(arg_names); out: - if (gjs_string_from_utf8(context, string, -1, rec.rval())) + if (gjs_string_from_utf8(context, string, rec.rval())) ret = true; if (free) diff --git a/gi/function.h b/gi/function.h index 6c1fc486..b1138b80 100644 --- a/gi/function.h +++ b/gi/function.h @@ -44,10 +44,9 @@ typedef enum { struct GjsCallbackTrampoline { gint ref_count; - JSContext *context; GICallableInfo *info; - GjsMaybeOwned js_function; + GClosure *js_function; ffi_cif cif; ffi_closure *closure; @@ -56,11 +55,12 @@ struct GjsCallbackTrampoline { GjsParamType *param_types; }; -GjsCallbackTrampoline* gjs_callback_trampoline_new(JSContext *context, - JS::HandleValue function, - GICallableInfo *callable_info, - GIScopeType scope, - bool is_vfunc); +GjsCallbackTrampoline* gjs_callback_trampoline_new(JSContext *context, + JS::HandleValue function, + GICallableInfo *callable_info, + GIScopeType scope, + JS::HandleObject scope_object, + bool is_vfunc); void gjs_callback_trampoline_unref(GjsCallbackTrampoline *trampoline); void gjs_callback_trampoline_ref(GjsCallbackTrampoline *trampoline); diff --git a/gi/fundamental.cpp b/gi/fundamental.cpp index 3a939056..0410a636 100644 --- a/gi/fundamental.cpp +++ b/gi/fundamental.cpp @@ -301,17 +301,11 @@ fundamental_instance_resolve(JSContext *context, bool *resolved) { FundamentalInstance *priv; - GjsAutoJSChar name(context); - - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; /* not resolved, but no error */ - } priv = priv_from_js(context, obj); gjs_debug_jsprop(GJS_DEBUG_GFUNDAMENTAL, - "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (priv == nullptr) return false; /* wrong class */ @@ -328,6 +322,12 @@ fundamental_instance_resolve(JSContext *context, return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; /* not resolved, but no error */ + } + /* We are the prototype, so look for methods and other class properties */ Fundamental *proto_priv = (Fundamental *) priv; GIFunctionInfo *method_info; @@ -870,13 +870,13 @@ gjs_typecheck_fundamental(JSContext *context, if (!result && throw_error) { if (priv->prototype->info) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s", g_base_info_get_namespace((GIBaseInfo *) priv->prototype->info), g_base_info_get_name((GIBaseInfo *) priv->prototype->info), g_type_name(expected_gtype)); } else { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s - cannot convert to %s", g_type_name(priv->prototype->gtype), g_type_name(expected_gtype)); diff --git a/gi/gerror.cpp b/gi/gerror.cpp index cd2f3650..db94367d 100644 --- a/gi/gerror.cpp +++ b/gi/gerror.cpp @@ -32,6 +32,7 @@ #include "cjs/mem.h" #include "repo.h" #include "gerror.h" +#include "util/error.h" #include @@ -96,7 +97,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(error) priv->domain = proto_priv->domain; JS::RootedObject params_obj(context, &argv[0].toObject()); - GjsAutoJSChar message(context); + GjsAutoJSChar message; if (!gjs_object_require_property(context, params_obj, "GError constructor", GJS_STRING_MESSAGE, &message)) @@ -170,7 +171,7 @@ error_get_message(JSContext *context, return false; } - return gjs_string_from_utf8(context, priv->gerror->message, -1, args.rval()); + return gjs_string_from_utf8(context, priv->gerror->message, args.rval()); } static bool @@ -199,40 +200,26 @@ error_to_string(JSContext *context, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, rec, self, Error, priv); - gchar *descr; - bool retval; if (priv == NULL) return false; - rec.rval().setUndefined(); - retval = false; - /* We follow the same pattern as standard JS errors, at the expense of hiding some useful information */ + GjsAutoChar descr; if (priv->gerror == NULL) { descr = g_strdup_printf("%s.%s", g_base_info_get_namespace(priv->info), g_base_info_get_name(priv->info)); - - if (!gjs_string_from_utf8(context, descr, -1, rec.rval())) - goto out; } else { descr = g_strdup_printf("%s.%s: %s", g_base_info_get_namespace(priv->info), g_base_info_get_name(priv->info), priv->gerror->message); - - if (!gjs_string_from_utf8(context, descr, -1, rec.rval())) - goto out; } - retval = true; - - out: - g_free(descr); - return retval; + return gjs_string_from_utf8(context, descr, rec.rval()); } static bool @@ -428,6 +415,48 @@ define_error_properties(JSContext *cx, exc.restore(); } +static JSProtoKey +proto_key_from_error_enum(int val) +{ + switch (val) { + case GJS_JS_ERROR_EVAL_ERROR: + return JSProto_EvalError; + case GJS_JS_ERROR_INTERNAL_ERROR: + return JSProto_InternalError; + case GJS_JS_ERROR_RANGE_ERROR: + return JSProto_RangeError; + case GJS_JS_ERROR_REFERENCE_ERROR: + return JSProto_ReferenceError; + case GJS_JS_ERROR_STOP_ITERATION: + return JSProto_StopIteration; + case GJS_JS_ERROR_SYNTAX_ERROR: + return JSProto_SyntaxError; + case GJS_JS_ERROR_TYPE_ERROR: + return JSProto_TypeError; + case GJS_JS_ERROR_URI_ERROR: + return JSProto_URIError; + case GJS_JS_ERROR_ERROR: + default: + return JSProto_Error; + } +} + +static JSObject * +gjs_error_from_js_gerror(JSContext *cx, + GError *gerror) +{ + JS::AutoValueArray<1> error_args(cx); + if (!gjs_string_from_utf8(cx, gerror->message, error_args[0])) + return nullptr; + + JSProtoKey error_kind = proto_key_from_error_enum(gerror->code); + JS::RootedObject error_constructor(cx); + if (!JS_GetClassObject(cx, error_kind, &error_constructor)) + return nullptr; + + return JS_New(cx, error_constructor, error_args); +} + JSObject* gjs_error_from_gerror(JSContext *context, GError *gerror, @@ -440,6 +469,9 @@ gjs_error_from_gerror(JSContext *context, if (gerror == NULL) return NULL; + if (gerror->domain == GJS_JS_ERROR) + return gjs_error_from_js_gerror(context, gerror); + info = find_error_domain_info(gerror->domain); if (!info) { @@ -520,3 +552,45 @@ gjs_typecheck_gerror (JSContext *context, return do_base_typecheck(context, obj, throw_error); } + +GError * +gjs_gerror_make_from_error(JSContext *cx, + JS::HandleObject obj) +{ + using AutoEnumClass = std::unique_ptr; + + if (gjs_typecheck_gerror(cx, obj, false)) { + /* This is already a GError, just copy it */ + GError *inner = gjs_gerror_from_error(cx, obj); + return g_error_copy(inner); + } + + /* Try to make something useful from the error + name and message (in case this is a JS error) */ + JS::RootedValue v_name(cx); + if (!gjs_object_get_property(cx, obj, GJS_STRING_NAME, &v_name)) + return nullptr; + + GjsAutoJSChar name; + if (!gjs_string_to_utf8(cx, v_name, &name)) + return nullptr; + + JS::RootedValue v_message(cx); + if (!gjs_object_get_property(cx, obj, GJS_STRING_MESSAGE, &v_message)) + return nullptr; + + GjsAutoJSChar message; + if (!gjs_string_to_utf8(cx, v_message, &message)) + return nullptr; + + AutoEnumClass klass(static_cast(g_type_class_ref(GJS_TYPE_JS_ERROR)), + g_type_class_unref); + const GEnumValue *value = g_enum_get_value_by_name(klass.get(), name); + int code; + if (value) + code = value->value; + else + code = GJS_JS_ERROR_ERROR; + + return g_error_new_literal(GJS_JS_ERROR, code, message); +} diff --git a/gi/gerror.h b/gi/gerror.h index be47d249..15f56cf9 100644 --- a/gi/gerror.h +++ b/gi/gerror.h @@ -44,6 +44,9 @@ bool gjs_typecheck_gerror (JSContext *context, JS::HandleObject obj, bool throw_error); +GError *gjs_gerror_make_from_error(JSContext *cx, + JS::HandleObject obj); + G_END_DECLS #endif /* __GJS_ERROR_H__ */ diff --git a/gi/gtype.cpp b/gi/gtype.cpp index d762a09e..b0498157 100644 --- a/gi/gtype.cpp +++ b/gi/gtype.cpp @@ -100,25 +100,24 @@ gjs_gtype_finalize(JSFreeOp *fop, } static bool -to_string_func(JSContext *context, +to_string_func(JSContext *cx, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, void, priv); - GType gtype; - gchar *strval; - bool ret; + GJS_GET_PRIV(cx, argc, vp, rec, obj, void, priv); + GType gtype = GPOINTER_TO_SIZE(priv); - gtype = GPOINTER_TO_SIZE(priv); + if (gtype == 0) { + JS::RootedString str(cx, JS_NewStringCopyZ(cx, "[object GType prototype]")); + if (!str) + return false; + rec.rval().setString(str); + return true; + } - if (gtype == 0) - strval = g_strdup("[object GType prototype]"); - else - strval = g_strdup_printf("[object GType for '%s']", - g_type_name(gtype)); - ret = gjs_string_from_utf8(context, strval, -1, rec.rval()); - g_free(strval); - return ret; + GjsAutoChar strval = g_strdup_printf("[object GType for '%s']", + g_type_name(gtype)); + return gjs_string_from_utf8(cx, strval, rec.rval()); } static bool @@ -135,7 +134,7 @@ get_name_func (JSContext *context, rec.rval().setNull(); return true; } - return gjs_string_from_utf8(context, g_type_name(gtype), -1, rec.rval()); + return gjs_string_from_utf8(context, g_type_name(gtype), rec.rval()); } /* Properties */ @@ -156,6 +155,9 @@ JSObject * gjs_gtype_create_gtype_wrapper (JSContext *context, GType gtype) { + g_assert(((void) "Attempted to create wrapper object for invalid GType", + gtype != 0)); + JSAutoRequest ar(context); auto heap_wrapper = diff --git a/gi/interface.cpp b/gi/interface.cpp index 62769e8b..4633724f 100644 --- a/gi/interface.cpp +++ b/gi/interface.cpp @@ -113,14 +113,8 @@ interface_resolve(JSContext *context, bool *resolved) { Interface *priv; - GjsAutoJSChar name(context); GIFunctionInfo *method_info; - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; - } - priv = priv_from_js(context, obj); if (priv == nullptr) @@ -134,6 +128,12 @@ interface_resolve(JSContext *context, return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; + } + method_info = g_interface_info_find_method((GIInterfaceInfo*) priv->info, name); if (method_info != NULL) { diff --git a/gi/ns.cpp b/gi/ns.cpp index a6a910b7..2e5b3ce5 100644 --- a/gi/ns.cpp +++ b/gi/ns.cpp @@ -43,11 +43,8 @@ extern struct JSClass gjs_ns_class; GJS_DEFINE_PRIV_FROM_JS(Ns, gjs_ns_class) -/* - * The *objp out parameter, on success, should be null to indicate that id - * was not resolved; and non-null, referring to obj or one of its prototypes, - * if id was resolved. - */ +/* The *resolved out parameter, on success, should be false to indicate that id + * was not resolved; and true if id was resolved. */ static bool ns_resolve(JSContext *context, JS::HandleObject obj, @@ -55,33 +52,39 @@ ns_resolve(JSContext *context, bool *resolved) { Ns *priv; - GjsAutoJSChar name(context); GIRepository *repo; GIBaseInfo *info; bool defined; - if (!gjs_get_string_id(context, id, &name)) { + if (!JSID_IS_STRING(id)) { *resolved = false; return true; /* not resolved, but no error */ } /* let Object.prototype resolve these */ - if (strcmp(name, "valueOf") == 0 || - strcmp(name, "toString") == 0) { + JSFlatString *str = JSID_TO_FLAT_STRING(id); + if (JS_FlatStringEqualsAscii(str, "valueOf") || + JS_FlatStringEqualsAscii(str, "toString")) { *resolved = false; return true; } priv = priv_from_js(context, obj); gjs_debug_jsprop(GJS_DEBUG_GNAMESPACE, - "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (priv == NULL) { *resolved = false; /* we are the prototype, or have the wrong class */ return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; /* not resolved, but no error */ + } + repo = g_irepository_get_default(); info = g_irepository_find_by_name(repo, priv->gi_namespace, name); @@ -123,7 +126,7 @@ get_name (JSContext *context, if (priv == NULL) return false; - return gjs_string_from_utf8(context, priv->gi_namespace, -1, args.rval()); + return gjs_string_from_utf8(context, priv->gi_namespace, args.rval()); } GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(ns) diff --git a/gi/object.cpp b/gi/object.cpp index 8a6bd3f6..aa96791a 100644 --- a/gi/object.cpp +++ b/gi/object.cpp @@ -23,12 +23,12 @@ #include -#include #include #include #include #include #include +#include #include #include "object.h" @@ -52,41 +52,106 @@ #include "cjs/mem.h" #include -#include #include +typedef class GjsListLink GjsListLink; +typedef struct ObjectInstance ObjectInstance; + +static GjsListLink* object_instance_get_link(ObjectInstance *priv); + +class GjsListLink { + private: + ObjectInstance *m_prev; + ObjectInstance *m_next; + + public: + ObjectInstance* prev() { + return m_prev; + } + + ObjectInstance* next() { + return m_next; + } + + void prepend(ObjectInstance *this_instance, + ObjectInstance *head) { + GjsListLink *elem = object_instance_get_link(head); + + g_assert(object_instance_get_link(this_instance) == this); + + if (elem->m_prev) { + GjsListLink *prev = object_instance_get_link(elem->m_prev); + prev->m_next = this_instance; + this->m_prev = elem->m_prev; + } + + elem->m_prev = this_instance; + this->m_next = head; + } + + void unlink() { + if (m_prev) + object_instance_get_link(m_prev)->m_next = m_next; + if (m_next) + object_instance_get_link(m_next)->m_prev = m_prev; + + m_prev = m_next = NULL; + } + + size_t size() { + GjsListLink *elem = this; + size_t count = 0; + + do { + count++; + if (!elem->m_next) + break; + elem = object_instance_get_link(elem->m_next); + } while (elem); + + return count; + } +}; + struct ObjectInstance { GIObjectInfo *info; GObject *gobj; /* NULL if we are the prototype and not an instance */ GjsMaybeOwned keep_alive; GType gtype; - /* a list of all signal connections, used when tracing */ - std::set signals; + /* a list of all GClosures installed on this object (from + * signals, trampolines and explicit GClosures), used when tracing */ + std::set closures; /* the GObjectClass wrapped by this JS Object (only used for prototypes) */ GTypeClass *klass; - /* A list of all vfunc trampolines, used when tracing */ - std::deque vfuncs; + GjsListLink instance_link; unsigned js_object_finalized : 1; + unsigned g_object_finalized : 1; + + /* True if this object has visible JS state, and thus its lifecycle is + * managed using toggle references. False if this object just keeps a + * hard ref on the underlying GObject, and may be finalized at will. */ + bool uses_toggle_ref : 1; }; static std::stack object_init_list; -static GHashTable *class_init_properties; + +using ParamRef = std::unique_ptr; +using ParamRefArray = std::vector; +static std::unordered_map class_init_properties; static bool weak_pointer_callback = false; -static std::set weak_pointer_list; +ObjectInstance *wrapped_gobject_list; extern struct JSClass gjs_object_instance_class; - -static std::set dissociate_list; - GJS_DEFINE_PRIV_FROM_JS(ObjectInstance, gjs_object_instance_class) -static void disassociate_js_gobject (GObject *gobj); +static void disassociate_js_gobject(ObjectInstance *priv); +static void ensure_uses_toggle_ref(JSContext *cx, ObjectInstance *priv); typedef enum { SOME_ERROR_OCCURRED = false, @@ -148,28 +213,13 @@ throw_priv_is_null_error(JSContext *context) " up to the parent _init properly?"); } -static void -dissociate_list_add(ObjectInstance *priv) -{ - bool inserted; - std::tie(std::ignore, inserted) = dissociate_list.insert(priv); - g_assert(inserted); -} - -static void -dissociate_list_remove(ObjectInstance *priv) -{ - size_t erased = dissociate_list.erase(priv); - g_assert(erased > 0); -} - static ObjectInstance * get_object_qdata(GObject *gobj) { auto priv = static_cast(g_object_get_qdata(gobj, gjs_object_priv_quark())); - if (priv && G_UNLIKELY(priv->js_object_finalized)) { + if (priv && priv->uses_toggle_ref && G_UNLIKELY(priv->js_object_finalized)) { g_critical("Object %p (a %s) resurfaced after the JS wrapper was finalized. " "This is some library doing dubious memory management inside dispose()", gobj, g_type_name(G_TYPE_FROM_INSTANCE(gobj))); @@ -388,16 +438,10 @@ object_instance_get_prop(JSContext *context, JS::HandleId id, JS::MutableHandleValue value_p) { - ObjectInstance *priv; - GjsAutoJSChar name(context); - - if (!gjs_get_string_id(context, id, &name)) - return true; /* not resolved, but no error */ - - priv = priv_from_js(context, obj); + ObjectInstance *priv = priv_from_js(context, obj); gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Get prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + "Get prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (priv == nullptr) /* If we reach this point, either object_instance_new_resolve @@ -408,6 +452,20 @@ object_instance_get_prop(JSContext *context, if (priv->gobj == NULL) /* prototype, not an instance. */ return true; + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already finalized. " + "Impossible to get any property from it.", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + return true; + } + + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) + return true; /* not resolved, but no error */ + if (!get_prop_from_g_param(context, obj, priv, name, value_p)) return false; @@ -437,6 +495,9 @@ set_g_param_from_prop(JSContext *context, case SOME_ERROR_OCCURRED: return false; case NO_SUCH_G_PROPERTY: + /* We need to keep the wrapper alive in order not to lose custom + * "expando" properties */ + ensure_uses_toggle_ref(context, priv); return result.succeed(); case VALUE_WAS_SET: default: @@ -499,17 +560,14 @@ object_instance_set_prop(JSContext *context, JS::ObjectOpResult& result) { ObjectInstance *priv; - GjsAutoJSChar name(context); bool ret = true; bool g_param_was_set = false; - if (!gjs_get_string_id(context, id, &name)) - return result.succeed(); /* not resolved, but no error */ - priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Set prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + + gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Set prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), + priv); if (priv == nullptr) /* see the comment in object_instance_get_prop() on this */ @@ -518,6 +576,25 @@ object_instance_set_prop(JSContext *context, if (priv->gobj == NULL) /* prototype, not an instance. */ return result.succeed(); + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already finalized. " + "Impossible to set any property to it.", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + return result.succeed(); + } + + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + /* We need to keep the wrapper alive in order not to lose custom + * "expando" properties. In this case if gjs_get_string_id() is false + * then a number or symbol property was probably set. */ + ensure_uses_toggle_ref(context, priv); + return result.succeed(); /* not resolved, but no error */ + } + ret = set_g_param_from_prop(context, priv, name, g_param_was_set, value_p, result); if (g_param_was_set || !ret) return ret; @@ -705,11 +782,8 @@ is_gobject_field_name(GIObjectInfo *info, return true; } -/* - * The *objp out parameter, on success, should be null to indicate that id - * was not resolved; and non-null, referring to obj or one of its prototypes, - * if id was resolved. - */ +/* The *resolved out parameter, on success, should be false to indicate that id + * was not resolved; and true if id was resolved. */ static bool object_instance_resolve(JSContext *context, JS::HandleObject obj, @@ -717,20 +791,12 @@ object_instance_resolve(JSContext *context, bool *resolved) { GIFunctionInfo *method_info; - ObjectInstance *priv; - GjsAutoJSChar name(context); - - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; /* not resolved, but no error */ - } - - priv = priv_from_js(context, obj); + ObjectInstance *priv = priv_from_js(context, obj); gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Resolve prop '%s' hook obj %p priv %p (%s.%s) gobj %p %s", - name.get(), - obj.get(), + "Resolve prop '%s' hook, obj %s, priv %p (%s.%s), gobj %p %s", + gjs_debug_id(id).c_str(), + gjs_debug_object(obj).c_str(), priv, priv && priv->info ? g_base_info_get_namespace (priv->info) : "", priv && priv->info ? g_base_info_get_name (priv->info) : "", @@ -755,6 +821,23 @@ object_instance_resolve(JSContext *context, return true; } + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already finalized. " + "Impossible to resolve it.", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + *resolved = false; + return true; + } + + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; /* not resolved, but no error */ + } + /* If we have no GIRepository information (we're a JS GObject subclass), * we need to look at exposing interfaces. Look up our interfaces through * GType data, and then hope that *those* are introspectable. */ @@ -899,7 +982,7 @@ object_instance_props_to_g_parameters(JSContext *context, } for (ix = 0, length = ids.length(); ix < length; ix++) { - GjsAutoJSChar name(context); + GjsAutoJSChar name; GParameter gparam = { NULL, { 0, }}; /* ids[ix] is reachable because props is rooted, but require_property @@ -933,32 +1016,59 @@ object_instance_props_to_g_parameters(JSContext *context, return true; } -#define DEBUG_DISPOSE 0 +static GjsListLink * +object_instance_get_link(ObjectInstance *priv) +{ + return &priv->instance_link; +} + static void -wrapped_gobj_dispose_notify(gpointer data, - GObject *where_the_object_was) +object_instance_unlink(ObjectInstance *priv) { - weak_pointer_list.erase(static_cast(data)); -#if DEBUG_DISPOSE - gjs_debug(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed", where_the_object_was); -#endif + if (wrapped_gobject_list == priv) + wrapped_gobject_list = priv->instance_link.next(); + priv->instance_link.unlink(); } static void -gobj_no_longer_kept_alive_func(JS::HandleObject obj, - void *data) +object_instance_link(ObjectInstance *priv) { - ObjectInstance *priv; + if (wrapped_gobject_list) + priv->instance_link.prepend(priv, wrapped_gobject_list); + wrapped_gobject_list = priv; +} - priv = (ObjectInstance *) data; +static void +wrapped_gobj_dispose_notify(gpointer data, + GObject *where_the_object_was) +{ + auto *priv = static_cast(data); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "GObject wrapper %p will no longer be kept alive, eligible for collection", - obj.get()); + priv->g_object_finalized = true; + object_instance_unlink(priv); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed", + where_the_object_was); +} - priv->keep_alive.reset(); - dissociate_list_remove(priv); - weak_pointer_list.erase(priv); +void +gjs_object_context_dispose_notify(void *data, + GObject *where_the_object_was) +{ + ObjectInstance *priv = wrapped_gobject_list; + while (priv) { + ObjectInstance *next = priv->instance_link.next(); + + if (priv->keep_alive.rooted()) { + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "GObject wrapper %p for GObject " + "%p (%s) was rooted but is now unrooted due to " + "GjsContext dispose", priv->keep_alive.get(), + priv->gobj, G_OBJECT_TYPE_NAME(priv->gobj)); + priv->keep_alive.reset(); + object_instance_unlink(priv); + } + + priv = next; + } } static void @@ -966,17 +1076,38 @@ handle_toggle_down(GObject *gobj) { ObjectInstance *priv = get_object_qdata(gobj); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "Toggle notify gobj %p obj %p is_last_ref true", - gobj, priv->keep_alive.get()); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Toggle notify DOWN for GObject " + "%p (%s), JS obj %p", gobj, G_OBJECT_TYPE_NAME(gobj), + priv->keep_alive.get()); /* Change to weak ref so the wrapper-wrappee pair can be * collected by the GC */ if (priv->keep_alive.rooted()) { - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Removing object from keep alive"); + GjsContext *context; + + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Unrooting object"); priv->keep_alive.switch_to_unrooted(); - dissociate_list_remove(priv); + + /* During a GC, the collector asks each object which other + * objects that it wants to hold on to so if there's an entire + * section of the heap graph that's not connected to anything + * else, and not reachable from the root set, then it can be + * trashed all at once. + * + * GObjects, however, don't work like that, there's only a + * reference count but no notion of who owns the reference so, + * a JS object that's proxying a GObject is unconditionally held + * alive as long as the GObject has >1 references. + * + * Since we cannot know how many more wrapped GObjects are going + * be marked for garbage collection after the owner is destroyed, + * always queue a garbage collection when a toggle reference goes + * down. + */ + context = gjs_context_get_current(); + if (!_gjs_context_destroying(context)) + _gjs_context_schedule_gc(context); } } @@ -992,9 +1123,9 @@ handle_toggle_up(GObject *gobj) if (!priv->keep_alive) /* Object already GC'd */ return; - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "Toggle notify gobj %p obj %p is_last_ref false", - gobj, priv->keep_alive.get()); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Toggle notify UP for GObject " + "%p (%s), JS obj %p", gobj, G_OBJECT_TYPE_NAME(gobj), + priv->keep_alive.get()); /* Change to strong ref so the wrappee keeps the wrapper alive * in case the wrapper has data in it that the app cares about @@ -1003,10 +1134,9 @@ handle_toggle_up(GObject *gobj) /* FIXME: thread the context through somehow. Maybe by looking up * the compartment that obj belongs to. */ GjsContext *context = gjs_context_get_current(); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Adding object to keep alive"); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Rooting object"); auto cx = static_cast(gjs_context_get_native_context(context)); - priv->keep_alive.switch_to_rooted(cx, gobj_no_longer_kept_alive_func, priv); - dissociate_list_add(priv); + priv->keep_alive.switch_to_rooted(cx); } } @@ -1115,7 +1245,10 @@ static void release_native_object (ObjectInstance *priv) { priv->keep_alive.reset(); - g_object_remove_toggle_ref(priv->gobj, wrapped_gobj_toggle_notify, NULL); + if (priv->uses_toggle_ref) + g_object_remove_toggle_ref(priv->gobj, wrapped_gobj_toggle_notify, nullptr); + else + g_object_unref(priv->gobj); priv->gobj = NULL; } @@ -1131,19 +1264,32 @@ gjs_object_clear_toggles(void) } void -gjs_object_prepare_shutdown(void) +gjs_object_shutdown_toggle_queue(void) { - /* First, get rid of anything left over on the main context */ - gjs_object_clear_toggles(); + auto& toggle_queue = ToggleQueue::get_default(); + toggle_queue.shutdown(); +} - /* Now, we iterate over all of the objects, breaking the JS <-> C +void +gjs_object_prepare_shutdown(void) +{ + /* We iterate over all of the objects, breaking the JS <-> C * association. We avoid the potential recursion implied in: * toggle ref removal -> gobj dispose -> toggle ref notify - * by simply ignoring toggle ref notifications during this process. - */ - for (auto iter : dissociate_list) - release_native_object(iter); - dissociate_list.clear(); + * by emptying the toggle queue earlier in the shutdown sequence. */ + std::vector to_be_released; + ObjectInstance *link = wrapped_gobject_list; + while (link) { + ObjectInstance *next = link->instance_link.next(); + if (link->keep_alive.rooted()) { + to_be_released.push_back(link); + object_instance_unlink(link); + } + + link = next; + } + for (ObjectInstance *priv : to_be_released) + release_native_object(priv); } static ObjectInstance * @@ -1163,10 +1309,6 @@ init_object_private (JSContext *context, g_assert(priv_from_js(context, object) == NULL); JS_SetPrivate(object, priv); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "obj instance constructor, obj %p priv %p", - object.get(), priv); - proto_priv = proto_priv_from_js(context, object); g_assert(proto_priv != NULL); @@ -1175,6 +1317,10 @@ init_object_private (JSContext *context, if (priv->info) g_base_info_ref( (GIBaseInfo*) priv->info); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Instance constructor of %s, " + "JS obj %p, priv %p", g_type_name(priv->gtype), + object.get(), priv); + JS_EndRequest(context); return priv; } @@ -1184,26 +1330,37 @@ update_heap_wrapper_weak_pointers(JSContext *cx, JSCompartment *compartment, gpointer data) { - std::vector to_be_disassociated; + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Weak pointer update callback, " + "%zu wrapped GObject(s) to examine", + wrapped_gobject_list->instance_link.size()); - for (auto iter = weak_pointer_list.begin(); iter != weak_pointer_list.end(); ) { - ObjectInstance *priv = *iter; - if (priv->keep_alive.rooted() || priv->keep_alive == nullptr || - !priv->keep_alive.update_after_gc()) { - iter++; - } else { + std::vector to_be_disassociated; + ObjectInstance *priv = wrapped_gobject_list; + + while (priv) { + ObjectInstance *next = priv->instance_link.next(); + + if (!priv->keep_alive.rooted() && + priv->keep_alive != nullptr && + priv->keep_alive.update_after_gc()) { /* Ouch, the JS object is dead already. Disassociate the * GObject and hope the GObject dies too. (Remove it from * the weak pointer list first, since the disassociation * may also cause it to be erased.) */ - to_be_disassociated.push_back(priv->gobj); - iter = weak_pointer_list.erase(iter); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Found GObject weak pointer " + "whose JS object %p is about to be finalized: " + "%p (%s)", priv->keep_alive.get(), priv->gobj, + G_OBJECT_TYPE_NAME(priv->gobj)); + to_be_disassociated.push_back(priv); + object_instance_unlink(priv); } + + priv = next; } - for (GObject *gobj : to_be_disassociated) - disassociate_js_gobject(gobj); + for (ObjectInstance *ex_object : to_be_disassociated) + disassociate_js_gobject(ex_object); } static void @@ -1225,16 +1382,28 @@ associate_js_gobject (JSContext *context, ObjectInstance *priv; priv = priv_from_js(context, object); + priv->uses_toggle_ref = false; priv->gobj = gobj; g_assert(!priv->keep_alive.rooted()); set_object_qdata(gobj, priv); + priv->keep_alive = object; ensure_weak_pointer_callback(context); - weak_pointer_list.insert(priv); + object_instance_link(priv); g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, priv); +} + +static void +ensure_uses_toggle_ref(JSContext *cx, + ObjectInstance *priv) +{ + if (priv->uses_toggle_ref) + return; + + g_assert(!priv->keep_alive.rooted()); /* OK, here is where things get complicated. We want the * wrapped gobj to keep the JSObject* wrapper alive, because @@ -1247,56 +1416,57 @@ associate_js_gobject (JSContext *context, * the wrapper to be garbage collected (and thus unref the * wrappee). */ - priv->keep_alive.root(context, object, gobj_no_longer_kept_alive_func, priv); - dissociate_list_add(priv); - g_object_add_toggle_ref(gobj, wrapped_gobj_toggle_notify, NULL); + priv->uses_toggle_ref = true; + priv->keep_alive.switch_to_rooted(cx); + g_object_add_toggle_ref(priv->gobj, wrapped_gobj_toggle_notify, nullptr); + + /* We now have both a ref and a toggle ref, we only want the toggle ref. + * This may immediately remove the GC root we just added, since refcount + * may drop to 1. */ + g_object_unref(priv->gobj); } static void -invalidate_all_signals(ObjectInstance *priv) +invalidate_all_closures(ObjectInstance *priv) { /* Can't loop directly through the items, since invalidating an item's * closure might have the effect of removing the item from the set in the * invalidate notifier */ - while (!priv->signals.empty()) { + while (!priv->closures.empty()) { /* This will also free cd, through the closure invalidation mechanism */ - GClosure *closure = *priv->signals.begin(); + GClosure *closure = *priv->closures.begin(); g_closure_invalidate(closure); /* Erase element if not already erased */ - priv->signals.erase(closure); + priv->closures.erase(closure); } } static void -disassociate_js_gobject(GObject *gobj) +disassociate_js_gobject(ObjectInstance *priv) { - ObjectInstance *priv = get_object_qdata(gobj); bool had_toggle_down, had_toggle_up; - g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv); + if (!priv->g_object_finalized) + g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv); - /* FIXME: this check fails when JS code runs after the main loop ends, - * because the idle functions are not dispatched without a main loop. - * The only situation I'm aware of where this happens is during the - * dbus_unregister stage in GApplication. Ideally this check should be an - * assertion. - * https://bugzilla.gnome.org/show_bug.cgi?id=778862 - */ auto& toggle_queue = ToggleQueue::get_default(); - std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(gobj); + std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(priv->gobj); if (had_toggle_down != had_toggle_up) { - g_critical("JS object wrapper for GObject %p (%s) is being released " - "while toggle references are still pending. This may happen " - "on exit in Gio.Application.vfunc_dbus_unregister(). If you " - "encounter it another situation, please report a GJS bug.", - gobj, G_OBJECT_TYPE_NAME(gobj)); + g_error("JS object wrapper for GObject %p (%s) is being released while " + "toggle references are still pending.", + priv->gobj, G_OBJECT_TYPE_NAME(priv->gobj)); } - invalidate_all_signals(priv); + /* Fist, remove the wrapper pointer from the wrapped GObject */ + set_object_qdata(priv->gobj, nullptr); + + /* Now release all the resources the current wrapper has */ + invalidate_all_closures(priv); release_native_object(priv); /* Mark that a JS object once existed, but it doesn't any more */ priv->js_object_finalized = true; + priv->keep_alive = nullptr; } static void @@ -1358,6 +1528,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS * we're not actually using it, so just let it get collected. Avoiding * this would require a non-trivial amount of work. * */ + ensure_uses_toggle_ref(context, other_priv); object.set(other_priv->keep_alive); g_object_unref(gobj); /* We already own a reference */ gobj = NULL; @@ -1385,15 +1556,9 @@ G_GNUC_END_IGNORE_DEPRECATIONS if (priv->gobj == NULL) associate_js_gobject(context, object, gobj); - /* We now have both a ref and a toggle ref, we only want the - * toggle ref. This may immediately remove the GC root - * we just added, since refcount may drop to 1. - */ - g_object_unref(gobj); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "JSObject created with GObject %p %s", - priv->gobj, g_type_name_from_instance((GTypeInstance*) priv->gobj)); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "JSObject created with GObject %p (%s)", + priv->gobj, G_OBJECT_TYPE_NAME(priv->gobj)); TRACE(GJS_OBJECT_PROXY_NEW(priv, priv->gobj, priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "_gjs_private", @@ -1439,19 +1604,25 @@ object_instance_trace(JSTracer *tracer, if (priv == NULL) return; - for (GClosure *closure : priv->signals) - gjs_closure_trace(closure, tracer); + if (priv->g_object_finalized) { + g_debug("Object %s.%s (%p), has been already finalized. " + "Impossible to trace it.", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + return; + } - for (auto vfunc : priv->vfuncs) - vfunc->js_function.trace(tracer, "ObjectInstance::vfunc"); + for (GClosure *closure : priv->closures) + gjs_closure_trace(closure, tracer); } static void -signal_connection_invalidated(void *data, - GClosure *closure) +closure_invalidated(void *data, + GClosure *closure) { auto priv = static_cast(data); - priv->signals.erase(closure); + priv->closures.erase(closure); } static void @@ -1461,13 +1632,10 @@ object_instance_finalize(JSFreeOp *fop, ObjectInstance *priv; priv = (ObjectInstance *) JS_GetPrivate(obj); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "finalize obj %p priv %p gtype %s gobj %p", obj, priv, - (priv && priv->gobj) ? - g_type_name_from_instance( (GTypeInstance*) priv->gobj) : - "", - priv ? priv->gobj : NULL); g_assert (priv != NULL); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, + "Finalizing %s, JS obj %p, priv %p, GObject %p", + g_type_name(priv->gtype), obj, priv, priv->gobj); TRACE(GJS_OBJECT_PROXY_FINALIZE(priv, priv->gobj, priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "_gjs_private", @@ -1475,7 +1643,7 @@ object_instance_finalize(JSFreeOp *fop, /* This applies only to instances, not prototypes, but it's possible that * an instance's GObject is already freed at this point. */ - invalidate_all_signals(priv); + invalidate_all_closures(priv); /* Object is instance, not prototype, AND GObject is not already freed */ if (priv->gobj) { @@ -1496,16 +1664,12 @@ object_instance_finalize(JSFreeOp *fop, priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "", priv->info ? g_base_info_get_name((GIBaseInfo*) priv->info) : g_type_name(priv->gtype)); } - + + if (!priv->g_object_finalized) + g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv); release_native_object(priv); } - /* We have to leak the trampolines, since the GType's vtable still refers - * to them */ - for (auto iter : priv->vfuncs) - iter->js_function.reset(); - priv->vfuncs.clear(); - if (priv->keep_alive.rooted()) { /* This happens when the refcount on the object is still >1, * for example with global objects GDK never frees like GdkDisplay, @@ -1514,12 +1678,11 @@ object_instance_finalize(JSFreeOp *fop, gjs_debug(GJS_DEBUG_GOBJECT, "Wrapper was finalized despite being kept alive, has refcount >1"); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, - "Removing from keep alive"); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Unrooting object"); priv->keep_alive.reset(); - dissociate_list_remove(priv); } + object_instance_unlink(priv); if (priv->info) { g_base_info_unref( (GIBaseInfo*) priv->info); @@ -1534,6 +1697,9 @@ object_instance_finalize(JSFreeOp *fop, GJS_DEC_COUNTER(object); priv->~ObjectInstance(); g_slice_free(ObjectInstance, priv); + + /* Remove the ObjectInstance pointer from the JSObject */ + JS_SetPrivate(obj, nullptr); } static JSObject * @@ -1564,7 +1730,9 @@ gjs_lookup_object_constructor_from_info(JSContext *context, /* In case we're looking for a private type, and we don't find it, we need to define it first. */ - gjs_define_object_class(context, in_object, NULL, gtype, &constructor); + JS::RootedObject ignored(context); + gjs_define_object_class(context, in_object, NULL, gtype, &constructor, + &ignored); } else { if (G_UNLIKELY (!value.isObject())) return NULL; @@ -1614,6 +1782,16 @@ gjs_lookup_object_prototype(JSContext *context, return proto; } +static void +do_associate_closure(ObjectInstance *priv, + GClosure *closure) +{ + /* This is a weak reference, and will be cleared when the closure is + * invalidated */ + priv->closures.insert(closure); + g_closure_add_invalidate_notifier(closure, priv, closure_invalidated); +} + static bool real_connect_func(JSContext *context, unsigned argc, @@ -1624,7 +1802,6 @@ real_connect_func(JSContext *context, GClosure *closure; gulong id; guint signal_id; - GjsAutoJSChar signal_name(context); GQuark signal_detail; gjs_debug_gsignal("connect obj %p priv %p argc %d", obj.get(), priv, argc); @@ -1639,15 +1816,28 @@ real_connect_func(JSContext *context, priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype)); return false; } + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already deallocated - impossible to connect to signal. " + "This might be caused by the fact that the object has been destroyed from C " + "code using something such as destroy(), dispose(), or remove() vfuncs", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + return true; + } + + ensure_uses_toggle_ref(context, priv); if (argc != 2 || !argv[0].isString() || !JS::IsCallable(&argv[1].toObject())) { gjs_throw(context, "connect() takes two args, the signal name and the callback"); return false; } - if (!gjs_string_to_utf8(context, argv[0], &signal_name)) { + JS::RootedString signal_str(context, argv[0].toString()); + GjsAutoJSChar signal_name = JS_EncodeStringToUTF8(context, signal_str); + if (!signal_name) return false; - } if (!g_signal_parse_name(signal_name, G_OBJECT_TYPE(priv->gobj), @@ -1663,11 +1853,7 @@ real_connect_func(JSContext *context, closure = gjs_closure_new_for_signal(context, &argv[1].toObject(), "signal callback", signal_id); if (closure == NULL) return false; - - /* This is a weak reference, and will be cleared when the closure is invalidated */ - priv->signals.insert(closure); - g_closure_add_invalidate_notifier(closure, priv, - signal_connection_invalidated); + do_associate_closure(priv, closure); id = g_signal_connect_closure_by_id(priv->gobj, signal_id, @@ -1705,7 +1891,6 @@ emit_func(JSContext *context, guint signal_id; GQuark signal_detail; GSignalQuery signal_query; - GjsAutoJSChar signal_name(context); GValue *instance_and_args; GValue rvalue = G_VALUE_INIT; unsigned int i; @@ -1726,12 +1911,25 @@ emit_func(JSContext *context, return false; } + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already deallocated - impossible to emit signal. " + "This might be caused by the fact that the object has been destroyed from C " + "code using something such as destroy(), dispose(), or remove() vfuncs", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + return true; + } + if (argc < 1 || !argv[0].isString()) { gjs_throw(context, "emit() first arg is the signal name"); return false; } - if (!gjs_string_to_utf8(context, argv[0], &signal_name)) + JS::RootedString signal_str(context, argv[0].toString()); + GjsAutoJSChar signal_name = JS_EncodeStringToUTF8(context, signal_str); + if (!signal_name) return false; if (!g_signal_parse_name(signal_name, @@ -1814,7 +2012,9 @@ to_string_func(JSContext *context, return false; /* wrong class passed in */ } - return _gjs_proxy_to_string_func(context, obj, "object", + return _gjs_proxy_to_string_func(context, obj, + (priv->g_object_finalized) ? + "object (FINALIZED)" : "object", (GIBaseInfo*)priv->info, priv->gtype, priv->gobj, rec.rval()); } @@ -1934,10 +2134,11 @@ gjs_define_object_class(JSContext *context, JS::HandleObject in_object, GIObjectInfo *info, GType gtype, - JS::MutableHandleObject constructor) + JS::MutableHandleObject constructor, + JS::MutableHandleObject prototype) { const char *constructor_name; - JS::RootedObject prototype(context), parent_proto(context); + JS::RootedObject parent_proto(context); ObjectInstance *priv; const char *ns; @@ -2006,7 +2207,7 @@ gjs_define_object_class(JSContext *context, NULL, /* funcs of constructor, MyConstructor.myfunc() */ NULL, - &prototype, + prototype, constructor)) { g_error("Can't init class %s", constructor_name); } @@ -2021,9 +2222,9 @@ gjs_define_object_class(JSContext *context, priv->klass = (GTypeClass*) g_type_class_ref (gtype); JS_SetPrivate(prototype, priv); - gjs_debug(GJS_DEBUG_GOBJECT, "Defined class %s prototype %p class %p in object %p", - constructor_name, prototype.get(), JS_GetClass(prototype), - in_object.get()); + gjs_debug(GJS_DEBUG_GOBJECT, "Defined class for %s (%s), prototype %p, " + "JSClass %p, in object %p", constructor_name, g_type_name(gtype), + prototype.get(), JS_GetClass(prototype), in_object.get()); if (info) gjs_object_define_static_methods(context, constructor, gtype, info); @@ -2068,9 +2269,6 @@ gjs_object_from_g_object(JSContext *context, g_object_ref_sink(gobj); associate_js_gobject(context, obj, gobj); - /* see the comment in init_object_instance() for this */ - g_object_unref(gobj); - g_assert(priv->keep_alive == obj.get()); } @@ -2133,6 +2331,18 @@ gjs_typecheck_object(JSContext *context, return false; } + if (priv->g_object_finalized) { + g_critical("Object %s.%s (%p), has been already deallocated - impossible to access to it. " + "This might be caused by the fact that the object has been destroyed from C " + "code using something such as destroy(), dispose(), or remove() vfuncs", + priv->info ? g_base_info_get_namespace( (GIBaseInfo*) priv->info) : "", + priv->info ? g_base_info_get_name( (GIBaseInfo*) priv->info) : g_type_name(priv->gtype), + priv->gobj); + gjs_dumpstack(); + + return true; + } + g_assert(priv->gtype == G_OBJECT_TYPE(priv->gobj)); if (expected_type != G_TYPE_NONE) @@ -2142,13 +2352,13 @@ gjs_typecheck_object(JSContext *context, if (!result && throw_error) { if (priv->info) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s", g_base_info_get_namespace((GIBaseInfo*) priv->info), g_base_info_get_name((GIBaseInfo*) priv->info), g_type_name(expected_type)); } else { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s - cannot convert to %s", g_type_name(priv->gtype), g_type_name(expected_type)); @@ -2240,7 +2450,7 @@ gjs_hook_up_vfunc(JSContext *cx, JS::Value *vp) { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GjsAutoJSChar name(cx); + GjsAutoJSChar name; JS::RootedObject object(cx), function(cx); ObjectInstance *priv; GType gtype, info_gtype; @@ -2312,30 +2522,20 @@ gjs_hook_up_vfunc(JSContext *cx, find_vfunc_info(cx, gtype, vfunc, name, &implementor_vtable, &field_info); if (field_info != NULL) { - GITypeInfo *type_info; - GIBaseInfo *interface_info; - GICallbackInfo *callback_info; gint offset; gpointer method_ptr; GjsCallbackTrampoline *trampoline; - type_info = g_field_info_get_type(field_info); - - interface_info = g_type_info_get_interface(type_info); - - callback_info = (GICallbackInfo*)interface_info; offset = g_field_info_get_offset(field_info); method_ptr = G_STRUCT_MEMBER_P(implementor_vtable, offset); JS::RootedValue v_function(cx, JS::ObjectValue(*function)); - trampoline = gjs_callback_trampoline_new(cx, v_function, callback_info, - GI_SCOPE_TYPE_NOTIFIED, true); + trampoline = gjs_callback_trampoline_new(cx, v_function, vfunc, + GI_SCOPE_TYPE_NOTIFIED, + object, true); *((ffi_closure **)method_ptr) = trampoline->closure; - priv->vfuncs.push_back(trampoline); - g_base_info_unref(interface_info); - g_base_info_unref(type_info); g_base_info_unref(field_info); } @@ -2484,7 +2684,7 @@ gjs_override_property(JSContext *cx, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GjsAutoJSChar name(cx); + GjsAutoJSChar name; JS::RootedObject type(cx); GParamSpec *pspec; GParamSpec *new_pspec; @@ -2531,49 +2731,45 @@ static void gjs_interface_init(GTypeInterface *g_iface, gpointer iface_data) { - GPtrArray *properties; - GType gtype; - guint i; - - gtype = G_TYPE_FROM_INTERFACE (g_iface); + GType gtype = G_TYPE_FROM_INTERFACE(g_iface); - properties = (GPtrArray *) gjs_hash_table_for_gsize_lookup(class_init_properties, gtype); - if (properties == NULL) + auto found = class_init_properties.find(gtype); + if (found == class_init_properties.end()) return; - for (i = 0; i < properties->len; i++) { - GParamSpec *pspec = (GParamSpec *) properties->pdata[i]; - g_param_spec_set_qdata(pspec, gjs_is_custom_property_quark(), GINT_TO_POINTER(1)); - g_object_interface_install_property(g_iface, pspec); + ParamRefArray& properties = found->second; + for (ParamRef& pspec : properties) { + g_param_spec_set_qdata(pspec.get(), gjs_is_custom_property_quark(), + GINT_TO_POINTER(1)); + g_object_interface_install_property(g_iface, pspec.get()); } - gjs_hash_table_for_gsize_remove(class_init_properties, gtype); + class_init_properties.erase(found); } static void gjs_object_class_init(GObjectClass *klass, gpointer user_data) { - GPtrArray *properties; - GType gtype; - guint i; - - gtype = G_OBJECT_CLASS_TYPE (klass); + GType gtype = G_OBJECT_CLASS_TYPE(klass); klass->constructor = gjs_object_constructor; klass->set_property = gjs_object_set_gproperty; klass->get_property = gjs_object_get_gproperty; - properties = (GPtrArray*) gjs_hash_table_for_gsize_lookup (class_init_properties, gtype); - if (properties != NULL) { - for (i = 0; i < properties->len; i++) { - GParamSpec *pspec = (GParamSpec*) properties->pdata[i]; - g_param_spec_set_qdata(pspec, gjs_is_custom_property_quark(), GINT_TO_POINTER(1)); - g_object_class_install_property (klass, i+1, pspec); - } - - gjs_hash_table_for_gsize_remove (class_init_properties, gtype); + auto found = class_init_properties.find(gtype); + if (found == class_init_properties.end()) + return; + + ParamRefArray& properties = found->second; + unsigned i = 0; + for (ParamRef& pspec : properties) { + g_param_spec_set_qdata(pspec.get(), gjs_is_custom_property_quark(), + GINT_TO_POINTER(1)); + g_object_class_install_property(klass, ++i, pspec.get()); } + + class_init_properties.erase(found); } static void @@ -2604,6 +2800,10 @@ gjs_object_custom_init(GTypeInstance *instance, associate_js_gobject(context, object, G_OBJECT (instance)); + /* Custom JS objects will most likely have visible state, so + * just do this from the start */ + ensure_uses_toggle_ref(context, priv); + JS::RootedValue v(context); if (!gjs_object_get_property(context, object, GJS_STRING_INSTANCE_INIT, &v)) { @@ -2706,36 +2906,26 @@ save_properties_for_class_init(JSContext *cx, uint32_t n_properties, GType gtype) { - GPtrArray *properties_native = NULL; - guint32 i; - - if (!class_init_properties) - class_init_properties = gjs_hash_table_new_for_gsize((GDestroyNotify) g_ptr_array_unref); - properties_native = g_ptr_array_new_with_free_func((GDestroyNotify) g_param_spec_unref); - for (i = 0; i < n_properties; i++) { - JS::RootedValue prop_val(cx); - - if (!JS_GetElement(cx, properties, i, &prop_val)) { - g_clear_pointer(&properties_native, g_ptr_array_unref); + ParamRefArray properties_native; + JS::RootedValue prop_val(cx); + JS::RootedObject prop_obj(cx); + for (uint32_t i = 0; i < n_properties; i++) { + if (!JS_GetElement(cx, properties, i, &prop_val)) return false; - } + if (!prop_val.isObject()) { - g_clear_pointer(&properties_native, g_ptr_array_unref); gjs_throw(cx, "Invalid parameter, expected object"); return false; } - JS::RootedObject prop_obj(cx, &prop_val.toObject()); - if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, true)) { - g_clear_pointer(&properties_native, g_ptr_array_unref); + prop_obj = &prop_val.toObject(); + if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, true)) return false; - } - g_ptr_array_add(properties_native, g_param_spec_ref(gjs_g_param_from_param(cx, prop_obj))); - } - gjs_hash_table_for_gsize_insert(class_init_properties, (gsize) gtype, - g_ptr_array_ref(properties_native)); - g_clear_pointer(&properties_native, g_ptr_array_unref); + properties_native.emplace_back(g_param_spec_ref(gjs_g_param_from_param(cx, prop_obj)), + g_param_spec_unref); + } + class_init_properties[gtype] = std::move(properties_native); return true; } @@ -2745,7 +2935,7 @@ gjs_register_interface(JSContext *cx, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GjsAutoJSChar name(cx); + GjsAutoJSChar name; guint32 i, n_interfaces, n_properties; GType *iface_types; GType interface_type; @@ -2810,21 +3000,45 @@ gjs_register_interface(JSContext *cx, return true; } +static void +gjs_object_base_init(void *klass) +{ + auto priv = static_cast(g_type_get_qdata(G_OBJECT_CLASS_TYPE(klass), + gjs_object_priv_quark())); + + if (priv) { + for (GClosure *closure : priv->closures) + g_closure_ref(closure); + } +} + +static void +gjs_object_base_finalize(void *klass) +{ + auto priv = static_cast(g_type_get_qdata(G_OBJECT_CLASS_TYPE(klass), + gjs_object_priv_quark())); + + if (priv) { + for (GClosure *closure : priv->closures) + g_closure_unref(closure); + } +} + static bool gjs_register_type(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GjsAutoJSChar name(cx); + GjsAutoJSChar name; GType instance_type, parent_type; GTypeQuery query; ObjectInstance *parent_priv; GTypeInfo type_info = { 0, /* class_size */ - (GBaseInitFunc) NULL, - (GBaseFinalizeFunc) NULL, + gjs_object_base_init, + gjs_object_base_finalize, (GClassInitFunc) gjs_object_class_init, (GClassFinalizeFunc) NULL, @@ -2897,8 +3111,13 @@ gjs_register_type(JSContext *cx, gjs_add_interface(instance_type, iface_types[i]); /* create a custom JSClass */ - JS::RootedObject module(cx, gjs_lookup_private_namespace(cx)), constructor(cx); - gjs_define_object_class(cx, module, NULL, instance_type, &constructor); + JS::RootedObject module(cx, gjs_lookup_private_namespace(cx)); + JS::RootedObject constructor(cx), prototype(cx); + gjs_define_object_class(cx, module, nullptr, instance_type, &constructor, + &prototype); + + ObjectInstance *priv = priv_from_js(cx, prototype); + g_type_set_qdata(instance_type, gjs_object_priv_quark(), priv); argv.rval().setObject(*constructor); @@ -2912,7 +3131,7 @@ gjs_signal_new(JSContext *cx, { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); GType gtype; - GjsAutoJSChar signal_name(cx); + GjsAutoJSChar signal_name; GSignalAccumulator accumulator; gint signal_id; guint i, n_parameters; @@ -3027,3 +3246,19 @@ gjs_lookup_object_constructor(JSContext *context, value_p.setObject(*constructor); return true; } + +bool +gjs_object_associate_closure(JSContext *cx, + JS::HandleObject object, + GClosure *closure) +{ + ObjectInstance *priv = priv_from_js(cx, object); + if (!priv) + return false; + + if (priv->gobj) + ensure_uses_toggle_ref(cx, priv); + + do_associate_closure(priv, closure); + return true; +} diff --git a/gi/object.h b/gi/object.h index 8271a6eb..d1b5fbe2 100644 --- a/gi/object.h +++ b/gi/object.h @@ -25,7 +25,8 @@ #define __GJS_OBJECT_H__ #include -#include + +#include #include #include "cjs/jsapi-util.h" @@ -35,7 +36,8 @@ void gjs_define_object_class(JSContext *context, JS::HandleObject in_object, GIObjectInfo *info, GType gtype, - JS::MutableHandleObject constructor); + JS::MutableHandleObject constructor, + JS::MutableHandleObject prototype); bool gjs_lookup_object_constructor(JSContext *context, GType gtype, @@ -58,6 +60,9 @@ bool gjs_typecheck_is_object(JSContext *context, void gjs_object_prepare_shutdown(void); void gjs_object_clear_toggles(void); +void gjs_object_shutdown_toggle_queue(void); +void gjs_object_context_dispose_notify(void *data, + GObject *where_the_object_was); void gjs_object_define_static_methods(JSContext *context, JS::HandleObject constructor, @@ -67,6 +72,10 @@ void gjs_object_define_static_methods(JSContext *context, bool gjs_define_private_gi_stuff(JSContext *cx, JS::MutableHandleObject module); +bool gjs_object_associate_closure(JSContext *cx, + JS::HandleObject obj, + GClosure *closure); + G_END_DECLS #endif /* __GJS_OBJECT_H__ */ diff --git a/gi/param.cpp b/gi/param.cpp index af2f6475..8bcc5917 100644 --- a/gi/param.cpp +++ b/gi/param.cpp @@ -58,20 +58,21 @@ param_resolve(JSContext *context, GIObjectInfo *info = NULL; GIFunctionInfo *method_info; Param *priv; - GjsAutoJSChar name(context); bool ret = false; - if (!gjs_get_string_id(context, id, &name)) - return true; /* not resolved, but no error */ - priv = priv_from_js(context, obj); - if (priv != NULL) { /* instance, not prototype */ *resolved = false; return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; /* not resolved, but no error */ + } + info = (GIObjectInfo*)g_irepository_find_by_gtype(g_irepository_get_default(), G_TYPE_PARAM); method_info = g_object_info_find_method(info, name); @@ -304,7 +305,7 @@ gjs_typecheck_param(JSContext *context, if (priv->gparam == NULL) { if (throw_error) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is GObject.ParamSpec.prototype, not an object instance - " "cannot convert to a GObject.ParamSpec instance"); } @@ -318,7 +319,7 @@ gjs_typecheck_param(JSContext *context, result = true; if (!result && throw_error) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s - cannot convert to %s", g_type_name(G_TYPE_FROM_INSTANCE (priv->gparam)), g_type_name(expected_type)); diff --git a/gi/proxyutils.cpp b/gi/proxyutils.cpp index 99746086..d90aa4d0 100644 --- a/gi/proxyutils.cpp +++ b/gi/proxyutils.cpp @@ -63,10 +63,10 @@ _gjs_proxy_to_string_func(JSContext *context, g_string_append_printf(buf, " jsobj@%p", this_obj); if (native_address != NULL) g_string_append_printf(buf, " native@%p", native_address); - + g_string_append_c(buf, ']'); - if (!gjs_string_from_utf8 (context, buf->str, -1, rval)) + if (!gjs_string_from_utf8(context, buf->str, rval)) goto out; ret = true; diff --git a/gi/repo.cpp b/gi/repo.cpp index 5e74e556..82604593 100644 --- a/gi/repo.cpp +++ b/gi/repo.cpp @@ -38,7 +38,6 @@ #include "gerror.h" #include "cjs/jsapi-class.h" #include "cjs/jsapi-wrapper.h" -#include "cjs/jsapi-private.h" #include "cjs/mem.h" #include @@ -92,7 +91,7 @@ resolve_namespace_object(JSContext *context, JSAutoRequest ar(context); - GjsAutoJSChar version(context); + GjsAutoJSChar version; if (!get_version_for_ns(context, repo_obj, ns_id, &version)) return false; @@ -114,7 +113,7 @@ resolve_namespace_object(JSContext *context, if (error != NULL) { gjs_throw(context, "Requiring %s, version %s: %s", - ns_name, version?version:"none", error->message); + ns_name, version ? version.get() : "none", error->message); g_error_free(error); return false; @@ -162,23 +161,23 @@ repo_resolve(JSContext *context, bool *resolved) { Repo *priv; - GjsAutoJSChar name(context); - if (!gjs_get_string_id(context, id, &name)) { + if (!JSID_IS_STRING(id)) { *resolved = false; return true; /* not resolved, but no error */ } + JSFlatString *str = JSID_TO_FLAT_STRING(id); /* let Object.prototype resolve these */ - if (strcmp(name, "valueOf") == 0 || - strcmp(name, "toString") == 0) { + if (JS_FlatStringEqualsAscii(str, "valueOf") || + JS_FlatStringEqualsAscii(str, "toString")) { *resolved = false; return true; } priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GREPO, "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + gjs_debug_jsprop(GJS_DEBUG_GREPO, "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (priv == NULL) { /* we are the prototype, or have the wrong class */ @@ -186,6 +185,12 @@ repo_resolve(JSContext *context, return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; + } + if (!resolve_namespace_object(context, obj, id, name)) { return false; } @@ -422,9 +427,9 @@ gjs_define_info(JSContext *context, if (g_type_is_a (gtype, G_TYPE_PARAM)) { gjs_define_param_class(context, in_object); } else if (g_type_is_a (gtype, G_TYPE_OBJECT)) { - JS::RootedObject ignored(context); - gjs_define_object_class(context, in_object, - (GIObjectInfo *) info, gtype, &ignored); + JS::RootedObject ignored1(context), ignored2(context); + gjs_define_object_class(context, in_object, info, gtype, + &ignored1, &ignored2); } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { JS::RootedObject ignored1(context), ignored2(context); if (!gjs_define_fundamental_class(context, in_object, diff --git a/gi/toggle.cpp b/gi/toggle.cpp index 0c3aa26e..9e402bed 100644 --- a/gi/toggle.cpp +++ b/gi/toggle.cpp @@ -82,9 +82,16 @@ ToggleQueue::is_queued(GObject *gobj) std::pair ToggleQueue::cancel(GObject *gobj) { + debug("cancel", gobj); std::lock_guard hold(lock); bool had_toggle_down = find_and_erase_operation_locked(gobj, DOWN); bool had_toggle_up = find_and_erase_operation_locked(gobj, UP); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "ToggleQueue: %p (%s) was %s", gobj, + G_OBJECT_TYPE_NAME(gobj), + had_toggle_down && had_toggle_up ? "queued to toggle BOTH" + : had_toggle_down ? "queued to toggle DOWN" + : had_toggle_up ? "queued to toggle UP" + : "not queued"); return {had_toggle_down, had_toggle_up}; } @@ -101,18 +108,36 @@ ToggleQueue::handle_toggle(Handler handler) handler(item.gobj, item.direction); q.pop_front(); } - + + debug("handle", item.gobj); if (item.needs_unref) g_object_unref(item.gobj); return true; } +void +ToggleQueue::shutdown(void) +{ + debug("shutdown", nullptr); + g_assert(((void)"Queue should have been emptied before shutting down", + q.empty())); + m_shutdown = true; +} + void ToggleQueue::enqueue(GObject *gobj, ToggleQueue::Direction direction, ToggleQueue::Handler handler) { + if (G_UNLIKELY (m_shutdown)) { + gjs_debug(GJS_DEBUG_GOBJECT, "Enqueuing GObject %p to toggle %s after " + "shutdown, probably from another thread (%p).", gobj, + direction == UP ? "UP" : "DOWN", + g_thread_self()); + return; + } + Item item{gobj, direction}; /* If we're toggling up we take a reference to the object now, * so it won't toggle down before we process it. This ensures we @@ -120,8 +145,11 @@ ToggleQueue::enqueue(GObject *gobj, * (either only up, or down-up) */ if (direction == UP) { + debug("enqueue UP", gobj); g_object_ref(gobj); item.needs_unref = true; + } else { + debug("enqueue DOWN", gobj); } /* If we're toggling down, we don't need to take a reference since * the associated JSObject already has one, and that JSObject won't diff --git a/gi/toggle.h b/gi/toggle.h index d1f9fbf6..f03b6ea4 100644 --- a/gi/toggle.h +++ b/gi/toggle.h @@ -26,10 +26,13 @@ #ifndef GJS_TOGGLE_H #define GJS_TOGGLE_H +#include #include #include #include +#include "util/log.h" + /* Thread-safe queue for enqueueing toggle-up or toggle-down events on GObjects * from any thread. For more information, see object.cpp, comments near * wrapped_gobj_toggle_notify(). */ @@ -51,9 +54,16 @@ class ToggleQueue { std::mutex lock; std::deque q; + std::atomic_bool m_shutdown = ATOMIC_VAR_INIT(false); + unsigned m_idle_id; Handler m_toggle_handler; + /* No-op unless GJS_VERBOSE_ENABLE_LIFECYCLE is defined to 1. */ + inline void debug(const char *did, void *what) { + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "ToggleQueue %s %p", did, what); + } + std::deque::iterator find_operation_locked(GObject *gobj, Direction direction); bool find_and_erase_operation_locked(GObject *gobj, Direction direction); @@ -72,7 +82,12 @@ class ToggleQueue { * want to wait for it to be processed in idle time. Returns false if queue * is empty. */ bool handle_toggle(Handler handler); - + + /* After calling this, the toggle queue won't accept any more toggles. Only + * intended for use when destroying the JSContext and breaking the + * associations between C and JS objects. */ + void shutdown(void); + /* Queues a toggle to be processed in idle time. */ void enqueue(GObject *gobj, Direction direction, diff --git a/gi/union.cpp b/gi/union.cpp index 22369ee7..b9d29cdf 100644 --- a/gi/union.cpp +++ b/gi/union.cpp @@ -60,17 +60,9 @@ union_resolve(JSContext *context, JS::HandleId id, bool *resolved) { - Union *priv; - GjsAutoJSChar name(context); - - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; /* not resolved, but no error */ - } - - priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GBOXED, "Resolve prop '%s' hook obj %p priv %p", - name.get(), obj.get(), priv); + Union *priv = priv_from_js(context, obj); + gjs_debug_jsprop(GJS_DEBUG_GBOXED, "Resolve prop '%s' hook, obj %s, priv %p", + gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); if (priv == nullptr) return false; /* wrong class */ @@ -87,6 +79,12 @@ union_resolve(JSContext *context, return true; } + GjsAutoJSChar name; + if (!gjs_get_string_id(context, id, &name)) { + *resolved = false; + return true; /* not resolved, but no error */ + } + /* We are the prototype, so look for methods and other class properties */ GIFunctionInfo *method_info; @@ -447,7 +445,7 @@ gjs_typecheck_union(JSContext *context, if (priv->gboxed == NULL) { if (throw_error) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is %s.%s.prototype, not an object instance - cannot convert to a union instance", g_base_info_get_namespace( (GIBaseInfo*) priv->info), g_base_info_get_name( (GIBaseInfo*) priv->info)); @@ -465,14 +463,14 @@ gjs_typecheck_union(JSContext *context, if (!result && throw_error) { if (expected_info != NULL) { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s.%s", g_base_info_get_namespace((GIBaseInfo*) priv->info), g_base_info_get_name((GIBaseInfo*) priv->info), g_base_info_get_namespace((GIBaseInfo*) expected_info), g_base_info_get_name((GIBaseInfo*) expected_info)); } else { - gjs_throw_custom(context, "TypeError", NULL, + gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is of type %s.%s - cannot convert to %s", g_base_info_get_namespace((GIBaseInfo*) priv->info), g_base_info_get_name((GIBaseInfo*) priv->info), diff --git a/gi/value.cpp b/gi/value.cpp index 5a52089d..d2fc3bd3 100644 --- a/gi/value.cpp +++ b/gi/value.cpp @@ -269,7 +269,7 @@ closure_marshal(GClosure *closure, g_base_info_unref((GIBaseInfo *)type_info_for[i]); JS::RootedValue rval(context); - gjs_closure_invoke(closure, argv, &rval); + gjs_closure_invoke(closure, nullptr, argv, &rval, false); if (return_value != NULL) { if (rval.isUndefined()) { @@ -342,6 +342,19 @@ gjs_value_guess_g_type(JSContext *context, return G_TYPE_INVALID; } +static bool +throw_expect_type(JSContext *cx, + JS::HandleValue value, + const char *expected_type, + GType gtype = 0) +{ + gjs_throw(cx, "Wrong type %s; %s%s%s expected", + JS::InformalValueTypeName(value), expected_type, + gtype ? " " : "", + gtype ? g_type_name(gtype) : ""); + return false; /* for convenience */ +} + static bool gjs_value_to_g_value_internal(JSContext *context, JS::HandleValue value, @@ -379,77 +392,56 @@ gjs_value_to_g_value_internal(JSContext *context, if (value.isNull()) { g_value_set_string(gvalue, NULL); } else if (value.isString()) { - GjsAutoJSChar utf8_string(context); - - if (!gjs_string_to_utf8(context, value, &utf8_string)) + JS::RootedString str(context, value.toString()); + GjsAutoJSChar utf8_string = JS_EncodeStringToUTF8(context, str); + if (!utf8_string) return false; g_value_take_string(gvalue, utf8_string.copy()); } else { - gjs_throw(context, - "Wrong type %s; string expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "string"); } } else if (gtype == G_TYPE_CHAR) { gint32 i; if (JS::ToInt32(context, value, &i) && i >= SCHAR_MIN && i <= SCHAR_MAX) { g_value_set_schar(gvalue, (signed char)i); } else { - gjs_throw(context, - "Wrong type %s; char expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "char"); } } else if (gtype == G_TYPE_UCHAR) { guint16 i; if (JS::ToUint16(context, value, &i) && i <= UCHAR_MAX) { g_value_set_uchar(gvalue, (unsigned char)i); } else { - gjs_throw(context, - "Wrong type %s; unsigned char expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "unsigned char"); } } else if (gtype == G_TYPE_INT) { gint32 i; if (JS::ToInt32(context, value, &i)) { g_value_set_int(gvalue, i); } else { - gjs_throw(context, - "Wrong type %s; integer expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "integer"); } } else if (gtype == G_TYPE_DOUBLE) { gdouble d; if (JS::ToNumber(context, value, &d)) { g_value_set_double(gvalue, d); } else { - gjs_throw(context, - "Wrong type %s; double expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "double"); } } else if (gtype == G_TYPE_FLOAT) { gdouble d; if (JS::ToNumber(context, value, &d)) { g_value_set_float(gvalue, d); } else { - gjs_throw(context, - "Wrong type %s; float expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "float"); } } else if (gtype == G_TYPE_UINT) { guint32 i; if (JS::ToUint32(context, value, &i)) { g_value_set_uint(gvalue, i); } else { - gjs_throw(context, - "Wrong type %s; unsigned integer expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "unsigned integer"); } } else if (gtype == G_TYPE_BOOLEAN) { /* JS::ToBoolean() can't fail */ @@ -468,11 +460,7 @@ gjs_value_to_g_value_internal(JSContext *context, gobj = gjs_g_object_from_object(context, obj); } else { - gjs_throw(context, - "Wrong type %s; object %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "object", gtype); } g_value_set_object(gvalue, gobj); @@ -493,10 +481,7 @@ gjs_value_to_g_value_internal(JSContext *context, GJS_STRING_LENGTH, &length)) { JS_ClearPendingException(context); - gjs_throw(context, - "Wrong type %s; strv expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "strv"); } else { void *result; char **strv; @@ -510,10 +495,7 @@ gjs_value_to_g_value_internal(JSContext *context, g_value_take_boxed (gvalue, strv); } } else { - gjs_throw(context, - "Wrong type %s; strv expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "strv"); } } } else if (g_type_is_a(gtype, G_TYPE_BOXED)) { @@ -587,11 +569,7 @@ gjs_value_to_g_value_internal(JSContext *context, } } } else { - gjs_throw(context, - "Wrong type %s; boxed type %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "boxed type", gtype); } if (no_copy) @@ -611,11 +589,7 @@ gjs_value_to_g_value_internal(JSContext *context, variant = (GVariant*) gjs_c_struct_from_boxed(context, obj); } else { - gjs_throw(context, - "Wrong type %s; boxed type %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "boxed type", gtype); } g_value_set_variant (gvalue, variant); @@ -639,11 +613,7 @@ gjs_value_to_g_value_internal(JSContext *context, g_value_set_enum(gvalue, v->value); } else { - gjs_throw(context, - "Wrong type %s; enum %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "enum", gtype); } } else if (g_type_is_a(gtype, G_TYPE_FLAGS)) { int64_t value_int64; @@ -655,11 +625,7 @@ gjs_value_to_g_value_internal(JSContext *context, /* See arg.c:_gjs_enum_to_int() */ g_value_set_flags(gvalue, (int)value_int64); } else { - gjs_throw(context, - "Wrong type %s; flags %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "flags", gtype); } } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { void *gparam; @@ -675,22 +641,15 @@ gjs_value_to_g_value_internal(JSContext *context, gparam = gjs_g_param_from_param(context, obj); } else { - gjs_throw(context, - "Wrong type %s; param type %s expected", - gjs_get_type_name(value), - g_type_name(gtype)); - return false; + return throw_expect_type(context, value, "param type", gtype); } g_value_set_param(gvalue, (GParamSpec*) gparam); } else if (g_type_is_a(gtype, G_TYPE_GTYPE)) { GType type; - if (!value.isObject()) { - gjs_throw(context, "Wrong type %s; expect a GType object", - gjs_get_type_name(value)); - return false; - } + if (!value.isObject()) + return throw_expect_type(context, value, "GType object"); JS::RootedObject obj(context, &value.toObject()); type = gjs_gtype_get_actual_gtype(context, obj); @@ -716,10 +675,7 @@ gjs_value_to_g_value_internal(JSContext *context, g_value_set_int(&int_value, i); g_value_transform(&int_value, gvalue); } else { - gjs_throw(context, - "Wrong type %s; integer expected", - gjs_get_type_name(value)); - return false; + return throw_expect_type(context, value, "integer"); } } else { gjs_debug(GJS_DEBUG_GCLOSURE, "JS::Value is number %d gtype fundamental %d transformable to int %d from int %d", @@ -801,7 +757,7 @@ gjs_value_from_g_value_internal(JSContext *context, "Converting NULL string to JS::NullValue()"); value_p.setNull(); } else { - if (!gjs_string_from_utf8(context, v, -1, value_p)) + if (!gjs_string_from_utf8(context, v, value_p)) return false; } } else if (gtype == G_TYPE_CHAR) { @@ -934,24 +890,15 @@ gjs_value_from_g_value_internal(JSContext *context, bool res; GArgument arg; GIArgInfo *arg_info; - GIBaseInfo *obj; GISignalInfo *signal_info; GITypeInfo type_info; - obj = g_irepository_find_by_gtype(NULL, signal_query->itype); - if (!obj) { - gjs_throw(context, "Signal argument with GType %s isn't introspectable", - g_type_name(signal_query->itype)); - return false; - } - - signal_info = g_object_info_find_signal((GIObjectInfo*)obj, signal_query->signal_name); - + signal_info = get_signal_info_if_available(signal_query); if (!signal_info) { gjs_throw(context, "Unknown signal."); - g_base_info_unref((GIBaseInfo*)obj); return false; } + arg_info = g_callable_info_get_arg(signal_info, arg_n - 1); g_arg_info_load_type(arg_info, &type_info); @@ -965,7 +912,6 @@ gjs_value_from_g_value_internal(JSContext *context, g_base_info_unref((GIBaseInfo*)arg_info); g_base_info_unref((GIBaseInfo*)signal_info); - g_base_info_unref((GIBaseInfo*)obj); return res; } else if (g_type_is_a(gtype, G_TYPE_POINTER)) { gpointer pointer; diff --git a/gjs-srcs.mk b/gjs-srcs.mk index 16916a54..3ca1dfa8 100644 --- a/gjs-srcs.mk +++ b/gjs-srcs.mk @@ -3,6 +3,7 @@ gjs_public_headers = \ cjs/coverage.h \ cjs/gjs.h \ cjs/macros.h \ + cjs/profiler.h \ util/error.h \ $(NULL) @@ -52,18 +53,15 @@ gjs_srcs = \ cjs/byteArray.h \ cjs/context.cpp \ cjs/context-private.h \ - cjs/coverage-internal.h \ cjs/coverage.cpp \ cjs/engine.cpp \ cjs/engine.h \ cjs/global.cpp \ - cjs/global.h \ + cjs/global.h \ cjs/importer.cpp \ cjs/importer.h \ cjs/jsapi-class.h \ cjs/jsapi-dynamic-class.cpp \ - cjs/jsapi-private.cpp \ - cjs/jsapi-private.h \ cjs/jsapi-util.cpp \ cjs/jsapi-util.h \ cjs/jsapi-util-args.h \ @@ -77,14 +75,14 @@ gjs_srcs = \ cjs/module.cpp \ cjs/native.cpp \ cjs/native.h \ + cjs/profiler.cpp \ + cjs/profiler-private.h \ cjs/stack.cpp \ modules/modules.cpp \ modules/modules.h \ util/error.cpp \ util/glib.cpp \ util/glib.h \ - util/hash-x32.cpp \ - util/hash-x32.h \ util/log.cpp \ util/log.h \ util/misc.cpp \ @@ -107,3 +105,9 @@ gjs_gtk_private_srcs = \ gjs_console_srcs = \ cjs/console.cpp \ $(NULL) + +gjs_sysprof_srcs = \ + util/sp-capture-types.h \ + util/sp-capture-writer.c \ + util/sp-capture-writer.h \ + $(NULL) diff --git a/gjs.doap b/gjs.doap index 5607a2e1..023841a4 100644 --- a/gjs.doap +++ b/gjs.doap @@ -9,19 +9,19 @@ gjs - GNOME JavaScript/Spidermonkey bindings + GNOME JavaScript bindings - GNOME JavaScript/Spidermonkey bindings + GNOME JavaScript bindings - + - + - + - C + C++ @@ -34,9 +34,9 @@ - Colin Walters - - walters + Cosimo Cecchi + + cosimoc diff --git a/installed-tests/extra/gjs.supp b/installed-tests/extra/gjs.supp index 5f10734e..f5843522 100644 --- a/installed-tests/extra/gjs.supp +++ b/installed-tests/extra/gjs.supp @@ -72,13 +72,13 @@ fun:malloc fun:js_malloc fun:js_new > - fun:_ZN2js5Mutex14heldMutexStackEv.part.293 + fun:_ZN2js5Mutex14heldMutexStackEv.part.* fun:heldMutexStack fun:_ZN2js5Mutex4lockEv fun:LockGuard fun:_ZN2js25AutoLockHelperThreadStateC1EON7mozilla6detail19GuardObjectNotifierE fun:_ZN2js12HelperThread10threadLoopEv - fun:callMain<0ul> + fun:callMain<0*> fun:_ZN2js6detail16ThreadTrampolineIRFvPvEJPNS_12HelperThreadEEE5StartES2_ fun:start_thread fun:clone diff --git a/installed-tests/js/complex.ui b/installed-tests/js/complex.ui index c11f1c1b..4096a4ad 100644 --- a/installed-tests/js/complex.ui +++ b/installed-tests/js/complex.ui @@ -10,6 +10,7 @@ Complex! True + diff --git a/installed-tests/js/modules/subA/subB/__init__.js b/installed-tests/js/modules/subA/subB/__init__.js index c7660dc1..15bab86f 100644 --- a/installed-tests/js/modules/subA/subB/__init__.js +++ b/installed-tests/js/modules/subA/subB/__init__.js @@ -14,4 +14,4 @@ ImporterClass.prototype = { testMethod : function() { return this._a; } -} +}; diff --git a/installed-tests/js/testCairo.js b/installed-tests/js/testCairo.js index eb8e775a..db20de0f 100644 --- a/installed-tests/js/testCairo.js +++ b/installed-tests/js/testCairo.js @@ -181,7 +181,7 @@ describe('Cairo', function () { cr = Gdk.cairo_create(da.window); expect(cr.save).toBeDefined(); - expect(_ts(cr.getTarget())).toEqual('Surface'); + expect(cr.getTarget()).toBeDefined(); }); }); @@ -228,3 +228,16 @@ describe('Cairo', function () { }); }); }); + +describe('Cairo imported via GI', function () { + const giCairo = imports.gi.cairo; + + it('has the same functionality as imports.cairo', function () { + const surface = new giCairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + void new giCairo.Context(surface); + }); + + it('has boxed types from the GIR file', function () { + void new giCairo.RectangleInt(); + }); +}); diff --git a/installed-tests/js/testCoverage.js b/installed-tests/js/testCoverage.js deleted file mode 100644 index a6fab7f1..00000000 --- a/installed-tests/js/testCoverage.js +++ /dev/null @@ -1,1195 +0,0 @@ -const Coverage = imports.coverage; - -describe('Coverage.expressionLinesForAST', function () { - let testTable = { - 'works with no trailing newline': [ - "let x;\n" + - "let y;", - [1, 2], - ], - - 'finds lines on both sides of an assignment expression': [ - "var x;\n" + - "x = (function() {\n" + - " return 10;\n" + - "})();\n", - [2, 3], - ], - - 'finds lines inside functions': [ - "function f(a, b) {\n" + - " let x = a;\n" + - " let y = b;\n" + - " return x + y;\n" + - "}\n" + - "\n" + - "var z = f(1, 2);\n", - [2, 3, 4, 7], - ], - - 'finds lines inside anonymous functions': [ - "var z = (function f(a, b) {\n" + - " let x = a;\n" + - " let y = b;\n" + - " return x + y;\n" + - " })();\n", - [1, 2, 3, 4], - ], - - 'finds lines inside body of function property': [ - "var o = {\n" + - " foo: function() {\n" + - " let x = a;\n" + - " }\n" + - "};\n", - [1, 2, 3], - ], - - 'finds lines inside arguments of function property': [ - "function f(a) {\n" + - "}\n" + - "f({\n" + - " foo: function() {\n" + - " let x = a;\n" + - " }\n" + - "});\n", - [1, 3, 4, 5], - ], - - 'finds lines inside multiline function arguments': [ - `function f(a, b, c) { - } - f(1, - 2 + 3, - 3 + 4);`, - [1, 3, 4, 5], - ], - - 'finds lines inside function argument that is an object': [ - `function f(o) { - } - let obj = { - Name: new f({ a: 1, - b: 2 + 3, - c: 3 + 4, - }) - } `, - [1, 3, 4, 5, 6], - ], - - 'finds lines inside a while loop': [ - "var a = 0;\n" + - "while (a < 1) {\n" + - " let x = 0;\n" + - " let y = 1;\n" + - " a++;" + - "\n" + - "}\n", - [1, 2, 3, 4, 5], - ], - - 'finds lines inside try, catch, and finally': [ - "var a = 0;\n" + - "try {\n" + - " a++;\n" + - "} catch (e) {\n" + - " a++;\n" + - "} finally {\n" + - " a++;\n" + - "}\n", - [1, 2, 3, 4, 5, 7], - ], - - 'finds lines inside case statements': [ - "var a = 0;\n" + - "switch (a) {\n" + - "case 1:\n" + - " a++;\n" + - " break;\n" + - "case 2:\n" + - " a++;\n" + - " break;\n" + - "}\n", - [1, 2, 4, 5, 7, 8], - ], - - 'finds lines inside case statements with character cases': [ - "var a = 'a';\n" + - "switch (a) {\n" + - "case 'a':\n" + - " a++;\n" + - " break;\n" + - "case 'b':\n" + - " a++;\n" + - " break;\n" + - "}\n", - [1, 2, 4, 5, 7, 8], - ], - - 'finds lines inside a for loop': [ - "for (let i = 0; i < 1; i++) {\n" + - " let x = 0;\n" + - " let y = 1;\n" + - "\n" + - "}\n", - [1, 2, 3], - ], - - 'finds lines inside if-statement branches': [ - "if (1 > 0) {\n" + - " let i = 0;\n" + - "} else {\n" + - " let j = 1;\n" + - "}\n", - [1, 2, 4], - ], - - 'finds all lines of multiline if-conditions': [ - "if (1 > 0 &&\n" + - " 2 > 0 &&\n" + - " 3 > 0) {\n" + - " let a = 3;\n" + - "}\n", - [1, 2, 3, 4], - ], - - 'finds lines for object property literals': [ - `var a = { - Name: 'foo' + 'bar', - Ex: 'bar' + 'foo', - }`, - [1, 2, 3], - ], - - 'finds lines for function-valued object properties': [ - "var a = {\n" + - " Name: function() {},\n" + - "};\n", - [1, 2], - ], - - 'finds lines inside object-valued object properties': [ - "var a = {\n" + - " Name: {},\n" + - "};\n", - [1, 2], - ], - - 'finds lines inside array-valued object properties': [ - "var a = {\n" + - " Name: [],\n" + - "};\n", - [1, 2], - ], - - 'finds lines inside object-valued argument to return statement': [ - "function f() {\n" + - " return {};\n" + - "}\n", - [2], - ], - - 'finds lines inside object-valued argument to throw statement': [ - `function f() { - throw { - a: 1 + 2, - b: 2 + 3, - } - }`, - [2, 3, 4], - ], - - 'does not find lines in empty var declarations': [ - 'var foo;', - [], - ], - - 'finds lines in empty let declarations': [ - 'let foo;', - [1], - ], - }; - - Object.keys(testTable).forEach(testcase => { - it(testcase, function () { - const ast = Reflect.parse(testTable[testcase][0]); - let foundLines = Coverage.expressionLinesForAST(ast); - expect(foundLines).toEqual(testTable[testcase][1]); - }); - }); -}); - -describe('Coverage.functionsForAST', function () { - let testTable = { - 'works with no trailing newline': [ - "function f1() {}\n" + - "function f2() {}", - [ - { key: "f1:1:0", line: 1, n_params: 0 }, - { key: "f2:2:0", line: 2, n_params: 0 }, - ], - ], - - 'finds functions': [ - "function f1() {}\n" + - "function f2() {}\n" + - "function f3() {}\n", - [ - { key: "f1:1:0", line: 1, n_params: 0 }, - { key: "f2:2:0", line: 2, n_params: 0 }, - { key: "f3:3:0", line: 3, n_params: 0 } - ], - ], - - 'finds nested functions': [ - "function f1() {\n" + - " let f2 = function() {\n" + - " let f3 = function() {\n" + - " }\n" + - " }\n" + - "}\n", - [ - { key: "f1:1:0", line: 1, n_params: 0 }, - { key: "(anonymous):2:0", line: 2, n_params: 0 }, - { key: "(anonymous):3:0", line: 3, n_params: 0 } - ], - ], - - /* Note the lack of newlines. This is all on one line */ - 'finds functions on the same line but with different arguments': [ - "function f1() {" + - " return (function(a) {" + - " return function(a, b) {}" + - " });" + - "}", - [ - { key: "f1:1:0", line: 1, n_params: 0 }, - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 } - ], - ], - - 'finds functions inside an array expression': [ - "let a = [function() {}];\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 }, - ], - ], - - 'finds functions inside an arrow expression': [ - "(a) => (function() {})();\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside a sequence': [ - "(function(a) {})()," + - "(function(a, b) {})();\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 }, - ], - ], - - 'finds functions inside a unary expression': [ - "let a = (function() {}())++;\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 }, - ], - ], - - 'finds functions inside a binary expression': [ - "let a = function(a) {}() +" + - " function(a, b) {}();\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 } - ], - ], - - 'finds functions inside an assignment expression': [ - "let a = function() {}();\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside a reflexive assignment expression': [ - "let a;\n" + - "a += function() {}();\n", - [ - { key: "(anonymous):2:0", line: 2, n_params: 0 } - ], - ], - - 'finds functions inside if-statement conditions': [ - "if (function(a) {}(a) >" + - " function(a, b) {}(a, b)) {}\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 } - ], - ], - - 'finds functions inside while-statement conditions': [ - "while (function(a) {}(a) >" + - " function(a, b) {}(a, b)) {};\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 } - ], - ], - - 'finds functions inside for-statement initializer': [ - "for (function() {}; ;) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - /* SpiderMonkey parses for (let i = ; ; ) as though - * they were let i = { for (; ) } so test the - * LetStatement initializer case too */ - 'finds functions inside let-statement in for-statement initializer': [ - "for (let i = function() {}; ;) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside var-statement inside for-statement initializer': [ - "for (var i = function() {}; ;) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside for-statement condition': [ - "for (; function() {}();) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside for-statement increment': [ - "for (; ;function() {}()) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside for-in-statement': [ - "for (let x in function() {}()) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside for-each statement': [ - "for each (x in function() {}()) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions inside for-of statement': [ - "for (x of (function() {}())) {}\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds function literals used as an object': [ - "f = function() {}.bind();\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds function literals used as an object in a dynamic property expression': [ - "f = function() {}['bind']();\n", - [ - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions on either side of a logical expression': [ - "let f = function(a) {} ||" + - " function(a, b) {};\n", - [ - { key: "(anonymous):1:1", line: 1, n_params: 1 }, - { key: "(anonymous):1:2", line: 1, n_params: 2 } - ], - ], - - 'finds functions on either side of a conditional expression': [ - "let a\n" + - "let f = a ? function(a) {}() :" + - " function(a, b) {}();\n", - [ - { key: "(anonymous):2:1", line: 2, n_params: 1 }, - { key: "(anonymous):2:2", line: 2, n_params: 2 } - ], - ], - - 'finds functions as the argument of a yield statement': [ - "function a() { yield function (){} };\n", - [ - { key: "a:1:0", line: 1, n_params: 0 }, - { key: "(anonymous):1:0", line: 1, n_params: 0 } - ], - ], - - 'finds functions in an array comprehension body': [ - "let a = new Array(1);\n" + - "let b = [function() {} for (i of a)];\n", - [ - { key: "(anonymous):2:0", line: 2, n_params: 0 } - ], - ], - - 'finds functions in an array comprehension block': [ - "let a = new Array(1);\n" + - "let b = [i for (i of function() {})];\n", - [ - { key: "(anonymous):2:0", line: 2, n_params: 0 } - ], - ], - - 'finds functions in an array comprehension filter': [ - "let a = new Array(1);\n" + - "let b = [i for (i of a)" + - "if (function() {}())];\n", - [ - { key: "(anonymous):2:0", line: 2, n_params: 0 } - ], - ], - - 'finds class methods': [ - `class Foo { - bar() {} - }`, - [ - { key: 'bar:2:0', line: 2, n_params: 0 }, - ], - ], - - 'finds class property setters': [ - `class Foo { - set bar(value) {} - }`, - [ - { key: 'set bar:2:1', line: 2, n_params: 1 }, - ], - ], - - 'finds class property getters': [ - `class Foo { - get bar() {} - }`, - [ - { key: 'get bar:2:0', line: 2, n_params: 0 }, - ], - ], - - 'finds class constructors': [ - `class Foo { - constructor(baz) {} - }`, - [ - { key: 'Foo:2:1', line: 2, n_params: 1 }, - ], - ], - - 'finds class expression methods': [ - `void class { - baz() {} - }`, - [ - { key: 'baz:2:0', line: 2, n_params: 0 }, - ], - ], - - 'finds class expression property setters': [ - `void class { - set baz(value) {} - }`, - [ - { key: 'set baz:2:1', line: 2, n_params: 1 }, - ], - ], - - 'finds class expression property getters': [ - `void class { - get baz() {} - }`, - [ - { key: 'get baz:2:0', line: 2, n_params: 0 }, - ], - ], - - 'finds class expression constructors': [ - `void class { - constructor(baz) {} - }`, - [ - { key: '(anonymous):2:1', line: 2, n_params: 1 }, - ], - ], - - 'finds functions inside labeled statement': [ - `loop: - for (function () {}; ; ) {}`, - [ - { key: '(anonymous):2:0', line: 2, n_params: 0 }, - ], - ], - - 'finds functions inside switch expression': [ - 'switch (function () {}) {}', - [ - { key: '(anonymous):1:0', line: 1, n_params: 0 }, - ], - ], - - 'finds functions inside function default arguments': [ - 'function foo(bar=function () {}) {}', - [ - { key: 'foo:1:1', line: 1, n_params: 1 }, - { key: '(anonymous):1:0', line: 1, n_params: 0 }, - ], - ], - - 'finds functions inside function expression default arguments': [ - 'void function foo(bar=function () {}) {}', - [ - { key: 'foo:1:1', line: 1, n_params: 1 }, - { key: '(anonymous):1:0', line: 1, n_params: 0 }, - ], - ], - }; - - Object.keys(testTable).forEach(testcase => { - it(testcase, function () { - const ast = Reflect.parse(testTable[testcase][0]); - let foundFuncs = Coverage.functionsForAST(ast); - expect(foundFuncs).toEqual(testTable[testcase][1]); - }); - }); -}); - -describe('Coverage.branchesForAST', function () { - let testTable = { - 'works with no trailing newline': [ - "if (1) { let a = 1; }", - [ - { point: 1, exits: [1] }, - ], - ], - - 'finds both branch exits for a simple branch': [ - "if (1) {\n" + - " let a = 1;\n" + - "} else {\n" + - " let b = 2;\n" + - "}\n", - [ - { point: 1, exits: [2, 4] } - ], - ], - - 'finds a single exit for a branch with one consequent': [ - "if (1) {\n" + - " let a = 1.0;\n" + - "}\n", - [ - { point: 1, exits: [2] } - ], - ], - - 'finds multiple exits for nested if-else branches': [ - "if (1) {\n" + - " let a = 1.0;\n" + - "} else if (2) {\n" + - " let b = 2.0;\n" + - "} else if (3) {\n" + - " let c = 3.0;\n" + - "} else {\n" + - " let d = 4.0;\n" + - "}\n", - [ - // the 'else if' line is actually an exit for the first branch - { point: 1, exits: [2, 3] }, - { point: 3, exits: [4, 5] }, - // 'else' by itself is not executable, it is the block it - // contains which is - { point: 5, exits: [6, 8] } - ], - ], - - 'finds a simple two-exit branch without blocks': [ - "let a, b;\n" + - "if (1)\n" + - " a = 1.0\n" + - "else\n" + - " b = 2.0\n" + - "\n", - [ - { point: 2, exits: [3, 5] } - ], - ], - - 'does not find a branch if the consequent was empty': [ - "let a, b;\n" + - "if (1) {}\n", - [], - ], - - 'finds a single exit if only the alternate exit was defined': [ - "let a, b;\n" + - "if (1) {}\n" + - "else\n" + - " a++;\n", - [ - { point: 2, exits: [4] } - ], - ], - - 'finds an implicit branch for while statement': [ - "while (1) {\n" + - " let a = 1;\n" + - "}\n" + - "let b = 2;", - [ - { point: 1, exits: [2] } - ], - ], - - 'finds an implicit branch for a do-while statement': [ - "do {\n" + - " let a = 1;\n" + - "} while (1)\n" + - "let b = 2;", - [ - // For do-while loops the branch-point is at the 'do' condition - // and not the 'while' - { point: 1, exits: [2] } - ], - ], - - 'finds all exits for case statements': [ - "let a = 1;\n" + - "switch (1) {\n" + - "case '1':\n" + - " a++;\n" + - " break;\n" + - "case '2':\n" + - " a++\n" + - " break;\n" + - "default:\n" + - " a++\n" + - " break;\n" + - "}\n", - [ - /* There are three potential exits here */ - { point: 2, exits: [4, 7, 10] } - ], - ], - - 'finds all exits for case statements with fallthrough': [ - "let a = 1;\n" + - "switch (1) {\n" + - "case '1':\n" + - "case 'a':\n" + - "case 'b':\n" + - " a++;\n" + - " break;\n" + - "case '2':\n" + - " a++\n" + - " break;\n" + - "default:\n" + - " a++\n" + - " break;\n" + - "}\n", - [ - /* There are three potential exits here */ - { point: 2, exits: [6, 9, 12] } - ], - ], - - 'finds no exits for case statements with only no-ops': [ - "let a = 1;\n" + - "switch (1) {\n" + - "case '1':\n" + - "case '2':\n" + - "default:\n" + - "}\n", - [], - ], - }; - - Object.keys(testTable).forEach(testcase => { - it(testcase, function () { - const ast = Reflect.parse(testTable[testcase][0]); - let foundBranchExits = Coverage.branchesForAST(ast); - expect(foundBranchExits).toEqual(testTable[testcase][1]); - }); - }); -}); - -describe('Coverage', function () { - it('gets the number of lines in the script', function () { - let script = "\n\n"; - let number = Coverage._getNumberOfLinesForScript(script); - expect(number).toEqual(3); - }); - - it('turns zero expression lines into counters', function () { - let expressionLines = []; - let nLines = 1; - let counters = Coverage._expressionLinesToCounters(expressionLines, nLines); - - expect(counters).toEqual([undefined, undefined]); - }); - - it('turns a single expression line into counters', function () { - let expressionLines = [1, 2]; - let nLines = 4; - let counters = Coverage._expressionLinesToCounters(expressionLines, nLines); - - expect(counters).toEqual([undefined, 0, 0, undefined, undefined]); - }); - - it('returns empty array for no branches', function () { - let counters = Coverage._branchesToBranchCounters([], 1); - expect(counters).toEqual([undefined, undefined]); - }); - - describe('branch counters', function () { - const MockFoundBranches = [ - { - point: 5, - exits: [6, 8] - }, - { - point: 1, - exits: [2, 4] - } - ]; - - const MockNLines = 9; - - let counters; - beforeEach(function () { - counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines); - }); - - it('gets same number of counters as number of lines plus one', function () { - expect(counters.length).toEqual(MockNLines + 1); - }); - - it('branches on lines for array indices', function () { - expect(counters[1]).toBeDefined(); - expect(counters[5]).toBeDefined(); - }); - - it('sets exits for branch', function () { - expect(counters[1].exits).toEqual([ - { line: 2, hitCount: 0 }, - { line: 4, hitCount: 0 }, - ]); - }); - - it('sets last exit to highest exit start line', function () { - expect(counters[1].lastExit).toEqual(4); - }); - - it('always has hit initially false', function () { - expect(counters[1].hit).toBeFalsy(); - }); - - describe('branch tracker', function () { - let branchTracker; - beforeEach(function () { - branchTracker = new Coverage._BranchTracker(counters); - }); - - it('sets branch to hit on point execution', function () { - branchTracker.incrementBranchCounters(1); - expect(counters[1].hit).toBeTruthy(); - }); - - it('sets exit to hit on execution', function () { - branchTracker.incrementBranchCounters(1); - branchTracker.incrementBranchCounters(2); - expect(counters[1].exits[0].hitCount).toEqual(1); - }); - - it('finds next branch', function () { - branchTracker.incrementBranchCounters(1); - branchTracker.incrementBranchCounters(2); - branchTracker.incrementBranchCounters(5); - expect(counters[5].hit).toBeTruthy(); - }); - }); - }); - - it('function key from function with name matches schema', function () { - let ast = Reflect.parse('function f(a, b) {}').body[0]; - let functionKeyForFunctionName = - Coverage._getFunctionKeyFromReflectedFunction(ast); - expect(functionKeyForFunctionName).toEqual('f:1:2'); - }); - - it('function key from function without name is anonymous', function () { - let ast = Reflect.parse('\nvoid function (a, b, c) {}').body[0].expression.argument; - let functionKeyForAnonymousFunction = - Coverage._getFunctionKeyFromReflectedFunction(ast); - expect(functionKeyForAnonymousFunction).toEqual('(anonymous):2:3'); - }); - - it('returns a function counter map for function keys', function () { - let ast = Reflect.parse('function name() {}'); - let detectedFunctions = Coverage.functionsForAST(ast); - let functionCounters = - Coverage._functionsToFunctionCounters('script', detectedFunctions); - expect(functionCounters.name['1']['0'].hitCount).toEqual(0); - }); - - it('reports an error when two indistinguishable functions are present', function () { - spyOn(window, 'log'); - let ast = Reflect.parse('() => {}; () => {}'); - let detectedFunctions = Coverage.functionsForAST(ast); - Coverage._functionsToFunctionCounters('script', detectedFunctions); - - expect(window.log).toHaveBeenCalledWith('script:1 Function ' + - 'identified as (anonymous):1:0 already seen in this file. ' + - 'Function coverage will be incomplete.'); - }); - - it('populates a known functions array', function () { - let functions = [ - { line: 1 }, - { line: 2 } - ]; - - let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 4); - - expect(knownFunctionsArray) - .toEqual([undefined, true, true, undefined, undefined]); - }); - - it('converts function counters to an array', function () { - let functionsMap = { - '(anonymous)': { - '2': { - '0': { - hitCount: 1 - }, - }, - }, - 'name': { - '1': { - '0': { - hitCount: 0 - }, - }, - } - }; - - let expectedFunctionCountersArray = [ - jasmine.objectContaining({ name: '(anonymous):2:0', hitCount: 1 }), - jasmine.objectContaining({ name: 'name:1:0', hitCount: 0 }) - ]; - - let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap); - - expect(convertedFunctionCounters).toEqual(expectedFunctionCountersArray); - }); -}); - -describe('Coverage.incrementFunctionCounters', function () { - it('increments for function on same execution start line', function () { - let functionCounters = Coverage._functionsToFunctionCounters('script', [ - { key: 'f:1:0', - line: 1, - n_params: 0 } - ]); - Coverage._incrementFunctionCounters(functionCounters, null, 'f', 1, 0); - - expect(functionCounters.f['1']['0'].hitCount).toEqual(1); - }); - - it('can disambiguate two functions with the same name', function () { - let functionCounters = Coverage._functionsToFunctionCounters('script', [ - { key: '(anonymous):1:0', - line: 1, - n_params: 0 }, - { key: '(anonymous):2:0', - line: 2, - n_params: 0 } - ]); - Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0); - Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 2, 0); - - expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1); - expect(functionCounters['(anonymous)']['2']['0'].hitCount).toEqual(1); - }); - - it('can disambiguate two functions on same line with different params', function () { - let functionCounters = Coverage._functionsToFunctionCounters('script', [ - { key: '(anonymous):1:0', - line: 1, - n_params: 0 }, - { key: '(anonymous):1:1', - line: 1, - n_params: 1 } - ]); - Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0); - Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 1); - - expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1); - expect(functionCounters['(anonymous)']['1']['1'].hitCount).toEqual(1); - }); - - it('can disambiguate two functions on same line by guessing closest params', function () { - let functionCounters = Coverage._functionsToFunctionCounters('script', [ - { key: '(anonymous):1:0', - line: 1, - n_params: 0 }, - { key: '(anonymous):1:3', - line: 1, - n_params: 3 } - ]); - - /* Eg, we called the function with 3 params with just two arguments. We - * should be able to work out that we probably intended to call the - * latter function as opposed to the former. */ - Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 2); - - expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(0); - expect(functionCounters['(anonymous)']['1']['3'].hitCount).toEqual(1); - }); - - it('increments for function on earlier start line', function () { - let ast = Reflect.parse('function name() {}'); - let detectedFunctions = Coverage.functionsForAST(ast); - let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3); - let functionCounters = Coverage._functionsToFunctionCounters('script', - detectedFunctions); - - /* We're entering at line two, but the function definition was actually - * at line one */ - Coverage._incrementFunctionCounters(functionCounters, knownFunctionsArray, 'name', 2, 0); - - expect(functionCounters.name['1']['0'].hitCount).toEqual(1); - }); - - it('throws an error on unexpected function', function () { - let ast = Reflect.parse('function name() {}'); - let detectedFunctions = Coverage.functionsForAST(ast); - let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3); - let functionCounters = Coverage._functionsToFunctionCounters('script', - detectedFunctions); - - /* We're entering at line two, but the function definition was actually - * at line one */ - expect(() => { - Coverage._incrementFunctionCounters(functionCounters, - knownFunctionsArray, - 'doesnotexist', - 2, - 0); - }).toThrow(); - }); - - it('throws if line out of range', function () { - let expressionCounters = [ - undefined, - 0 - ]; - - expect(() => { - Coverage._incrementExpressionCounters(expressionCounters, 'script', 2); - }).toThrow(); - }); - - it('increments if in range', function () { - let expressionCounters = [ - undefined, - 0 - ]; - - Coverage._incrementExpressionCounters(expressionCounters, 'script', 1); - expect(expressionCounters[1]).toEqual(1); - }); - - it('warns if we hit a non-executable line', function () { - spyOn(window, 'log'); - let expressionCounters = [ - undefined, - 0, - undefined - ]; - - Coverage._incrementExpressionCounters(expressionCounters, 'script', 2, - true); - - expect(window.log).toHaveBeenCalledWith("script:2 Executed line " + - "previously marked non-executable by Reflect"); - expect(expressionCounters[2]).toEqual(1); - }); -}); - -describe('Coverage statistics container', function () { - const MockFiles = { - 'prefix/filename': - `function f() { - return 1; - } - if (f()) - f = 0; - `, - 'prefix/uncached': - `function f() { - return 1; - } - `, - 'unprefixed': - `function f() { - return 1; - } - `, - 'prefix/shebang': - `#!/usr/bin/env gjs - function f() {} - `, - }; - - const MockFilenames = Object.keys(MockFiles).concat(['prefix/nonexistent']); - - let container; - - beforeEach(function () { - Coverage.getFileContents = - jasmine.createSpy('getFileContents').and.callFake(f => MockFiles[f]); - Coverage.getFileChecksum = - jasmine.createSpy('getFileChecksum').and.returnValue('abcd'); - Coverage.getFileModificationTime = - jasmine.createSpy('getFileModificationTime').and.returnValue([1, 2]); - container = new Coverage.CoverageStatisticsContainer(['prefix/']); - }); - - it('fetches valid statistics for file', function () { - let statistics = container.fetchStatistics('prefix/filename'); - expect(statistics).toBeDefined(); - - let files = container.getCoveredFiles(); - expect(files).toEqual(['prefix/filename']); - }); - - it('throws for nonexisting file', function () { - expect(() => container.fetchStatistics('prefix/nonexistent')).toThrow(); - }); - - it('handles a shebang on line 1', function () { - let statistics = container.fetchStatistics('prefix/shebang'); - expect(statistics).toBeDefined(); - }); - - it('ignores a file in angle brackets (our convention for programmatic scripts)', function () { - let statistics = container.fetchStatistics('