diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a1a979..dba5cae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,19 +38,19 @@ shared: &shared fi jobs: - "mint19": + "mint20": <<: *shared docker: - - image: linuxmintd/mint19-amd64 + - image: linuxmintd/mint20-amd64 - "lmde3": + "lmde4": <<: *shared docker: - - image: linuxmintd/lmde3-amd64 + - image: linuxmintd/lmde4-amd64 workflows: version: 2 build: jobs: - - "mint19" - - "lmde3" + - "mint20" + - "lmde4" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f24c66a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,344 @@ +# Contributing to GJS # + +## Introduction ## + +Thank you for considering contributing to GJS! +As with any open source project, we can't make it as good as possible +without help from you and others. + +We do have some guidelines for contributing, set out in this file. +Following these guidelines helps communicate that you respect the time +of the developers who work on GJS. +In return, they should reciprocate that respect in addressing your +issue, reviewing your work, and helping finalize your merge requests. + +### What kinds of contributions we are looking for ### + +There are many ways to contribute to GJS, not only writing code. +We encourage all of them. +You can write example programs, tutorials, or blog posts; improve the +documentation; [submit bug reports and feature requests][bugtracker]; +triage existing bug reports; vote on issues with a thumbs-up or +thumbs-down; or write code which is incorporated into [GJS itself][gjs]. + +### What kinds of contributions we are not looking for ### + +Please don't use the [issue tracker][bugtracker] for support questions. +Instead, check out the [#javascript][irc] IRC channel on irc.gnome.org. +You can also try the [javascript-list][mailinglist] mailing list, or +Stack Overflow. + +If you are writing code, please do not submit merge requests that only +fix linter errors in code that you are not otherwise changing (unless +you have discussed it in advance with a maintainer on [IRC][irc].) + +When writing code or submitting a feature request, make sure to first +read the section below titled "Roadmap". +Contributions that run opposite to the roadmap are not likely to be +accepted. + +## Ground Rules ## + +Your responsibilities as a contributor: + +- Be welcoming and encouraging to newcomers. +- Conduct yourself professionally; rude, abusive, harrassing, or + discriminatory behaviour is not tolerated. +- For any major changes and enhancements you want to make, first create + an issue in the [bugtracker], discuss things transparently, and get + community feedback. +- Ensure all jobs are green on GitLab CI for your merge requests. +- Your code must pass the tests. +- Your code must pass the linters; code should not introduce any new + linting errors. +- Your code should not cause any compiler warnings. +- Add tests for any new functionality you write, and regression tests + for any bugs you fix. + +## Your First Contribution ## + +Unsure where to start? + +Try looking through the [issues labeled "Newcomers"][newcomers]. +We try to have these issues contain some step-by-step explanation on how +a newcomer might approach them. +If that explanation is missing from an issue marked "Newcomers", feel +free to leave a comment on there asking for help on how to get started. + +[Issues marked "Help Wanted"][helpwanted] may be a bit more involved +than the Newcomers issues, but many of them still do not require +in-depth familiarity with GJS. + +## How to contribute documentation or tutorials ## + +If you don't have an account on [gitlab.gnome.org], first create one. + +Some contributions are done in different places than the main GJS +repository. +To contribute to the documentation, go to the [DevDocs][devdocs] +repository. +To contribute to tutorials, go to [GJS Guide][gjsguide]. + +Next, read the [workflow guide to contributing to GNOME][workflow]. +(In short, create a fork of the repository, make your changes on a +branch, push them to your fork, and create a merge request.) + +If your contribution fixes an existing issue, please refer to the issue +in your commit message with `Closes #123` (for issue 123). +Otherwise, creating a separate issue is not required. +See the section on "Commit messages" below. + +When you submit your merge request, make sure to click "Allow commits +from contributors with push access". +This is so that the maintainers can re-run the GitLab CI jobs, since +there is currently a bug in the infrastructure that makes some of the +jobs fail unnecessarily. + +!157 is an example of a small documentation bugfix in a merge request. + +That's all! + +## How to contribute code ## + +To contribute code, follow the instructions above for contributing +documentation. +There are further instructions for how to set up a development +environment and install the correct tools for GJS development in the +[Hacking.md][hacking] file. + +## How to report a bug ## + +If you don't have an account on [gitlab.gnome.org], first create one. +Go to the [issue tracker][bugtracker] and click "New issue". + +Use the "bug" template when reporting a bug. +Make sure to answer the questions in the template, as otherwise it might +make your bug harder to track down. + +_If you find a security vulnerability,_ make sure to mark the issue as +"confidential"! + +If in doubt, ask on [IRC][irc] whether you should report a bug about +something, but generally it's OK to just go ahead. + +Bug report #170 is a good example of a bug report with an independently +runnable code snippet for testing, and lots of information, although it +was written before the templates existed. + +## How to suggest a feature or enhancement ## + +If you find yourself wishing for a feature that doesn't exist in GJS, +you are probably not alone. +Open an issue on our [issue tracker][bugtracker] which describes the +feature you would like to see, why you need it, and how it should work. +Use the "feature" template for this. +However, for a new feature, the likelihood that it will be implemented +goes way up if you or someone else plans to submit a merge request along +with it. + +If the feature is small enough that you won't feel like your time was +wasted if we decide not to adopt it, you can just submit a merge +request rather than going to the issue tracker. +Make sure to explain why you think it's a good feature to have! +!213 is an example of a small feature suggestion that was submitted as a +merge request. + +In cases where you've seen something that needs to be fixed or +refactored in the code, it's OK not to use a template. +It's OK to be less rigorous here, since this type of report is usually +used by people who plan to fix the issue themselves later. + +## How to triage bugs ## + +You can help the maintainers by examining the existing bug reports in +the bugtracker and adding instructions to reproduce them, or +categorizing them with the correct labels. + +For bugs that cause a crash (segmentation fault, not just a JS +exception) use the "1. Crash" label. +For other bugs, use the "1. Bug" label. +Feature requests should get the "1. Feature" label. +Any crashes, or bugs that prevent most or all users from using GJS or +GNOME Shell, should also get the "To Do" label. + +If some information is missing from the bug (for example, you can't +reproduce it based on their instructions,) add the "2. Needs +information" label. + +Add any topic labels from the "5" group (e.g. "5. Performance") as you +see fit. + +As for reproducer instructions, a small, self-contained JS program that +exhibits the bug, to be run with the command-line `gjs` interpreter, is +best. +Instructions that provide code to be loaded as a GNOME Shell extension +are less helpful, because they are more tedious to test. + +## Code review process ## + +Once you have submitted your merge request, a maintainer will review it. +You should get a first response within a few days. +Sometimes maintainers are busy; if it's been a week and you've heard +nothing, feel free to ping the maintainer and ask for an estimate of +when they might be able to review the merge request. + +You might get a review even if some of the GitLab CI jobs have not yet +succeeded. +In that case, acceptance of the merge request is contingent on fixing +whatever needs to be fixed to get all the jobs to turn green. + +In general, unless the merge request is very simple, it will not be +ready to accept immediately. +You should normally expect one to three rounds of code review, depending +on the size and complexity of the merge request. +Be prepared to accept constructive criticism on your code and to work on +improving it before it's merged; code review comments don't mean it's +bad. + +!242 is an example of a bug fix merge request with a few code review +comments on it, if you want to get a feel for the process. + +Contributors with a GNOME developer account have automatic push access +to the main GJS repository. +However, even if you have this access, you are still expected to submit +a merge request and have a GJS maintainer review it. +The exception to this is if there is an emergency such as GNOME +Continuous being broken. + +## Community ## + +For general questions and support, visit the [#javascript][irc] channel +on irc.gnome.org. + +The maintainers are listed in the [DOAP file][doap] in the root of the +repository. + +## Roadmap and Philosophy ## + +This section explains what kinds of changes we do and don't intend to +make in GJS in the future and what direction we intend to take the +project. + +Internally, GJS uses Firefox's Javascript engine, called SpiderMonkey. + +First of all, we will not consider switching GJS to use a different +Javascript engine. +If you believe that should be done, the best way to make it happen is to +start a new project, copy GJS's regression test suite, and make sure all +the tests pass and you can run GNOME Shell with it. + +Every year when a new ESR (extended support release) of Firefox appears, +we try to upgrade GJS to use the accompanying version of SpiderMonkey +as soon as possible. +Sometimes upgrading SpiderMonkey requires breaking backwards +compatibility, and in that case we try to make it as easy as possible +for existing code to adapt. + +Other than the above exception, we avoid all changes that break existing +code, even if they would be convenient. +However, it is OK to break compatibility with GJS's documented behaviour +if in practice the behaviour never actually worked as documented. +(That happens more often than you might think.) + +We also try to avoid surprises for people who are used to modern ES +standard Javascript, so custom GJS classes should not deviate from the +behaviour that people would be used to in the standard. + +The Node.js ecosystem is quite popular and many Javascript developers +are accustomed to it. +In theory, we would like to move in the direction of providing all the +same facilities as Node.js, but we do not necessarily want to copy the +exact way things work in Node.js. +The platforms are different and so the implementations sometimes need to +be different too. + +The module system in GJS should be considered legacy. +We don't want to make big changes to it or add any features. +Instead, we want to enable ES6-style imports for the GJS platform. + +We do have some overrides for GNOME libraries such as GLib, to make +their APIs more Javascript-like. +However, we like to keep these to a minimum, so that GNOME code remains +straightforward to read if you are used to using the GNOME libraries in +other programming languages. + +GJS was originally written in C, and the current state of the C++ code +reflects that. +Gradually, we want to move the code to a more idiomatic C++ style, using +smart pointer classes such as `GjsAutoChar` to help avoid memory leaks. +Even farther in the future, we expect the Rust bindings for SpiderMonkey +to mature as Mozilla's Servo browser engine progresses, and we may +consider rewriting part or all of GJS in Rust. + +We believe in automating as much as possible to prevent human error. +GJS is a complex program that powers a lot of GNOME, so breakages can be +have far-reaching effects in other programs. +We intend to move in the direction of having more static code checking +in the future. +We would also like to have more automated integration testing, for +example trying to start a GNOME Shell session with each new change in +GJS. + +Lastly, changes should in principle be compatible with other platforms +than only Linux and GNOME. +Although we don't have automated testing for other platforms, we will +occasionally build and test things there, and gladly accept +contributions to fix breakages on other platforms. + +## Conventions ## + +### Coding style ### + +We use the [Google style guide][googlestyle] for C++ code, with a few +exceptions, 4-space indents being the main one. +There is a handy git commit hook that will autoformat your code when you +commit it; see the [Hacking.md][hacking] file. + +For C++ coding style concerns that can't be checked with a linter or an +autoformatter, read the [CPP_Style_Guide.md][cppstyle] file. + +For Javascript code, an [ESLint configuration file][eslint] is included +in the root of the GJS repository. +This is not integrated with a git commit hook, so you need to manually +manually sure that all your new code conforms to the style. +Don't rewrite old code with `eslint --fix` unless you are already +changing that code for some other reason. + +### Commit messages ### + +The title of the commit should say what you changed, and the body of the +commit message should explain why you changed it. +We look in the commit history quite often to figure out why code was +written a certain way, so it's important to justify each change so that +in the future people will realize why it was needed. + +For further guidelines about line length and commit messages, read +[this guide][commitmessages]. + +If the commit is related to an open issue in the issue tracker, note +that on the last line of the commit message. For example, `See #153`, or +`Closes #277` if the issue should be automatically closed when the merge +request is accepted. + +## Thanks ## + +Thanks to [@nayafia][contributingtemplate] for the inspiration to write +this guide! + +[gitlab.gnome.org]: https://gitlab.gnome.org +[bugtracker]: https://gitlab.gnome.org/GNOME/gjs/issues +[gjs]: https://gitlab.gnome.org/GNOME/gjs +[irc]: https://riot.im/app/#/room/#_gimpnet_#javascript:matrix.org +[mailinglist]: https://mail.gnome.org/mailman/listinfo/javascript-list +[newcomers]: https://gitlab.gnome.org/GNOME/gjs/issues?label_name%5B%5D=4.+Newcomers +[helpwanted]: https://gitlab.gnome.org/GNOME/gjs/issues?label_name%5B%5D=4.+Help+Wanted +[devdocs]: https://github.com/ptomato/devdocs +[gjsguide]: https://gitlab.gnome.org/rockon999/gjs-guide +[workflow]: https://wiki.gnome.org/GitLab#Using_a_fork_-_Non_GNOME_developer +[hacking]: https://gitlab.gnome.org/GNOME/gjs/blob/master/doc/Hacking.md +[doap]: https://gitlab.gnome.org/GNOME/gjs/blob/master/gjs.doap +[googlestyle]: https://google.github.io/styleguide/cppguide.html +[cppstyle]: https://gitlab.gnome.org/GNOME/gjs/blob/master/doc/CPP_Style_Guide.md +[eslint]: https://eslint.org/ +[commitmessages]: https://chris.beams.io/posts/git-commit/ +[contributingtemplate]: https://github.com/nayafia/contributing-template diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 0000000..1d7ab35 --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,13 @@ +# This is the toplevel CPPLINT.cfg file +set noparent + +# We give a limit to clang-format of 80, but we allow 100 here for cases where +# it really is more readable to have a longer line +linelength=100 + +# Exceptions to Google style +# - build/include_order: We have a special order for include files, see "Header +# inclusion order" in CPP_Style_Guide.md. +# - build/c++11: This rule bans certain C++ standard library features, which +# have their own alternatives in the Chromium codebase, doesn't apply to us. +filter=-build/include_order,-build/c++11 diff --git a/Makefile-examples.am b/Makefile-examples.am deleted file mode 100644 index 43df2de..0000000 --- a/Makefile-examples.am +++ /dev/null @@ -1,6 +0,0 @@ -EXTRA_DIST += \ - examples/clutter.js \ - examples/gio-cat.js \ - examples/gtk.js \ - examples/http-server.js \ - examples/test.jpg diff --git a/Makefile-insttest.am b/Makefile-insttest.am deleted file mode 100644 index 650c7c1..0000000 --- a/Makefile-insttest.am +++ /dev/null @@ -1,45 +0,0 @@ -EXTRA_DIST += \ - installed-tests/minijasmine.test.in \ - installed-tests/script.test.in \ - installed-tests/js/jsunit.gresources.xml \ - $(NULL) - -gjsinsttestdir = $(pkglibexecdir)/installed-tests -installedtestmetadir = $(datadir)/installed-tests/cjs -jstestsdir = $(gjsinsttestdir)/js -jsscripttestsdir = $(gjsinsttestdir)/scripts - -gjsinsttest_PROGRAMS = -gjsinsttest_DATA = -installedtestmeta_DATA = -jstests_DATA = -jsscripttests_DATA = -pkglib_LTLIBRARIES = - -if BUILDOPT_INSTALL_TESTS - -gjsinsttest_PROGRAMS += minijasmine -gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS) -installedtestmeta_DATA += \ - $(jasmine_tests:.js=.test) \ - $(simple_tests:%=%.test) \ - $(NULL) -jstests_DATA += $(jasmine_tests) -jsscripttests_DATA += $(simple_tests) -pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la - -%.test: %.js installed-tests/minijasmine.test.in Makefile - $(AM_V_GEN)$(MKDIR_P) $(@D) && \ - $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir),g \ - -e s,@name\@,$(notdir $<), \ - < $(srcdir)/installed-tests/minijasmine.test.in > $@.tmp && \ - mv $@.tmp $@ - -%.test: % installed-tests/script.test.in Makefile - $(AM_V_GEN)$(MKDIR_P) $(@D) && \ - $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir), \ - -e s,@name\@,$(notdir $<), \ - < $(srcdir)/installed-tests/script.test.in > $@.tmp && \ - mv $@.tmp $@ - -endif BUILDOPT_INSTALL_TESTS diff --git a/Makefile-modules.am b/Makefile-modules.am deleted file mode 100644 index 04d06c7..0000000 --- a/Makefile-modules.am +++ /dev/null @@ -1,45 +0,0 @@ - -NATIVE_MODULES = libconsole.la libsystem.la libmodules_resources.la - -if ENABLE_CAIRO -NATIVE_MODULES += libcairoNative.la -endif - -noinst_LTLIBRARIES += $(NATIVE_MODULES) -libcjs_la_LIBADD += $(NATIVE_MODULES) - -JS_NATIVE_MODULE_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - -DGJS_COMPILATION \ - $(GJS_CFLAGS) \ - $(GJS_PRIVATE_CFLAGS) -JS_NATIVE_MODULE_LIBADD = \ - $(GJS_LIBS) \ - $(GJS_PRIVATE_LIBS) - -modules_resource_files := $(shell glib-compile-resources --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/modules/modules.gresource.xml) -modules-resources.h: $(srcdir)/modules/modules.gresource.xml $(modules_resource_files) - $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) --generate --c-name modules_resources $< -modules-resources.c: $(srcdir)/modules/modules.gresource.xml $(modules_resource_files) - $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) --generate --c-name modules_resources $< - -EXTRA_DIST += $(modules_resource_files) $(srcdir)/modules/modules.gresource.xml - -include gjs-modules-srcs.mk - -nodist_libmodules_resources_la_SOURCES = $(module_resource_srcs) -CLEANFILES += $(nodist_libmodules_resources_la_SOURCES) -libmodules_resources_la_CPPFLAGS = $(JS_NATIVE_MODULE_CPPFLAGS) -libmodules_resources_la_LIBADD = $(JS_NATIVE_MODULE_LIBADD) - -libcairoNative_la_CPPFLAGS = $(JS_NATIVE_MODULE_CPPFLAGS) $(GJS_CAIRO_CFLAGS) $(GJS_CAIRO_XLIB_CFLAGS) -libcairoNative_la_LIBADD = $(JS_NATIVE_MODULE_LIBADD) $(GJS_CAIRO_LIBS) $(GJS_CAIRO_XLIB_LIBS) -libcairoNative_la_SOURCES = $(module_cairo_srcs) - -libsystem_la_CPPFLAGS = $(JS_NATIVE_MODULE_CPPFLAGS) -libsystem_la_LIBADD = $(JS_NATIVE_MODULE_LIBADD) -libsystem_la_SOURCES = $(module_system_srcs) - -libconsole_la_CPPFLAGS = $(JS_NATIVE_MODULE_CPPFLAGS) -libconsole_la_LIBADD = $(JS_NATIVE_MODULE_LIBADD) $(READLINE_LIBS) -libconsole_la_SOURCES = $(module_console_srcs) diff --git a/Makefile-test.am b/Makefile-test.am deleted file mode 100644 index b8b92cc..0000000 --- a/Makefile-test.am +++ /dev/null @@ -1,320 +0,0 @@ -EXTRA_DIST += \ - test/test-bus.conf \ - test/run-test \ - $(NULL) - -if XVFB_TESTS -XVFB_INVOCATION = $(XVFB) -ac -noreset -screen 0 1024x768x16 -XIDS = 101 102 103 104 105 106 107 197 199 211 223 227 293 307 308 309 310 311 \ - 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 \ - 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 \ - 1008 1009 4703 4721 4723 4729 4733 4751 9973 9974 9975 9976 9977 9978 9979 \ - 9980 9981 9982 9983 9984 9985 9986 9987 9988 9989 9990 9991 9992 9993 9994 \ - 9995 9996 9997 9998 9999 -XVFB_START = \ -${XVFB_INVOCATION} -help 2>/dev/null 1>&2 \ - && XID=`for id in $(XIDS) ; do test -e /tmp/.X$$id-lock || { echo $$id; exit 0; }; done; exit 1` \ - && { ${XVFB} :$$XID -nolisten tcp -auth /dev/null >/dev/null 2>&1 & \ - trap "kill -15 $$! " 0 HUP INT QUIT TRAP USR1 PIPE TERM ; } \ - || { echo "Gtk+Tests:ERROR: Failed to start Xvfb environment for X11 target tests."; exit 1; } \ - && export DISPLAY=:$$XID; -else -XVFB_START = -endif - -### TEST RESOURCES ##################################################### - -mock_js_resources_files := $(shell glib-compile-resources --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/test/mock-js-resources.gresource.xml) -mock-js-resources.h: $(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-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 $< - -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 $< -jsunit-resources.c: $(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 $< - -BUILT_SOURCES += mock-js-resources.c jsunit-resources.h jsunit-resources.c - -EXTRA_DIST += \ - $(mock_js_resources_files) \ - $(srcdir)/test/mock-js-resources.gresource.xml \ - $(jsunit_resources_files) \ - $(NULL) - -CLEANFILES += \ - mock-js-resources.c \ - jsunit-resources.c \ - jsunit-resources.h \ - $(NULL) - -### TEST PROGRAMS ###################################################### - -# gjs-tests.gtester checks private APIs and is run only uninstalled, -# on "make check". -# -# jsunit checks public APIs using JS test scripts, and is run on "make check", -# as well as installed if --enable-installed-tests is given at configure time. -# See Makefile-insttest.am for the build rules installing the tests. - -check_PROGRAMS += gjs-tests.gtester minijasmine - -gjs_tests_gtester_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - -DGJS_COMPILATION \ - $(GJSTESTS_CFLAGS) \ - $(gjs_directory_defines) \ - -I$(top_srcdir)/test - -gjs_tests_gtester_LDADD = \ - libcjs.la \ - $(GJSTESTS_LIBS) - -gjs_tests_gtester_SOURCES = \ - test/gjs-tests.cpp \ - test/gjs-test-utils.cpp \ - test/gjs-test-utils.h \ - test/gjs-test-call-args.cpp \ - test/gjs-test-coverage.cpp \ - test/gjs-test-rooting.cpp \ - mock-js-resources.c \ - $(NULL) - -minijasmine_SOURCES = \ - installed-tests/minijasmine.cpp \ - jsunit-resources.c \ - jsunit-resources.h \ - $(NULL) - -minijasmine_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - $(GJS_CFLAGS) \ - $(GJS_PRIVATE_CFLAGS) \ - -I$(top_srcdir) \ - -DINSTTESTDIR=\"$(gjsinsttestdir)\" \ - -DPKGLIBDIR=\"$(pkglibdir)\" \ - $(NULL) - -minijasmine_LDADD = $(GJS_LIBS) $(GJS_PRIVATE_LIBS) libcjs.la - -### TEST GIRS ########################################################## - -TEST_INTROSPECTION_GIRS = -common_test_ldflags = $(AM_LDFLAGS) -avoid-version -common_test_libadd = $(GJS_LIBS) $(GJS_PRIVATE_LIBS) - -if !BUILDOPT_INSTALL_TESTS -# This rpath /nowhere thing is the libtool upstream recommended way to -# force generation of shared libraries, which we need in order for the -# tests to work uninstalled. -common_test_ldflags += -rpath /nowhere -# In the installed tests case, these libraries are built for pkglibdir; sadly we -# can only have one destination at a time -check_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la -endif - -# These sources are installed as part of gobject-introspection, and symlinked -# into the build directory at configure time. They should not be distributed -# with GJS. -nodist_libregress_la_SOURCES = \ - installed-tests/regress.c \ - installed-tests/regress.h \ - $(NULL) -libregress_la_CPPFLAGS = $(AM_CPPFLAGS) $(GJS_CFLAGS) $(GJS_PRIVATE_CFLAGS) $(GJS_GDBUS_CFLAGS) -libregress_la_LDFLAGS = $(common_test_ldflags) $(GJS_GDBUS_LIBS) -libregress_la_LIBADD = $(common_test_libadd) -libregress_scannerflags_includes = --include=Gio-2.0 -if ENABLE_CAIRO -libregress_la_CPPFLAGS += $(GJS_CAIRO_CFLAGS) -libregress_la_LDFLAGS += $(GJS_CAIRO_LIBS) -libregress_scannerflags_includes += --include=cairo-1.0 -else -libregress_la_CPPFLAGS += -D_GI_DISABLE_CAIRO -endif -Regress-1.0.gir: libregress.la -Regress_1_0_gir_LIBS = libregress.la -Regress_1_0_gir_FILES = $(nodist_libregress_la_SOURCES) -Regress_1_0_gir_SCANNERFLAGS = \ - --warn-all --warn-error \ - $(libregress_scannerflags_includes) \ - $(WARN_SCANNERFLAGS) \ - $(NULL) -if !ENABLE_CAIRO -Regress_1_0_gir_CFLAGS = -D_GI_DISABLE_CAIRO -endif -TEST_INTROSPECTION_GIRS += Regress-1.0.gir - -nodist_libwarnlib_la_SOURCES = \ - installed-tests/warnlib.c \ - installed-tests/warnlib.h \ - $(NULL) -libwarnlib_la_CPPFLAGS = $(AM_CPPFLAGS) $(GJS_CFLAGS) $(GJS_PRIVATE_CFLAGS) -libwarnlib_la_LDFLAGS = $(common_test_ldflags) -libwarnlib_la_LIBADD = $(common_test_libadd) -WarnLib-1.0.gir: libwarnlib.la -WarnLib_1_0_gir_LIBS = libwarnlib.la -WarnLib_1_0_gir_INCLUDES = Gio-2.0 -WarnLib_1_0_gir_FILES = $(nodist_libwarnlib_la_SOURCES) -WarnLib_1_0_gir_SCANNERFLAGS = \ - --c-include="warnlib.h" \ - --symbol-prefix=warnlib_ \ - $(NULL) -TEST_INTROSPECTION_GIRS += WarnLib-1.0.gir - -nodist_libgimarshallingtests_la_SOURCES = \ - installed-tests/gimarshallingtests.c \ - installed-tests/gimarshallingtests.h \ - $(NULL) -libgimarshallingtests_la_CPPFLAGS = $(AM_CPPFLAGS) $(GJS_CFLAGS) $(GJS_PRIVATE_CFLAGS) -libgimarshallingtests_la_LDFLAGS = $(common_test_ldflags) -libgimarshallingtests_la_LIBADD = $(common_test_libadd) -GIMarshallingTests-1.0.gir: libgimarshallingtests.la -GIMarshallingTests_1_0_gir_LIBS = libgimarshallingtests.la -GIMarshallingTests_1_0_gir_INCLUDES = Gio-2.0 -GIMarshallingTests_1_0_gir_FILES = $(nodist_libgimarshallingtests_la_SOURCES) -GIMarshallingTests_1_0_gir_SCANNERFLAGS = \ - --symbol-prefix=gi_marshalling_tests \ - --warn-all --warn-error \ - $(WARN_SCANNERFLAGS) \ - $(NULL) -TEST_INTROSPECTION_GIRS += GIMarshallingTests-1.0.gir - -$(foreach gir,$(TEST_INTROSPECTION_GIRS),$(eval $(call introspection-scanner,$(gir)))) -TEST_INTROSPECTION_TYPELIBS = $(TEST_INTROSPECTION_GIRS:.gir=.typelib) -noinst_DATA += $(TEST_INTROSPECTION_TYPELIBS) -CLEANFILES += $(TEST_INTROSPECTION_GIRS) $(TEST_INTROSPECTION_TYPELIBS) - -### JAVASCRIPT TESTS ################################################### - -common_jstests_files = \ - installed-tests/js/testself.js \ - installed-tests/js/testByteArray.js \ - installed-tests/js/testExceptions.js \ - installed-tests/js/testEverythingBasic.js \ - installed-tests/js/testEverythingEncapsulated.js \ - installed-tests/js/testFormat.js \ - installed-tests/js/testFundamental.js \ - installed-tests/js/testGettext.js \ - installed-tests/js/testGIMarshalling.js \ - installed-tests/js/testGLib.js \ - installed-tests/js/testGObjectClass.js \ - installed-tests/js/testGObjectInterface.js \ - installed-tests/js/testGTypeClass.js \ - installed-tests/js/testGio.js \ - installed-tests/js/testImporter.js \ - installed-tests/js/testLang.js \ - installed-tests/js/testLegacyClass.js \ - installed-tests/js/testLegacyGObject.js \ - installed-tests/js/testLocale.js \ - installed-tests/js/testMainloop.js \ - installed-tests/js/testNamespace.js \ - installed-tests/js/testPackage.js \ - installed-tests/js/testParamSpec.js \ - installed-tests/js/testSignals.js \ - installed-tests/js/testSystem.js \ - installed-tests/js/testTweener.js \ - $(NULL) - -jasmine_tests = $(common_jstests_files) - -if DBUS_TESTS -jasmine_tests += installed-tests/js/testGDBus.js -endif - -if ENABLE_GTK -jasmine_tests += \ - installed-tests/js/testGtk.js \ - installed-tests/js/testGObjectDestructionAccess.js \ - installed-tests/js/testLegacyGtk.js \ - $(NULL) -endif - -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/testGObjectDestructionAccess.js \ - installed-tests/js/testLegacyGtk.js \ - installed-tests/extra/gjs.supp \ - installed-tests/extra/lsan.supp \ - $(NULL) - -### TEST EXECUTION ##################################################### - -#@VALGRIND_CHECK_RULES@ -VALGRIND_SUPPRESSIONS_FILES = \ - $(datadir)/glib-2.0/valgrind/glib.supp \ - $(top_srcdir)/installed-tests/extra/gjs.supp \ - $(NULL) - -if DBUS_TESTS -DBUS_SESSION_COMMAND = $(DBUS_RUN_SESSION) --config-file=$(srcdir)/test/test-bus.conf -- -else -DBUS_SESSION_COMMAND = -endif - -if CODE_COVERAGE_ENABLED -COVERAGE_TESTS_ENVIRONMENT = \ - export GJS_UNIT_COVERAGE_OUTPUT=lcov; \ - export GJS_UNIT_COVERAGE_PREFIX=resource:///org/gnome/gjs/; \ - $(NULL) -else -COVERAGE_TESTS_ENVIRONMENT = -endif - -# GJS_PATH is empty here since we want to force the use of our own -# resources. G_FILENAME_ENCODING ensures filenames are not UTF-8. -AM_TESTS_ENVIRONMENT = \ - export TOP_BUILDDIR="$(abs_top_builddir)"; \ - export GJS_USE_UNINSTALLED_FILES=1; \ - export GJS_PATH=; \ - export GI_TYPELIB_PATH="$(builddir):$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH}"; \ - export LD_LIBRARY_PATH="$(builddir)/.libs:$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}"; \ - 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) \ - $(NULL) - -simple_tests = \ - installed-tests/scripts/testCommandLine.sh \ - installed-tests/scripts/testWarnings.sh \ - $(NULL) -EXTRA_DIST += $(simple_tests) - -TESTS = \ - gjs-tests.gtester \ - $(simple_tests) \ - $(jasmine_tests) \ - $(NULL) - -TEST_EXTENSIONS = .gtester .sh .js - -LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh - -GTESTER_LOG_DRIVER = $(LOG_DRIVER) -GTESTER_LOG_COMPILER = $(top_srcdir)/test/run-test - -SH_LOG_DRIVER = $(LOG_DRIVER) - -JS_LOG_DRIVER = $(LOG_DRIVER) -JS_LOG_COMPILER = $$LOG_COMPILER $$LOG_FLAGS $(top_builddir)/minijasmine - -CODE_COVERAGE_IGNORE_PATTERN = */{include,mfbt,gjs/test}/* -CODE_COVERAGE_GENHTML_OPTIONS = \ - $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) \ - lcov/coverage.lcov \ - --prefix $(abs_top_builddir)/lcov/org/gnome/gjs \ - --prefix $(abs_top_builddir) \ - $(NULL) -@CODE_COVERAGE_RULES@ diff --git a/Makefile.am b/Makefile.am deleted file mode 100644 index 444ed93..0000000 --- a/Makefile.am +++ /dev/null @@ -1,228 +0,0 @@ -# http://people.gnome.org/~walters/docs/build-api.txt -.buildapi-allow-builddir: - --include $(INTROSPECTION_MAKEFILE) - -bin_PROGRAMS = -lib_LTLIBRARIES = -noinst_HEADERS = -noinst_LTLIBRARIES = -noinst_DATA = -dist_gjsjs_DATA = -BUILT_SOURCES = -CLEANFILES = -EXTRA_DIST = -check_PROGRAMS = -check_LTLIBRARIES = -INTROSPECTION_GIRS = -## ACLOCAL_AMFLAGS can be removed for Automake 1.13 -ACLOCAL_AMFLAGS = -I m4 -AM_CFLAGS = $(WARN_CFLAGS) $(CODE_COVERAGE_CFLAGS) -AM_CXXFLAGS = $(WARN_CXXFLAGS) $(CODE_COVERAGE_CXXFLAGS) -AM_CPPFLAGS = -DG_LOG_DOMAIN=\"Cjs\" $(CODE_COVERAGE_CPPFLAGS) -AM_LDFLAGS = $(WARN_LDFLAGS) $(CODE_COVERAGE_LIBS) -MAINTAINERCLEANFILES = \ - $(GITIGNORE_MAINTAINERCLEANFILES_TOPLEVEL) \ - $(GITIGNORE_MAINTAINERCLEANFILES_MAKEFILE_IN) \ - $(GITIGNORE_MAINTAINERCLEANFILES_M4_LIBTOOL) \ - tap-driver.sh \ - $(NULL) -GITIGNOREFILES = INSTALL m4 - -gjsjsdir = @gjsjsdir@ -gjsoverridedir = $(gjsjsdir)/overrides - -gjs_public_includedir = $(includedir)/cjs-1.0 - -######################################################################## -include gjs-srcs.mk -######################################################################## -nobase_gjs_public_include_HEADERS = $(gjs_public_headers) - -######################################################################## -pkgconfig_DATA = cjs-1.0.pc - -EXTRA_DIST += cjs-1.0.pc.in - -######################################################################## -gjs_directory_defines = \ - -DGJS_TOP_SRCDIR=\"$(top_srcdir)\" \ - -DGJS_TOP_BUILDDIR=\"$(top_builddir)\" \ - -DGJS_JS_DIR=\"$(gjsjsdir)\" \ - -DPKGLIBDIR=\"$(pkglibdir)\" - -######################################################################## -lib_LTLIBRARIES += libcjs.la - -libcjs_la_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - $(GJS_CFLAGS) \ - $(GJS_PRIVATE_CFLAGS) \ - $(gjs_directory_defines)\ - -I$(top_srcdir)/gi \ - -DGJS_COMPILATION -libcjs_la_LDFLAGS = \ - $(AM_LDFLAGS) \ - -export-symbols-regex "^[^_]" \ - -version-info 0:0:0 \ - $(NO_UNDEFINED_FLAG) \ - $(NULL) -libcjs_la_LIBADD = \ - $(GJS_LIBS) \ - $(GJS_PRIVATE_LIBS) - -if ENABLE_GTK -libcjs_la_CPPFLAGS += $(GJS_GTK_CFLAGS) -libcjs_la_LIBADD += $(GJS_GTK_LIBS) -endif - -# Please see gjs-srcs.mk for brief expanations -# of the layout of the sources due to historical -# 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) - -if ENABLE_GTK -libgjs_private_source_files += $(gjs_gtk_private_srcs) -endif - -libcjs_la_SOURCES += $(libgjs_private_source_files) - -CjsPrivate-1.0.gir: libcjs.la -CjsPrivate_1_0_gir_LIBS = libcjs.la -CjsPrivate_1_0_gir_INCLUDES = GObject-2.0 Gio-2.0 -CjsPrivate_1_0_gir_CFLAGS = -I$(top_srcdir) -CjsPrivate_1_0_gir_FILES = $(libgjs_private_source_files) -CjsPrivate_1_0_gir_SCANNERFLAGS = \ - --identifier-prefix=Gjs \ - --symbol-prefix=gjs_ \ - --warn-all \ - $(WARN_SCANNERFLAGS) \ - $(NULL) - -if ENABLE_GTK -CjsPrivate_1_0_gir_INCLUDES += Gtk-3.0 -endif - -INTROSPECTION_GIRS += CjsPrivate-1.0.gir - -if ENABLE_DTRACE -gjs_gi_probes.h: gi/gjs_gi_probes.d - $(DTRACE) -C -h -s $< -o $@ -gjs_gi_probes.o: gi/gjs_gi_probes.d - $(DTRACE) -G -s $< -o $@ -BUILT_SOURCES += gjs_gi_probes.h gjs_gi_probes.o -libcjs_la_LIBADD += gjs_gi_probes.o -endif -EXTRA_DIST += gi/gjs_gi_probes.d - -tapset_in_files = cjs/cjs.stp.in -EXTRA_DIST += $(tapset_in_files) -if ENABLE_SYSTEMTAP -cjs/cjs.stp: cjs/cjs.stp.in Makefile - $(AM_V_GEN)$(MKDIR_P) $(@D) && \ - $(SED) -e s,@EXPANDED_LIBDIR@,$(libdir), < $< > $@.tmp && mv $@.tmp $@ -tapsetdir = $(datadir)/systemtap/tapset -tapset_DATA = $(tapset_in_files:.stp.in=.stp) -endif - -include Makefile-modules.am -include Makefile-examples.am - -typelibdir = $(pkglibdir)/girepository-1.0 -typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib) - -CLEANFILES += $(INTROSPECTION_GIRS) $(typelib_DATA) - -######################################################################## -bin_PROGRAMS += cjs-console - -cjs_console_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - $(GJS_CONSOLE_CFLAGS) \ - $(NULL) -cjs_console_LDADD = \ - $(GJS_CONSOLE_LIBS) \ - libcjs.la -cjs_console_LDFLAGS = $(AM_LDFLAGS) -rdynamic -cjs_console_SOURCES = $(gjs_console_srcs) - -install-exec-hook: - (cd $(DESTDIR)$(bindir) && $(LN_S) -f cjs-console$(EXEEXT) cjs$(EXEEXT)) - -include Makefile-test.am -include Makefile-insttest.am - -EXTRA_DIST += \ - autogen.sh \ - COPYING.LGPL \ - doc/ByteArray.md \ - doc/cairo.md \ - doc/Hacking.md \ - doc/SpiderMonkey_Memory.md \ - doc/Style_Guide.md \ - win32/build-rules-msvc.mak \ - win32/config-msvc.mak \ - win32/config.h.win32 \ - win32/create-lists-msvc.mak \ - win32/create-lists.bat \ - win32/detectenv-msvc.mak \ - win32/generate-msvc.mak \ - win32/gjs-introspection-msvc.mak \ - win32/info-msvc.mak \ - win32/install.mak \ - win32/introspection-msvc.mak \ - win32/Makefile.vc \ - win32/README.txt \ - $(NULL) - -# Colin's handy Makefile bits for: -# 1) stuffing tarballs with pre-generated scripts from your workstation -# 2) bumping configure.ac version post-release -# 3) tagging correctly in git -# 4) uploading to gnome.org -# To use: -# $ make check -# $ make dist -# $ make prepare-minor-release - -# Customize to taste -TAG_PREFIX=GJS_ -COMPRESSION=.bz2 - - -PACKAGE=@PACKAGE@ -VERSION=@VERSION@ -DISTNAME=$(PACKAGE)-$(VERSION).tar$(COMPRESSION) -TAG_VERSION := $(shell echo $(VERSION) | $(SED) s/\\\./_/g) - -prepare-release-tag: Makefile - git tag -m "Tag $(TAG_VERSION)" -a $(TAG_PREFIX)$(TAG_VERSION) - -prepare-minor-release: $(DISTNAME) prepare-release-tag Makefile - env top_srcdir=$(top_srcdir) python $(top_srcdir)/verbump.py - -upload-release: $(DISTNAME) Makefile - git log origin/master..master - @echo -n "Ok to push? [y/N] "; read ans; test x$$ans == xy || exit 1 - git push --tags origin master:master - scp $(DISTNAME) master.gnome.org: - ssh master.gnome.org install-module $(DISTNAME) - - -CPPCHECK=cppcheck -### cppcheck static code analysis -# -cppcheck: - $(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 43feb1f..c651a44 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,1307 @@ +Version 1.66.0 +-------------- + +- No change from 1.65.92. + +Version 1.65.92 +--------------- + +- Closed bugs and merge requests: + + * CI: Make iwyu idempotent [!481, Simon McVittie] + * Enum and flags test failing in s390x [#319, !480, Simon McVittie] + * Bring back Visual Studio build support for GJS master [!482, Chun-wei Fan] + * gjs_dbus_implementation_emit_signal: don't try to unref NULL [!482, Adam + Williamson] + * doc: add third party applications [!484, Sonny Piers] + * boxed: Initialize all the private BoxedInstance members [!487, Marco + Trevisan] + * object: Fix GjsCallBackTrampoline's leaks [!490, Marco Trevisan] + * Various maintenance [!485, Philip Chimento] + * Crash using shell's looking glass [#344, !486, Marco Trevisan] + +Version 1.65.91 +--------------- + +- Closed bugs and merge requests: + + * Crash in gjs_dbus_implementation_flush() [#332, !471, Andy Holmes] + * eslint: Bump ecmaScript version [!473, Florian Müllner] + * Documentation: add documentation for ENV variables [!474, Andy Holmes] + * Fix build for master on Windows (due to SpiderMonkey-78.x upgrade) [!475, + Chun-wei Fan] + * Argument cache causes test failure in armhf [#342, !476, Marco Trevisan] + * Argument cache causes test regressions in s390x [#341, !477, Simon McVittie] + * ByteArray.toString use-after-free [#339, !472, Evan Welsh] + * Crash accessing `vfunc_` methods of `Clutter.Actor`s [#313, !478, Evan + Welsh] + +- Various refactors for type safety [Marco Trevisan] + +Version 1.65.90 +--------------- + +- GJS now has an optional, Linux-only, dependency on libsysprof-capture-4 + instead of libsysprof-capture-3 for the profiler functionality. + +- New API: gjs_coverage_enable() allows the collection of code coverage metrics. + If you are using GjsCoverage, it is now required to call gjs_coverage_enable() + before you create the first GjsContext. Previously this was not necessary, but + due to changes in SpiderMonkey 78 you must now indicate in advance if you want + to collect code coverage metrics. + +- New JavaScript features! This version of GJS is based on SpiderMonkey 78, an + upgrade from the previous ESR (Extended Support Release) of SpiderMonkey 68. + Here are the highlights of the new JavaScript features. + For more information, look them up on MDN or devdocs.io. + + * New language features + + A new regular expression engine, supporting lookbehind and named capture + groups, among other things + + * New syntax + + The ?? operator ("nullish coalescing operator") is now supported + + The ?. operator ("optional chaining operator") is now supported + + Public static class fields are now supported + + Separators in numeric literals are now supported: for example, 1_000_000 + + * New APIs + + String.replaceAll() for replacing all instances of a string inside another + string + + Promise.allSettled() for awaiting until all Promises in an array have + either fulfilled or rejected + + Intl.Locale + + Intl.ListFormat + + Intl.RelativeTimeFormat.formatToParts() + + * New behaviour + + There are a lot of minor behaviour changes as SpiderMonkey's JS + implementation conforms ever closer to existing ECMAScript standards and + adopts new ones. For complete information, read the Firefox developer + release notes: + https://developer.mozilla.org/en-US/Firefox/Releases/69#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/70#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/71#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/72#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/73#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/74#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/75#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/76#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/77#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/78#JavaScript + + * Backwards-incompatible changes + + The Object.toSource() method has been removed + + The uneval() global function has been removed + + A leading zero is now never allowed for BigInt literals, making 08n and + 09n invalid similar to the existing error when legacy octal numbers like + 07n are used + + The Function.caller property now has the value of null if the caller is a + strict, async, or generator function, instead of throwing a TypeError + +- Backwards-incompatible change: Paths specified on the command line with + the --coverage-prefix argument, are now always interpreted as paths. If they + are relative paths, they will be resolved relative to the current working + directory. In previous versions, they would be treated as string prefixes, + which led to unexpected behaviour when the path of the script was absolute + and the coverage prefix relative, or vice versa. + +- Closed bugs and merge requests: + * Port to libsysprof-capture-4.a [!457, Philip Withnall, Philip Chimento] + * CI: Switch ASAN jobs to runners tagged so [!461, Bartłomiej Piotrowski] + * Rework global code to support multiple global "types". (Part 1) [!453, Evan + Welsh] + * SpiderMonkey 78 [#329, !462, !458, Evan Welsh, Philip Chimento] + * GIArgument inlines [!460, Marco Trevisan, Philip Chimento] + * gjs stopped building on 32 bits [#335, !463, Marco Trevisan, Philip + Chimento] + * Improve performance of argument marshalling [#70, !48, Giovanni Campagna, + Philip Chimento] + * Build failure on 32-bit [#336, !465, Michael Catanzaro] + * Various maintenance [!464, Philip Chimento] + * arg-cache.cpp: Fix build on Visual Studio [!466, Chun-wei Fan] + * [regression] Super+A crashes gnome-shell [#338, !467, Philip Chimento] + * Generating coverage information seems to be broken [#322, !470, Philip + Chimento] + +- Various refactors for type safety [Marco Trevisan] + +- Various maintenance [Philip Chimento] + +Version 1.65.4 +-------------- + +- New language features! Public class fields are now supported. See for more + information: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields + +- Closed bugs and merge requests: + * arg.cpp: Add required messages for static_assert (fix building on pre-C++17) + [!441, Chun-wei Fan] + * Add include-what-you-use CI job [!448, !449, Philip Chimento] + * Let's enable class fields! [!445, Evan Welsh] + * examples: add GListModel implementation [!452, Andy Holmes] + * Update ESLint CI image. [!451, Evan Welsh] + * function: Only get function name if we actually warn [!454, Jonas Dreßler] + * Split print into native library. [!444, Evan Welsh] + * Various maintenance [!459, Philip Chimento] + +- Various refactors for type safety [Marco Trevisan] + +Version 1.64.4 +-------------- + +- Closed bugs and merge requests: + * Fix CI failure caused by GTK4 update [!447, Philip Chimento] + +Version 1.65.3 +-------------- + +- In GTK 4, Gtk.Widget is now an iterable object which iterates through its + child widgets. (`for (let child of widget) { ... }`) + +- Closed bugs and merge requests: + * Installed tests are not in preferred directories [#318, !427, Ross Burton] + * Build new test CI images with Buildah [!429, Philip Chimento] + * CI fixes for new test images [!433, Philip Chimento] + * Various maintenance [!428, Philip Chimento] + * Fix dead link [!436, prnsml] + * overrides/Gtk: Make GTK4 widgets iteratable [!437, Florian Müllner] + * arg.cpp: Fix building on Visual Studio [!439, Chun-wei Fan] + * Separate closures and vfuncs [!438, Philip Chimento] + * Improvements to IWYU script [!435, Philip Chimento] + * Various refactors in preparation for ES modules [!440, Evan Welsh, Philip + Chimento] + +- Various refactors for type safety [Marco Trevisan] + +Version 1.64.3 +-------------- + +- Closed bugs and merge requests: + * arg: Don't sink GClosure ref if it's a return value [!426, Philip Chimento] + * overrides/Gtk: Adjust gtk_container_child_set_property() check [!431, + Florian Müllner] + * 1.63.3: test suite is failing [#298, !430, Philip Chimento] + * Simplify private pointers [!434, Philip Chimento] + +- Various backports: + * Use memory GSettings backend in tests [Philip Chimento] + * Update debug message from trimLeft/trimRight to trimStart/trimEnd [Philip + Chimento] + * Various fixes for potential crash and memory issues [Philip Chimento] + +Version 1.58.8 +-------------- + +- Various backports + * 1.63.3: test suite is failing [Philip Chimento] + * Various fixes for potential crash and memory issues [Philip Chimento] + +Version 1.65.2 +-------------- + +- It's now possible to omit the getter and setter for a GObject property on your + class, if you only need the default behaviour (reading and writing the + property, respecting the default value if not set, and implementing property + notifications if the setter changes the value.) This should cut down on + boilerplate code and any mistakes made in it. + +- The log level of exception messages has changed. Previously, some exceptions + would be logged as critical-level messages even when they were logged + intentionally with logError(). Now, critical-level messages are only logged + when an exception goes uncaught (programmer error) and in all other cases a + warning-level message is logged. + +- Closed bugs and merge requests: + * build: Use '!=' instead of 'is not' to compare string [Robert Mader, !414] + * Various maintenance [Philip Chimento, !413, !425] + * doc fixes [Sonny Piers, !415, !416] + * jsapi-util: Make log levels of exceptions consistent [Philip Chimento, !418] + * Too much recursion error accessing overrided gobject interface property from + a subclass [Philip Chimento, #306, !408] + * JS: migrate from the global `window` to `globalThis` [Andy Holmes, !423] + * doc: Fix a typo [Matthew Leeds, !424] + +Version 1.64.2 +-------------- + +- Closed bugs and merge requests: + * GList of int not correctly demarshalled on 64-bit big-endian [Philip + Chimento, Simon McVittie, #309, !417, !419] + * Fix template use in GTK4 [Florian Müllner, !420] + * Don't crash if a callback doesn't return an expected array of values [Marco + Trevisan, !405] + * Crash passing integer to strv in constructor [Evan Welsh, #315, !422] + * Skip some tests if GTK can't be initialised [Ross Burton, !421] + +- Various backports: + * Fix gjs_log_exception() for InternalError [Philip Chimento] + * Fix signal match mechanism [Philip Chimento] + +Version 1.58.7 +-------------- + +- Various backports: + * Don't crash if a callback doesn't return an expected array of values [Marco + Trevisan] + * GList of int not correctly demarshalled on 64-bit big-endian [Philip + Chimento, Simon McVittie] + * Crash passing integer to strv in constructor [Evan Welsh] + * Ignore format-nonliteral warning [Marco Trevisan] + +Version 1.65.1 +-------------- + +- Closed bugs and merge requests: + * boxed: Implement newEnumerate hook for boxed objects [Ole Jørgen Brønner, + !400] + * ns: Implement newEnumerate hook for namespaces [Ole Jørgen Brønner, !401] + * CI: Tag sanitizer jobs as "privileged" [Philip Chimento, !407] + * overrides/Gio: Allow promisifying static methods [Florian Müllner, !410] + * overrides/Gio: Guard against repeated _promisify() calls [Florian Müllner, + !411] + +Version 1.64.1 +-------------- + +- The BigInt type is now _actually_ available, as it wasn't enabled in the + 1.64.0 release even though it was mentioned in the release notes. + +- Closed bugs and merge requests: + * testCommandLine's Unicode tests failing on Alpine Linux [Philip Chimento, + #296, !399] + * build: Various clean-ups [Jan Tojnar, !403] + * Correctly handle vfunc inout parameters [Marco Trevisan, !404] + * Fix failed redirect of output in CommandLine tests [Liban Parker, !409] + +Version 1.58.6 +-------------- + +- Various backports: + * Correctly handle vfunc inout parameters [Marco Trevisan] + * Fix failed redirect of output in CommandLine tests [Liban Parker] + * Avoid filename conflict when tests run in parallel [Philip Chimento] + +Version 1.64.0 +-------------- + +- No change from 1.63.92. + +Version 1.63.92 +--------------- + +- Closed bugs and merge requests: + * object: Use g_irepository_get_object_gtype_interfaces [Colin Walters, Philip + Chimento, #55, !52] + * Add -fno-semantic-interposition to -Bsymbolic-functions [Jan Alexander + Steffens (heftig), #303, !397] + * examples: add a dbus-client and dbus-service example [Andy Holmes, !398] + * Various GNOME Shell crashes during GC, mozjs68 regression [Jan Alexander + Steffens (heftig), Philip Chimento, #301, !396] + +Version 1.63.91 +--------------- + +- Closed bugs and merge requests: + * [mozjs68] Reorganize modules for ESM. [Evan Welsh, Philip Chimento, !383] + * Various maintenance [Philip Chimento, !388] + * Fix building GJS master with Visual Studio and update build instructions + [Chun-wei Fan, !389] + * Resolve "Gnome Shell crash on GC run with mozjs68" [Philip Chimento, !391] + * installed-tests/js: Add missing dep on warnlib_typelib [Jan Alexander + Steffens, !393] + * object: Cache known unresolvable properties [Daniel van Vugt, Philip + Chimento, !394, #302] + +Version 1.58.5 +-------------- + +- Closed bugs and merge requests: + * Fix Visual Studio builds of gnome-3-34 (1.58.x) branch [Chun-wei Fan, !392] + * Can not access GObject properties of classes without GI information [Juan + Pablo Ugarte, !385, #299] + +Version 1.63.90 +--------------- + +- New JS API: The GObject module has gained new overrides: + GObject.signal_handler_find(), GObject.signal_handlers_block_matched(), + GObject.signal_handlers_unblock_matched(), and + GObject.signal_handlers_disconnect_matched(). These overrides replace the + corresponding C API, which was not idiomatic for JavaScript and was not fully + functional because it used bare C pointers for some of its functionality. + See modules/overrides/GObject.js for API documentation. + +- New JavaScript features! This version of GJS is based on SpiderMonkey 68, an + upgrade from the previous ESR (Extended Support Release) of SpiderMonkey 60. + Here are the highlights of the new JavaScript features. + For more information, look them up on MDN or devdocs.io. + + * New language features + + The BigInt type, currently a stage 3 proposal in the ES standard, is now + available. + + * New syntax + + `globalThis` is now the ES-standard supported way to get the global + object, no matter what kind of JS environment. The old way, `window`, will + still work, but is no longer preferred. + + BigInt literals are expressed by a number with "n" appended to it: for + example, `1n`, `9007199254740992n`. + + * New APIs + + String.prototype.trimStart() and String.prototype.trimEnd() now exist and + are preferred instead of trimLeft() and trimRight() which are nonstandard. + + String.prototype.matchAll() allows easier access to regex capture groups. + + Array.prototype.flat() flattens nested arrays, well-known from lodash and + similar libraries. + + Array.prototype.flatMap() acts like a reverse filter(), allowing adding + elements to an array while iterating functional-style. + + Object.fromEntries() creates an object from iterable key-value pairs. + + Intl.RelativeTimeFormat is useful for formatting time differences into + human-readable strings such as "1 day ago". + + BigInt64Array and BigUint64Array are two new typed array types. + + * New behaviour + + There are a lot of minor behaviour changes as SpiderMonkey's JS + implementation conforms ever closer to existing ECMAScript standards and + adopts new ones. For complete information, read the Firefox developer + release notes: + https://developer.mozilla.org/en-US/Firefox/Releases/61#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/62#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/63#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/64#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/65#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/66#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/67#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/68#JavaScript + + * Backwards-incompatible changes + + The nonstandard String generics were removed. These had only ever been + implemented by Mozilla and never made it into a standard. (An example of a + String generic is calling a string method on something that might not be a + string like this: `String.endsWith(foo, 5)`. The proper way is + `String.prototype.endsWith.call(foo, 5)` or converting `foo` to a string.) + This should not pose much of a problem for existing code, since in the + previous version these would already print a deprecation warning whenever + they were used. + You can use `moz68tool` from mozjs-deprecation-tools + (https://gitlab.gnome.org/ptomato/moz60tool) to scan your code for this + nonstandard usage. + +- Closed bugs and merge requests: + * invalid import on signal.h [#295, !382, Philip Chimento] + * SpiderMonkey 68 [#270, !386, Philip Chimento] + * GObject: Add override for GObject.handler_block_by_func [#290, !371, Philip + Chimento] + +Version 1.63.3 +-------------- + +- Closed bugs and merge requests: + * JS ERROR: TypeError: this._rooms.get(...) is undefined [Philip Chimento, + #289, !367] + * Run CI build with --werror [Philip Chimento, #286, !365] + * build: Remove Autotools build system [Philip Chimento, !364] + * gjs-symlink script is incompatible with distro builds [Michael Catanzaro, + Bastien Nocera, #291, !369, !370] + * installed-tests: Don't hardcode the path of bash [Ting-Wei Lan, !372] + * Update Visual Studio build instructions (after migrating to full Meson-based + builds) [Chun-wei Fan, !375] + * object: Warn when setting a deprecated property [Florian Müllner, !378] + * CI: Create mozjs68 CI images [Philip Chimento, !379] + * Various maintenance [Philip Chimento, !374, !380, !381] + +Version 1.58.4 +-------------- + +- Now prints a warning when constructing an unregistered object inheriting from + GObject (i.e. if you forgot to use GObject.registerClass.) In 1.58.2 this + would throw an exception, which broke some existing code, so that change was + reverted in 1.58.3. In this version the check is reinstated, but we log a + warning instead of throwing an exception, so that people know to fix their + code, but without breaking things. + NOTE: In 1.64 (the next stable release) the warning will be changed back into + an exception, because code with this problem can be subtly broken and cause + unexpected errors elsewhere. So make sure to fix your code if you get this + warning. + +- Closed bugs and merge requests: + * GSettings crash fixes [Andy Holmes, !373] + +- Memory savings for Cairo objects [Philip Chimento, !374] + +- Fix for crash in debug functions [Philip Chimento, !374] + +Version 1.63.2 +-------------- + +- There is an option for changing the generated GType name for GObject classes + created in GJS to a new scheme that is less likely to have collisions. This + scheme is not yet the default, but you can opt into it by setting + `GObject.gtypeNameBasedOnJSPath = true;` as early as possible in your + prograṁ. Doing this may require some changes in Glade files if you use + composite widget templates. + + We recommend you make this change in your codebase as soon as possible, to + avoid any surprises in the future. + +- New JS API: GObject.Object has gained a stop_emission_by_name() method which + is a bit more idiomatic than calling GObject.signal_stop_emission_by_name(). + +- It's now supported to use the "object" attribute in a signal connection in a + composite widget template in a Glade file. + +- Closed bugs and merge requests: + * CI: Tweak eslint rule for unneeded parentheses [Florian Müllner, !353] + * Smarter GType name computation [Marco Trevisan, !337] + * Meson CI [Philip Chimento, !354] + * Visual Studio builds using Meson [Chun-wei Fan, !355] + * Hide internal symbols from ABI [Marco Trevisan, #194, !352] + * Allow creating custom tree models [Giovanni Campagna, #71] + * build: Fix dist files [Florian Müllner, !357] + * GObject: Add convenience wrapper for signal_stop_emission_by_name() [Florian + Müllner, !358] + * Various maintenance [Philip Chimento, !356] + * object_instance_props_to_g_parameters should do more check on argv [Philip + Chimento, #63, !359] + * Support flat C arrays of structures [Philip Chimento, !361] + * Gtk Templates: support connectObj argument [Andy Holmes, !363] + +- Various build fixes [Philip Chimento] + +Version 1.58.2 +-------------- + +- Closed bugs and merge requests: + * GObject based class initialization checks [Marco Trevisan, Philip Chimento, + !336] + * Silently leaked return value of callbacks [Xavier Claessens, Philip + Chimento, #86, !44] + * Crash when calling Gio.Initable.async_init with not vfunc_async_init + implementation [Philip Chimento, #287, !362] + * [cairo] insufficient checking [Philip Chimento, #49, !360] + +- Various crash fixes backported from the development branch that didn't close + a bug or merge request. + +Version 1.63.1 +-------------- + +- Note that the 1.59, 1.60, 1.61, and 1.62 releases are hereby skipped, because + we are calling the next stable series 1.64 to match gobject-introspection and + GLib. + +- GJS now includes a Meson build system. This is now the preferred way to build + it; however, the old Autotools build system is still available for a + transitional period. + +- Closed bugs and merge requests: + * GObject: Add convenience wrapper for signal_handler_(un)block() [Florian + Müllner, !326] + * GObject based class initialization checks [Marco Trevisan, Philip Chimento, + !336] + * Meson port [Philip Chimento, !338] + * add http client example [Sonny Piers, !342] + * Smaller CI, phase 2 [Philip Chimento, !343] + * add websocket client example [Sonny Piers, !344] + * Fix Docker images build [Philip Chimento, !345] + * CI: Use new Docker images [Philip Chimento, !346] + * docs: Update internal links [Andy Holmes, !348] + * Don't pass generic marshaller to g_signal_newv() [Niels De Graef, !349] + * tests: Fail debugger tests if command failed [Philip Chimento, !350] + * Minor CI image fixes [Philip Chimento, !351] + * Various fixes [Marco Trevisan, Philip Chimento] + +Version 1.58.1 +-------------- + +- Closed bugs and merge requests: + * Import wiki documentation [Sonny Piers, !341] + * Smaller CI, phase 1 [Philip Chimento, !339] + * Crashes after setting child property 'icon-name' on GtkStack then displaying + another GtkStack [Florian Müllner, #284, !347] + * GLib.strdelimit crashes [Philip Chimento, #283, !340] + +Version 1.58.0 +-------------- + +- No change from 1.57.92. + +Version 1.57.92 +--------------- + +- Closed bugs and merge requests: + * tests: Enable regression test cases for GPtrArrays and GArrays of structures + [Stéphane Seng, !334] + * Various maintenance [Philip Chimento, !333, !335] + +Version 1.57.91 +--------------- + +- GJS no longer links to libgtk-3. This makes it possible to load the Gtk-4.0 + typelib in GJS and write programs that use GTK 4. + +- The heapgraph tool has gained some improvements; it is now possible to print a + heap graph of multiple targets. You can also mark an object for better + identification in the heap graph by assigning a magic property: for example, + myObject.__heapgraph_name = 'Button' will make that object identify itself as + "Button" in heap graphs. + +- Closed bugs and merge requests: + * Remove usage of Lang in non legacy code [Sonny Piers, !322] + * GTK4 [Florian Müllner, #99, !328, !330] + * JS syntax fixes [Marco Trevisan, Philip Chimento, !306, !323] + * gi: Avoid infinite recursion when converting GValues [Florian Müllner, !329] + * Implement all GObject-introspection test suites [Philip Chimento, !327, + !332] + * Heapgraph improvements [Philip Chimento, !325] + +Version 1.57.90 +--------------- + +- New JS API: GLib.Variant has gained a recursiveUnpack() method which + transforms the variant entirely into a JS object, discarding all type + information. This can be useful for dealing with a{sv} dictionaries, where + deepUnpack() will keep the values as GLib.Variant instances in order to + preserve the type information. + +- New JS API: GLib.Variant has gained a deepUnpack() method which is exactly the + same as the already existing deep_unpack(), but fits with the other camelCase + APIs that GJS adds. + +- Closed bugs and merge requests: + * Marshalling of GPtrArray broken [#9, !311, Stéphane Seng] + * Fix locale chooser [!313, Philip Chimento] + * dbus-wrapper: Remove interface skeleton flush idle on dispose [!312, Marco + Trevisan] + * gobject: Use auto-compartment when getting property as well [!316, Florian + Müllner] + * modules/signals: Use array destructuring in _emit [!317, Jonas Dreßler] + * GJS can't call glibtop_init function from libgtop [#259, !319, + Philip Chimento] + * GLib's VariantDict is missing lookup [#263, !320, Sonny Piers] + * toString on an object implementing an interface fails [#252, !299, Marco + Trevisan] + * Regression in GstPbutils.Discoverer::discovered callback [#262, !318, Philip + Chimento] + * GLib.Variant.deep_unpack not working properly with a{sv} variants [#225, + !321, Fabián Orccón, Philip Chimento] + * Various maintenance [!315, Philip Chimento] + +- Various CI fixes [Philip Chimento] + +Version 1.57.4 +-------------- + +- Closed bugs and merge requests: + * gjs 1.57 requires a recent sysprof version for sysprof-capture-3 [#258, + !309, Olivier Fourdan] + +- Misc documentation changes [Philip Chimento] + +Version 1.57.3 +-------------- + +- The GJS profiler is now integrated directly into Sysprof 3, via the + GJS_TRACE_FD environment variable. Call stack information and garbage + collector timing will show up in Sysprof. See also GNOME/Initiatives#10 + +- New JS API: System.addressOfGObject(obj) will return a string with the hex + address of the underlying GObject of `obj` if it is a GObject wrapper, or + throw an exception if it is not. This is intended for debugging. + +- New JS API: It's now possible to pass a value from Gio.DBusProxyFlags to the + constructor of a class created by Gio.DBusProxy.makeProxyWrapper(). + +- Backwards-incompatible change: Trying to read a write-only property on a DBus + proxy object, or write a read-only property, will now throw an exception. + Previously it would fail silently. It seems unlikely any code is relying on + the old behaviour, and if so then it was probably masking a bug. + +- Closed bugs and merge requests: + * Build failure on Continuous [#253, !300, Philip Chimento] + * build: Bump glib requirement [!302, Florian Müllner] + * profiler: avoid clearing 512 bytes of stack [!304, Christian Hergert] + * system: add addressOfGObject method [!296, Marco Trevisan] + * Add support for GJS_TRACE_FD [!295, Christian Hergert] + * Gio: Make possible to pass DBusProxyFlags to proxy wrapper [!297, Marco + Trevisan] + * Various maintenance [!301, Philip Chimento] + * Marshalling of GPtrArray broken [#9, !307, Stéphane Seng] + * Build fix [!308, Philip Chimento] + * Gio: sync dbus wrapper properties flags [!298, Marco Trevisan] + * GjsMaybeOwned: Reduce allocation when used as Object member [!303, Marco + Trevisan] + +Version 1.57.2 +-------------- + +- There are now overrides for Gio.SettingsSchema and Gio.Settings which avoid + aborting the whole process when trying to access a nonexistent key or child + schema. The original API from GLib was intended for apps, since apps should + have complete control over which settings keys they are allowed to access. + However, it is not a good fit for shell extensions, which may need to access + different settings keys depending on the version of GNOME shell they're + running on. + + This feature is based on code from Cinnamon which the copyright holders have + kindly agreed to relicense to GJS's license. + +- New JS API: It is now possible to pass GObject.TypeFlags to + GObject.registerClass(). For example, passing + `GTypeFlags: GObject.TypeFlags.ABSTRACT` in the class info object, will create + a class that cannot be instantiated. This functionality was present in + Lang.Class but has been missing from GObject.registerClass(). + +- Closed bugs and merge requests: + * Document logging features [#230, !288, Andy Holmes] + * Support optional GTypeFlags value in GObject subclasses [!290, Florian + Müllner] + * Ensure const-correctness in C++ objects [#105, !291, Onur Şahin] + * Programmer errors with GSettings cause segfaults [#205, !284, Philip + Chimento] + * Various maintenance [!292, Philip Chimento] + * debugger: Fix summary help [!293, Florian Müllner] + * context: Use Heap pointers for GC objects stored in vectors [!294, Philip + Chimento] + +Version 1.56.2 +-------------- + +- Closed bugs and merge requests: + * Crash in BoxedInstance when struct could not be allocated directly [#240, + !285, Philip Chimento] + * Cairo conversion bugs [!286, Philip Chimento] + * Gjs crashes when binding inherited property to js added gobject-property + [#246, !289, Marco Trevisan] + * console: Don't accept --profile after the script name [!287, Philip + Chimento] + +Version 1.57.1 +-------------- + +- Closed bugs and merge requests: + * Various maintenance [!279, Philip Chimento] + * mainloop: Assign null to property instead of deleting [!280, Jason Hicks] + * Added -d version note README.md [!282, Nauman Umer] + * Extra help for debugger commands [#236, !283, Nauman Umer] + * Crash in BoxedInstance when struct could not be allocated directly [#240, + !285, Philip Chimento] + * Cairo conversion bugs [!286, Philip Chimento] + +Version 1.56.1 +-------------- + +- Closed bugs and merge requests: + * Calling dumpHeap() on non-existent directory causes crash [#134, !277, + Philip Chimento] + * Using Gio.MemoryInputStream.new_from_data ("string") causes segfault [#221, + !278, Philip Chimento] + * Fix gjs_context_eval() for non-zero-terminated strings [!281, Philip + Chimento] + +Version 1.56.0 +-------------- + +- No change from 1.55.92. + +Version 1.55.92 +--------------- + +- Closed bugs and merge requests: + * Fix CI failures [!269, Philip Chimento] + * Possible memory allocation/deallocation bug (possibly in js_free() in GJS) + [!270, Chun-wei Fan, Philip Chimento] + * cairo-context: Special-case 0-sized vector [!271, Florian Müllner] + * Add some more eslint rules [!272, Florian Müllner] + * win32/NMake: Fix introspection builds [!274, Chun-wei Fan] + * NMake/libgjs-private: Export all the public symbols there [!275, Chun-wei + Fan] + +Version 1.55.91 +--------------- + +- The problem of freezing while running the tests using GCC's sanitizers was + determined to be a bug in GCC, which was fixed in GCC 9.0.1. + +- Closed bugs and merge requests: + * gnome-sound-recorder crashes deep inside libgjs [#223, !266, Philip + Chimento] + * Various maintenance [!267, Philip Chimento] + * wrapperutils: Define $gtype property as non-enumerable [!268, Philip + Chimento] + +Version 1.55.90 +--------------- + +- New JS API: It's now possible to call and implement DBus methods whose + parameters or return types include file descriptor lists (type signature 'h'.) + This involves passing or receiving a Gio.UnixFDList instance along with the + parameters or return values. + + To call a method with a file descriptor list, pass the Gio.UnixFDList along + with the rest of the parameters, in any order, the same way you would pass a + Gio.Cancellable or async callback. + + For return values, things are a little more complicated, in order to avoid + breaking existing code. Previously, synchronously called DBus proxy methods + would return an unpacked GVariant. Now, but only if called with a + Gio.UnixFDList, they will return [unpacked GVariant, Gio.UnixFDList]. This + does not break existing code because it was not possible to call a method with + a Gio.UnixFDList before, and the return value is unchanged if not calling with + a Gio.UnixFDList. This does mean, unfortunately, that if you have a method + with an 'h' in its return signature but not in its argument signatures, you + will have to call it with an empty FDList in order to receive an FDList with + the return value, when calling synchronously. + + On the DBus service side, when receiving a method call, we now pass the + Gio.UnixFDList received from DBus to the called method. Previously, sync + methods were passed the parameters, and async methods were passed the + parameters plus the Gio.DBusInvocation object. Appending the Gio.UnixFDList to + those parameters also should not break existing code. + + See the new tests in installed-tests/js/testGDBus.js for examples of calling + methods with FD lists. + +- We have observed on the CI server that GJS 1.55.90 will hang forever while + running the test suite compiled with GCC 9.0.0 and configured with the + --enable-asan and --enable-ubsan arguments. This should be addressed in one of + the following 1.55.x releases. + +- Closed bugs and merge requests: + * GDBus proxy overrides should support Gio.DBusProxy.call_with_unix_fd_list() + [#204, !263, Philip Chimento] + * Add regression tests for GObject vfuncs [!259, Jason Hicks] + * GjsPrivate: Sources should be C files [!262, Philip Chimento] + * build: Vendor last-good version of AX_CODE_COVERAGE [!264, Philip Chimento] + +Version 1.55.4 +-------------- + +- Closed bugs and merge requests: + * Various maintenance [!258, Philip Chimento] + * Boxed copy constructor should not be called, split Boxed into prototype and + instance structs [#215, !260, Philip Chimento] + +Version 1.55.3 +-------------- + +- Closed bugs and merge requests: + * Manually constructed ByteArray toString segfaults [#219, !254, Philip + Chimento] + * signals: Add _signalHandlerIsConnected method [!255, Jason Hicks] + * Various maintenance [!257, Philip Chimento] + +Version 1.52.5 +-------------- + +- This was a release consisting only of backports from the GNOME 3.30 branch to + the GNOME 3.28 branch. + +- This release includes the "Big Hammer" patch from GNOME 3.30 to reduce memory + usage. For more information, read the blog post at + https://feaneron.com/2018/04/20/the-infamous-gnome-shell-memory-leak/ + It was not originally intended to be backported to GNOME 3.28, but in practice + several Linux distributions already backported it, and it has been working + well to reduce memory usage, and the bugs have been ironed out of it. + + It does decrease performance somewhat, so if you don't want that then don't + install this update. + +- Closed bugs and merge requests: + + * Ensure not to miss the force_gc flag [#150, !132, Carlos Garnacho] + * Make GC much more aggressive [#62, !50, Giovanni Campagna, Georges Basile + Stavracas Neto, Philip Chimento] + * Queue GC when a GObject reference is toggled down [#140, !114, !127, Georges + Basile Stavracas Neto] + * Reduce memory overhead of g_object_weak_ref() [#144, !122, Carlos Garnacho, + Philip Chimento] + * context: Defer and therefore batch forced GC runs [performance] [!236, + Daniel van Vugt] + * context: use timeout with seconds to schedule a gc trigger [!239, Marco + Trevisan] + * Use compacting GC on RSS size growth [!133, #151, Carlos Garnacho] + * GType memleak fixes [!244, Marco Trevisan] + +Version 1.55.2 +-------------- + +- Closed bugs and merge requests: + * Gnome-shell crashes on destroying cached param specs [#213, !240, Marco + Trevisan] + * Various maintenance [!235, !250, Philip Chimento] + * Auto pointers builder [!243, Marco Trevisan] + * configure.ac: Update bug link [!245, Andrea Azzarone] + * SIGSEGV when exiting gnome-shell [#212, !247, Andrea Azzarone, Philip + Chimento] + * Fix build with --enable-dtrace and create CI job to ensure it doesn't break + in the future [#196, !237, !253, Philip Chimento] + * Delay JSString-to-UTF8 conversion [!249, Philip Chimento] + * Annotate return values [!251, Philip Chimento] + * Fix a regression with GError toString() [!252, Philip Chimento] + * GType memleak fixes [!244, Marco Trevisan] + * Atoms refactor [!233, Philip Chimento, Marco Trevisan] + * Write a "Code Hospitable" README file [#17, !248, Philip Chimento, Andy + Holmes, Avi Zajac] + * object: Method lookup repeatedly traverses introspection [#54, !53, Colin + Walters, Philip Chimento] + * Handler of GtkEditable::insert-text signal is not run [#147, !143, Tomasz + Miąsko, Philip Chimento] + +Version 1.54.3 +-------------- + +- Closed bugs and merge requests: + * object: Fix write-only properties [!246, Philip Chimento] + * SIGSEGV when exiting gnome-shell [#212, !247, Andrea Azzarone] + * SelectionData.get_targets crashes with "Unable to resize vector" [#201, + !241, Philip Chimento] + * Gnome-shell crashes on destroying cached param specs [#213, !240, Marco + Trevisan] + * GType memleak fixes [!244, Marco Trevisan] + * Fix build with --enable-dtrace and create CI job to ensure it doesn't break + in the future [#196, !253, Philip Chimento] + +Version 1.54.2 +-------------- + +- Closed bugs and merge requests: + * context: Defer and therefore batch forced GC runs [performance] [!236, + Daniel van Vugt] + * context: use timeout with seconds to schedule a gc trigger [!239, Marco + Trevisan] + * fundamental: Check if gtype is valid before using it [!242, Georges Basile + Stavracas Neto] + +- Backported a fix for a crash in the interactive interpreter when executing + something like `throw "foo"` [Philip Chimento] + +- Backported various maintenance from 3.31 [Philip Chimento] + +Version 1.55.1 +-------------- + +- New API for programs that embed GJS: gjs_memory_report(). This was already an + internal API, but now it is exported. + +- Closed bugs and merge requests: + + * object: Implement newEnumerate hook for GObject [!155, Ole Jørgen Brønner] + * Various maintenance [!228, Philip Chimento] + * ByteArray.toString should stop at null bytes [#195, !232, Philip Chimento] + * Byte arrays that represent encoded strings should be 0-terminated [#203, + !232, Philip Chimento] + * context: Defer and therefore batch forced GC runs [performance] [!236, + Daniel van Vugt] + * context: use timeout with seconds to schedule a gc trigger [!239, Marco + Trevisan] + * arg: Add special-case for byte arrays going to C [#67, !49, Jasper + St. Pierre, Philip Chimento] + +Version 1.52.4 +-------------- + +- This was a release consisting only of backports from the GNOME 3.30 branch to + the GNOME 3.28 branch. + +- Closed bugs and merge requests: + + * `ARGV` encoding issues [#22, !108, Evan Welsh] + * Segfault on enumeration of GjSFileImporter properties when a searchpath + entry contains a symlink [#154, !144, Ole Jørgen Brønner] + * Possible refcounting bug around GtkListbox signal handlers [#24, !154, + Philip Chimento] + * Fix up GJS_DISABLE_JIT flag now the JIT is enabled by default in + SpiderMonkey [!159, Christopher Wheeldon] + * Expose GObject static property symbols. [!197, Evan Welsh] + * Do not run linters on tagged commits [!181, Claudio André] + * gjs-1.52.0 fails to compile against x86_64 musl systems [#132, !214, Philip + Chimento] + * gjs no longer builds after recent autoconf-archive updates [#149, !217, + Philip Chimento] + +Version 1.54.1 +-------------- + +- Closed bugs and merge requests: + * legacy: Ensure generated GType names are valid [!229, Florian Müllner] + * Fix GJS profiler with MozJS 60 [!230, Georges Basile Stavracas Neto] + * Regression with DBus proxies [#202, !231, Philip Chimento] + +Version 1.54.0 +-------------- + +- Compatibility fix for byte arrays: the legacy toString() behaviour of byte + arrays returned from GObject-introspected functions is now restored. If you + use the functionality, a warning will be logged asking you to upgrade your + code. + +- Closed bugs and merge requests: + * byteArray: Add compatibility toString property [Philip Chimento, !227] + +Version 1.53.92 +--------------- + +- Technology preview of a GNOME 3.32 feature: native Promises for GIO-style + asynchronous operations. This is the result of Avi Zajac's summer internship. + To use it, you can opt in once for each specific asynchronous method, by + including code such as the following: + + Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async', + 'read_bytes_finish'); + + After executing this, you will be able to use native Promises with the + Gio.InputStream.prototype.read_async() method, simply by not passing a + callback to it: + + try { + let bytes = await stream.read_bytes_async(count, priority, cancel); + } catch (e) { + logError(e, 'Failed to read bytes'); + } + + Note that any "success" boolean return values are deleted from the array of + return values from the async method. That is, + + let [contents, etag] = file.load_contents_async(cancel); + + whereas the callback version still returns a useless [ok, contents, etag] + that can never be false, since on false an exception would be thrown. In the + callback version, we must keep this for compatibility reasons. + + Note that due to a bug in GJS (https://gitlab.gnome.org/GNOME/gjs/issues/189), + promisifying methods on Gio.File.prototype and other interface prototypes will + not work. We provide the API Gio._LocalFilePrototype on which you can + promisify methods that will work on Gio.File instances on the local disk only: + + Gio._promisify(Gio._LocalFilePrototype, 'load_contents_async', + 'load_contents_finish'); + + We estimate this will cover many common use cases. + + Since this is a technology preview, we do not guarantee API stability with + the version coming in GNOME 3.32. These APIs are marked with underscores to + emphasize that they are not stable yet. Use them at your own risk. + +- Closed bugs and merge requests: + * Added promisify to GJS GIO overrides [!225, Avi Zajac] + * Temporary fix for Gio.File.prototype [!226, Avi Zajac] + +Version 1.53.91 +--------------- + +- Closed bugs and merge requests: + * CI: add webkit and gtk-app tests [!222, Claudio André] + * Fix example eslint errors [!207, Claudio André, Philip Chimento] + * Fix more "lost" GInterface properties [!223, Florian Müllner] + * Fix --enable-installed-tests when built from a tarball [!224, Simon + McVittie] + +Version 1.53.90 +--------------- + +- GJS now depends on SpiderMonkey 60 and requires a compiler capable of C++14. + +- GJS includes a simple debugger now. It has basic stepping, breaking, and + printing commands, that work like GDB. Activate it by running the GJS console + interpreter with the -d or --debugger flag before the name of the JS program + on the command line. + +- New API for programs that embed GJS: gjs_context_setup_debugger_console(). + To integrate the debugger into programs that embed the GJS interpreter, call + this before executing the JS program. + +- New JavaScript features! This version of GJS is based on SpiderMonkey 60, an + upgrade from the previous ESR (Extended Support Release) of SpiderMonkey 52. + Here are the highlights of the new JavaScript features. + For more information, look them up on MDN or devdocs.io. + + * New syntax + + `for await (... of ...)` syntax is used for async iteration. + + The rest operator is now supported in object destructuring: e.g. + `({a, b, ...cd} = {a: 1, b: 2, c: 3, d: 4});` + + The spread operator is now supported in object literals: e.g. + `mergedObject = {...obj1, ...obj2};` + + Generator methods can now be async, using the `async function*` syntax, + or `async* f() {...}` method shorthand. + + It's now allowed to omit the variable binding from a catch statement, if + you don't need to access the thrown exception: `try {...} catch {}` + + * New APIs + + Promise.prototype.finally(), popular in many third-party Promise + libraries, is now available natively. + + String.prototype.toLocaleLowerCase() and + String.prototype.toLocaleUpperCase() now take an optional locale or + array of locales. + + Intl.PluralRules is now available. + + Intl.NumberFormat.protoype.formatToParts() is now available. + + Intl.Collator now has a caseFirst option. + + Intl.DateTimeFormat now has an hourCycle option. + + * New behaviour + + There are a lot of minor behaviour changes as SpiderMonkey's JS + implementation conforms ever closer to ECMAScript standards. For complete + information, read the Firefox developer release notes: + https://developer.mozilla.org/en-US/Firefox/Releases/53#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/54#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/55#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/56#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/57#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/58#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/59#JavaScript + https://developer.mozilla.org/en-US/Firefox/Releases/60#JavaScript + + * Backwards-incompatible changes + + Conditional catch clauses have been removed, as they were a Mozilla + extension which will not be standardized. This requires some attention in + GJS programs, as previously we condoned code like `catch (e if + e.matches(Gio.IOError, Gio.IOError.EXISTS))` with a comment in + overrides/GLib.js, so it's likely this is used in several places. + + The nonstandard `for each (... in ...)` loop was removed. + + The nonstandard legacy lambda syntax (`function(x) x*x`) was removed. + + The nonstandard Mozilla iteration protocol was removed, as well as + nonstandard Mozilla generators, including the Iterator and StopIteration + objects, and the Function.prototype.isGenerator() method. + + Array comprehensions and generator comprehensions have been removed. + + Several nonstandard methods were removed: ArrayBuffer.slice() (but not + the standard version, ArrayBuffer.prototype.slice()), + Date.prototype.toLocaleFormat(), Function.prototype.isGenerator(), + Object.prototype.watch(), and Object.prototype.unwatch(). + +- Many of the above backwards-incompatible changes can be caught by scanning + your source code using https://gitlab.gnome.org/ptomato/moz60tool, or + https://extensions.gnome.org/extension/1455/spidermonkey-60-migration-validator/ + +- Deprecation: the custom ByteArray is now discouraged. Instead of ByteArray, + use Javascript's native Uint8Array. The ByteArray module still contains + functions for converting between byte arrays, strings, and GLib.Bytes + instances. + + The old ByteArray will continue to work as before, except that Uint8Array + will now be returned from introspected functions that previously returned a + ByteArray. To keep your old code working, change this: + + let byteArray = functionThatReturnsByteArray(); + + to this: + + let byteArray = new ByteArray.ByteArray(functionThatReturnsByteArray()); + + To port to the new code: + + * ByteArray.ByteArray -> Uint8Array + * ByteArray.fromArray() -> Uint8Array.from() + * ByteArray.ByteArray.prototype.toString() -> ByteArray.toString() + * ByteArray.ByteArray.prototype.toGBytes() -> ByteArray.toGBytes() + * ByteArray.fromString(), ByteArray.fromGBytes() remain the same + + * Unlike ByteArray, Uint8Array's length is fixed. Assigning an element past + the end of a ByteArray would lengthen the array. Now, it is ignored. + Instead use Uint8Array.of(), for example, this code: + + let a = ByteArray.fromArray([97, 98, 99, 100]); + a[4] = 101; + + should be replaced by this code: + + let a = Uint8Array.from([97, 98, 99, 100]); + a = Uint8Array.of(...a, 101); + + The length of the byte array must be set at creation time. This code will + not work anymore: + + let a = new ByteArray.ByteArray(); + a[0] = 255; + + Instead, use "new Uint8Array(1)" to reserve the correct length. + +- Closed bugs and merge requests: + + * Run tests using real software [#178, !192, Claudio André] + * Script tests are missing some errors [#179, !192, Claudio André] + * Create a '--disable-readline' option and use it [!196, Claudio André] + * CI: stop using Fedora for clang builds [!198, Claudio André] + * Expose GObject static property symbols. [!197, Evan Welsh] + * CI fixes [!200, Claudio André] + * Docker images creation [!201, Claudio André] + * Get Docker images built and stored in GJS registry [#185, !203, !208, + Claudio André, Philip Chimento] + * Clear the static analysis image a bit more [!205, Claudio André] + * Rename the packaging job to flatpak [!210, Claudio André] + * Create SpiderMonkey 60 docker images [!202, Claudio André] + * Debugger [#110, !204, Philip Chimento] + * Add convenience g_object_set() replacement [!213, Florian Müllner] + * Add dependencies of the real tests (examples) [!215, Claudio André] + * CWE-126 [#174, !218, Philip Chimento] + * gjs no longer builds after recent autoconf-archive updates [#149, !217, + Philip Chimento] + * gjs-1.52.0 fails to compile against x86_64 musl systems [#132, !214, Philip + Chimento] + * Run the GTK real tests (recently added) [!212, Claudio André] + * Fix thorough tests failures [!220, Philip Chimento] + * Port to SpiderMonkey 60 [#161, !199, Philip Chimento] + * Replace ByteArray with native ES6 TypedArray [#5, !199, Philip Chimento] + * Overriding GInterface properties broke [#186, !216, Florian Müllner, Philip + Chimento] + * Avoid segfault when checking for GByteArray [!221, Florian Müllner] + +- Various build fixes [Philip Chimento] + +Version 1.53.4 +-------------- + +- Refactored the way GObject properties are accessed. This should be a bit more + efficient, as property info (GParamSpec) is now cached for every object type. + There may still be some regressions from this; please be on the lookout so + we can fix them in the next release. + +- The memory usage for each object instance has been reduced, resulting in + several dozens of megabytes less memory usage in GNOME Shell. + +- The CI pipeline was refactored, now runs a lot faster, detects more failure + situations, builds on different architectures, uses the GitLab Docker + registry, and publishes code coverage statistics to + https://gnome.pages.gitlab.gnome.org/gjs/ + +- For contributors, the C++ style guide has been updated, and there is now a + script in the tools/ directory that will install a Git hook to automatically + format your code when you commit it. The configuration may not be perfect yet, + so bear with us while we get it right. + +- Closed bugs and merge requests: + + * Define GObject properties and fields as JS properties [#160, !151, Philip + Chimento] + * Possible refcounting bug around GtkListbox signal handlers [#24, !154, + Philip Chimento] + * Fix up GJS_DISABLE_JIT flag now the JIT is enabled by default in + SpiderMonkey [!159, Christopher Wheeldon] + * Various CI maintenance [!160, !161, !162, !169, !172, !180, !191, !193, + Claudio André] + * Update GJS wiki URL [!157, Seth Woodworth] + * Build failure in GNOME Continuous [#104, !156, Philip Chimento] + * Refactor ObjectInstance into C++ class [!158, !164, Philip Chimento] + * Use Ubuntu in the coverage job [!163, Claudio André] + * Remove unused files [!165, Claudio André] + * Add a ARMv8 build test [!166, Claudio André] + * Make CI faster [!167, Claudio André] + * Add a PPC4LE build test [!168, Claudio André] + * gdbus: Fix deprecated API [!170, Philip Chimento] + * Change Docker images names pattern [#173, !174, Claudio André] + * Assert failure on a GC_ZEAL run [#165, !173, Philip Chimento] + * Do not run linters on tagged commits [!181, Claudio André] + * Fail on compiler warnings [!182, Claudio André] + * Save code statistics in GitLab Pages [!183, Claudio André] + * Build static analysis Docker image in GitLab [!184, !185, !187, !189, + Claudio André] + * GNOME Shell always crashes with SIGBUS [#171, !188, Georges Basile + Stavracas Neto] + * Coverage badge is no longer able to show its value [#177, !186, Claudio + André] + * Move the Docker images creation to GitLab [!190, Claudio André] + * Cut the Gordian knot of coding style [#172, !171, Philip Chimento] + * Some GObect/GInterface properties broke [#182, !195, Philip Chimento] + +Version 1.53.3 +-------------- + +- This release was made from an earlier state of master, before a bug was + introduced, while we sort out how to fix it. As a result, we don't have too + many changes this round. + +- Closed bugs and merge requests: + + * Adding multiple ESLint rules for spacing [!152, Avi Zajac] + * Various maintenance [!153, Philip Chimento] + +Version 1.53.2 +-------------- + +- The `Template` parameter passed to `GObject.registerClass()` now accepts + file:/// URIs as well as resource:/// URIs and byte arrays. + +- New API: `gjs_get_js_version()` returns a string identifying the version of + the underlying SpiderMonkey JS engine. The interpreter executable has also + gained a `--jsversion` argument which will print this string. + +- Several fixes for memory efficiency and performance. + +- Once again we welcomed contributions from a number of first-time contributors! + +- Closed bugs and merge requests: + + * Add support for file:/// uri to glade template [#108, !41, Jesus Bermudez, + Philip Chimento] + * Reduce memory overhead of g_object_weak_ref() [#144, !122, Carlos Garnacho, + Philip Chimento] + * gjs: JS_GetContextPrivate(): gjs-console killed by SIGSEGV [#148, !121, + Philip Chimento] + * Use compacting GC on RSS size growth [#151, !133, Carlos Garnacho] + * Segfault on enumeration of GjSFileImporter properties when a searchpath + entry contains a symlink [#154, !144, Ole Jørgen Brønner] + * Compare linter jobs to correct base [#156, !140, Claudio André] + * Various maintenance [!141, Philip Chimento] + * Support interface signal handlers [!142, Tomasz Miąsko] + * Remove unnecessary inline [!145, Emmanuele Bassi] + * Add badges to the readme [!146, !147, Claudio André] + * Fix debug logging [!148, Philip Chimento] + * CI: add a GC zeal test [!149, Claudio André] + +Version 1.53.1 +-------------- + +- Improvements to garbage collection performance. Read for more information: + https://feaneron.com/2018/04/20/the-infamous-gnome-shell-memory-leak/ + +- Now, when building a class from a UI template file (using the `Template` + parameter passed to `GObject.registerClass()`, for example) signals defined in + the UI template file will be automatically connected. + +- As an experimental feature, we now offer a flatpak built with each GJS commit, + including branches. You can use this to test your apps with a particular GJS + branch before it is merged. Look for it in the "Artifacts" section of the CI + pipeline. + +- Closed bugs and merge requests: + + * Tweener: Add min/max properties [!67, Jason Hicks] + * `ARGV` encoding issues [#22, !108, Evan Welsh] + * Make GC much more aggressive [#62, !50, Giovanni Campagna, Georges Basile + Stavracas Neto, Philip Chimento] + * Queue GC when a GObject reference is toggled down [#140, !114, !127, Georges + Basile Stavracas Neto] + * overrides: support Gtk template callbacks [!124, Andy Holmes] + * Ensure not to miss the force_gc flag [#150, !132, Carlos Garnacho] + * Create a flatpak on CI [#153, !135, Claudio André] + * Readme update [!138, Claudio André] + +Version 1.52.3 +-------------- + +- Closed bugs and merge requests: + + * Include calc.js example from Seed [!130, William Barath, Philip Chimento] + * CI: Un-pin the Fedora Docker image [#141, !131, Claudio André] + * Reduce overhead of wrapped objects [#142, !121, Carlos Garnacho, Philip + Chimento] + * Various CI changes [!134, !136, Claudio André] + +Version 1.52.2 +-------------- + +- This is an unscheuled release in order to revert a commit that causes a crash + on exit, with some Cairo versions. + +- Closed bugs and merge requests: + + * CI: pinned Fedora to old tag [!119, Claudio André] + * heapgraph.py: adjust terminal output style [!120, Andy Holmes] + * CI: small tweaks [!123, Claudio André] + * Warn about compilation warnings [!125, Claudio André] + * Miscellaneous commits [Philip Chimento, Jason Hicks] + Version 1.52.1 -------------- diff --git a/README.MSVC.md b/README.MSVC.md new file mode 100644 index 0000000..f005daf --- /dev/null +++ b/README.MSVC.md @@ -0,0 +1,157 @@ +Instructions for building GJS on Visual Studio or clang-cl +========================================================== +Building the GJS on Windows is now supported using Visual Studio +versions 2017 or later with or without clang-cl in both 32-bit and +64-bit (x64) flavors, via Meson. It should be noted that a +recent-enough Windows SDK from Microsoft is still required if using +clang-cl, as we will still use items from the Windows SDK. If using +Visual Studio, Visual Studio 2017 15.9.x or later are known to work; +earlier versions at after Visual Studio 2017 15.6 may or may not work, +please let us know how things went if Visual Studio 15.6, 15.7 or 15.8 +is used. + +Recent official binary installers of CLang (which contains clang-cl) +from the LLVM website are known to work to build SpiderMonkey 78 and +GJS. + +You will need the following items to build GJS using Visual Studio +or clang-cl (they can be built with Visual Studio 2015 or later, +unless otherwise noted): +-SpiderMonkey 78.x (mozjs-78). This must be built with clang-cl as + the Visual Studio compiler is no longer supported for building this. + Please see the below section carefully on this... +-GObject-Introspection (G-I) 1.61.2 or later +-GLib 2.58.x or later, (which includes GIO, GObject, and the + associated tools) +-Cairo including Cairo-GObject support (Optional) +-GTK+-3.20.x or later (Optional) +-and anything that the above items depend on. + +Note again that SpiderMonkey must be built using Visual Studio with +clang-cl, and the rest should preferably be built with Visual Studio +or clang-cl as well. The Visual Studio version used for building the +other dependencies should preferably be the same across the board, or, +if using Visual Studio 2015 or later, Visual Studio 2015 through 2019. + +Please also be aware that the Rust MSVC toolchains that correspond to +the platform you are building for must also be present to build +SpiderMonkey. Please refer to the Rust website on how to install the +Rust compilers and toolchains for MSVC. This applies to clang-cl +builds as well. + +Be aware that it is often hard to find a suitable source release for +SpiderMonkey nowadays, so it may be helpful to look in + +ftp://ftp.gnome.org/pub/gnome/teams/releng/tarballs-needing-help/mozjs/ + +for the suitable release series of SpiderMonkey that corresponds to +the GJS version that is being built, as GJS depends on ESR (Extended +Service Release, a.k.a Long-term support) releases of SpiderMonkey. + +You may also be able to obtain the SpiderMonkey 78.x sources via the +FireFox (ESR) or Thunderbird 78.x sources, in $(srcroot)/js. + +Please do note that the build must be done carefully, in addition to the +official instructions that are posted on the Mozilla website: + +https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Build_Documentation + +For the configuration step, you will need to run the following: + +(64-bit/x64 builds) +JS_STANDALONE=1 $(mozjs_srcroot)/js/src/configure --enable-nspr-build --host=x86_64-pc-mingw32 --target=x86_64-pc-mingw32 --prefix= --disable-jemalloc --with-libclang-path= --with-clang-path= + +(32-bit builds) +JS_STANDALONE=1 $(mozjs_srcroot)/js/src/configure --enable-nspr-build --host=i686-pc-mingw32 --target=i686-pc-mingw32 --prefix= --disable-jemalloc --with-libclang-path= --with-clang-path= + +Notice that "JS_STANDALONE=1" and "--disable-jemalloc" are absolutely required, +otherwise GJS will not build/run correctly. If your GJS build crashes upon +launch, use Depedency Walker to ensure that mozjs-78.dll does not depend on +mozglue.dll! If it does, or if GJS fails to link with missing arena_malloc() and +friends symbols, you have built SpiderMoney incorrectly and will need to rebuild +SpiderMonkey (with the build options as noted above) and retry the build. +Note in particular that a mozglue.dll should *not* be in $(builddir)/dist/bin, +although there will be a mozglue.lib somewhere in the build tree (which, you can +safely delete after building SpiderMonkey). The --host=... and --target=... +are absolutely required for all builds, as per the Mozilla's SpiderMonkey build +instructions, as Rust is being involved here. + +You may want to pass in --disable-js-shell to not build the JS +shell that comes with SpiderMonkey to save time, and perhaps +use --with-system-nspr (instead of the --enable-nspr-build as +above), --with-system-zlib and --with-system-icu if you know +what you are doing and that their pkg-config files +(or headers/LIB's) can be found directly or using configuration +options, to further save time. + +After the configuration finishes successfully, you may run 'mozmake' and +'mozmake install' as you would for a standard SpiderMonkey build. If +'mozmake install' does not work for you for some reason, the DLLs you +need and js.exe (if you did not pass in --disable-js-shell) can be +found in $(buildroot)/dist/bin (you need *all* the DLLs, make sure +that there is no mozglue.dll, otherwise you will need to redo your +build as noted above), and the required headers are found in +$(buildroot)/dist/include. Note that for PDB files and .lib files, +you will need to search for them in $(buildroot), +where the PDB file names match the filenames for the DLLs/EXEs in +$(buildroot)/dist/bin, and you will need to look for the following .lib files: +-mozjs-78.lib +-js_static.lib (optional) +-nspr4.lib (optional, recommended for future use, if --enable-nspr-build is used) +-plc4.lib (optional, recommended for future use, if --enable-nspr-build is used) +-plds4.lib (optional, recommended for future use, if --enable-nspr-build is used) + +You may want to put the .lib's and DLLs/EXEs into $(PREFIX)\lib and +$(PREFIX)\bin respectively, and put the headers into +$(PREFIX)\include\mozjs-78 for convenience. + +You will need to place the generated mozjs-78.pc pkg-config file into +$(PREFIX)\lib\pkgconfig and ensure that pkg-config can find it by +setting PKG_CONFIG_PATH. Ensure that the 'includedir' and 'libdir' +in there is correct, and remove the 'nspr' entry from the +'Requires.private:' line and change +'-include ${includedir}/mozjs-78/js/RequiredDefines.h' to +'-FI${includedir}/mozjs-78/js/RequiredDefines.h', so that the +mozjs-78.pc can be used correctly in Visual Studio/clang-cl builds. You +will also need to ensure that the existing GObject-Introspection +installation (if used) is on the same drive where the GJS sources +are (and therefore where the GJS build is being carried out). + +Since Mozilla insisted that clang-cl is to be used to build SpiderMonkey, +note that some SpideMonkey headers might need be updated as follows, if intending +to build without clang-cl, since there are some GCC-ish assumptions here: + +-Update $(includedir)/mozjs-78/js/AllocPolicy.h (after the build): + +Get rid of the 'JS_FRIEND_API' macro from the class +'TempAllocPolicy : public AllocPolicyBase' (ca. lines 112 and 178), +for the member method definitions of onOutOfMemory() and reportAllocOverflow() + +====================== +To carry out the build +====================== +If using clang-cl, you will need to set *both* the environment variables CC +and CXX to: 'clang-cl [--target=]' (without the quotes); please +see https://clang.llvm.org/docs/CrossCompilation.html on how the target triplet +can be defined, which is used if using the cross-compilation capabilities of CLang. +In this case, you need to ensure that 'clang-cl.exe' and 'lld-link.exe' (i.e. your +LLVM bindir) are present in your PATH. + +You need to install Python 3.5.x or later, as well as the +pkg-config tool, Meson (via pip) and Ninja. Perform a build by doing the +following, in an appropriate Visual Studio command prompt +in an empty build directory: + +meson --buildtype=... --prefix= -Dskip_dbus_tests=true + +(Note that -Dskip_dbus_tests=true is required for MSVC/clang-cl builds; please +see the Meson documentation for the values accepted by buildtype) + +You may want to view the build options after the configuration succeeds +by using 'meson configure' + +When the configuration succeeds, run: +ninja + +You may choose to install the build results using 'ninja install' +or running the 'install' project when the build succeeds. diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index 92ba175..0000000 --- a/autogen.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -# Run this to generate all the initial makefiles, etc. -test -n "$srcdir" || srcdir=`dirname "$0"` -test -z "$srcdir" && srcdir=. -olddir=`pwd` -cd "$srcdir" - -(test -f configure.ac) || { - echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" - echo " top-level directory" - exit 1 -} - -if [ "$#" = 0 -a "x$NOCONFIGURE" = "x" ]; then - echo "*** WARNING: I am going to run 'configure' with no arguments." >&2 - echo "*** If you wish to pass any to it, please specify them on the" >&2 - echo "*** '$0' command line." >&2 - echo "" >&2 -fi - -mkdir -p m4 - -autoreconf --verbose --force --install || exit 1 - -cd "$olddir" -if [ "$NOCONFIGURE" = "" ]; then - "$srcdir/configure" "$@" || exit 1 - - if [ "$1" = "--help" ]; then exit 0 else - echo "Now type 'make' to compile" || exit 1 - fi -else - echo "Skipping configure process." -fi diff --git a/build/choose-tests-locale.sh b/build/choose-tests-locale.sh new file mode 100755 index 0000000..73b273b --- /dev/null +++ b/build/choose-tests-locale.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +if ! which locale > /dev/null; then + exit 1 +fi + +locales=$(locale -a | xargs -n1) + +case $locales in + # Prefer C.UTF-8 although it is only available with newer libc + *C.UTF-8*) tests_locale=C.UTF-8 ;; + # C.utf8 has also been observed in the wild + *C.utf8*) tests_locale=C.utf8 ;; + + # Most systems will probably have this + *en_US.UTF-8*) tests_locale=en_US.UTF-8 ;; + *en_US.utf8*) tests_locale=en_US.utf8 ;; + + # If not, fall back to any English UTF-8 locale or any UTF-8 locale at all + *en_*.UTF-8*) tests_locale=$(echo $locales | grep -m1 en_.\*\\.UTF-8) ;; + *en_*.utf8*) tests_locale=$(echo $locales | grep -m1 en_.\*\\.utf8) ;; + *.UTF-8*) tests_locale=$(echo $locales | grep -m1 \\.UTF-8) ;; + *.utf8*) tests_locale=$(echo $locales | grep -m1 \\.utf8) ;; + + *) tests_locale=C ;; +esac + +echo $tests_locale diff --git a/build/compile-gschemas.py b/build/compile-gschemas.py new file mode 100644 index 0000000..fc0b1af --- /dev/null +++ b/build/compile-gschemas.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import sys + +if len(sys.argv) < 2: + sys.exit("usage: compile-gschemas.py ") + +schemadir = sys.argv[1] + +if os.environ.get('DESTDIR') is None: + print('Compiling GSettings schemas...') + subprocess.call(['glib-compile-schemas', schemadir]) diff --git a/build/symlink-gjs.py b/build/symlink-gjs.py new file mode 100644 index 0000000..2170c3c --- /dev/null +++ b/build/symlink-gjs.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import os +import shutil +import sys +import tempfile + +assert(len(sys.argv) == 2) + +destdir = os.environ.get('DESTDIR') +install_prefix = os.environ.get('MESON_INSTALL_PREFIX') +bindir = sys.argv[1] +if destdir is not None: + # os.path.join() doesn't concat paths if one of them is absolute + if install_prefix[0] == '/' and os.name != 'nt': + installed_bin_dir = os.path.join(destdir, install_prefix[1:], bindir) + else: + installed_bin_dir = os.path.join(destdir, install_prefix, bindir) +else: + installed_bin_dir = os.path.join(install_prefix, bindir) + +if os.name == 'nt': + # Using symlinks on Windows often require administrative privileges, + # which is not what we want. Instead, copy gjs-console.exe. + shutil.copyfile('cjs-console.exe', os.path.join(installed_bin_dir, 'gjs.exe')) +else: + try: + temp_link = tempfile.mktemp(dir=installed_bin_dir) + os.symlink('cjs-console', temp_link) + os.replace(temp_link, os.path.join(installed_bin_dir, 'cjs')) + finally: + if os.path.islink(temp_link): + os.remove(temp_link) diff --git a/cjs-1.0.pc.in b/cjs-1.0.pc.in deleted file mode 100644 index b0b4b85..0000000 --- a/cjs-1.0.pc.in +++ /dev/null @@ -1,18 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -bindir=@bindir@ -includedir=@includedir@ -datarootdir=@datarootdir@ -datadir=@datadir@ - -cjs_console=${bindir}/cjs-console - -Cflags: -I${includedir}/cjs-1.0 -Requires: @CJS_PACKAGE_REQUIRES@ -Requires.private: @CJS_PACKAGE_REQUIRES_PRIVATE@ -Libs: -L${libdir} -lcjs - -Name: cjs-1.0 -Description: Cinnamon JS bindings for GObjects -Version: @VERSION@ diff --git a/cjs/atoms.cpp b/cjs/atoms.cpp new file mode 100644 index 0000000..08d93ab --- /dev/null +++ b/cjs/atoms.cpp @@ -0,0 +1,82 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2018 Philip Chimento + * Marco Trevisan + * + * 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. + */ + +#define GJS_USE_ATOM_FOREACH + +#include + +#include +#include +#include +#include +#include +#include // for JS_AtomizeAndPinString + +#include "cjs/atoms.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} + +bool GjsAtom::init(JSContext* cx, const char* str) { + JSString* s = JS_AtomizeAndPinString(cx, str); + if (!s) + return false; + m_jsid = JS::Heap{JS::PropertyKey::fromPinnedString(s)}; + return true; +} + +bool GjsSymbolAtom::init(JSContext* cx, const char* str) { + JS::RootedString descr(cx, JS_AtomizeAndPinString(cx, str)); + if (!descr) + return false; + JS::Symbol* symbol = JS::NewSymbol(cx, descr); + if (!symbol) + return false; + m_jsid = JS::Heap{SYMBOL_TO_JSID(symbol)}; + return true; +} + +/* Requires a current realm. This can GC, so it needs to be done after the + * tracing has been set up. */ +bool GjsAtoms::init_atoms(JSContext* cx) { +#define INITIALIZE_ATOM(identifier, str) \ + if (!identifier.init(cx, str)) \ + return false; + FOR_EACH_ATOM(INITIALIZE_ATOM) + FOR_EACH_SYMBOL_ATOM(INITIALIZE_ATOM) + return true; +} + +void GjsAtoms::trace(JSTracer* trc) { +#define TRACE_ATOM(identifier, str) \ + JS::TraceEdge(trc, identifier.id(), "Atom " str); + FOR_EACH_ATOM(TRACE_ATOM) + FOR_EACH_SYMBOL_ATOM(TRACE_ATOM) +#undef TRACE_ATOM +} diff --git a/cjs/atoms.h b/cjs/atoms.h new file mode 100644 index 0000000..d93c44f --- /dev/null +++ b/cjs/atoms.h @@ -0,0 +1,136 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2018 Philip Chimento + * Marco Trevisan + * + * 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. + */ + +#ifndef GJS_ATOMS_H_ +#define GJS_ATOMS_H_ + +#include + +#include +#include +#include + +#include "cjs/macros.h" + +class JSTracer; + +// clang-format off +#define FOR_EACH_ATOM(macro) \ + macro(code, "code") \ + macro(column_number, "columnNumber") \ + macro(connect_after, "connect_after") \ + macro(constructor, "constructor") \ + macro(debuggee, "debuggee") \ + macro(detail, "detail") \ + macro(emit, "emit") \ + macro(file, "__file__") \ + macro(file_name, "fileName") \ + macro(func, "func") \ + macro(gi, "gi") \ + macro(gio, "Gio") \ + macro(glib, "GLib") \ + macro(gobject, "GObject") \ + macro(gtype, "$gtype") \ + macro(height, "height") \ + macro(imports, "imports") \ + macro(init, "_init") \ + macro(instance_init, "_instance_init") \ + macro(interact, "interact") \ + macro(length, "length") \ + macro(line_number, "lineNumber") \ + macro(message, "message") \ + macro(module_init, "__init__") \ + macro(module_name, "__moduleName__") \ + macro(module_path, "__modulePath__") \ + macro(name, "name") \ + macro(new_, "new") \ + macro(new_internal, "_new_internal") \ + macro(overrides, "overrides") \ + macro(param_spec, "ParamSpec") \ + macro(parent_module, "__parentModule__") \ + macro(program_invocation_name, "programInvocationName") \ + macro(prototype, "prototype") \ + macro(search_path, "searchPath") \ + macro(signal_id, "signalId") \ + macro(stack, "stack") \ + macro(to_string, "toString") \ + macro(value_of, "valueOf") \ + macro(version, "version") \ + macro(versions, "versions") \ + macro(width, "width") \ + macro(window, "window") \ + macro(x, "x") \ + macro(y, "y") + +#define FOR_EACH_SYMBOL_ATOM(macro) \ + macro(hook_up_vfunc, "__GObject__hook_up_vfunc") \ + macro(private_ns_marker, "__gjsPrivateNS") \ + macro(signal_find, "__GObject__signal_find") \ + macro(signals_block, "__GObject__signals_block") \ + macro(signals_disconnect, "__GObject__signals_disconnect") \ + macro(signals_unblock, "__GObject__signals_unblock") +// clang-format on + +struct GjsAtom { + GJS_JSAPI_RETURN_CONVENTION bool init(JSContext* cx, const char* str); + + /* It's OK to return JS::HandleId here, to avoid an extra root, with the + * caveat that you should not use this value after the GjsContext has been + * destroyed.*/ + [[nodiscard]] JS::HandleId operator()() const { + return JS::HandleId::fromMarkedLocation(&m_jsid.get()); + } + + [[nodiscard]] JS::Heap* id() { return &m_jsid; } + + protected: + JS::Heap m_jsid; +}; + +struct GjsSymbolAtom : GjsAtom { + GJS_JSAPI_RETURN_CONVENTION bool init(JSContext* cx, const char* str); +}; + +class GjsAtoms { + public: + GjsAtoms(void) {} + ~GjsAtoms(void) {} // prevents giant destructor from being inlined + GJS_JSAPI_RETURN_CONVENTION bool init_atoms(JSContext* cx); + + void trace(JSTracer* trc); + +#define DECLARE_ATOM_MEMBER(identifier, str) GjsAtom identifier; +#define DECLARE_SYMBOL_ATOM_MEMBER(identifier, str) GjsSymbolAtom identifier; + FOR_EACH_ATOM(DECLARE_ATOM_MEMBER) + FOR_EACH_SYMBOL_ATOM(DECLARE_SYMBOL_ATOM_MEMBER) +#undef DECLARE_ATOM_MEMBER +#undef DECLARE_SYMBOL_ATOM_MEMBER +}; + +#ifndef GJS_USE_ATOM_FOREACH +# undef FOR_EACH_ATOM +# undef FOR_EACH_SYMBOL_ATOM +#endif + +#endif // GJS_ATOMS_H_ diff --git a/cjs/byteArray.cpp b/cjs/byteArray.cpp index 930850f..68d18bb 100644 --- a/cjs/byteArray.cpp +++ b/cjs/byteArray.cpp @@ -22,546 +22,240 @@ */ #include -#include -#include -#include "byteArray.h" -#include "gi/boxed.h" -#include "jsapi-class.h" -#include "jsapi-wrapper.h" -#include "jsapi-util-args.h" -#include -#include - -typedef struct { - GByteArray *array; - GBytes *bytes; -} ByteArrayInstance; - -extern struct JSClass gjs_byte_array_class; -GJS_DEFINE_PRIV_FROM_JS(ByteArrayInstance, gjs_byte_array_class) - -static bool byte_array_get_prop (JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p); -static bool byte_array_set_prop (JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p, - JS::ObjectOpResult& result); -GJS_NATIVE_CONSTRUCTOR_DECLARE(byte_array); -static void byte_array_finalize (JSFreeOp *fop, - JSObject *obj); - -static JSObject *gjs_byte_array_get_proto(JSContext *); - -static const struct JSClassOps gjs_byte_array_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - byte_array_get_prop, - byte_array_set_prop, - NULL, /* enumerate */ - NULL, /* resolve */ - nullptr, /* mayResolve */ - byte_array_finalize -}; - -struct JSClass gjs_byte_array_class = { - "ByteArray", - JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE, - &gjs_byte_array_class_ops -}; -bool -gjs_typecheck_bytearray(JSContext *context, - JS::HandleObject object, - bool throw_error) -{ - return do_base_typecheck(context, object, throw_error); -} +#include +#include // for strcmp, memchr, strlen -static JS::Value -gjs_value_from_gsize(gsize v) -{ - if (v <= (gsize) JSVAL_INT_MAX) { - return JS::Int32Value(v); - } - return JS::NumberValue(v); -} +#include +#include +#include -static void -byte_array_ensure_array (ByteArrayInstance *priv) -{ - if (priv->bytes) { - priv->array = g_bytes_unref_to_array(priv->bytes); - priv->bytes = NULL; - } else { - g_assert(priv->array); - } -} +#include +#include +#include // for AutoCheckCannotGC +#include +#include +#include +#include // for UniqueChars +#include // for JS_DefineFunctionById, JS_DefineFun... +#include // for JS_NewUint8ArrayWithBuffer, GetUint... -static void -byte_array_ensure_gbytes (ByteArrayInstance *priv) -{ - if (priv->array) { - priv->bytes = g_byte_array_free_to_bytes(priv->array); - priv->array = NULL; - } else { - g_assert(priv->bytes); - } -} +#include "gi/boxed.h" +#include "cjs/atoms.h" +#include "cjs/byteArray.h" +#include "cjs/context-private.h" +#include "cjs/deprecation.h" +#include "cjs/jsapi-util-args.h" +#include "cjs/jsapi-util.h" -static bool -gjs_value_to_gsize(JSContext *context, - JS::HandleValue value, - gsize *v_p) -{ - guint32 val32; - - /* Just JS::ToUint32() would work. However, we special case ints for a nicer - * error message on negative indices. - */ - if (value.isInt32()) { - int i = value.toInt32(); - if (i < 0) { - gjs_throw(context, "Negative length or index %d is not allowed for ByteArray", - i); - return false; - } - *v_p = i; - return true; - } else { - bool ret; - /* This is pretty liberal (it converts about anything to - * a number) but it's what we use elsewhere in gjs too. - */ +/* Callbacks to use with JS::NewExternalArrayBuffer() */ - ret = JS::ToUint32(context, value, &val32); - *v_p = val32; - return ret; - } +static void gfree_arraybuffer_contents(void* contents, void*) { + g_free(contents); } -static bool -gjs_value_to_byte(JSContext *context, - JS::HandleValue value, - guint8 *v_p) -{ - gsize v; - - if (!gjs_value_to_gsize(context, value, &v)) - return false; - - if (v >= 256) { - gjs_throw(context, - "Value %" G_GSIZE_FORMAT " is not a valid byte; must be in range [0,255]", - v); - return false; - } - - *v_p = v; - return true; +static void bytes_unref_arraybuffer(void* contents [[maybe_unused]], + void* user_data) { + auto* gbytes = static_cast(user_data); + g_bytes_unref(gbytes); } -static bool -byte_array_get_index(JSContext *context, - JS::HandleObject obj, - ByteArrayInstance *priv, - gsize idx, - JS::MutableHandleValue value_p) -{ - gsize len; - guint8 *data; - - gjs_byte_array_peek_data(context, obj, &data, &len); - - if (idx >= len) { - gjs_throw(context, - "Index %" G_GSIZE_FORMAT " is out of range for ByteArray length %lu", - idx, - (unsigned long)len); +GJS_JSAPI_RETURN_CONVENTION +bool to_string_impl_slow(JSContext* cx, uint8_t* data, uint32_t len, + const char* encoding, JS::MutableHandleValue rval) { + size_t bytes_written; + GError* error = nullptr; + GjsAutoChar u16_str = g_convert(reinterpret_cast(data), len, + // Make sure the bytes of the UTF-16 string are laid out in memory + // such that we can simply reinterpret_cast them. +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + "UTF-16LE", +#else + "UTF-16BE", +#endif + encoding, nullptr, /* bytes read */ + &bytes_written, &error); + if (!u16_str) + return gjs_throw_gerror_message(cx, error); // frees GError + + // bytes_written should be bytes in a UTF-16 string so should be a multiple + // of 2 + g_assert((bytes_written % 2) == 0); + + // g_convert 0-terminates the string, although the 0 isn't included in + // bytes_written + JSString* s = + JS_NewUCStringCopyZ(cx, reinterpret_cast(u16_str.get())); + if (!s) return false; - } - - value_p.setInt32(data[idx]); + rval.setString(s); return true; } -/* a hook on getting a property; set value_p to override property's value. - * Return value is false on OOM/exception. - */ -static bool -byte_array_get_prop(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p) -{ - ByteArrayInstance *priv; - - priv = priv_from_js(context, obj); - - if (!priv) - return true; /* prototype, not an instance. */ - - JS::RootedValue id_value(context); - if (!JS_IdToValue(context, id, &id_value)) - return false; - - /* First handle array indexing */ - if (id_value.isNumber()) { - gsize idx; - if (!gjs_value_to_gsize(context, id_value, &idx)) - return false; - return byte_array_get_index(context, obj, priv, idx, value_p); - } - - /* We don't special-case anything else for now. Regular JS arrays - * allow string versions of ints for the index, we don't bother. - */ - - return true; -} - -static bool -byte_array_length_getter(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, to, ByteArrayInstance, priv); - gsize len = 0; - - if (!priv) - return true; /* prototype, not an instance. */ - - if (priv->array != NULL) - len = priv->array->len; - else if (priv->bytes != NULL) - len = g_bytes_get_size (priv->bytes); - args.rval().set(gjs_value_from_gsize(len)); - return true; -} - -static bool -byte_array_length_setter(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, to, ByteArrayInstance, priv); - gsize len = 0; - - if (!priv) - return true; /* prototype, not instance */ - - byte_array_ensure_array(priv); - - if (!gjs_value_to_gsize(context, args[0], &len)) { +/* implement toString() with an optional encoding arg */ +GJS_JSAPI_RETURN_CONVENTION +static bool to_string_impl(JSContext* context, JS::HandleObject byte_array, + const char* encoding, JS::MutableHandleValue rval) { + if (!JS_IsUint8Array(byte_array)) { gjs_throw(context, - "Can't set ByteArray length to non-integer"); + "Argument to ByteArray.toString() must be a Uint8Array"); return false; } - g_byte_array_set_size(priv->array, len); - args.rval().setUndefined(); - return true; -} -static bool -byte_array_set_index(JSContext *context, - JS::HandleObject obj, - ByteArrayInstance *priv, - gsize idx, - JS::MutableHandleValue value_p, - JS::ObjectOpResult& result) -{ - guint8 v; + bool encoding_is_utf8; + uint8_t* data; - if (!gjs_value_to_byte(context, value_p, &v)) { - return false; + if (encoding) { + /* maybe we should be smarter about utf8 synonyms here. + * doesn't matter much though. encoding_is_utf8 is + * just an optimization anyway. + */ + encoding_is_utf8 = (strcmp(encoding, "UTF-8") == 0); + } else { + encoding_is_utf8 = true; } - byte_array_ensure_array(priv); + uint32_t len; + bool is_shared_memory; + js::GetUint8ArrayLengthAndData(byte_array, &len, &is_shared_memory, &data); - /* grow the array if necessary */ - if (idx >= priv->array->len) { - g_byte_array_set_size(priv->array, - idx + 1); + if (len == 0) { + rval.setString(JS_GetEmptyString(context)); + return true; } - g_array_index(priv->array, guint8, idx) = v; - - /* Stop JS from storing a copy of the value */ - value_p.setUndefined(); + if (!encoding_is_utf8) + return to_string_impl_slow(context, data, len, encoding, rval); - return result.succeed(); -} - -/* a hook on setting a property; set value_p to override property value to - * be set. Return value is false on OOM/exception. - */ -static bool -byte_array_set_prop(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p, - JS::ObjectOpResult& result) -{ - ByteArrayInstance *priv; - - priv = priv_from_js(context, obj); + // optimization, avoids iconv overhead and runs libmozjs hardwired + // utf8-to-utf16 - if (!priv) - return result.succeed(); /* prototype, not an instance. */ - - JS::RootedValue id_value(context); - if (!JS_IdToValue(context, id, &id_value)) - return false; - - /* First handle array indexing */ - if (id_value.isNumber()) { - gsize idx; - if (!gjs_value_to_gsize(context, id_value, &idx)) + // If there are any 0 bytes, including the terminating byte, stop at the + // first one + if (data[len - 1] == 0 || memchr(data, 0, len)) { + if (!gjs_string_from_utf8(context, reinterpret_cast(data), rval)) return false; - - return byte_array_set_index(context, obj, priv, idx, value_p, result); - } - - /* We don't special-case anything else for now */ - - return result.succeed(); -} - -static GByteArray * -gjs_g_byte_array_new(int preallocated_length) -{ - GByteArray *array; - - /* can't use g_byte_array_new() because we need to clear to zero. - * We nul-terminate too for ease of toString() and for security - * paranoia. - */ - array = (GByteArray*) g_array_sized_new (true, /* nul-terminated */ - true, /* clear to zero */ - 1, /* element size */ - preallocated_length); - if (preallocated_length > 0) { - /* we want to not only allocate the size, but have it - * already be the array's length. - */ - g_byte_array_set_size(array, preallocated_length); - } - - return array; -} - -GJS_NATIVE_CONSTRUCTOR_DECLARE(byte_array) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(byte_array) - ByteArrayInstance *priv; - gsize preallocated_length; - - GJS_NATIVE_CONSTRUCTOR_PRELUDE(byte_array); - - preallocated_length = 0; - if (argc >= 1) { - if (!gjs_value_to_gsize(context, argv[0], &preallocated_length)) { - gjs_throw(context, - "Argument to ByteArray constructor should be a positive number for array length"); + } else { + if (!gjs_string_from_utf8_n(context, reinterpret_cast(data), len, + rval)) return false; - } } - priv = g_slice_new0(ByteArrayInstance); - priv->array = gjs_g_byte_array_new(preallocated_length); - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - GJS_NATIVE_CONSTRUCTOR_FINISH(byte_array); + uint8_t* current_data; + uint32_t current_len; + bool ignore_val; + + // If a garbage collection occurs between when we call + // js::GetUint8ArrayLengthAndData and return from gjs_string_from_utf8, a + // use-after-free corruption can occur if the garbage collector shifts the + // location of the Uint8Array's private data. To mitigate this we call + // js::GetUint8ArrayLengthAndData again and then compare if the length and + // pointer are still the same. If the pointers differ, we use the slow path + // to ensure no data corruption occurred. The shared-ness of an array cannot + // change between calls, so we ignore it. + js::GetUint8ArrayLengthAndData(byte_array, ¤t_len, &ignore_val, + ¤t_data); + + // Ensure the private data hasn't changed + if (current_len == len && current_data == data) + return true; - return true; + // This was the UTF-8 optimized path, so we explicitly pass the encoding + return to_string_impl_slow(context, current_data, current_len, "UTF-8", + rval); } -static void -byte_array_finalize(JSFreeOp *fop, - JSObject *obj) -{ - ByteArrayInstance *priv; - - priv = (ByteArrayInstance*) JS_GetPrivate(obj); - - if (!priv) - return; /* prototype, not instance */ +GJS_JSAPI_RETURN_CONVENTION +static bool to_string_func(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::UniqueChars encoding; + JS::RootedObject byte_array(cx); - if (priv->array) { - g_byte_array_free(priv->array, true); - priv->array = NULL; - } else if (priv->bytes) { - g_clear_pointer(&priv->bytes, g_bytes_unref); - } + if (!gjs_parse_call_args(cx, "toString", args, "o|s", "byteArray", + &byte_array, "encoding", &encoding)) + return false; - g_slice_free(ByteArrayInstance, priv); + return to_string_impl(cx, byte_array, encoding.get(), args.rval()); } -/* implement toString() with an optional encoding arg */ -static bool -to_string_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, argv, to, ByteArrayInstance, priv); - GjsAutoJSChar encoding; - bool encoding_is_utf8; - gchar *data; +/* Workaround to keep existing code compatible. This function is tacked onto + * any Uint8Array instances created in situations where previously a ByteArray + * would have been created. It logs a compatibility warning. */ +GJS_JSAPI_RETURN_CONVENTION +static bool instance_to_string_func(JSContext* cx, unsigned argc, + JS::Value* vp) { + GJS_GET_THIS(cx, argc, vp, args, this_obj); + JS::UniqueChars encoding; - if (!priv) - return true; /* prototype, not instance */ - - byte_array_ensure_array(priv); - - if (argc >= 1 && argv[0].isString()) { - 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. - * doesn't matter much though. encoding_is_utf8 is - * just an optimization anyway. - */ - encoding_is_utf8 = (strcmp(encoding, "UTF-8") == 0); - } else { - encoding_is_utf8 = true; - } - - if (priv->array->len == 0) - /* the internal data pointer could be NULL in this case */ - data = (gchar*)""; - else - data = (gchar*)priv->array->data; - - if (encoding_is_utf8) { - /* optimization, avoids iconv overhead and runs - * libmozjs hardwired utf8-to-utf16 - */ - return gjs_string_from_utf8_n(context, data, priv->array->len, argv.rval()); - } else { - bool ok = false; - gsize bytes_written; - GError *error; - JSString *s; - char *u16_str; - char16_t *u16_out; - - error = NULL; - u16_str = g_convert(data, - priv->array->len, - "UTF-16", - encoding, - NULL, /* bytes read */ - &bytes_written, - &error); - if (u16_str == NULL) { - /* frees the GError */ - gjs_throw_g_error(context, error); - return false; - } + _gjs_warn_deprecated_once_per_callsite( + cx, GjsDeprecationMessageId::ByteArrayInstanceToString); - /* bytes_written should be bytes in a UTF-16 string so - * should be a multiple of 2 - */ - g_assert((bytes_written % 2) == 0); - - u16_out = g_new(char16_t, bytes_written / 2); - memcpy(u16_out, u16_str, bytes_written); - s = JS_NewUCStringCopyN(context, u16_out, bytes_written / 2); - if (s != NULL) { - ok = true; - argv.rval().setString(s); - } + if (!gjs_parse_call_args(cx, "toString", args, "|s", "encoding", &encoding)) + return false; - g_free(u16_str); - g_free(u16_out); - return ok; - } + return to_string_impl(cx, this_obj, encoding.get(), args.rval()); } +GJS_JSAPI_RETURN_CONVENTION static bool to_gbytes_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, to, ByteArrayInstance, priv); - JSObject *ret_bytes_obj; + JS::CallArgs rec = JS::CallArgsFromVp(argc, vp); GIBaseInfo *gbytes_info; + JS::RootedObject byte_array(context); - if (!priv) - return true; /* prototype, not instance */ + if (!gjs_parse_call_args(context, "toGBytes", rec, "o", + "byteArray", &byte_array)) + return false; - byte_array_ensure_gbytes(priv); + if (!JS_IsUint8Array(byte_array)) { + gjs_throw(context, + "Argument to ByteArray.toGBytes() must be a Uint8Array"); + return false; + } + GBytes* bytes = gjs_byte_array_get_bytes(byte_array); + + g_irepository_require(nullptr, "GLib", "2.0", GIRepositoryLoadFlags(0), + nullptr); gbytes_info = g_irepository_find_by_gtype(NULL, G_TYPE_BYTES); - ret_bytes_obj = gjs_boxed_from_c_struct(context, (GIStructInfo*)gbytes_info, - priv->bytes, GJS_BOXED_CREATION_NONE); + JSObject* ret_bytes_obj = + BoxedInstance::new_for_c_struct(context, gbytes_info, bytes); + g_bytes_unref(bytes); + if (!ret_bytes_obj) + return false; - rec.rval().setObjectOrNull(ret_bytes_obj); + rec.rval().setObject(*ret_bytes_obj); return true; } -static JSObject* -byte_array_new(JSContext *context) -{ - ByteArrayInstance *priv; - - JS::RootedObject proto(context, gjs_byte_array_get_proto(context)); - JS::RootedObject array(context, - JS_NewObjectWithGivenProto(context, &gjs_byte_array_class, proto)); - - priv = g_slice_new0(ByteArrayInstance); - - g_assert(priv_from_js(context, array) == NULL); - JS_SetPrivate(array, priv); - - return array; -} - /* fromString() function implementation */ +GJS_JSAPI_RETURN_CONVENTION static bool from_string_func(JSContext *context, unsigned argc, JS::Value *vp) { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - ByteArrayInstance *priv; - GjsAutoJSChar encoding; + JS::UniqueChars encoding; + JS::UniqueChars utf8; bool encoding_is_utf8; - JS::RootedObject obj(context, byte_array_new(context)); - - if (!obj) - return false; - - priv = priv_from_js(context, obj); - g_assert (priv != NULL); + JS::RootedObject obj(context), array_buffer(context); - g_assert(argc > 0); /* because we specified min args 1 */ - - priv->array = gjs_g_byte_array_new(0); - - if (!argv[0].isString()) { - gjs_throw(context, - "byteArray.fromString() called with non-string as first arg"); + if (!gjs_parse_call_args(context, "fromString", argv, "s|s", + "string", &utf8, + "encoding", &encoding)) return false; - } - - if (argc > 1 && argv[1].isString()) { - JS::RootedString str(context, argv[1].toString()); - encoding = JS_EncodeStringToUTF8(context, str); - if (!encoding) - return false; + if (argc > 1) { /* maybe we should be smarter about utf8 synonyms here. * doesn't matter much though. encoding_is_utf8 is * just an optimization anyway. */ - encoding_is_utf8 = (strcmp(encoding, "UTF-8") == 0); + encoding_is_utf8 = (strcmp(encoding.get(), "UTF-8") == 0); } else { encoding_is_utf8 = true; } @@ -570,15 +264,9 @@ from_string_func(JSContext *context, /* optimization? avoids iconv overhead and runs * libmozjs hardwired utf16-to-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); - g_byte_array_append(priv->array, - reinterpret_cast(utf8.get()), - strlen(utf8)); + size_t len = strlen(utf8.get()); + array_buffer = + JS::NewArrayBufferWithContents(context, len, utf8.release()); } else { JSString *str = argv[0].toString(); /* Rooted by argv */ GError *error = NULL; @@ -598,7 +286,7 @@ from_string_func(JSContext *context, return false; encoded = g_convert((char *) chars, len, - encoding, /* to_encoding */ + encoding.get(), // to_encoding "LATIN1", /* from_encoding */ NULL, /* bytes read */ &bytes_written, &error); @@ -609,94 +297,35 @@ from_string_func(JSContext *context, return false; encoded = g_convert((char *) chars, len * 2, - encoding, /* to_encoding */ + encoding.get(), // to_encoding "UTF-16", /* from_encoding */ NULL, /* bytes read */ &bytes_written, &error); } } - if (encoded == NULL) { - /* frees the GError */ - gjs_throw_g_error(context, error); - return false; - } - - g_byte_array_set_size(priv->array, 0); - g_byte_array_append(priv->array, (guint8*) encoded, bytes_written); + if (!encoded) + return gjs_throw_gerror_message(context, error); // frees GError - g_free(encoded); + array_buffer = + JS::NewExternalArrayBuffer(context, bytes_written, encoded, + gfree_arraybuffer_contents, nullptr); } - argv.rval().setObject(*obj); - return true; -} - -/* fromArray() function implementation */ -static bool -from_array_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - ByteArrayInstance *priv; - guint32 len; - guint32 i; - bool is_array; - JS::RootedObject obj(context, byte_array_new(context)); - - if (!obj) + if (!array_buffer) return false; + obj = JS_NewUint8ArrayWithBuffer(context, array_buffer, 0, -1); - priv = priv_from_js(context, obj); - g_assert (priv != NULL); - - g_assert(argc > 0); /* because we specified min args 1 */ - - priv->array = gjs_g_byte_array_new(0); - - JS::RootedObject array_obj(context, &argv[0].toObject()); - if (!JS_IsArrayObject(context, array_obj, &is_array)) - return false; - if (!is_array) { - gjs_throw(context, - "byteArray.fromArray() called with non-array as first arg"); - return false; - } - - if (!JS_GetArrayLength(context, array_obj, &len)) { - gjs_throw(context, - "byteArray.fromArray() can't get length of first array arg"); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (!JS_DefineFunctionById(context, obj, atoms.to_string(), + instance_to_string_func, 1, 0)) return false; - } - - g_byte_array_set_size(priv->array, len); - - JS::RootedValue elem(context); - for (i = 0; i < len; ++i) { - guint8 b; - - elem = JS::UndefinedValue(); - if (!JS_GetElement(context, array_obj, i, &elem)) { - /* this means there was an exception, while elem.isUndefined() - * means no element found - */ - return false; - } - - if (elem.isUndefined()) - continue; - - if (!gjs_value_to_byte(context, elem, &b)) - return false; - - g_array_index(priv->array, guint8, i) = b; - } argv.rval().setObject(*obj); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool from_gbytes_func(JSContext *context, unsigned argc, @@ -705,133 +334,93 @@ from_gbytes_func(JSContext *context, JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); JS::RootedObject bytes_obj(context); GBytes *gbytes; - ByteArrayInstance *priv; - if (!gjs_parse_call_args(context, "overrides_gbytes_to_array", argv, "o", + if (!gjs_parse_call_args(context, "fromGBytes", argv, "o", "bytes", &bytes_obj)) return false; - if (!gjs_typecheck_boxed(context, bytes_obj, NULL, G_TYPE_BYTES, true)) + if (!BoxedBase::typecheck(context, bytes_obj, nullptr, G_TYPE_BYTES)) + return false; + + gbytes = BoxedBase::to_c_ptr(context, bytes_obj); + if (!gbytes) return false; - gbytes = (GBytes*) gjs_c_struct_from_boxed(context, bytes_obj); + size_t len; + const void* data = g_bytes_get_data(gbytes, &len); + JS::RootedObject array_buffer( + context, + JS::NewExternalArrayBuffer( + context, len, + const_cast(data), // the ArrayBuffer won't modify the data + bytes_unref_arraybuffer, gbytes)); + if (!array_buffer) + return false; + g_bytes_ref(gbytes); // now owned by both ArrayBuffer and BoxedBase - JS::RootedObject obj(context, byte_array_new(context)); + JS::RootedObject obj( + context, JS_NewUint8ArrayWithBuffer(context, array_buffer, 0, -1)); if (!obj) return false; - priv = priv_from_js(context, obj); - g_assert (priv != NULL); - priv->bytes = g_bytes_ref(gbytes); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (!JS_DefineFunctionById(context, obj, atoms.to_string(), + instance_to_string_func, 1, 0)) + return false; argv.rval().setObject(*obj); return true; } -JSObject * -gjs_byte_array_from_byte_array (JSContext *context, - GByteArray *array) -{ - ByteArrayInstance *priv; - - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(array != NULL, NULL); - - JS::RootedObject proto(context, gjs_byte_array_get_proto(context)); - JS::RootedObject object(context, - JS_NewObjectWithGivenProto(context, &gjs_byte_array_class, proto)); - - if (!object) { - gjs_throw(context, "failed to create byte array"); - return NULL; - } +JSObject* gjs_byte_array_from_data(JSContext* cx, size_t nbytes, void* data) { + JS::RootedObject array_buffer(cx); + // a null data pointer takes precedence over whatever `nbytes` says + if (data) + array_buffer = + JS::NewArrayBufferWithContents(cx, nbytes, g_memdup(data, nbytes)); + else + array_buffer = JS::NewArrayBuffer(cx, 0); + if (!array_buffer) + return nullptr; - priv = g_slice_new0(ByteArrayInstance); - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - priv->array = g_byte_array_new(); - priv->array->data = (guint8*) g_memdup(array->data, array->len); - priv->array->len = array->len; + JS::RootedObject array(cx, + JS_NewUint8ArrayWithBuffer(cx, array_buffer, 0, -1)); - return object; + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (!JS_DefineFunctionById(cx, array, atoms.to_string(), + instance_to_string_func, 1, 0)) + return nullptr; + return array; } -GBytes * -gjs_byte_array_get_bytes (JSContext *context, - JS::HandleObject object) -{ - ByteArrayInstance *priv; - priv = priv_from_js(context, object); - g_assert(priv != NULL); - - byte_array_ensure_gbytes(priv); - - return g_bytes_ref (priv->bytes); +JSObject* gjs_byte_array_from_byte_array(JSContext* cx, GByteArray* array) { + return gjs_byte_array_from_data(cx, array->len, array->data); } -GByteArray * -gjs_byte_array_get_byte_array (JSContext *context, - JS::HandleObject obj) -{ - ByteArrayInstance *priv; - priv = priv_from_js(context, obj); - g_assert(priv != NULL); - - byte_array_ensure_array(priv); +GBytes* gjs_byte_array_get_bytes(JSObject* obj) { + bool is_shared_memory; + uint32_t len; + uint8_t* data; - return g_byte_array_ref (priv->array); + js::GetUint8ArrayLengthAndData(obj, &len, &is_shared_memory, &data); + return g_bytes_new(data, len); } -void -gjs_byte_array_peek_data (JSContext *context, - JS::HandleObject obj, - guint8 **out_data, - gsize *out_len) -{ - ByteArrayInstance *priv; - priv = priv_from_js(context, obj); - g_assert(priv != NULL); - - if (priv->array != NULL) { - *out_data = (guint8*)priv->array->data; - *out_len = (gsize)priv->array->len; - } else if (priv->bytes != NULL) { - *out_data = (guint8*)g_bytes_get_data(priv->bytes, out_len); - } else { - g_assert_not_reached(); - } +GByteArray* gjs_byte_array_get_byte_array(JSObject* obj) { + return g_bytes_unref_to_array(gjs_byte_array_get_bytes(obj)); } -static JSPropertySpec gjs_byte_array_proto_props[] = { - JS_PSGS("length", byte_array_length_getter, byte_array_length_setter, - JSPROP_PERMANENT), - JS_PS_END -}; - -static JSFunctionSpec gjs_byte_array_proto_funcs[] = { - JS_FS("toString", to_string_func, 0, 0), - JS_FS("toGBytes", to_gbytes_func, 0, 0), - JS_FS_END -}; - -static JSFunctionSpec *gjs_byte_array_static_funcs = nullptr; - static JSFunctionSpec gjs_byte_array_module_funcs[] = { - JS_FS("fromString", from_string_func, 1, 0), - JS_FS("fromArray", from_array_func, 1, 0), - JS_FS("fromGBytes", from_gbytes_func, 1, 0), - JS_FS_END -}; - -GJS_DEFINE_PROTO_FUNCS(byte_array) + JS_FN("fromString", from_string_func, 2, 0), + JS_FN("fromGBytes", from_gbytes_func, 1, 0), + JS_FN("toGBytes", to_gbytes_func, 1, 0), + JS_FN("toString", to_string_func, 2, 0), + JS_FS_END}; bool gjs_define_byte_array_stuff(JSContext *cx, JS::MutableHandleObject module) { module.set(JS_NewPlainObject(cx)); - - JS::RootedObject proto(cx); - return gjs_byte_array_define_proto(cx, module, &proto) && - JS_DefineFunctions(cx, module, gjs_byte_array_module_funcs); + return JS_DefineFunctions(cx, module, gjs_byte_array_module_funcs); } diff --git a/cjs/byteArray.h b/cjs/byteArray.h index 8372887..58e6f75 100644 --- a/cjs/byteArray.h +++ b/cjs/byteArray.h @@ -21,36 +21,31 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_BYTE_ARRAY_H__ -#define __GJS_BYTE_ARRAY_H__ +#ifndef GJS_BYTEARRAY_H_ +#define GJS_BYTEARRAY_H_ + +#include + +#include // for size_t -#include #include -#include "cjs/jsapi-util.h" -G_BEGIN_DECLS +#include -bool gjs_typecheck_bytearray(JSContext *context, - JS::HandleObject obj, - bool throw_error); +#include "cjs/macros.h" +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_byte_array_stuff(JSContext *context, JS::MutableHandleObject module); +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_byte_array_from_data(JSContext* cx, size_t nbytes, void* data); + +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_byte_array_from_byte_array (JSContext *context, GByteArray *array); -GByteArray *gjs_byte_array_get_byte_array(JSContext *context, - JS::HandleObject object); - -GBytes *gjs_byte_array_get_bytes(JSContext *context, - JS::HandleObject object); - -void gjs_byte_array_peek_data(JSContext *context, - JS::HandleObject object, - guint8 **out_data, - gsize *out_len); - -G_END_DECLS +[[nodiscard]] GByteArray* gjs_byte_array_get_byte_array(JSObject* obj); +[[nodiscard]] GBytes* gjs_byte_array_get_bytes(JSObject* obj); -#endif /* __GJS_BYTE_ARRAY_H__ */ +#endif // GJS_BYTEARRAY_H_ diff --git a/cjs/cjs.stp.in b/cjs/cjs.stp.in index 4170f23..ae7f895 100644 --- a/cjs/cjs.stp.in +++ b/cjs/cjs.stp.in @@ -1,18 +1,18 @@ -probe gjs.object_proxy_new = process("@EXPANDED_LIBDIR@/libgjs-gi.so.0.0.0").mark("object__proxy__new") +probe gjs.object_wrapper_new = process("@EXPANDED_LIBDIR@/libgjs-gi.so.0.0.0").mark("object__wrapper__new") { - proxy_address = $arg1; + wrapper_address = $arg1; gobject_address = $arg2; gi_namespace = user_string($arg3); gi_name = user_string($arg4); - probestr = sprintf("gjs.object_proxy_new(%p, %s, %s)", proxy_address, gi_namespace, gi_name); + probestr = sprintf("gjs.object_wrapper_new(%p, %s, %s)", wrapper_address, gi_namespace, gi_name); } -probe gjs.object_proxy_finalize = process("@EXPANDED_LIBDIR@/libgjs-gi.so.0.0.0").mark("object__proxy__finalize") +probe gjs.object_wrapper_finalize = process("@EXPANDED_LIBDIR@/libgjs-gi.so.0.0.0").mark("object__wrapper__finalize") { - proxy_address = $arg1; + wrapper_address = $arg1; gobject_address = $arg2; gi_namespace = user_string($arg3); gi_name = user_string($arg4); - probestr = sprintf("gjs.object_proxy_finalize(%p, %s, %s)", proxy_address, gi_namespace, gi_name); + probestr = sprintf("gjs.object_wrapper_finalize(%p, %s, %s)", wrapper_address, gi_namespace, gi_name); } diff --git a/cjs/console.cpp b/cjs/console.cpp index 917e71d..4bbe6e6 100644 --- a/cjs/console.cpp +++ b/cjs/console.cpp @@ -21,12 +21,21 @@ * IN THE SOFTWARE. */ -#include -#include -#include -#include +#include // for PACKAGE_STRING + +#include // for setlocale, LC_ALL +#include // for exit +#include // for strcmp, strlen + +#ifdef HAVE_UNISTD_H +# include // for close +#elif defined (_WIN32) +# include +#endif #include +#include +#include #include @@ -37,11 +46,12 @@ static char *profile_output_path = nullptr; static char *command = NULL; static gboolean print_version = false; static gboolean print_js_version = false; +static gboolean debugging = 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() */ +// clang-format off 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, @@ -54,13 +64,12 @@ static GOptionEntry entries[] = { G_OPTION_ARG_CALLBACK, reinterpret_cast(&parse_profile_arg), "Enable the profiler and write output to FILE (default: gjs-$PID.syscap)", "FILE" }, + { "debugger", 'd', 0, G_OPTION_ARG_NONE, &debugging, "Start in debug mode" }, { NULL } }; +// clang-format on -static char ** -strndupv(int n, - char * const *strv) -{ +[[nodiscard]] static char** strndupv(int n, char* const* strv) { int ix; if (n == 0) return NULL; @@ -71,10 +80,7 @@ strndupv(int n, return retval; } -static char ** -strcatv(char **strv1, - char **strv2) -{ +[[nodiscard]] static char** strcatv(char** strv1, char** strv2) { if (strv1 == NULL && strv2 == NULL) return NULL; if (strv1 == NULL) @@ -96,32 +102,14 @@ strcatv(char **strv1, return retval; } -static gboolean -parse_profile_arg(const char *option_name, - const char *value, - void *data, - GError **error_out) -{ +static gboolean parse_profile_arg(const char* option_name [[maybe_unused]], + const char* value, void*, GError**) { enable_profiler = true; g_free(profile_output_path); - if (value) - profile_output_path = g_strdup(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) @@ -130,13 +118,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[] = { + // Don't add new entries here. This is only for arguments that were + // previously accepted after the script name on the command line, for + // backwards compatibility. + 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); @@ -189,6 +177,29 @@ check_script_args_for_stray_gjs_args(int argc, g_strfreev(argv_copy); } +int define_argv_and_eval_script(GjsContext* js_context, int argc, + char* const* argv, const char* script, + size_t len, const char* filename) { + GError* error = nullptr; + + /* prepare command line arguments */ + if (!gjs_context_define_string_array( + js_context, "ARGV", argc, const_cast(argv), &error)) { + g_critical("Failed to define ARGV: %s", error->message); + g_clear_error(&error); + return 1; + } + + /* evaluate the script */ + int code; + if (!gjs_context_eval(js_context, script, len, filename, &code, &error)) { + if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT)) + g_critical("%s", error->message); + g_clear_error(&error); + } + return code; +} + int main(int argc, char **argv) { @@ -200,12 +211,11 @@ main(int argc, char **argv) const char *filename; const char *program_name; gsize len; - int code, gjs_argc = argc, script_argc, ix; + int gjs_argc = argc, script_argc, ix; char **argv_copy = g_strdupv(argv), **argv_copy_addr = argv_copy; char **gjs_argv, **gjs_argv_addr; char * const *script_argv; const char *env_coverage_output_path; - const char *env_coverage_prefixes; bool interactive_mode = false; setlocale(LC_ALL, ""); @@ -248,10 +258,16 @@ main(int argc, char **argv) command = NULL; print_version = false; print_js_version = false; + debugging = 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)) - g_error("option parsing failed: %s", error->message); + if (!g_option_context_parse_strv(context, &gjs_argv, &error)) { + char* help_text = g_option_context_get_help(context, true, nullptr); + g_printerr("%s\n\n%s\n", error->message, help_text); + g_free(help_text); + g_option_context_free(context); + exit(1); + } g_option_context_free (context); @@ -292,11 +308,31 @@ 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); + /* Check for GJS_TRACE_FD for sysprof profiling */ + const char* env_tracefd = g_getenv("GJS_TRACE_FD"); + int tracefd = -1; + if (env_tracefd) { + tracefd = g_ascii_strtoll(env_tracefd, nullptr, 10); + g_setenv("GJS_TRACE_FD", "", true); + if (tracefd > 0) + enable_profiler = true; + } + 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() */ + g_unsetenv("GJS_TRACE_FD"); /* ignore env var in eval() */ + } + + const char* env_coverage_prefixes = g_getenv("GJS_COVERAGE_PREFIXES"); + if (env_coverage_prefixes) { + if (coverage_prefixes) + g_strfreev(coverage_prefixes); + coverage_prefixes = g_strsplit(env_coverage_prefixes, ":", -1); } + if (coverage_prefixes) + gjs_coverage_enable(); js_context = (GjsContext*) g_object_new(GJS_TYPE_CONTEXT, "search-path", include_path, @@ -310,13 +346,6 @@ main(int argc, char **argv) coverage_output_path = g_strdup(env_coverage_output_path); } - env_coverage_prefixes = g_getenv("GJS_COVERAGE_PREFIXES"); - if (env_coverage_prefixes != NULL) { - if (coverage_prefixes != NULL) - g_strfreev(coverage_prefixes); - coverage_prefixes = g_strsplit(env_coverage_prefixes, ":", -1); - } - if (coverage_prefixes) { if (!coverage_output_path) g_error("--coverage-output is required when taking coverage statistics"); @@ -329,28 +358,25 @@ main(int argc, char **argv) if (enable_profiler && profile_output_path) { GjsProfiler *profiler = gjs_context_get_profiler(js_context); gjs_profiler_set_filename(profiler, profile_output_path); + } else if (enable_profiler && tracefd > -1) { + GjsProfiler* profiler = gjs_context_get_profiler(js_context); + gjs_profiler_set_fd(profiler, tracefd); + tracefd = -1; } - /* prepare command line arguments */ - if (!gjs_context_define_string_array(js_context, "ARGV", - script_argc, (const char **) script_argv, - &error)) { - code = 1; - g_printerr("Failed to defined ARGV: %s", error->message); - g_clear_error(&error); - goto out; + if (tracefd != -1) { + close(tracefd); + tracefd = -1; } - /* evaluate the script */ - if (!gjs_context_eval(js_context, script, len, - filename, &code, &error)) { - if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT)) - g_printerr("%s\n", error->message); - g_clear_error(&error); - goto out; - } + /* If we're debugging, set up the debugger. It will break on the first + * frame. */ + if (debugging) + gjs_context_setup_debugger_console(js_context); + + int code = define_argv_and_eval_script(js_context, script_argc, script_argv, + script, len, filename); - out: g_strfreev(gjs_argv_addr); /* Probably doesn't make sense to write statistics on failure */ @@ -364,5 +390,8 @@ main(int argc, char **argv) g_object_unref(coverage); g_object_unref(js_context); g_free(script); + + if (debugging) + g_print("Program exited with code %d\n", code); exit(code); } diff --git a/cjs/context-private.h b/cjs/context-private.h index 49c0cf9..dc3ec00 100644 --- a/cjs/context-private.h +++ b/cjs/context-private.h @@ -21,48 +21,239 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_CONTEXT_PRIVATE_H__ -#define __GJS_CONTEXT_PRIVATE_H__ +#ifndef GJS_CONTEXT_PRIVATE_H_ +#define GJS_CONTEXT_PRIVATE_H_ -#include +#include -#include "context.h" -#include "jsapi-util.h" -#include "jsapi-wrapper.h" +#include +#include // for ssize_t -G_BEGIN_DECLS +#include // for is_same +#include -bool _gjs_context_destroying (GjsContext *js_context); +#include +#include -void _gjs_context_schedule_gc_if_needed (GjsContext *js_context); +#include +#include +#include +#include +#include +#include +#include // for JS_GetContextPrivate +#include // for ScriptEnvironmentPreparer +#include // for DefaultHasher +#include -void _gjs_context_schedule_gc(GjsContext *js_context); +#include "cjs/context.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "cjs/profiler.h" -void _gjs_context_exit(GjsContext *js_context, - uint8_t exit_code); +namespace js { +class SystemAllocPolicy; +} +class GjsAtoms; +class JSTracer; -bool _gjs_context_get_is_owner_thread(GjsContext *js_context); +using JobQueueStorage = + JS::GCVector, 0, js::SystemAllocPolicy>; +using ObjectInitList = + JS::GCVector, 0, js::SystemAllocPolicy>; +using FundamentalTable = + JS::GCHashMap, js::DefaultHasher, + js::SystemAllocPolicy>; +using GTypeTable = + JS::GCHashMap, js::DefaultHasher, + js::SystemAllocPolicy>; -bool _gjs_context_should_exit(GjsContext *js_context, - uint8_t *exit_code_p); +struct Dummy {}; +using GTypeNotUint64 = + std::conditional_t, GType, Dummy>; -void _gjs_context_set_sweeping(GjsContext *js_context, - bool sweeping); +// The GC sweep method should ignore FundamentalTable and GTypeTable's key types +namespace JS { +// Forward declarations +template +class WeakCache; +template +struct GCPolicy; -bool _gjs_context_is_sweeping(JSContext *cx); +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +// We need GCPolicy for GTypeTable. SpiderMonkey already defines +// GCPolicy which is equal to GType on some systems; for others we +// need to define it. (macOS's uint64_t is unsigned long long, which is a +// different type from unsigned long, even if they are the same width) +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} // namespace JS -bool _gjs_context_enqueue_job(GjsContext *gjs_context, - JS::HandleObject job); +class GjsContextPrivate : public JS::JobQueue { + GjsContext* m_public_context; + JSContext* m_cx; + JS::Heap m_global; + GThread* m_owner_thread; -bool _gjs_context_run_jobs(GjsContext *gjs_context); + char* m_program_name; -void _gjs_context_unregister_unhandled_promise_rejection(GjsContext *gjs_context, - uint64_t promise_id); + char** m_search_path; -G_END_DECLS + unsigned m_auto_gc_id; -void _gjs_context_register_unhandled_promise_rejection(GjsContext *gjs_context, - uint64_t promise_id, - GjsAutoChar&& stack); + GjsAtoms* m_atoms; -#endif /* __GJS_CONTEXT_PRIVATE_H__ */ + JobQueueStorage m_job_queue; + unsigned m_idle_drain_handler; + + std::unordered_map m_unhandled_rejection_stacks; + + GjsProfiler* m_profiler; + + /* Environment preparer needed for debugger, taken from SpiderMonkey's + * JS shell */ + struct EnvironmentPreparer final : protected js::ScriptEnvironmentPreparer { + JSContext* m_cx; + + explicit EnvironmentPreparer(JSContext* cx) : m_cx(cx) { + js::SetScriptEnvironmentPreparer(m_cx, this); + } + + void invoke(JS::HandleObject scope, Closure& closure) override; + }; + EnvironmentPreparer m_environment_preparer; + + // Weak pointer mapping from fundamental native pointer to JSObject + JS::WeakCache* m_fundamental_table; + JS::WeakCache* m_gtype_table; + + // List that holds JSObject GObject wrappers for JS-created classes, from + // the time of their creation until their GObject instance init function is + // called + ObjectInitList m_object_init_list; + + uint8_t m_exit_code; + + /* flags */ + bool m_destroying : 1; + bool m_in_gc_sweep : 1; + bool m_should_exit : 1; + bool m_force_gc : 1; + bool m_draining_job_queue : 1; + bool m_should_profile : 1; + bool m_should_listen_sigusr2 : 1; + + int64_t m_sweep_begin_time; + + void schedule_gc_internal(bool force_gc); + static gboolean trigger_gc_if_needed(void* data); + + class SavedQueue; + void start_draining_job_queue(void); + void stop_draining_job_queue(void); + static gboolean drain_job_queue_idle_handler(void* data); + + void warn_about_unhandled_promise_rejections(void); + + class AutoResetExit { + GjsContextPrivate* m_self; + + public: + explicit AutoResetExit(GjsContextPrivate* self) { m_self = self; } + ~AutoResetExit() { + m_self->m_should_exit = false; + m_self->m_exit_code = 0; + } + }; + + public: + /* Retrieving a GjsContextPrivate from JSContext or GjsContext */ + [[nodiscard]] static GjsContextPrivate* from_cx(JSContext* cx) { + return static_cast(JS_GetContextPrivate(cx)); + } + [[nodiscard]] static GjsContextPrivate* from_object( + GObject* public_context); + [[nodiscard]] static GjsContextPrivate* from_object( + GjsContext* public_context); + [[nodiscard]] static GjsContextPrivate* from_current_context(); + + GjsContextPrivate(JSContext* cx, GjsContext* public_context); + ~GjsContextPrivate(void); + + /* Accessors */ + [[nodiscard]] GjsContext* public_context() const { + return m_public_context; + } + [[nodiscard]] JSContext* context() const { return m_cx; } + [[nodiscard]] JSObject* global() const { return m_global.get(); } + [[nodiscard]] GjsProfiler* profiler() const { return m_profiler; } + [[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; } + [[nodiscard]] bool destroying() const { return m_destroying; } + [[nodiscard]] bool sweeping() const { return m_in_gc_sweep; } + [[nodiscard]] const char* program_name() const { return m_program_name; } + void set_program_name(char* value) { m_program_name = value; } + void set_search_path(char** value) { m_search_path = value; } + void set_should_profile(bool value) { m_should_profile = value; } + void set_should_listen_sigusr2(bool value) { + m_should_listen_sigusr2 = value; + } + [[nodiscard]] bool is_owner_thread() const { + return m_owner_thread == g_thread_self(); + } + [[nodiscard]] JS::WeakCache& fundamental_table() { + return *m_fundamental_table; + } + [[nodiscard]] JS::WeakCache& gtype_table() { + return *m_gtype_table; + } + [[nodiscard]] ObjectInitList& object_init_list() { + return m_object_init_list; + } + [[nodiscard]] static const GjsAtoms& atoms(JSContext* cx) { + return *(from_cx(cx)->m_atoms); + } + + GJS_JSAPI_RETURN_CONVENTION + bool eval(const char* script, ssize_t script_len, const char* filename, + int* exit_status_p, GError** error); + GJS_JSAPI_RETURN_CONVENTION + bool eval_with_scope(JS::HandleObject scope_object, const char* script, + ssize_t script_len, const char* filename, + JS::MutableHandleValue retval); + GJS_JSAPI_RETURN_CONVENTION + bool call_function(JS::HandleObject this_obj, JS::HandleValue func_val, + const JS::HandleValueArray& args, + JS::MutableHandleValue rval); + + void schedule_gc(void) { schedule_gc_internal(true); } + void schedule_gc_if_needed(void); + + void exit(uint8_t exit_code); + [[nodiscard]] bool should_exit(uint8_t* exit_code_p) const; + + // Implementations of JS::JobQueue virtual functions + GJS_JSAPI_RETURN_CONVENTION + JSObject* getIncumbentGlobal(JSContext* cx) override; + GJS_JSAPI_RETURN_CONVENTION + bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise, + JS::HandleObject job, + JS::HandleObject allocation_site, + JS::HandleObject incumbent_global) override; + void runJobs(JSContext* cx) override; + [[nodiscard]] bool empty() const override { return m_job_queue.empty(); } + js::UniquePtr saveJobQueue( + JSContext* cx) override; + + GJS_JSAPI_RETURN_CONVENTION bool run_jobs_fallible(void); + void register_unhandled_promise_rejection(uint64_t id, GjsAutoChar&& stack); + void unregister_unhandled_promise_rejection(uint64_t id); + + void set_sweeping(bool value); + + static void trace(JSTracer* trc, void* data); + + void free_profiler(void); + void dispose(void); +}; +#endif // GJS_CONTEXT_PRIVATE_H_ diff --git a/cjs/context.cpp b/cjs/context.cpp index f44a8de..bf803c7 100644 --- a/cjs/context.cpp +++ b/cjs/context.cpp @@ -23,40 +23,73 @@ #include -#include -#include -#include +#include // for sigaction, SIGUSR1, sa_handler +#include +#include // for FILE, fclose, size_t +#include // for memset + +#ifdef HAVE_UNISTD_H +# include // for getpid +#elif defined (_WIN32) +# include +#endif -#include +#include +#include // for u16string +#include // for remove_reference<>::type #include +#include // for move +#include #include - -#include "context-private.h" -#include "engine.h" -#include "global.h" -#include "importer.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" - -#include - -#include -#include -#include +#include +#include +#include #ifdef G_OS_WIN32 #define WIN32_LEAN_AND_MEAN #include #endif -#include +#include // for SystemAllocPolicy +#include // for UndefinedHandleValue +#include +#include +#include // for JS_GC, JS_AddExtraGCRootsTr... +#include // for WeakCache +#include // for RootedVector +#include // for JobQueue::SavedJobQueue +#include // for JSPROP_PERMANENT, JSPROP_RE... +#include +#include +#include +#include +#include +#include // for DeletePolicy +#include +#include +#include // for JS_IsExceptionPending, ... +#include // for DumpHeap, IgnoreNurseryObjects +#include + +#include "gi/object.h" +#include "gi/private.h" +#include "gi/repo.h" +#include "cjs/atoms.h" +#include "cjs/byteArray.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/engine.h" +#include "cjs/error-types.h" +#include "cjs/global.h" +#include "cjs/importer.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem.h" +#include "cjs/native.h" +#include "cjs/profiler-private.h" +#include "cjs/profiler.h" +#include "modules/modules.h" +#include "util/log.h" static void gjs_context_dispose (GObject *object); static void gjs_context_finalize (GObject *object); @@ -70,67 +103,40 @@ static void gjs_context_set_property (GObject *object, const GValue *value, GParamSpec *pspec); -using JobQueue = JS::GCVector; +void GjsContextPrivate::EnvironmentPreparer::invoke(JS::HandleObject scope, + Closure& closure) { + g_assert(!JS_IsExceptionPending(m_cx)); + + JSAutoRealm ar(m_cx, scope); + if (!closure(m_cx)) + gjs_log_exception(m_cx); +} struct _GjsContext { GObject parent; - - JSContext *context; - JS::Heap global; - GThread *owner_thread; - - char *program_name; - - char **search_path; - - bool destroying; - bool in_gc_sweep; - - bool should_exit; - uint8_t exit_code; - - guint auto_gc_id; - bool force_gc; - - std::array const_strings; - - JS::PersistentRooted *job_queue; - unsigned idle_drain_handler; - 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 */ -static const char *const_strings[] = { - "constructor", "prototype", "length", - "imports", "__parentModule__", "__init__", "searchPath", - "__gjsKeepAlive", "__gjsPrivateNS", - "gi", "versions", "overrides", - "_init", "_instance_init", "_new_internal", "new", - "message", "code", "stack", "fileName", "lineNumber", "columnNumber", - "name", "x", "y", "width", "height", "__modulePath__" -}; - -G_STATIC_ASSERT(G_N_ELEMENTS(const_strings) == GJS_STRING_LAST); - 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 +G_DEFINE_TYPE_WITH_PRIVATE(GjsContext, gjs_context, G_TYPE_OBJECT); + +GjsContextPrivate* GjsContextPrivate::from_object(GObject* js_context) { + g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr); + return static_cast( + gjs_context_get_instance_private(GJS_CONTEXT(js_context))); +} + +GjsContextPrivate* GjsContextPrivate::from_object(GjsContext* js_context) { + g_return_val_if_fail(GJS_IS_CONTEXT(js_context), nullptr); + return static_cast( + gjs_context_get_instance_private(js_context)); +} + +GjsContextPrivate* GjsContextPrivate::from_current_context() { + return from_object(gjs_context_get_current()); +} enum { PROP_0, @@ -146,6 +152,8 @@ static GList *all_contexts = NULL; static GjsAutoChar dump_heap_output; static unsigned dump_heap_idle_id = 0; +#ifdef G_OS_UNIX +/* Currently heap dumping is only supported on UNIX platforms! */ static void gjs_context_dump_heaps(void) { @@ -163,16 +171,14 @@ gjs_context_dump_heaps(void) 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); + auto* gjs = static_cast(l->data); + js::DumpHeap(gjs->context(), fp, js::IgnoreNurseryObjects); } fclose(fp); } -static gboolean -dump_heap_idle(gpointer user_data) -{ +static gboolean dump_heap_idle(void*) { dump_heap_idle_id = 0; gjs_context_dump_heaps(); @@ -180,13 +186,12 @@ dump_heap_idle(gpointer user_data) return false; } -static void -dump_heap_signal_handler(int signum) -{ +static void dump_heap_signal_handler(int signum [[maybe_unused]]) { if (dump_heap_idle_id == 0) dump_heap_idle_id = g_idle_add_full(G_PRIORITY_HIGH_IDLE, dump_heap_idle, nullptr, nullptr); } +#endif static void setup_dump_heap(void) @@ -198,6 +203,7 @@ setup_dump_heap(void) /* install signal handler only if environment variable is set */ const char *heap_output = g_getenv("GJS_DEBUG_HEAP_OUTPUT"); if (heap_output) { +#ifdef G_OS_UNIX struct sigaction sa; dump_heap_output = g_strdup(heap_output); @@ -205,6 +211,10 @@ setup_dump_heap(void) memset(&sa, 0, sizeof(sa)); sa.sa_handler = dump_heap_signal_handler; sigaction(SIGUSR1, &sa, nullptr); +#else + g_message( + "heap dump is currently only supported on UNIX platforms"); +#endif } } } @@ -288,7 +298,7 @@ gjs_context_class_init(GjsContextClass *klass) #ifdef G_OS_WIN32 extern HMODULE gjs_dll; char *basedir = g_win32_get_package_installation_directory_of_module (gjs_dll); - char *priv_typelib_dir = g_build_filename (basedir, "lib", "girepository-1.0", NULL); + char *priv_typelib_dir = g_build_filename (basedir, "lib", "gjs", "girepository-1.0", NULL); g_free (basedir); #else char *priv_typelib_dir = g_build_filename (PKGLIBDIR, "girepository-1.0", NULL); @@ -297,24 +307,23 @@ gjs_context_class_init(GjsContextClass *klass) g_free (priv_typelib_dir); } - gjs_register_native_module("byteArray", gjs_define_byte_array_stuff); + gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff); gjs_register_native_module("_gi", gjs_define_private_gi_stuff); gjs_register_native_module("gi", gjs_define_repo); gjs_register_static_modules(); } -static void -gjs_context_tracer(JSTracer *trc, void *data) -{ - GjsContext *gjs_context = reinterpret_cast(data); - JS::TraceEdge(trc, &gjs_context->global, "GJS global object"); +void GjsContextPrivate::trace(JSTracer* trc, void* data) { + auto* gjs = static_cast(data); + JS::TraceEdge(trc, &gjs->m_global, "GJS global object"); + gjs->m_atoms->trace(trc); + gjs->m_job_queue.trace(trc); + gjs->m_object_init_list.trace(trc); } -static void -warn_about_unhandled_promise_rejections(GjsContext *gjs_context) -{ - for (auto& kv : gjs_context->unhandled_rejection_stacks) { +void GjsContextPrivate::warn_about_unhandled_promise_rejections(void) { + for (auto& kv : m_unhandled_rejection_stacks) { const char *stack = kv.second; g_warning("Unhandled promise rejection. To suppress this warning, add " "an error handler to your promise chain with .catch() or a " @@ -323,7 +332,7 @@ warn_about_unhandled_promise_rejections(GjsContext *gjs_context) "Unfortunately there is no stack trace of the failed promise.", stack ? stack : ""); } - gjs_context->unhandled_rejection_stacks.clear(); + m_unhandled_rejection_stacks.clear(); } static void @@ -331,14 +340,10 @@ gjs_context_dispose(GObject *object) { gjs_debug(GJS_DEBUG_CONTEXT, "JS shutdown sequence"); - GjsContext *js_context; - - js_context = GJS_CONTEXT(object); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(object); /* 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); + gjs->free_profiler(); /* Stop accepting entries in the toggle queue before running dispose * notifications, which causes all GjsMaybeOwned instances to unroot. @@ -353,74 +358,74 @@ gjs_context_dispose(GObject *object) "Notifying reference holders of GjsContext dispose"); G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object); - if (js_context->context != NULL) { + gjs->dispose(); +} +void GjsContextPrivate::free_profiler(void) { + gjs_debug(GJS_DEBUG_CONTEXT, "Stopping profiler"); + if (m_profiler) + g_clear_pointer(&m_profiler, _gjs_profiler_free); +} + +void GjsContextPrivate::dispose(void) { + if (m_cx) { gjs_debug(GJS_DEBUG_CONTEXT, "Checking unhandled promise rejections"); - warn_about_unhandled_promise_rejections(js_context); + warn_about_unhandled_promise_rejections(); - JS_BeginRequest(js_context->context); + gjs_debug(GJS_DEBUG_CONTEXT, "Releasing cached JS wrappers"); + m_fundamental_table->clear(); + m_gtype_table->clear(); /* Do a full GC here before tearing down, since once we do * 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); + JS_GC(m_cx); gjs_debug(GJS_DEBUG_CONTEXT, "Destroying JS context"); - js_context->destroying = true; + m_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(); + ObjectInstance::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; + if (m_auto_gc_id > 0) { + g_source_remove(m_auto_gc_id); + m_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; + JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this); + m_global = nullptr; gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources"); - delete js_context->job_queue; + delete m_fundamental_table; + delete m_gtype_table; + delete m_atoms; /* Tear down JS */ - JS_DestroyContext(js_context->context); - js_context->context = NULL; + JS_DestroyContext(m_cx); + m_cx = nullptr; + // don't use g_clear_pointer() as we want the pointer intact while we + // destroy the context in case we dump stack gjs_debug(GJS_DEBUG_CONTEXT, "JS context destroyed"); } } +GjsContextPrivate::~GjsContextPrivate(void) { + g_clear_pointer(&m_search_path, g_strfreev); + g_clear_pointer(&m_program_name, g_free); +} + static void gjs_context_finalize(GObject *object) { - GjsContext *js_context; - - js_context = GJS_CONTEXT(object); - - if (js_context->search_path != NULL) { - g_strfreev(js_context->search_path); - js_context->search_path = NULL; - } - - if (js_context->program_name != NULL) { - g_free(js_context->program_name); - js_context->program_name = NULL; - } - if (gjs_context_get_current() == (GjsContext*)object) gjs_context_make_current(NULL); @@ -428,9 +433,8 @@ gjs_context_finalize(GObject *object) all_contexts = g_list_remove(all_contexts, object); g_mutex_unlock(&contexts_lock); - js_context->global.~Heap(); - js_context->const_strings.~array(); - js_context->unhandled_rejection_stacks.~unordered_map(); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(object); + gjs->~GjsContextPrivate(); G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object); } @@ -438,86 +442,91 @@ static void gjs_context_constructed(GObject *object) { GjsContext *js_context = GJS_CONTEXT(object); - int i; G_OBJECT_CLASS(gjs_context_parent_class)->constructed(object); - js_context->owner_thread = g_thread_self(); - - JSContext *cx = gjs_create_js_context(js_context); + GjsContextPrivate* gjs_location = GjsContextPrivate::from_object(object); + JSContext* cx = gjs_create_js_context(gjs_location); if (!cx) g_error("Failed to create javascript context"); - js_context->context = cx; + + new (gjs_location) GjsContextPrivate(cx, js_context); + + 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, &ObjectInstance::context_dispose_notify, nullptr); +} + +GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context) + : m_public_context(public_context), + m_cx(cx), + m_environment_preparer(cx) { + m_owner_thread = g_thread_self(); const char *env_profiler = g_getenv("GJS_ENABLE_PROFILER"); - if (env_profiler || js_context->should_listen_sigusr2) - js_context->should_profile = true; + if (env_profiler || m_should_listen_sigusr2) + m_should_profile = true; - if (js_context->should_profile) { - js_context->profiler = _gjs_profiler_new(js_context); + if (m_should_profile) { + m_profiler = _gjs_profiler_new(public_context); - if (!js_context->profiler) { - js_context->should_profile = false; + if (!m_profiler) { + m_should_profile = false; } else { - if (js_context->should_listen_sigusr2) - _gjs_profiler_setup_signals(js_context->profiler, js_context); + if (m_should_listen_sigusr2) + _gjs_profiler_setup_signals(m_profiler, public_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++) { - js_context->const_strings[i] = new JS::PersistentRootedId(cx, - gjs_intern_string_to_id(cx, const_strings[i])); - } + JSRuntime* rt = JS_GetRuntime(m_cx); + m_fundamental_table = new JS::WeakCache(rt); + m_gtype_table = new JS::WeakCache(rt); - js_context->job_queue = new JS::PersistentRooted(cx); - if (!js_context->job_queue) - g_error("Failed to initialize promise job queue"); + m_atoms = new GjsAtoms(); - JS_BeginRequest(cx); + JS::RootedObject global( + m_cx, gjs_create_global_object(cx, GjsGlobalType::DEFAULT)); - JS::RootedObject global(cx, gjs_create_global_object(cx)); if (!global) { - gjs_log_exception(js_context->context); + gjs_log_exception(m_cx); g_error("Failed to initialize global object"); } - JSAutoCompartment ac(cx, global); + JSAutoRealm ar(m_cx, global); - new (&js_context->global) JS::Heap(global); - JS_AddExtraGCRootsTracer(cx, gjs_context_tracer, js_context); + m_global = global; + JS_AddExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this); + + if (!m_atoms->init_atoms(m_cx)) { + gjs_log_exception(m_cx); + g_error("Failed to initialize global strings"); + } - JS::RootedObject importer(cx, gjs_create_root_importer(cx, - js_context->search_path ? js_context->search_path : nullptr)); - if (!importer) + std::vector paths; + if (m_search_path) + paths = {m_search_path, m_search_path + g_strv_length(m_search_path)}; + JS::RootedObject importer(m_cx, gjs_create_root_importer(m_cx, paths)); + if (!importer) { + gjs_log_exception(cx); g_error("Failed to create root importer"); + } - JS::Value v_importer = gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS); + JS::Value v_importer = gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS); g_assert(((void) "Someone else already created root importer", v_importer.isUndefined())); - gjs_set_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS, JS::ObjectValue(*importer)); + gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS, + JS::ObjectValue(*importer)); - if (!gjs_define_global_properties(cx, global, "default")) { - gjs_log_exception(cx); + if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT, + "GJS", "default")) { + gjs_log_exception(m_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 @@ -526,13 +535,11 @@ gjs_context_get_property (GObject *object, GValue *value, GParamSpec *pspec) { - GjsContext *js_context; - - js_context = GJS_CONTEXT (object); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(object); switch (prop_id) { case PROP_PROGRAM_NAME: - g_value_set_string(value, js_context->program_name); + g_value_set_string(value, gjs->program_name()); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -546,22 +553,20 @@ gjs_context_set_property (GObject *object, const GValue *value, GParamSpec *pspec) { - GjsContext *js_context; - - js_context = GJS_CONTEXT (object); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(object); switch (prop_id) { case PROP_SEARCH_PATH: - js_context->search_path = (char**) g_value_dup_boxed(value); + gjs->set_search_path(static_cast(g_value_dup_boxed(value))); break; case PROP_PROGRAM_NAME: - js_context->program_name = g_value_dup_string(value); + gjs->set_program_name(g_value_dup_string(value)); break; case PROP_PROFILER_ENABLED: - js_context->should_profile = g_value_get_boolean(value); + gjs->set_should_profile(g_value_get_boolean(value)); break; case PROP_PROFILER_SIGUSR2: - js_context->should_listen_sigusr2 = g_value_get_boolean(value); + gjs->set_should_listen_sigusr2(g_value_get_boolean(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -584,135 +589,143 @@ gjs_context_new_with_search_path(char** search_path) NULL); } -bool -_gjs_context_destroying (GjsContext *context) -{ - return context->destroying; -} +gboolean GjsContextPrivate::trigger_gc_if_needed(void* data) { + auto* gjs = static_cast(data); + gjs->m_auto_gc_id = 0; -static gboolean -trigger_gc_if_needed (gpointer user_data) -{ - GjsContext *js_context = GJS_CONTEXT(user_data); - js_context->auto_gc_id = 0; - - if (js_context->force_gc) - JS_GC(js_context->context); - else - gjs_gc_if_needed(js_context->context); - - js_context->force_gc = false; + if (gjs->m_force_gc) { + gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer hit"); + JS_GC(gjs->m_cx); + } else { + gjs_gc_if_needed(gjs->m_cx); + } + gjs->m_force_gc = false; return G_SOURCE_REMOVE; } +void GjsContextPrivate::schedule_gc_internal(bool force_gc) { + m_force_gc |= force_gc; -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) + if (m_auto_gc_id > 0) return; - js_context->auto_gc_id = g_idle_add_full(G_PRIORITY_LOW, - trigger_gc_if_needed, - js_context, NULL); -} + if (force_gc) + gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Big Hammer scheduled"); -void -_gjs_context_schedule_gc(GjsContext *js_context) -{ - _gjs_context_schedule_gc_internal(js_context, true); + m_auto_gc_id = g_timeout_add_seconds_full(G_PRIORITY_LOW, 10, + trigger_gc_if_needed, this, + nullptr); } -void -_gjs_context_schedule_gc_if_needed(GjsContext *js_context) -{ - _gjs_context_schedule_gc_internal(js_context, false); -} +/* + * GjsContextPrivate::schedule_gc_if_needed: + * + * Does a minor GC immediately if the JS engine decides one is needed, but also + * schedules a full GC in the next idle time. + */ +void GjsContextPrivate::schedule_gc_if_needed(void) { + // We call JS_MaybeGC immediately, but defer a check for a full GC cycle + // to an idle handler. + JS_MaybeGC(m_cx); -void -_gjs_context_exit(GjsContext *js_context, - uint8_t exit_code) -{ - g_assert(!js_context->should_exit); - js_context->should_exit = true; - js_context->exit_code = exit_code; + schedule_gc_internal(false); } -bool -_gjs_context_should_exit(GjsContext *js_context, - uint8_t *exit_code_p) -{ - if (exit_code_p != NULL) - *exit_code_p = js_context->exit_code; - return js_context->should_exit; +void GjsContextPrivate::set_sweeping(bool value) { + // If we have a profiler enabled, record the duration of GC sweep + if (this->m_profiler != nullptr) { + int64_t now = g_get_monotonic_time() * 1000L; + + if (value) { + m_sweep_begin_time = now; + } else { + if (m_sweep_begin_time != 0) { + _gjs_profiler_add_mark(this->m_profiler, m_sweep_begin_time, + now - m_sweep_begin_time, "GJS", "Sweep", + nullptr); + m_sweep_begin_time = 0; + } + } + } + + m_in_gc_sweep = value; } -static void -context_reset_exit(GjsContext *js_context) -{ - js_context->should_exit = false; - js_context->exit_code = 0; +void GjsContextPrivate::exit(uint8_t exit_code) { + g_assert(!m_should_exit); + m_should_exit = true; + m_exit_code = exit_code; } -bool -_gjs_context_get_is_owner_thread(GjsContext *js_context) -{ - return js_context->owner_thread == g_thread_self(); +bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const { + if (exit_code_p != NULL) + *exit_code_p = m_exit_code; + return m_should_exit; } -void -_gjs_context_set_sweeping(GjsContext *js_context, - bool sweeping) -{ - js_context->in_gc_sweep = sweeping; +void GjsContextPrivate::start_draining_job_queue(void) { + if (!m_idle_drain_handler) + m_idle_drain_handler = g_idle_add_full( + G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr); } -bool -_gjs_context_is_sweeping(JSContext *cx) -{ - auto js_context = static_cast(JS_GetContextPrivate(cx)); - return js_context->in_gc_sweep; +void GjsContextPrivate::stop_draining_job_queue(void) { + m_draining_job_queue = false; + if (m_idle_drain_handler) { + g_source_remove(m_idle_drain_handler); + m_idle_drain_handler = 0; + } } -static gboolean -drain_job_queue_idle_handler(void *data) -{ - auto gjs_context = static_cast(data); - _gjs_context_run_jobs(gjs_context); +gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) { + auto* gjs = static_cast(data); + gjs->runJobs(gjs->context()); /* Uncatchable exceptions are swallowed here - no way to get a handle on * the main loop to exit it from this idle handler */ - g_assert(((void) "_gjs_context_run_jobs() should have emptied queue", - gjs_context->idle_drain_handler == 0)); + g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 && + "GjsContextPrivate::runJobs() should have emptied queue"); return G_SOURCE_REMOVE; } +JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) { + // This is equivalent to SpiderMonkey's behavior. + return JS::CurrentGlobalOrNull(cx); +} + /* See engine.cpp and JS::SetEnqueuePromiseJobCallback(). */ -bool -_gjs_context_enqueue_job(GjsContext *gjs_context, - JS::HandleObject job) -{ - if (gjs_context->idle_drain_handler) - g_assert(gjs_context->job_queue->length() > 0); +bool GjsContextPrivate::enqueuePromiseJob( + JSContext* cx, JS::HandleObject promise [[maybe_unused]], + JS::HandleObject job, JS::HandleObject allocation_site [[maybe_unused]], + JS::HandleObject incumbent_global [[maybe_unused]]) { + g_assert(cx == m_cx); + g_assert(from_cx(cx) == this); + + if (m_idle_drain_handler) + g_assert(!empty()); else - g_assert(gjs_context->job_queue->length() == 0); + g_assert(empty()); - if (!gjs_context->job_queue->append(job)) + if (!m_job_queue.append(job)) { + JS_ReportOutOfMemory(m_cx); return false; - if (!gjs_context->idle_drain_handler) - gjs_context->idle_drain_handler = - g_idle_add_full(G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, - gjs_context, nullptr); + } + start_draining_job_queue(); return true; } -/** - * _gjs_context_run_jobs: - * @gjs_context: The #GjsContext instance +// Override of JobQueue::runJobs(). Called by js::RunJobs(), and when execution +// of the job queue was interrupted by the debugger and is resuming. +void GjsContextPrivate::runJobs(JSContext* cx) { + g_assert(cx == m_cx); + g_assert(from_cx(cx) == this); + if (!run_jobs_fallible()) + gjs_log_exception(cx); +} + +/* + * GjsContext::run_jobs_fallible: * * Drains the queue of promise callbacks that the JS engine has reported * finished, calling each one and logging any exceptions that it throws. @@ -723,33 +736,27 @@ _gjs_context_enqueue_job(GjsContext *gjs_context, * Returns: false if one of the jobs threw an uncatchable exception; * otherwise true. */ -bool -_gjs_context_run_jobs(GjsContext *gjs_context) -{ +bool GjsContextPrivate::run_jobs_fallible(void) { bool retval = true; - g_assert(gjs_context->job_queue); - if (gjs_context->draining_job_queue || gjs_context->should_exit) + if (m_draining_job_queue || m_should_exit) return true; - auto cx = static_cast(gjs_context_get_native_context(gjs_context)); - JSAutoRequest ar(cx); + m_draining_job_queue = true; // Ignore reentrant calls - gjs_context->draining_job_queue = true; /* Ignore reentrant calls */ - - JS::RootedObject job(cx); + JS::RootedObject job(m_cx); JS::HandleValueArray args(JS::HandleValueArray::empty()); - JS::RootedValue rval(cx); + JS::RootedValue rval(m_cx); /* Execute jobs in a loop until we've reached the end of the queue. * Since executing a job can trigger enqueueing of additional jobs, * it's crucial to recheck the queue length during each iteration. */ - for (size_t ix = 0; ix < gjs_context->job_queue->length(); ix++) { + for (size_t ix = 0; ix < m_job_queue.length(); ix++) { /* A previous job might have set this flag. e.g., System.exit(). */ - if (gjs_context->should_exit) + if (m_should_exit) break; - job = gjs_context->job_queue->get()[ix]; + job = m_job_queue[ix]; /* It's possible that job draining was interrupted prematurely, * leaving the queue partly processed. In that case, slots for @@ -758,50 +765,76 @@ _gjs_context_run_jobs(GjsContext *gjs_context) if (!job) continue; - gjs_context->job_queue->get()[ix] = nullptr; + m_job_queue[ix] = nullptr; { - JSAutoCompartment ac(cx, job); - if (!JS::Call(cx, JS::UndefinedHandleValue, job, args, &rval)) { + JSAutoRealm ar(m_cx, job); + if (!JS::Call(m_cx, JS::UndefinedHandleValue, job, args, &rval)) { /* Uncatchable exception - return false so that * System.exit() works in the interactive shell and when * exiting the interpreter. */ - if (!JS_IsExceptionPending(cx)) { + if (!JS_IsExceptionPending(m_cx)) { /* System.exit() is an uncatchable exception, but does not * indicate a bug. Log everything else. */ - if (!_gjs_context_should_exit(gjs_context, nullptr)) + if (!should_exit(nullptr)) g_critical("Promise callback terminated with uncatchable exception"); retval = false; continue; } /* There's nowhere for the exception to go at this point */ - gjs_log_exception(cx); + gjs_log_exception_uncaught(m_cx); } } } - gjs_context->draining_job_queue = false; - gjs_context->job_queue->clear(); - if (gjs_context->idle_drain_handler) { - g_source_remove(gjs_context->idle_drain_handler); - gjs_context->idle_drain_handler = 0; - } + m_job_queue.clear(); + stop_draining_job_queue(); return retval; } -void -_gjs_context_register_unhandled_promise_rejection(GjsContext *gjs_context, - uint64_t id, - GjsAutoChar&& stack) -{ - gjs_context->unhandled_rejection_stacks[id] = std::move(stack); +class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue { + private: + GjsContextPrivate* m_gjs; + JS::PersistentRooted m_queue; + bool m_was_draining : 1; + + public: + explicit SavedQueue(GjsContextPrivate* gjs) + : m_gjs(gjs), + m_queue(gjs->m_cx, std::move(gjs->m_job_queue)), + m_was_draining(gjs->m_draining_job_queue) { + gjs->stop_draining_job_queue(); + } + + ~SavedQueue(void) { + m_gjs->m_job_queue = std::move(m_queue.get()); + if (m_was_draining) + m_gjs->start_draining_job_queue(); + } +}; + +js::UniquePtr GjsContextPrivate::saveJobQueue( + JSContext* cx) { + g_assert(cx == m_cx); + g_assert(from_cx(cx) == this); + + auto saved_queue = js::MakeUnique(this); + if (!saved_queue) { + JS_ReportOutOfMemory(cx); + return nullptr; + } + + g_assert(m_job_queue.empty()); + return saved_queue; } -void -_gjs_context_unregister_unhandled_promise_rejection(GjsContext *gjs_context, - uint64_t id) -{ - size_t erased = gjs_context->unhandled_rejection_stacks.erase(id); +void GjsContextPrivate::register_unhandled_promise_rejection( + uint64_t id, GjsAutoChar&& stack) { + m_unhandled_rejection_stacks[id] = std::move(stack); +} + +void GjsContextPrivate::unregister_unhandled_promise_rejection(uint64_t id) { + size_t erased = m_unhandled_rejection_stacks.erase(id); g_assert(((void)"Handler attached to rejected promise that wasn't " "previously marked as unhandled", erased == 1)); } @@ -809,10 +842,10 @@ _gjs_context_unregister_unhandled_promise_rejection(GjsContext *gjs_context, /** * gjs_context_maybe_gc: * @context: a #GjsContext - * + * * Similar to the Spidermonkey JS_MaybeGC() call which * heuristically looks at JS runtime memory usage and - * may initiate a garbage collection. + * may initiate a garbage collection. * * This function always unconditionally invokes JS_MaybeGC(), but * additionally looks at memory usage from the system malloc() @@ -826,24 +859,26 @@ _gjs_context_unregister_unhandled_promise_rejection(GjsContext *gjs_context, * * A good time to call this function is when your application * transitions to an idle state. - */ + */ void gjs_context_maybe_gc (GjsContext *context) { - gjs_maybe_gc(context->context); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(context); + gjs_maybe_gc(gjs->context()); } /** * gjs_context_gc: * @context: a #GjsContext - * + * * Initiate a full GC; may or may not block until complete. This * function just calls Spidermonkey JS_GC(). - */ + */ void gjs_context_gc (GjsContext *context) { - JS_GC(context->context); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(context); + JS_GC(gjs->context()); } /** @@ -878,7 +913,8 @@ void* gjs_context_get_native_context (GjsContext *js_context) { g_return_val_if_fail(GJS_IS_CONTEXT(js_context), NULL); - return js_context->context; + GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context); + return gjs->context(); } bool @@ -889,48 +925,55 @@ gjs_context_eval(GjsContext *js_context, int *exit_status_p, GError **error) { - bool ret = false; + g_return_val_if_fail(GJS_IS_CONTEXT(js_context), 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; + GjsAutoUnref js_context_ref(js_context, GjsAutoTakeOwnership()); - JSAutoCompartment ac(js_context->context, js_context->global); - JSAutoRequest ar(js_context->context); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context); + return gjs->eval(script, script_len, filename, exit_status_p, error); +} - g_object_ref(G_OBJECT(js_context)); +bool GjsContextPrivate::eval(const char* script, ssize_t script_len, + const char* filename, int* exit_status_p, + GError** error) { + AutoResetExit reset(this); + + bool auto_profile = m_should_profile; + if (auto_profile && + (_gjs_profiler_is_running(m_profiler) || m_should_listen_sigusr2)) + auto_profile = false; + + JSAutoRealm ar(m_cx, m_global); if (auto_profile) - gjs_profiler_start(js_context->profiler); + gjs_profiler_start(m_profiler); - JS::RootedValue retval(js_context->context); - bool ok = gjs_eval_with_scope(js_context->context, nullptr, script, - script_len, filename, &retval); + JS::RootedValue retval(m_cx); + bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval); /* 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. */ { - JS::AutoSaveExceptionState saved_exc(js_context->context); - ok = _gjs_context_run_jobs(js_context) && ok; + JS::AutoSaveExceptionState saved_exc(m_cx); + ok = run_jobs_fallible() && ok; } if (auto_profile) - gjs_profiler_stop(js_context->profiler); + gjs_profiler_stop(m_profiler); if (!ok) { uint8_t code; - if (_gjs_context_should_exit(js_context, &code)) { + if (should_exit(&code)) { /* exit_status_p is public API so can't be changed, but should be * uint8_t, not int */ *exit_status_p = code; g_set_error(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT, "Exit with code %d", code); - goto out; /* Don't log anything */ + return false; // Don't log anything } - if (!JS_IsExceptionPending(js_context->context)) { + if (!JS_IsExceptionPending(m_cx)) { g_critical("Script %s terminated with an uncatchable exception", filename); g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, @@ -941,10 +984,10 @@ gjs_context_eval(GjsContext *js_context, "Script %s threw an exception", filename); } - gjs_log_exception(js_context->context); + gjs_log_exception_uncaught(m_cx); /* No exit code from script, but we don't want to exit(0) */ *exit_status_p = 1; - goto out; + return false; } if (exit_status_p) { @@ -959,12 +1002,7 @@ gjs_context_eval(GjsContext *js_context, } } - ret = true; - - out: - g_object_unref(G_OBJECT(js_context)); - context_reset_exit(js_context); - return ret; + return true; } bool @@ -986,6 +1024,92 @@ gjs_context_eval_file(GjsContext *js_context, exit_status_p, error); } +/* + * GjsContextPrivate::eval_with_scope: + * @scope_object: an object to use as the global scope, or nullptr + * @script: JavaScript program encoded in UTF-8 + * @script_len: length of @script, or -1 if @script is 0-terminated + * @filename: filename to use as the origin of @script + * @retval: location for the return value of @script + * + * Executes @script with a local scope so that nothing from the script leaks out + * into the global scope. + * If @scope_object is given, then everything that @script placed in the global + * namespace is defined on @scope_object. + * Otherwise, the global definitions are just discarded. + */ +bool GjsContextPrivate::eval_with_scope(JS::HandleObject scope_object, + const char* script, ssize_t script_len, + const char* filename, + JS::MutableHandleValue retval) { + /* log and clear exception if it's set (should not be, normally...) */ + if (JS_IsExceptionPending(m_cx)) { + g_warning("eval_with_scope() called with a pending exception"); + return false; + } + + JS::RootedObject eval_obj(m_cx, scope_object); + if (!eval_obj) + eval_obj = JS_NewPlainObject(m_cx); + + std::u16string utf16_string = gjs_utf8_script_to_utf16(script, script_len); + // COMPAT: This could use JS::SourceText directly, + // but that messes up code coverage. See bug + // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784 + JS::SourceText buf; + if (!buf.init(m_cx, utf16_string.c_str(), utf16_string.size(), + JS::SourceOwnership::Borrowed)) + return false; + + JS::RootedObjectVector scope_chain(m_cx); + if (!scope_chain.append(eval_obj)) { + JS_ReportOutOfMemory(m_cx); + return false; + } + + JS::CompileOptions options(m_cx); + options.setFileAndLine(filename, 1); + + if (!JS::Evaluate(m_cx, scope_chain, options, buf, retval)) + return false; + + schedule_gc_if_needed(); + + if (JS_IsExceptionPending(m_cx)) { + g_warning( + "JS::Evaluate() returned true but exception was pending; " + "did somebody call gjs_throw() without returning false?"); + return false; + } + + gjs_debug(GJS_DEBUG_CONTEXT, "Script evaluation succeeded"); + + return true; +} + +/* + * GjsContextPrivate::call_function: + * @this_obj: Object to use as the 'this' for the function call + * @func_val: Callable to call, as a JS value + * @args: Arguments to pass to the callable + * @rval: Location for the return value + * + * Use this instead of JS_CallFunctionValue(), because it schedules a GC if + * one is needed. It's good practice to check if a GC should be run every time + * we return from JS back into C++. + */ +bool GjsContextPrivate::call_function(JS::HandleObject this_obj, + JS::HandleValue func_val, + const JS::HandleValueArray& args, + JS::MutableHandleValue rval) { + if (!JS_CallFunctionValue(m_cx, this_obj, func_val, args, rval)) + return false; + + schedule_gc_if_needed(); + + return true; +} + bool gjs_context_define_string_array(GjsContext *js_context, const char *array_name, @@ -993,15 +1117,22 @@ gjs_context_define_string_array(GjsContext *js_context, const char **array_values, GError **error) { - JSAutoCompartment ac(js_context->context, js_context->global); - JSAutoRequest ar(js_context->context); - - JS::RootedObject global_root(js_context->context, js_context->global); - if (!gjs_define_string_array(js_context->context, - global_root, - array_name, array_length, array_values, - JSPROP_READONLY | JSPROP_PERMANENT)) { - gjs_log_exception(js_context->context); + g_return_val_if_fail(GJS_IS_CONTEXT(js_context), false); + GjsContextPrivate* gjs = GjsContextPrivate::from_object(js_context); + + JSAutoRealm ar(gjs->context(), gjs->global()); + + std::vector strings; + if (array_values) { + if (array_length < 0) + array_length = g_strv_length(const_cast(array_values)); + strings = {array_values, array_values + array_length}; + } + + JS::RootedObject global_root(gjs->context(), gjs->global()); + if (!gjs_define_string_array(gjs->context(), global_root, array_name, + strings, JSPROP_READONLY | JSPROP_PERMANENT)) { + gjs_log_exception(gjs->context()); g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, @@ -1028,43 +1159,6 @@ gjs_context_make_current (GjsContext *context) current_context = context; } -/* It's OK to return JS::HandleId here, to avoid an extra root, with the - * caveat that you should not use this value after the GjsContext has - * been destroyed. */ -JS::HandleId -gjs_context_get_const_string(JSContext *context, - GjsConstString name) -{ - GjsContext *gjs_context = (GjsContext *) JS_GetContextPrivate(context); - return *gjs_context->const_strings[name]; -} - -/** - * gjs_get_import_global: - * @context: a #JSContext - * - * Gets the "import global" for the context's runtime. The import - * global object is the global object for the context. It is used - * as the root object for the scope of modules loaded by GJS in this - * runtime, and should also be used as the globals 'obj' argument passed - * to JS_InitClass() and the parent argument passed to JS_ConstructObject() - * when creating a native classes that are shared between all contexts using - * the runtime. (The standard JS classes are not shared, but we share - * classes such as GObject proxy classes since objects of these classes can - * easily migrate between contexts and having different classes depending - * on the context where they were first accessed would be confusing.) - * - * Return value: the "import global" for the context's - * runtime. Will never return %NULL while GJS has an active context - * for the runtime. - */ -JSObject* -gjs_get_import_global(JSContext *context) -{ - GjsContext *gjs_context = (GjsContext *) JS_GetContextPrivate(context); - return gjs_context->global; -} - /** * gjs_context_get_profiler: * @self: the #GjsContext @@ -1077,7 +1171,7 @@ gjs_get_import_global(JSContext *context) GjsProfiler * gjs_context_get_profiler(GjsContext *self) { - return self->profiler; + return GjsContextPrivate::from_object(self)->profiler(); } /** @@ -1091,4 +1185,4 @@ 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 129c59a..9e6cd42 100644 --- a/cjs/context.h +++ b/cjs/context.h @@ -21,15 +21,21 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_CONTEXT_H__ -#define __GJS_CONTEXT_H__ +#ifndef GJS_CONTEXT_H_ +#define GJS_CONTEXT_H_ -#if !defined (__GJS_GJS_H__) && !defined (GJS_COMPILATION) -#error "Only can be included directly." +#if !defined(INSIDE_GJS_H) && !defined(GJS_COMPILATION) +# error "Only can be included directly." +#endif + +#include /* IWYU pragma: keep */ + +#ifndef _WIN32 +# include /* for siginfo_t */ #endif -#include #include +#include #include #include @@ -46,37 +52,26 @@ typedef struct _GjsContextClass GjsContextClass; #define GJS_IS_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GJS_TYPE_CONTEXT)) #define GJS_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GJS_TYPE_CONTEXT, GjsContextClass)) -GJS_EXPORT -GType gjs_context_get_type (void) G_GNUC_CONST; +GJS_EXPORT GJS_USE GType gjs_context_get_type(void) G_GNUC_CONST; -GJS_EXPORT -GjsContext* gjs_context_new (void); -GJS_EXPORT -GjsContext* gjs_context_new_with_search_path (char **search_path); -GJS_EXPORT -bool gjs_context_eval_file (GjsContext *js_context, - const char *filename, - int *exit_status_p, - GError **error); -GJS_EXPORT -bool gjs_context_eval (GjsContext *js_context, - const char *script, - gssize script_len, - const char *filename, - int *exit_status_p, - GError **error); -GJS_EXPORT -bool gjs_context_define_string_array (GjsContext *js_context, - const char *array_name, - gssize array_length, - const char **array_values, - GError **error); +GJS_EXPORT GJS_USE GjsContext* gjs_context_new(void); +GJS_EXPORT GJS_USE GjsContext* gjs_context_new_with_search_path( + char** search_path); +GJS_EXPORT GJS_USE bool gjs_context_eval_file(GjsContext* js_context, + const char* filename, + int* exit_status_p, + GError** error); +GJS_EXPORT GJS_USE bool gjs_context_eval(GjsContext* js_context, + const char* script, gssize script_len, + const char* filename, + int* exit_status_p, GError** error); +GJS_EXPORT GJS_USE bool gjs_context_define_string_array( + GjsContext* js_context, const char* array_name, gssize array_length, + const char** array_values, GError** error); -GJS_EXPORT -GList* gjs_context_get_all (void); +GJS_EXPORT GJS_USE GList* gjs_context_get_all(void); -GJS_EXPORT -GjsContext *gjs_context_get_current (void); +GJS_EXPORT GJS_USE GjsContext* gjs_context_get_current(void); GJS_EXPORT void gjs_context_make_current (GjsContext *js_context); @@ -92,19 +87,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 GJS_USE GjsProfiler* gjs_context_get_profiler(GjsContext* self); -GJS_EXPORT -bool gjs_profiler_chain_signal(GjsContext *context, - siginfo_t *info); +GJS_EXPORT GJS_USE bool gjs_profiler_chain_signal(GjsContext* context, + siginfo_t* info); GJS_EXPORT void gjs_dumpstack (void); +GJS_EXPORT GJS_USE const char* gjs_get_js_version(void); + GJS_EXPORT -const char *gjs_get_js_version(void); +void gjs_context_setup_debugger_console(GjsContext* gjs); G_END_DECLS -#endif /* __GJS_CONTEXT_H__ */ \ No newline at end of file +#endif /* GJS_CONTEXT_H_ */ diff --git a/cjs/coverage.cpp b/cjs/coverage.cpp index 355e4f0..aef172e 100644 --- a/cjs/coverage.cpp +++ b/cjs/coverage.cpp @@ -23,16 +23,34 @@ * Authored By: Sam Spilsbury */ -#include -#include +#include + +#include // for free, size_t +#include // for strcmp, strlen -#include +#include -#include "coverage.h" -#include "global.h" -#include "importer.h" -#include "jsapi-util-args.h" -#include "util/error.h" +#include +#include + +#include // for JS_AddExtraGCRootsTracer, JS_Remove... +#include +#include +#include +#include +#include // for EnableCodeCoverage +#include // for JSAutoRealm, JS_SetPropertyById +#include // for GetCodeCoverageSummary + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/coverage.h" +#include "cjs/global.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +static bool s_coverage_enabled = false; struct _GjsCoverage { GObject parent; @@ -41,21 +59,14 @@ struct _GjsCoverage { typedef struct { gchar **prefixes; GjsContext *context; - JS::Heap compartment; + JS::Heap global; GFile *output_dir; } 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, @@ -68,28 +79,22 @@ enum { static GParamSpec *properties[PROP_N] = { NULL, }; -static char * -get_file_identifier(GFile *source_file) { +[[nodiscard]] static char* get_file_identifier(GFile* source_file) { char *path = g_file_get_path(source_file); if (!path) path = g_file_get_uri(source_file); return path; } -static bool -write_source_file_header(GOutputStream *stream, - GFile *source_file, - GError **error) -{ +[[nodiscard]] static bool write_source_file_header(GOutputStream* stream, + GFile* source_file, + GError** error) { GjsAutoChar path = get_file_identifier(source_file); return g_output_stream_printf(stream, NULL, NULL, error, "SF:%s\n", path.get()); } -static bool -copy_source_file_to_coverage_output(GFile *source_file, - GFile *destination_file, - GError **error) -{ +[[nodiscard]] static bool copy_source_file_to_coverage_output( + GFile* source_file, GFile* destination_file, GError** error) { /* 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); @@ -107,9 +112,7 @@ copy_source_file_to_coverage_output(GFile *source_file, * the string with the URI scheme stripped or NULL * if the path was not a valid URI */ -static char * -strip_uri_scheme(const char *potential_uri) -{ +[[nodiscard]] static char* strip_uri_scheme(const char* potential_uri) { char *uri_header = g_uri_parse_scheme(potential_uri); if (uri_header) { @@ -142,10 +145,8 @@ strip_uri_scheme(const char *potential_uri) * automatically return the full URI path with * the URI scheme and leading slash stripped out. */ -static char * -find_diverging_child_components(GFile *child, - GFile *parent) -{ +[[nodiscard]] static char* find_diverging_child_components(GFile* child, + GFile* parent) { g_object_ref(parent); GFile *ancestor = parent; while (ancestor != NULL) { @@ -174,13 +175,15 @@ find_diverging_child_components(GFile *child, return stripped_uri; } -static bool -filename_has_coverage_prefixes(GjsCoverage *self, const char *filename) -{ +[[nodiscard]] static bool filename_has_coverage_prefixes(GjsCoverage* self, + const char* filename) { auto priv = static_cast(gjs_coverage_get_instance_private(self)); + GjsAutoChar workdir = g_get_current_dir(); + GjsAutoChar abs_filename = g_canonicalize_filename(filename, workdir); for (const char * const *prefix = priv->prefixes; *prefix; prefix++) { - if (g_str_has_prefix(filename, *prefix)) + GjsAutoChar abs_prefix = g_canonicalize_filename(*prefix, workdir); + if (g_str_has_prefix(abs_filename, abs_prefix)) return true; } return false; @@ -194,13 +197,14 @@ write_line(GOutputStream *out, return g_output_stream_printf(out, nullptr, nullptr, error, "%s\n", line); } -static GjsAutoUnref -write_statistics_internal(GjsCoverage *coverage, - JSContext *cx, - GError **error) -{ - using AutoCChar = std::unique_ptr; - using AutoStrv = std::unique_ptr; +[[nodiscard]] static GjsAutoUnref write_statistics_internal( + GjsCoverage* coverage, JSContext* cx, GError** error) { + if (!s_coverage_enabled) { + g_critical( + "Code coverage requested, but gjs_coverage_enable() was not called." + " You must call this function before creating any GjsContext."); + return nullptr; + } GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); @@ -214,7 +218,7 @@ write_statistics_internal(GjsCoverage *coverage, GFile *output_file = g_file_get_child(priv->output_dir, "coverage.lcov"); size_t lcov_length; - AutoCChar lcov(js::GetCodeCoverageSummary(cx, &lcov_length), free); + JS::UniqueChars lcov = js::GetCodeCoverageSummary(cx, &lcov_length); GjsAutoUnref ostream = G_OUTPUT_STREAM(g_file_append_to(output_file, @@ -224,8 +228,8 @@ write_statistics_internal(GjsCoverage *coverage, if (!ostream) return nullptr; - AutoStrv lcov_lines(g_strsplit(lcov.get(), "\n", -1), g_strfreev); - GjsAutoChar test_name; + GjsAutoStrv lcov_lines = g_strsplit(lcov.get(), "\n", -1); + const char* test_name = NULL; bool ignoring_file = false; for (const char * const *iter = lcov_lines.get(); *iter; iter++) { @@ -248,7 +252,7 @@ write_statistics_internal(GjsCoverage *coverage, } /* Now we can write the test name before writing the source file */ - if (!write_line(ostream, test_name.get(), error)) + if (!write_line(ostream, test_name, error)) return nullptr; /* The source file could be a resource, so we must use @@ -297,8 +301,7 @@ gjs_coverage_write_statistics(GjsCoverage *coverage) GError *error = nullptr; auto cx = static_cast(gjs_context_get_native_context(priv->context)); - JSAutoCompartment ac(cx, gjs_get_import_global(cx)); - JSAutoRequest ar(cx); + JSAutoRealm ar(cx, gjs_get_import_global(cx)); GjsAutoUnref output_file = write_statistics_internal(coverage, cx, &error); if (!output_file) { @@ -311,9 +314,11 @@ gjs_coverage_write_statistics(GjsCoverage *coverage) g_message("Wrote coverage statistics to %s", output_file_path.get()); } -static void -gjs_coverage_init(GjsCoverage *self) -{ +static void gjs_coverage_init(GjsCoverage*) { + if (!s_coverage_enabled) + g_critical( + "Code coverage requested, but gjs_coverage_enable() was not called." + " You must call this function before creating any GjsContext."); } static void @@ -322,37 +327,39 @@ coverage_tracer(JSTracer *trc, void *data) GjsCoverage *coverage = (GjsCoverage *) data; GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - JS::TraceEdge(trc, &priv->compartment, "Coverage compartment"); + JS::TraceEdge(trc, &priv->global, "Coverage global object"); } +GJS_JSAPI_RETURN_CONVENTION static bool bootstrap_coverage(GjsCoverage *coverage) { GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context); - JSAutoRequest ar(context); JSObject *debuggee = gjs_get_import_global(context); - JS::RootedObject debugger_compartment(context, - gjs_create_global_object(context)); + JS::RootedObject debugger_global( + context, gjs_create_global_object(context, GjsGlobalType::DEBUGGER)); { - JSAutoCompartment compartment(context, debugger_compartment); + JSAutoRealm ar(context, debugger_global); JS::RootedObject debuggeeWrapper(context, debuggee); if (!JS_WrapObject(context, &debuggeeWrapper)) return false; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue debuggeeWrapperValue(context, JS::ObjectValue(*debuggeeWrapper)); - if (!JS_SetProperty(context, debugger_compartment, "debuggee", - debuggeeWrapperValue) || - !gjs_define_global_properties(context, debugger_compartment, - "coverage")) + if (!JS_SetPropertyById(context, debugger_global, atoms.debuggee(), + debuggeeWrapperValue) || + !gjs_define_global_properties(context, debugger_global, + GjsGlobalType::DEBUGGER, + "GJS coverage", "coverage")) return false; /* Add a tracer, as suggested by jdm on #jsapi */ JS_AddExtraGCRootsTracer(context, coverage_tracer, coverage); - priv->compartment = debugger_compartment; + priv->global = debugger_global; } return true; @@ -365,11 +372,11 @@ gjs_coverage_constructed(GObject *object) GjsCoverage *coverage = GJS_COVERAGE(object); GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage); - new (&priv->compartment) JS::Heap(); + new (&priv->global) JS::Heap(); if (!bootstrap_coverage(coverage)) { JSContext *context = static_cast(gjs_context_get_native_context(priv->context)); - JSAutoCompartment compartment(context, gjs_get_import_global(context)); + JSAutoRealm ar(context, gjs_get_import_global(context)); gjs_log_exception(context); } } @@ -411,7 +418,7 @@ gjs_coverage_dispose(GObject *object) * disposing of the context */ auto cx = static_cast(gjs_context_get_native_context(priv->context)); JS_RemoveExtraGCRootsTracer(cx, coverage_tracer, coverage); - priv->compartment = nullptr; + priv->global = nullptr; g_clear_object(&priv->context); @@ -426,7 +433,7 @@ gjs_coverage_finalize (GObject *object) g_strfreev(priv->prefixes); g_clear_object(&priv->output_dir); - priv->compartment.~Heap(); + priv->global.~Heap(); G_OBJECT_CLASS(gjs_coverage_parent_class)->finalize(object); } @@ -497,4 +504,17 @@ gjs_coverage_new (const char * const *prefixes, NULL)); return coverage; -} \ No newline at end of file +} + +/** + * gjs_coverage_enable: + * + * This function must be called before creating any #GjsContext, if you intend + * to use any #GjsCoverage APIs. + * + * Since: 1.66 + */ +void gjs_coverage_enable() { + js::EnableCodeCoverage(); + s_coverage_enabled = true; +} diff --git a/cjs/coverage.h b/cjs/coverage.h index 7bff175..4f68e9a 100644 --- a/cjs/coverage.h +++ b/cjs/coverage.h @@ -23,55 +23,35 @@ * Authored By: Sam Spilsbury */ -#ifndef _GJS_COVERAGE_H -#define _GJS_COVERAGE_H +#ifndef GJS_COVERAGE_H_ +#define GJS_COVERAGE_H_ + +#if !defined(INSIDE_GJS_H) && !defined(GJS_COMPILATION) +# error "Only can be included directly." +#endif -#include #include +#include +#include /* for G_BEGIN_DECLS, G_END_DECLS */ -#include "context.h" +#include +#include G_BEGIN_DECLS #define GJS_TYPE_COVERAGE gjs_coverage_get_type() -#define GJS_COVERAGE(obj) \ - (G_TYPE_CHECK_INSTANCE_CAST((obj), \ - GJS_TYPE_COVERAGE, GjsCoverage)) - -#define GJS_COVERAGE_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_CAST((klass), \ - GJS_TYPE_COVERAGE, GjsCoverageClass)) - -#define GJS_IS_COVERAGE(obj) \ - (G_TYPE_CHECK_INSTANCE_TYPE((obj), \ - GJS_TYPE_COVERAGE)) +G_DECLARE_FINAL_TYPE(GjsCoverage, gjs_coverage, GJS, COVERAGE, GObject); -#define GJS_IS_COVERAGE_CLASS(klass) \ - (G_TYPE_CHECK_CLASS_TYPE ((klass), \ - GJS_TYPE_COVERAGE)) - -#define GJS_COVERAGE_GET_CLASS(obj) \ - (G_TYPE_INSTANCE_GET_CLASS ((obj), \ - GJS_TYPE_COVERAGE, GjsCoverageClass)) - -typedef struct _GjsCoverage GjsCoverage; -typedef struct _GjsCoverageClass GjsCoverageClass; - -struct _GjsCoverageClass { - GObjectClass parent_class; -}; - -GType gjs_coverage_get_type(void); +GJS_EXPORT void gjs_coverage_enable(void); GJS_EXPORT void gjs_coverage_write_statistics(GjsCoverage *self); -GJS_EXPORT -GjsCoverage * gjs_coverage_new(const char * const *coverage_prefixes, - GjsContext *coverage_context, - GFile *output_dir); +GJS_EXPORT GJS_USE GjsCoverage* gjs_coverage_new( + const char* const* coverage_prefixes, GjsContext* coverage_context, + GFile* output_dir); G_END_DECLS -#endif +#endif /* GJS_COVERAGE_H_ */ diff --git a/cjs/debugger.cpp b/cjs/debugger.cpp new file mode 100644 index 0000000..418125e --- /dev/null +++ b/cjs/debugger.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2018 Philip Chimento + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Authored By: Philip Chimento + */ + +#include // for HAVE_READLINE_READLINE_H, HAVE_UNISTD_H + +#include +#include // for feof, fflush, fgets, stdin, stdout + +#ifdef HAVE_READLINE_READLINE_H +# include +# include +#endif + +#ifdef HAVE_UNISTD_H +# include // for isatty, STDIN_FILENO +#elif defined(_WIN32) +# include +# ifndef STDIN_FILENO +# define STDIN_FILENO 0 +# endif +#endif + +#include + +#include +#include +#include +#include +#include // for UniqueChars +#include +#include // for JS_DefineFunctions, JS_NewStringCopyZ + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/global.h" +#include "cjs/jsapi-util-args.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION +static bool quit(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + int32_t exitcode; + if (!gjs_parse_call_args(cx, "quit", args, "i", "exitcode", &exitcode)) + return false; + + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + gjs->exit(exitcode); + return false; // without gjs_throw() == "throw uncatchable exception" +} + +GJS_JSAPI_RETURN_CONVENTION +static bool do_readline(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::UniqueChars prompt; + if (!gjs_parse_call_args(cx, "readline", args, "|s", "prompt", &prompt)) + return false; + + GjsAutoChar line; + do { + const char* real_prompt = prompt ? prompt.get() : "db> "; +#ifdef HAVE_READLINE_READLINE_H + if (isatty(STDIN_FILENO)) { + line = readline(real_prompt); + } else { +#else + { +#endif // HAVE_READLINE_READLINE_H + char buf[256]; + g_print("%s", real_prompt); + fflush(stdout); + if (!fgets(buf, sizeof buf, stdin)) + buf[0] = '\0'; + line.reset(g_strchomp(g_strdup(buf))); + + if (!isatty(STDIN_FILENO)) { + if (feof(stdin)) { + g_print("[quit due to end of input]\n"); + line.reset(g_strdup("quit")); + } else { + g_print("%s\n", line.get()); + } + } + } + + /* EOF, return null */ + if (!line) { + args.rval().setUndefined(); + return true; + } + } while (line && line.get()[0] == '\0'); + + /* Add line to history and convert it to a JSString so that we can pass it + * back as the return value */ +#ifdef HAVE_READLINE_READLINE_H + add_history(line); +#endif + args.rval().setString(JS_NewStringCopyZ(cx, line)); + return true; +} + +// clang-format off +static JSFunctionSpec debugger_funcs[] = { + JS_FN("quit", quit, 1, GJS_MODULE_PROP_FLAGS), + JS_FN("readline", do_readline, 1, GJS_MODULE_PROP_FLAGS), + JS_FS_END +}; +// clang-format on + +void gjs_context_setup_debugger_console(GjsContext* gjs) { + auto cx = static_cast(gjs_context_get_native_context(gjs)); + + JS::RootedObject debuggee(cx, gjs_get_import_global(cx)); + JS::RootedObject debugger_global( + cx, gjs_create_global_object(cx, GjsGlobalType::DEBUGGER)); + + // Enter realm of the debugger and initialize it with the debuggee + JSAutoRealm ar(cx, debugger_global); + JS::RootedObject debuggee_wrapper(cx, debuggee); + if (!JS_WrapObject(cx, &debuggee_wrapper)) { + gjs_log_exception(cx); + return; + } + + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + JS::RootedValue v_wrapper(cx, JS::ObjectValue(*debuggee_wrapper)); + if (!JS_SetPropertyById(cx, debugger_global, atoms.debuggee(), v_wrapper) || + !JS_DefineFunctions(cx, debugger_global, debugger_funcs) || + !gjs_define_global_properties(cx, debugger_global, + GjsGlobalType::DEBUGGER, "GJS debugger", + "debugger")) + gjs_log_exception(cx); +} diff --git a/cjs/deprecation.cpp b/cjs/deprecation.cpp new file mode 100644 index 0000000..0c20354 --- /dev/null +++ b/cjs/deprecation.cpp @@ -0,0 +1,122 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2018 Philip Chimento + * + * 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 // for size_t +#include // for string +#include // for unordered_set +#include // for hash, move + +#include // for g_warning + +#include +#include +#include +#include +#include // for UniqueChars +#include +#include // for MaxFrames, CaptureCurrentStack +#include // for FormatStackDump + +#include "cjs/deprecation.h" +#include "cjs/macros.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} + +const char* messages[] = { + // None: + "(invalid message)", + + // ByteArrayInstanceToString: + "Some code called array.toString() on a Uint8Array instance. Previously " + "this would have interpreted the bytes of the array as a string, but that " + "is nonstandard. In the future this will return the bytes as " + "comma-separated digits. For the time being, the old behavior has been " + "preserved, but please fix your code anyway to explicitly call ByteArray" + ".toString(array).\n" + "(Note that array.toString() may have been called implicitly.)", + + // DeprecatedGObjectProperty: + "Some code tried to set a deprecated GObject property.", +}; + +struct DeprecationEntry { + GjsDeprecationMessageId id; + std::string loc; + + DeprecationEntry(GjsDeprecationMessageId an_id, const char* a_loc) + : id(an_id), loc(a_loc) {} + + bool operator==(const DeprecationEntry& other) const { + return id == other.id && loc == other.loc; + } +}; + +namespace std { +template <> +struct hash { + size_t operator()(const DeprecationEntry& key) const { + return hash()(key.id) ^ hash()(key.loc); + } +}; +}; // namespace std + +static std::unordered_set logged_messages; + +GJS_JSAPI_RETURN_CONVENTION +static JS::UniqueChars get_callsite(JSContext* cx) { + JS::RootedObject stack_frame(cx); + if (!JS::CaptureCurrentStack(cx, &stack_frame, + JS::StackCapture(JS::MaxFrames(1))) || + !stack_frame) + return nullptr; + + JS::RootedValue v_frame(cx, JS::ObjectValue(*stack_frame)); + JS::RootedString frame_string(cx, JS::ToString(cx, v_frame)); + if (!frame_string) + return nullptr; + + return JS_EncodeStringToUTF8(cx, frame_string); +} + +/* Note, this can only be called from the JS thread because it uses the full + * stack dump API and not the "safe" gjs_dumpstack() which can only print to + * stdout or stderr. Do not use this function during GC, for example. */ +void _gjs_warn_deprecated_once_per_callsite(JSContext* cx, + const GjsDeprecationMessageId id) { + JS::UniqueChars callsite(get_callsite(cx)); + DeprecationEntry entry(id, callsite.get()); + if (!logged_messages.count(entry)) { + JS::UniqueChars stack_dump = + JS::FormatStackDump(cx, false, false, false); + g_warning("%s\n%s", messages[id], stack_dump.get()); + logged_messages.insert(std::move(entry)); + } +} diff --git a/libgjs-private/gjs-gtk-util.h b/cjs/deprecation.h similarity index 65% rename from libgjs-private/gjs-gtk-util.h rename to cjs/deprecation.h index 7ea2a15..e7eb5e0 100644 --- a/libgjs-private/gjs-gtk-util.h +++ b/cjs/deprecation.h @@ -1,5 +1,6 @@ -/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* Copyright 2014 Endless Mobile, Inc. +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2018 Philip Chimento * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -20,24 +21,18 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_PRIVATE_GTK_UTIL_H__ -#define __GJS_PRIVATE_GTK_UTIL_H__ +#ifndef GJS_DEPRECATION_H_ +#define GJS_DEPRECATION_H_ -#include "config.h" +struct JSContext; -#ifdef ENABLE_GTK +enum GjsDeprecationMessageId { + None, + ByteArrayInstanceToString, + DeprecatedGObjectProperty, +}; -#include +void _gjs_warn_deprecated_once_per_callsite(JSContext* cx, + GjsDeprecationMessageId message); -G_BEGIN_DECLS - -void gjs_gtk_container_child_set_property (GtkContainer *container, - GtkWidget *child, - const gchar *property, - const GValue *value); - -G_END_DECLS - -#endif - -#endif /* __GJS_PRIVATE_GTK_UTIL_H__ */ +#endif // GJS_DEPRECATION_H_ diff --git a/cjs/engine.cpp b/cjs/engine.cpp index 67911ee..2a96bce 100644 --- a/cjs/engine.cpp +++ b/cjs/engine.cpp @@ -23,105 +23,38 @@ #include -#include "jsapi-wrapper.h" -#include +#include -#include "context-private.h" -#include "engine.h" -#include "gi/object.h" -#include "jsapi-util.h" -#include "util/log.h" - -#ifdef G_OS_WIN32 -#define WIN32_LEAN_AND_MEAN -#include +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include #endif -/* Implementations of locale-specific operations; these are used - * in the implementation of String.localeCompare(), Date.toLocaleDateString(), - * and so forth. We take the straight-forward approach of converting - * to UTF-8, using the appropriate GLib functions, and converting - * back if necessary. - */ -static bool -gjs_locale_to_upper_case (JSContext *context, - JS::HandleString src, - JS::MutableHandleValue retval) -{ - GjsAutoJSChar utf8 = JS_EncodeStringToUTF8(context, src); - if (!utf8) - return false; - - GjsAutoChar upper_case_utf8 = g_utf8_strup(utf8, -1); - return gjs_string_from_utf8(context, upper_case_utf8, retval); -} - -static bool -gjs_locale_to_lower_case (JSContext *context, - JS::HandleString src, - JS::MutableHandleValue retval) -{ - GjsAutoJSChar utf8 = JS_EncodeStringToUTF8(context, src); - if (!utf8) - return false; +#include // for move - GjsAutoChar lower_case_utf8 = g_utf8_strdown(utf8, -1); - return gjs_string_from_utf8(context, lower_case_utf8, retval); -} +#include +#include -static bool -gjs_locale_compare (JSContext *context, - JS::HandleString src_1, - JS::HandleString src_2, - JS::MutableHandleValue retval) -{ - GjsAutoJSChar utf8_1 = JS_EncodeStringToUTF8(context, src_1); - if (!utf8_1) - return false; +#include +#include // for JS_SetGCParameter, JS_AddFin... +#include // for JS_Init, JS_ShutDown +#include +#include +#include +#include +#include +#include // for InitSelfHostedCode, JS_Destr... +#include - GjsAutoJSChar utf8_2 = JS_EncodeStringToUTF8(context, src_2); - if (!utf8_2) - return false; - - retval.setInt32(g_utf8_collate(utf8_1, utf8_2)); - - return true; -} - -static bool -gjs_locale_to_unicode (JSContext *context, - const char *src, - JS::MutableHandleValue retval) -{ - GError *error = NULL; - - GjsAutoChar utf8 = g_locale_to_utf8(src, -1, NULL, NULL, &error); - if (!utf8) { - gjs_throw(context, - "Failed to convert locale string to UTF8: %s", - error->message); - g_error_free(error); - return false; - } - - return gjs_string_from_utf8(context, utf8, retval); -} - -static JSLocaleCallbacks gjs_locale_callbacks = -{ - gjs_locale_to_upper_case, - gjs_locale_to_lower_case, - gjs_locale_compare, - gjs_locale_to_unicode -}; +#include "gi/object.h" +#include "cjs/context-private.h" +#include "cjs/engine.h" +#include "cjs/jsapi-util.h" +#include "util/log.h" -static void -gjs_finalize_callback(JSFreeOp *fop, - JSFinalizeStatus status, - bool isCompartment, - void *data) -{ - auto js_context = static_cast(data); +static void gjs_finalize_callback(JSFreeOp*, JSFinalizeStatus status, + void* data) { + auto* gjs = static_cast(data); /* Implementation note for mozjs 24: sweeping happens in two phases, in the first phase all @@ -163,57 +96,65 @@ gjs_finalize_callback(JSFreeOp *fop, code, so we can probably rely on this behavior. */ - if (status == JSFINALIZE_GROUP_START) - _gjs_context_set_sweeping(js_context, true); + if (status == JSFINALIZE_GROUP_PREPARE) + gjs->set_sweeping(true); else if (status == JSFINALIZE_GROUP_END) - _gjs_context_set_sweeping(js_context, false); + gjs->set_sweeping(false); } -static void -on_garbage_collect(JSContext *cx, - JSGCStatus status, - void *data) -{ +static void on_garbage_collect(JSContext*, JSGCStatus status, JS::GCReason, + void*) { /* We finalize any pending toggle refs before doing any garbage collection, * so that we can collect the JS wrapper objects, and in order to minimize * the chances of objects having a pending toggle up queued when they are * garbage collected. */ - if (status == JSGC_BEGIN) + if (status == JSGC_BEGIN) { + gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "Begin garbage collection"); gjs_object_clear_toggles(); + } else if (status == JSGC_END) { + gjs_debug_lifecycle(GJS_DEBUG_CONTEXT, "End garbage collection"); + } } -static bool -on_enqueue_promise_job(JSContext *cx, - JS::HandleObject callback, - JS::HandleObject allocation_site, - JS::HandleObject global, - void *data) -{ - auto gjs_context = static_cast(data); - return _gjs_context_enqueue_job(gjs_context, callback); -} - -static void -on_promise_unhandled_rejection(JSContext *cx, - JS::HandleObject promise, - PromiseRejectionHandlingState state, - void *data) -{ - auto gjs_context = static_cast(data); +static void on_promise_unhandled_rejection( + JSContext* cx, bool mutedErrors [[maybe_unused]], JS::HandleObject promise, + JS::PromiseRejectionHandlingState state, void* data) { + auto gjs = static_cast(data); uint64_t id = JS::GetPromiseID(promise); - if (state == PromiseRejectionHandlingState::Handled) { + if (state == JS::PromiseRejectionHandlingState::Handled) { /* This happens when catching an exception from an await expression. */ - _gjs_context_unregister_unhandled_promise_rejection(gjs_context, id); + gjs->unregister_unhandled_promise_rejection(id); return; } JS::RootedObject allocation_site(cx, JS::GetPromiseAllocationSite(promise)); GjsAutoChar stack = gjs_format_stack_trace(cx, allocation_site); - _gjs_context_register_unhandled_promise_rejection(gjs_context, id, - std::move(stack)); + gjs->register_unhandled_promise_rejection(id, std::move(stack)); } +bool gjs_load_internal_source(JSContext* cx, const char* filename, char** src, + size_t* length) { + GError* error = nullptr; + const char* path = filename + 11; // len("resource://") + GBytes* script_bytes = + g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + if (!script_bytes) + return gjs_throw_gerror_message(cx, error); + + *src = static_cast(g_bytes_unref_to_data(script_bytes, length)); + return true; +} + +class GjsSourceHook : public js::SourceHook { + bool load(JSContext* cx, const char* filename, + char16_t** two_byte_source [[maybe_unused]], char** utf8_source, + size_t* length) { + // caller owns the source, per documentation of SourceHook + return gjs_load_internal_source(cx, filename, utf8_source, length); + } +}; + #ifdef G_OS_WIN32 HMODULE gjs_dll; static bool gjs_is_inited = false; @@ -254,34 +195,29 @@ public: JS_ShutDown(); } - operator bool() { - return true; - } + explicit operator bool() const { return true; } }; static GjsInit gjs_is_inited; #endif -JSContext * -gjs_create_js_context(GjsContext *js_context) -{ +JSContext* gjs_create_js_context(GjsContextPrivate* uninitialized_gjs) { g_assert(gjs_is_inited); JSContext *cx = JS_NewContext(32 * 1024 * 1024 /* max bytes */); if (!cx) return nullptr; - if (!JS::InitSelfHostedCode(cx)) + if (!JS::InitSelfHostedCode(cx)) { + JS_DestroyContext(cx); return nullptr; + } // commented are defaults in moz-24 JS_SetNativeStackQuota(cx, 1024 * 1024); - JS_SetGCParameter(cx, JSGC_MAX_MALLOC_BYTES, 128 * 1024 * 1024); JS_SetGCParameter(cx, JSGC_MAX_BYTES, -1); JS_SetGCParameter(cx, JSGC_MODE, JSGC_MODE_INCREMENTAL); - JS_SetGCParameter(cx, JSGC_SLICE_TIME_BUDGET, 10); /* ms */ + JS_SetGCParameter(cx, JSGC_SLICE_TIME_BUDGET_MS, 10); /* ms */ // JS_SetGCParameter(cx, JSGC_HIGH_FREQUENCY_TIME_LIMIT, 1000); /* ms */ - JS_SetGCParameter(cx, JSGC_DYNAMIC_MARK_SLICE, true); - JS_SetGCParameter(cx, JSGC_DYNAMIC_HEAP_GROWTH, true); // JS_SetGCParameter(cx, JSGC_LOW_FREQUENCY_HEAP_GROWTH, 150); // JS_SetGCParameter(cx, JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN, 150); // JS_SetGCParameter(cx, JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX, 300); @@ -291,30 +227,41 @@ gjs_create_js_context(GjsContext *js_context) // JS_SetGCParameter(cx, JSGC_DECOMMIT_THRESHOLD, 32); /* set ourselves as the private data */ - JS_SetContextPrivate(cx, js_context); + JS_SetContextPrivate(cx, uninitialized_gjs); - JS_AddFinalizeCallback(cx, gjs_finalize_callback, js_context); - JS_SetGCCallback(cx, on_garbage_collect, js_context); - JS_SetLocaleCallbacks(cx, &gjs_locale_callbacks); + JS_AddFinalizeCallback(cx, gjs_finalize_callback, uninitialized_gjs); + JS_SetGCCallback(cx, on_garbage_collect, uninitialized_gjs); JS::SetWarningReporter(cx, gjs_warning_reporter); - JS::SetGetIncumbentGlobalCallback(cx, gjs_get_import_global); - JS::SetEnqueuePromiseJobCallback(cx, on_enqueue_promise_job, js_context); + JS::SetJobQueue(cx, dynamic_cast(uninitialized_gjs)); JS::SetPromiseRejectionTrackerCallback(cx, on_promise_unhandled_rejection, - js_context); - - /* setExtraWarnings: Be extra strict about code that might hide a bug */ - if (!g_getenv("GJS_DISABLE_EXTRA_WARNINGS")) { - gjs_debug(GJS_DEBUG_CONTEXT, "Enabling extra warnings"); - JS::ContextOptionsRef(cx).setExtraWarnings(true); + uninitialized_gjs); + + // We use this to handle "lazy sources" that SpiderMonkey doesn't need to + // keep in memory. Most sources should be kept in memory, but we can skip + // doing that for the realm bootstrap code, as it is already in memory in + // the form of a GResource. Instead we use the "source hook" to retrieve it. + auto hook = mozilla::MakeUnique(); + js::SetSourceHook(cx, std::move(hook)); + + if (g_getenv("GJS_DISABLE_EXTRA_WARNINGS")) { + g_warning( + "GJS_DISABLE_EXTRA_WARNINGS has been removed, GJS no longer logs " + "extra warnings."); } - if (!g_getenv("GJS_DISABLE_JIT")) { + bool enable_jit = !(g_getenv("GJS_DISABLE_JIT")); + if (enable_jit) { gjs_debug(GJS_DEBUG_CONTEXT, "Enabling JIT"); - JS::ContextOptionsRef(cx) - .setIon(true) - .setBaseline(true) - .setAsmJS(true); } + JS::ContextOptionsRef(cx) + .setAsmJS(enable_jit); + + uint32_t value = enable_jit ? 1 : 0; + + JS_SetGlobalJitCompilerOption( + cx, JSJitCompilerOption::JSJITCOMPILER_ION_ENABLE, value); + JS_SetGlobalJitCompilerOption( + cx, JSJitCompilerOption::JSJITCOMPILER_BASELINE_ENABLE, value); return cx; } diff --git a/cjs/engine.h b/cjs/engine.h index 46bce81..953dbab 100644 --- a/cjs/engine.h +++ b/cjs/engine.h @@ -21,12 +21,17 @@ * IN THE SOFTWARE. */ -#ifndef GJS_ENGINE_H -#define GJS_ENGINE_H +#ifndef GJS_ENGINE_H_ +#define GJS_ENGINE_H_ -#include "context.h" -#include "jsapi-wrapper.h" +#include // for size_t -JSContext *gjs_create_js_context(GjsContext *js_context); +class GjsContextPrivate; +struct JSContext; -#endif /* GJS_ENGINE_H */ +JSContext* gjs_create_js_context(GjsContextPrivate* uninitialized_gjs); + +bool gjs_load_internal_source(JSContext* cx, const char* filename, char** src, + size_t* length); + +#endif // GJS_ENGINE_H_ diff --git a/util/error.cpp b/cjs/error-types.cpp similarity index 88% rename from util/error.cpp rename to cjs/error-types.cpp index b653b08..ab91ac1 100644 --- a/util/error.cpp +++ b/cjs/error-types.cpp @@ -21,27 +21,16 @@ * IN THE SOFTWARE. */ -#include - #include -#include "error.h" - -GQuark -gjs_error_quark (void) -{ - return g_quark_from_static_string ("gjs-error-quark"); -} +#include "cjs/error-types.h" -GQuark -gjs_js_error_quark(void) -{ - return g_quark_from_static_string("gjs-js-error-quark"); -} +// clang-format off +G_DEFINE_QUARK(gjs-error-quark, gjs_error) +G_DEFINE_QUARK(gjs-js-error-quark, gjs_js_error) +// clang-format on -GType -gjs_js_error_get_type(void) -{ +GType gjs_js_error_get_type(void) { static volatile GType g_type_id; if (g_once_init_enter(&g_type_id)) { diff --git a/util/error.h b/cjs/error-types.h similarity index 89% rename from util/error.h rename to cjs/error-types.h index 946ce42..883faa0 100644 --- a/util/error.h +++ b/cjs/error-types.h @@ -21,10 +21,15 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_UTIL_ERROR_H__ -#define __GJS_UTIL_ERROR_H__ +#ifndef GJS_ERROR_TYPES_H_ +#define GJS_ERROR_TYPES_H_ + +#if !defined(INSIDE_GJS_H) && !defined(GJS_COMPILATION) +# error "Only can be included directly." +#endif #include +#include #include @@ -61,4 +66,4 @@ typedef enum { G_END_DECLS -#endif /* __GJS_UTIL_ERROR_H__ */ +#endif /* GJS_ERROR_TYPES_H_ */ diff --git a/cjs/gjs.h b/cjs/gjs.h index aae48c2..6e84e3b 100644 --- a/cjs/gjs.h +++ b/cjs/gjs.h @@ -21,13 +21,18 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_GJS_H__ -#define __GJS_GJS_H__ +#ifndef GJS_GJS_H_ +#define GJS_GJS_H_ + +#define INSIDE_GJS_H -#include #include #include +#include +#include +#include #include -#include -#endif /* __GJS_GJS_H__ */ +#undef INSIDE_GJS_H + +#endif /* GJS_GJS_H_ */ diff --git a/cjs/global.cpp b/cjs/global.cpp index e725311..cac74c6 100644 --- a/cjs/global.cpp +++ b/cjs/global.cpp @@ -3,6 +3,7 @@ * Copyright (c) 2008 litl, LLC * Copyright (c) 2009 Red Hat, Inc. * Copyright (c) 2017 Philip Chimento + * Copyright (c) 2020 Evan Welsh * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -23,262 +24,191 @@ * IN THE SOFTWARE. */ -#include - -#include "global.h" -#include "importer.h" -#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; - } +#include + +#include // for size_t + +#include + +#include // for CallArgs, CallArgsFromVp +#include // for JS_EncodeStringToUTF8 +#include +#include +#include +#include // for JSPROP_PERMANENT, JSPROP_RE... +#include +#include // for GetObjectRealmOrNull, SetRealmPrivate +#include +#include +#include +#include +#include // for UniqueChars +#include // for AutoSaveExceptionState, ... + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/engine.h" +#include "cjs/global.h" +#include "cjs/jsapi-util.h" +#include "cjs/native.h" + +namespace mozilla { +union Utf8Unit; +} - JSAutoCompartment ac(cx, global); +class GjsBaseGlobal { + static JSObject* base(JSContext* cx, const JSClass* clasp, + JS::RealmCreationOptions options) { + JS::RealmBehaviors behaviors; + JS::RealmOptions compartment_options(options, behaviors); - GjsAutoChar uri = g_strconcat("resource://", path.get(), nullptr); - JS::CompileOptions options(cx); - options.setUTF8(true) - .setFileAndLine(uri, 1) - .setSourceIsLazy(true); + JS::RootedObject global( + cx, JS_NewGlobalObject(cx, clasp, nullptr, JS::FireOnNewGlobalHook, + compartment_options)); - 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; + if (!global) + return nullptr; - JS::RootedValue ignored(cx); - return JS::CloneAndExecuteScript(cx, compiled_script, &ignored); -} + JSAutoRealm ac(cx, global); -static bool -gjs_log(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + if (!JS_InitReflectParse(cx, global) || + !JS_DefineDebuggerObject(cx, global)) + return nullptr; - if (argc != 1) { - gjs_throw(cx, "Must pass a single argument to log()"); - return false; + return global; } - JSAutoRequest ar(cx); - - /* JS::ToString might throw, in which case we will only log that the value - * could not be converted to string */ - JS::AutoSaveExceptionState exc_state(cx); - JS::RootedString jstr(cx, JS::ToString(cx, argv[0])); - exc_state.restore(); - - if (!jstr) { - g_message("JS LOG: "); - return true; + protected: + [[nodiscard]] static JSObject* create( + JSContext* cx, const JSClass* clasp, + JS::RealmCreationOptions options = JS::RealmCreationOptions()) { + options.setNewCompartmentAndZone(); + return base(cx, clasp, options); } - GjsAutoJSChar s = JS_EncodeStringToUTF8(cx, jstr); - if (!s) - return false; - - g_message("JS LOG: %s", s.get()); - - argv.rval().setUndefined(); - return true; -} - -static bool -gjs_log_error(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); - - if ((argc != 1 && argc != 2) || !argv[0].isObject()) { - gjs_throw(cx, "Must pass an exception and optionally a message to logError()"); - return false; + [[nodiscard]] static JSObject* create_with_compartment( + JSContext* cx, JS::HandleObject existing, const JSClass* clasp, + JS::RealmCreationOptions options = JS::RealmCreationOptions()) { + options.setExistingCompartment(existing); + return base(cx, clasp, options); } - JSAutoRequest ar(cx); + GJS_JSAPI_RETURN_CONVENTION + static bool run_bootstrap(JSContext* cx, const char* bootstrap_script, + JS::HandleObject global) { + GjsAutoChar uri = g_strdup_printf( + "resource:///org/gnome/gjs/modules/script/_bootstrap/%s.js", + bootstrap_script); - JS::RootedString jstr(cx); + JSAutoRealm ar(cx, global); - if (argc == 2) { - /* JS::ToString might throw, in which case we will only log that the - * value could not be converted to string */ - JS::AutoSaveExceptionState exc_state(cx); - jstr = JS::ToString(cx, argv[1]); - exc_state.restore(); - } + JS::CompileOptions options(cx); + options.setFileAndLine(uri, 1).setSourceIsLazy(true); - gjs_log_exception_full(cx, argv[0], jstr); + char* script; + size_t script_len; + if (!gjs_load_internal_source(cx, uri, &script, &script_len)) + return false; - argv.rval().setUndefined(); - return true; -} + JS::SourceText source; + if (!source.init(cx, script, script_len, + JS::SourceOwnership::TakeOwnership)) + return false; -static bool -gjs_print_parse_args(JSContext *cx, - JS::CallArgs& argv, - GjsAutoChar *buffer) -{ - GString *str; - guint n; - - JSAutoRequest ar(cx); - - str = g_string_new(""); - for (n = 0; n < argv.length(); ++n) { - /* JS::ToString might throw, in which case we will only log that the - * value could not be converted to string */ - JS::AutoSaveExceptionState exc_state(cx); - JS::RootedString jstr(cx, JS::ToString(cx, argv[n])); - exc_state.restore(); - - if (jstr) { - GjsAutoJSChar s = JS_EncodeStringToUTF8(cx, jstr); - if (!s) { - g_string_free(str, true); - return false; - } - - g_string_append(str, s); - if (n < (argv.length()-1)) - g_string_append_c(str, ' '); - } else { - *buffer = g_string_free(str, true); - if (!*buffer) - *buffer = g_strdup(""); - return true; - } + JS::RootedScript compiled_script(cx, JS::Compile(cx, options, source)); + if (!compiled_script) + return false; + JS::RootedValue ignored(cx); + return JS::CloneAndExecuteScript(cx, compiled_script, &ignored); } - *buffer = g_string_free(str, false); - - return true; -} - -static bool -gjs_print(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GjsAutoChar buffer; - if (!gjs_print_parse_args(context, argv, &buffer)) - return false; + GJS_JSAPI_RETURN_CONVENTION + static bool load_native_module(JSContext* m_cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); - g_print("%s\n", buffer.get()); + // This function should never be directly exposed to user code, so we + // can be strict. + g_assert(argc == 1); + g_assert(argv[0].isString()); - argv.rval().setUndefined(); - return true; -} + JS::RootedString str(m_cx, argv[0].toString()); + JS::UniqueChars id(JS_EncodeStringToUTF8(m_cx, str)); -static bool -gjs_printerr(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + if (!id) + return false; - GjsAutoChar buffer; - if (!gjs_print_parse_args(context, argv, &buffer)) - return false; + JS::RootedObject native_obj(m_cx); - g_printerr("%s\n", buffer.get()); + if (!gjs_load_native_module(m_cx, id.get(), &native_obj)) { + gjs_throw(m_cx, "Failed to load native module: %s", id.get()); + return false; + } - argv.rval().setUndefined(); - return true; -} + argv.rval().setObject(*native_obj); + return true; + } +}; -class GjsGlobal { - static constexpr JSClassOps class_ops = { - nullptr, /* addProperty */ - nullptr, /* deleteProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - nullptr, /* finalize */ - nullptr, /* call */ - nullptr, /* hasInstance */ - nullptr, /* construct */ - JS_GlobalObjectTraceHook - }; +const JSClassOps defaultclassops = JS::DefaultGlobalClassOps; +class GjsGlobal : GjsBaseGlobal { static constexpr JSClass klass = { + // Jasmine depends on the class name "GjsGlobal" to detect GJS' global + // object. "GjsGlobal", - JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(GJS_GLOBAL_SLOT_LAST), - &GjsGlobal::class_ops, - }; - - static constexpr JSFunctionSpec static_funcs[] = { - JS_FS("log", gjs_log, 1, GJS_MODULE_PROP_FLAGS), - JS_FS("logError", gjs_log_error, 2, GJS_MODULE_PROP_FLAGS), - JS_FS("print", gjs_print, 0, GJS_MODULE_PROP_FLAGS), - JS_FS("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS), - JS_FS_END + JSCLASS_GLOBAL_FLAGS_WITH_SLOTS( + static_cast(GjsGlobalSlot::LAST)), + &defaultclassops, }; -public: + // clang-format off + static constexpr JSPropertySpec static_props[] = { + JS_STRING_SYM_PS(toStringTag, "GjsGlobal", JSPROP_READONLY), + JS_PS_END}; + // clang-format on - static JSObject * - create(JSContext *cx) - { - JS::CompartmentOptions compartment_options; - compartment_options.behaviors().setVersion(JSVERSION_LATEST); - JS::RootedObject global(cx, - JS_NewGlobalObject(cx, &GjsGlobal::klass, nullptr, - JS::FireOnNewGlobalHook, compartment_options)); - if (!global) - return nullptr; - - JSAutoCompartment ac(cx, global); + static constexpr JSFunctionSpec static_funcs[] = { + JS_FS_END}; - if (!JS_InitStandardClasses(cx, global) || - !JS_InitReflectParse(cx, global) || - !JS_DefineDebuggerObject(cx, global)) - return nullptr; + public: + [[nodiscard]] static JSObject* create(JSContext* cx) { + return GjsBaseGlobal::create(cx, &klass); + } - return global; + [[nodiscard]] static JSObject* create_with_compartment( + JSContext* cx, JS::HandleObject cmp_global) { + return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass); } - static bool - define_properties(JSContext *cx, - JS::HandleObject global, - const char *bootstrap_script) - { - if (!JS_DefineProperty(cx, global, "window", global, - JSPROP_READONLY | JSPROP_PERMANENT) || - !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs)) + GJS_JSAPI_RETURN_CONVENTION + static bool define_properties(JSContext* cx, JS::HandleObject global, + const char* realm_name, + const char* bootstrap_script) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (!JS_DefinePropertyById(cx, global, atoms.window(), global, + JSPROP_READONLY | JSPROP_PERMANENT) || + !JS_DefineFunctions(cx, global, GjsGlobal::static_funcs) || + !JS_DefineProperties(cx, global, GjsGlobal::static_props)) return false; - JS::Value v_importer = gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS); + JS::Realm* realm = JS::GetObjectRealmOrNull(global); + g_assert(realm && "Global object must be associated with a realm"); + // const_cast is allowed here if we never free the realm data + JS::SetRealmPrivate(realm, const_cast(realm_name)); + + JS::Value v_importer = + gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS); g_assert(((void) "importer should be defined before passing null " "importer to GjsGlobal::define_properties", v_importer.isObject())); JS::RootedObject root_importer(cx, &v_importer.toObject()); - /* Wrapping is a no-op if the importer is already in the same - * compartment. */ + // Wrapping is a no-op if the importer is already in the same realm. if (!JS_WrapObject(cx, &root_importer) || - !gjs_object_define_property(cx, global, GJS_STRING_IMPORTS, - root_importer, GJS_MODULE_PROP_FLAGS)) + !JS_DefinePropertyById(cx, global, atoms.imports(), root_importer, + GJS_MODULE_PROP_FLAGS)) return false; if (bootstrap_script) { @@ -290,6 +220,52 @@ public: } }; +class GjsDebuggerGlobal : GjsBaseGlobal { + static constexpr JSClass klass = { + "GjsDebuggerGlobal", + JSCLASS_GLOBAL_FLAGS_WITH_SLOTS( + static_cast(GjsDebuggerGlobalSlot::LAST)), + &defaultclassops, + }; + + static constexpr JSFunctionSpec static_funcs[] = { + JS_FN("loadNative", &load_native_module, 1, 0), JS_FS_END}; + + public: + [[nodiscard]] static JSObject* create(JSContext* cx) { + JS::RealmCreationOptions options; + options.setToSourceEnabled(true); // debugger uses uneval() + return GjsBaseGlobal::create(cx, &klass, options); + } + + [[nodiscard]] static JSObject* create_with_compartment( + JSContext* cx, JS::HandleObject cmp_global) { + return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass); + } + + static bool define_properties(JSContext* cx, JS::HandleObject global, + const char* realm_name, + const char* bootstrap_script) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (!JS_DefinePropertyById(cx, global, atoms.window(), global, + JSPROP_READONLY | JSPROP_PERMANENT) || + !JS_DefineFunctions(cx, global, GjsDebuggerGlobal::static_funcs)) + return false; + + JS::Realm* realm = JS::GetObjectRealmOrNull(global); + g_assert(realm && "Global object must be associated with a realm"); + // const_cast is allowed here if we never free the realm data + JS::SetRealmPrivate(realm, const_cast(realm_name)); + + if (bootstrap_script) { + if (!run_bootstrap(cx, bootstrap_script, global)) + return false; + } + + return true; + } +}; + /** * gjs_create_global_object: * @cx: a #JSContext @@ -299,19 +275,61 @@ public: * Returns: the created global object on success, nullptr otherwise, in which * case an exception is pending on @cx */ -JSObject * -gjs_create_global_object(JSContext *cx) -{ - return GjsGlobal::create(cx); +JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type, + JS::HandleObject current_global) { + if (current_global) { + switch (global_type) { + case GjsGlobalType::DEFAULT: + return GjsGlobal::create_with_compartment(cx, current_global); + case GjsGlobalType::DEBUGGER: + return GjsDebuggerGlobal::create_with_compartment( + cx, current_global); + default: + return nullptr; + } + } + + switch (global_type) { + case GjsGlobalType::DEFAULT: + return GjsGlobal::create(cx); + case GjsGlobalType::DEBUGGER: + return GjsDebuggerGlobal::create(cx); + default: + return nullptr; + } +} + +GjsGlobalType gjs_global_get_type(JSContext* cx) { + auto global = JS::CurrentGlobalOrNull(cx); + + g_assert(global && + "gjs_global_get_type called before a realm was entered."); + + auto global_type = + gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE); + + g_assert(global_type.isInt32()); + + return static_cast(global_type.toInt32()); +} + +GjsGlobalType gjs_global_get_type(JSObject* global) { + auto global_type = + gjs_get_global_slot(global, GjsBaseGlobalSlot::GLOBAL_TYPE); + + g_assert(global_type.isInt32()); + + return static_cast(global_type.toInt32()); } /** * gjs_define_global_properties: * @cx: a #JSContext * @global: a JS global object that has not yet been passed to this function + * @realm_name: (nullable): name of the realm, for debug output * @bootstrap_script: (nullable): name of a bootstrap script (found at - * resource://org/gnome/gjs/modules/_bootstrap/@bootstrap_script) or %NULL for - * none + * resource://org/gnome/gjs/modules/script/_bootstrap/@bootstrap_script) or + * %NULL for none * * 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 @@ -323,38 +341,44 @@ gjs_create_global_object(JSContext *cx) * root importer in between calling gjs_create_global_object() and * gjs_define_global_properties(). * - * The caller of this function should be in the compartment for @global. - * If the root importer object belongs to a different compartment, this - * function will create a cross-compartment wrapper for it. + * The caller of this function should be in the realm for @global. + * If the root importer object belongs to a different realm, this function will + * create a wrapper for it. * * Returns: true on success, false otherwise, in which case an exception is * pending on @cx */ -bool -gjs_define_global_properties(JSContext *cx, - JS::HandleObject global, - const char *bootstrap_script) -{ - return GjsGlobal::define_properties(cx, global, bootstrap_script); +bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global, + GjsGlobalType global_type, + const char* realm_name, + const char* bootstrap_script) { + gjs_set_global_slot(global.get(), GjsBaseGlobalSlot::GLOBAL_TYPE, + JS::Int32Value(static_cast(global_type))); + + switch (global_type) { + case GjsGlobalType::DEFAULT: + return GjsGlobal::define_properties(cx, global, realm_name, + bootstrap_script); + case GjsGlobalType::DEBUGGER: + return GjsDebuggerGlobal::define_properties(cx, global, realm_name, + bootstrap_script); + default: + return true; + } } -void -gjs_set_global_slot(JSContext *cx, - GjsGlobalSlot slot, - JS::Value value) -{ - JSObject *global = gjs_get_import_global(cx); +void detail::set_global_slot(JSObject* global, uint32_t slot, JS::Value value) { JS_SetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot, value); } -JS::Value -gjs_get_global_slot(JSContext *cx, - GjsGlobalSlot slot) -{ - JSObject *global = gjs_get_import_global(cx); +JS::Value detail::get_global_slot(JSObject* global, uint32_t slot) { return JS_GetReservedSlot(global, JSCLASS_GLOBAL_SLOT_COUNT + slot); } -decltype(GjsGlobal::class_ops) constexpr GjsGlobal::class_ops; decltype(GjsGlobal::klass) constexpr GjsGlobal::klass; decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs; +decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props; + +decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass; +decltype( + GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs; diff --git a/cjs/global.h b/cjs/global.h index ae99b15..4045a26 100644 --- a/cjs/global.h +++ b/cjs/global.h @@ -1,6 +1,7 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* * Copyright (c) 2017 Philip Chimento + * Copyright (c) 2020 Evan Welsh * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -21,53 +22,91 @@ * IN THE SOFTWARE. */ -#ifndef GJS_GLOBAL_H -#define GJS_GLOBAL_H - -#include - -#include "jsapi-wrapper.h" - -G_BEGIN_DECLS - -typedef enum { - GJS_GLOBAL_SLOT_IMPORTS, - GJS_GLOBAL_SLOT_PROTOTYPE_gtype, - GJS_GLOBAL_SLOT_PROTOTYPE_function, - GJS_GLOBAL_SLOT_PROTOTYPE_ns, - GJS_GLOBAL_SLOT_PROTOTYPE_repo, - GJS_GLOBAL_SLOT_PROTOTYPE_byte_array, - GJS_GLOBAL_SLOT_PROTOTYPE_importer, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_context, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_gradient, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_image_surface, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_linear_gradient, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_path, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_pattern, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_pdf_surface, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_ps_surface, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_radial_gradient, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_region, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_solid_pattern, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_surface, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_surface_pattern, - GJS_GLOBAL_SLOT_PROTOTYPE_cairo_svg_surface, - GJS_GLOBAL_SLOT_LAST, -} GjsGlobalSlot; - -JSObject *gjs_create_global_object(JSContext *cx); - -bool gjs_define_global_properties(JSContext *cx, - JS::HandleObject global, - const char *bootstrap_script); - -JS::Value gjs_get_global_slot(JSContext *cx, - GjsGlobalSlot slot); - -void gjs_set_global_slot(JSContext *context, - GjsGlobalSlot slot, - JS::Value value); - -G_END_DECLS - -#endif /* GJS_GLOBAL_H */ +#ifndef GJS_GLOBAL_H_ +#define GJS_GLOBAL_H_ + +#include + +#include // for Handle +#include +#include + +#include + +#include "cjs/macros.h" + +enum class GjsGlobalType { + DEFAULT, + DEBUGGER, +}; + +enum class GjsBaseGlobalSlot : uint32_t { + GLOBAL_TYPE = 0, + LAST, +}; + +enum class GjsDebuggerGlobalSlot : uint32_t { + LAST = static_cast(GjsBaseGlobalSlot::LAST), +}; + +enum class GjsGlobalSlot : uint32_t { + IMPORTS = static_cast(GjsBaseGlobalSlot::LAST), + PROTOTYPE_gtype, + PROTOTYPE_importer, + PROTOTYPE_function, + PROTOTYPE_ns, + PROTOTYPE_repo, + PROTOTYPE_cairo_context, + PROTOTYPE_cairo_gradient, + PROTOTYPE_cairo_image_surface, + PROTOTYPE_cairo_linear_gradient, + PROTOTYPE_cairo_path, + PROTOTYPE_cairo_pattern, + PROTOTYPE_cairo_pdf_surface, + PROTOTYPE_cairo_ps_surface, + PROTOTYPE_cairo_radial_gradient, + PROTOTYPE_cairo_region, + PROTOTYPE_cairo_solid_pattern, + PROTOTYPE_cairo_surface, + PROTOTYPE_cairo_surface_pattern, + PROTOTYPE_cairo_svg_surface, + LAST, +}; + +GjsGlobalType gjs_global_get_type(JSContext* cx); +GjsGlobalType gjs_global_get_type(JSObject* global); + +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type, + JS::HandleObject existing_global = nullptr); + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global, + GjsGlobalType global_type, + const char* realm_name, + const char* bootstrap_script); + +namespace detail { +void set_global_slot(JSObject* global, uint32_t slot, JS::Value value); +JS::Value get_global_slot(JSObject* global, uint32_t slot); +} // namespace detail + +template +inline void gjs_set_global_slot(JSObject* global, Slot slot, JS::Value value) { + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v, + "Must use a GJS global slot enum"); + detail::set_global_slot(global, static_cast(slot), value); +} + +template +inline JS::Value gjs_get_global_slot(JSObject* global, Slot slot) { + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v, + "Must use a GJS global slot enum"); + return detail::get_global_slot(global, static_cast(slot)); +} + +#endif // GJS_GLOBAL_H_ diff --git a/cjs/importer.cpp b/cjs/importer.cpp index df7f2af..5810fce 100644 --- a/cjs/importer.cpp +++ b/cjs/importer.cpp @@ -23,70 +23,85 @@ #include -#include - -#include - -#include "importer.h" -#include "jsapi-class.h" -#include "jsapi-wrapper.h" -#include "mem.h" -#include "module.h" -#include "native.h" -#include "util/glib.h" -#include "util/log.h" +#include // for size_t, strcmp, strlen -#ifdef G_OS_WIN32 -#define WIN32_LEAN_AND_MEAN -#include +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include #endif -#include +#include +#include // for vector -#define MODULE_INIT_FILENAME "__init__.js" +#include +#include +#include + +#include +#include +#include +#include +#include // for PropertyKey, JSID_IS_STRING +#include +#include +#include +#include +#include +#include // for UniqueChars +#include +#include // for JS_DefinePropertyById, JS_DefineP... +#include // for JSProto_Error +#include +#include + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/importer.h" +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "cjs/module.h" +#include "cjs/native.h" +#include "util/log.h" -static char **gjs_search_path = NULL; +#define MODULE_INIT_FILENAME "__init__.js" typedef struct { bool is_root; } Importer; -typedef struct { - GPtrArray *elements; - unsigned int index; -} ImporterIterator; - -extern const js::Class gjs_importer_real_class; - -/* Bizarrely, the API for safely casting const js::Class * to const JSClass * - * is called "js::Jsvalify" */ -static const JSClass gjs_importer_class = *js::Jsvalify(&gjs_importer_real_class); +extern const JSClass gjs_importer_class; GJS_DEFINE_PRIV_FROM_JS(Importer, gjs_importer_class) -static JSObject *gjs_define_importer(JSContext *, JS::HandleObject, - const char *, const char * const *, bool); +GJS_JSAPI_RETURN_CONVENTION +static JSObject* gjs_define_importer(JSContext*, JS::HandleObject, const char*, + const std::vector&, bool); +GJS_JSAPI_RETURN_CONVENTION static bool importer_to_string(JSContext *cx, unsigned argc, JS::Value *vp) { - GJS_GET_THIS(cx, argc, vp, args, importer); - const JSClass *klass = JS_GetClass(importer); - - JS::RootedValue module_path(cx); - if (!gjs_object_get_property(cx, importer, GJS_STRING_MODULE_PATH, - &module_path)) - return false; + GJS_GET_PRIV(cx, argc, vp, args, importer, Importer, priv); GjsAutoChar output; - if (module_path.isNull()) { + const JSClass* klass = JS_GetClass(importer); + + if (!priv) { + output = g_strdup_printf("[%s prototype]", klass->name); + } else if (priv->is_root) { output = g_strdup_printf("[%s root]", klass->name); } else { - GjsAutoJSChar path; - if (!gjs_string_to_utf8(cx, module_path, &path)) + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + JS::RootedValue module_path(cx); + if (!JS_GetPropertyById(cx, importer, atoms.module_path(), + &module_path)) + return false; + JS::UniqueChars path = gjs_string_to_utf8(cx, module_path); + if (!path) return false; output = g_strdup_printf("[%s %s]", klass->name, path.get()); } @@ -95,14 +110,16 @@ importer_to_string(JSContext *cx, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool define_meta_properties(JSContext *context, JS::HandleObject module_obj, - const char *full_path, + const char *parse_name, const char *module_name, JS::HandleObject parent) { bool parent_is_module; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); /* For these meta-properties, don't set ENUMERATE since we wouldn't want to * copy these symbols to any other object for example. RESOLVING is used to @@ -120,9 +137,12 @@ define_meta_properties(JSContext *context, parent.get(), module_obj.get(), module_name ? module_name : "", parent_is_module); - if (full_path != NULL) { - JS::RootedString file(context, JS_NewStringCopyZ(context, full_path)); - if (!JS_DefineProperty(context, module_obj, "__file__", file, attrs)) + if (parse_name != nullptr) { + JS::RootedValue file(context); + if (!gjs_string_from_utf8(context, parse_name, &file)) + return false; + if (!JS_DefinePropertyById(context, module_obj, atoms.file(), file, + attrs)) return false; } @@ -133,43 +153,46 @@ define_meta_properties(JSContext *context, JS::RootedValue module_path(context, JS::NullValue()); JS::RootedValue to_string_tag(context); if (parent_is_module) { - module_name_val.setString(JS_NewStringCopyZ(context, module_name)); + if (!gjs_string_from_utf8(context, module_name, &module_name_val)) + return false; parent_module_val.setObject(*parent); JS::RootedValue parent_module_path(context); - if (!gjs_object_get_property(context, parent, - GJS_STRING_MODULE_PATH, - &parent_module_path)) + if (!JS_GetPropertyById(context, parent, atoms.module_path(), + &parent_module_path)) return false; GjsAutoChar module_path_buf; if (parent_module_path.isNull()) { module_path_buf = g_strdup(module_name); } else { - GjsAutoJSChar parent_path; - if (!gjs_string_to_utf8(context, parent_module_path, &parent_path)) + JS::UniqueChars parent_path = + gjs_string_to_utf8(context, parent_module_path); + if (!parent_path) return false; module_path_buf = g_strdup_printf("%s.%s", parent_path.get(), module_name); } - module_path.setString(JS_NewStringCopyZ(context, module_path_buf)); + if (!gjs_string_from_utf8(context, module_path_buf, &module_path)) + return false; GjsAutoChar to_string_tag_buf = g_strdup_printf("GjsModule %s", module_path_buf.get()); - to_string_tag.setString(JS_NewStringCopyZ(context, to_string_tag_buf)); + if (!gjs_string_from_utf8(context, to_string_tag_buf, &to_string_tag)) + return false; } else { to_string_tag.setString(JS_AtomizeString(context, "GjsModule")); } - if (!JS_DefineProperty(context, module_obj, - "__moduleName__", module_name_val, attrs)) + if (!JS_DefinePropertyById(context, module_obj, atoms.module_name(), + module_name_val, attrs)) return false; - if (!JS_DefineProperty(context, module_obj, - "__parentModule__", parent_module_val, attrs)) + if (!JS_DefinePropertyById(context, module_obj, atoms.parent_module(), + parent_module_val, attrs)) return false; - if (!JS_DefineProperty(context, module_obj, "__modulePath__", module_path, - attrs)) + if (!JS_DefinePropertyById(context, module_obj, atoms.module_path(), + module_path, attrs)) return false; JS::RootedId to_string_tag_name(context, @@ -179,29 +202,23 @@ define_meta_properties(JSContext *context, to_string_tag, attrs); } -static bool -import_directory(JSContext *context, - JS::HandleObject obj, - const char *name, - const char * const *full_paths) -{ - JSObject *importer; - +GJS_JSAPI_RETURN_CONVENTION +static bool import_directory(JSContext* context, JS::HandleObject obj, + const char* name, + const std::vector& full_paths) { gjs_debug(GJS_DEBUG_IMPORTER, "Importing directory '%s'", name); - /* We define a sub-importer that has only the given directories on - * its search path. gjs_define_importer() exits if it fails, so - * this always succeeds. - */ - importer = gjs_define_importer(context, obj, name, full_paths, false); - return importer != nullptr; + // We define a sub-importer that has only the given directories on its + // search path. + return !!gjs_define_importer(context, obj, name, full_paths, false); } /* Make the property we set in gjs_module_import() permanent; * we do this after the import succesfully completes. */ +GJS_JSAPI_RETURN_CONVENTION static bool seal_import(JSContext *cx, JS::HandleObject obj, @@ -266,121 +283,125 @@ cancel_import(JSContext *context, * gjs_import_native_module: * @cx: the #JSContext * @importer: the root importer - * @name: Name under which the module was registered with - * gjs_register_native_module() + * @parse_name: Name under which the module was registered with + * gjs_register_native_module(), should be in the format as returned by + * g_file_get_parse_name() * * Imports a builtin native-code module so that it is available to JS code as - * `imports[name]`. + * `imports[parse_name]`. * * Returns: true on success, false if an exception was thrown. */ bool gjs_import_native_module(JSContext *cx, JS::HandleObject importer, - const char *name) + const char *parse_name) { - gjs_debug(GJS_DEBUG_IMPORTER, "Importing '%s'", name); + gjs_debug(GJS_DEBUG_IMPORTER, "Importing '%s'", parse_name); 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); + return gjs_load_native_module(cx, parse_name, &module) && + define_meta_properties(cx, module, nullptr, parse_name, importer) && + JS_DefineProperty(cx, importer, parse_name, module, GJS_MODULE_PROP_FLAGS); } +GJS_JSAPI_RETURN_CONVENTION static bool import_module_init(JSContext *context, GFile *file, JS::HandleObject module_obj) { - bool ret = false; - char *script = NULL; - char *full_path = NULL; gsize script_len = 0; GError *error = NULL; + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); JS::RootedValue ignored(context); - if (!(g_file_load_contents(file, NULL, &script, &script_len, NULL, &error))) { + char* script_unowned; + if (!(g_file_load_contents(file, nullptr, &script_unowned, &script_len, + nullptr, &error))) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY) && !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) && !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) - gjs_throw_g_error(context, error); + gjs_throw_gerror_message(context, error); else g_error_free(error); - goto out; + return false; } - g_assert(script != NULL); + GjsAutoChar script = script_unowned; + g_assert(script); - full_path = g_file_get_parse_name (file); + GjsAutoChar full_path = g_file_get_parse_name(file); - if (!gjs_eval_with_scope(context, module_obj, script, script_len, - full_path, &ignored)) - goto out; - - ret = true; - - out: - g_free(script); - g_free(full_path); - return ret; + return gjs->eval_with_scope(module_obj, script, script_len, full_path, + &ignored); } -static JSObject * -load_module_init(JSContext *context, - JS::HandleObject in_object, - const char *full_path) -{ +GJS_JSAPI_RETURN_CONVENTION +static JSObject* load_module_init(JSContext* cx, JS::HandleObject in_object, + const char* full_path) { bool found; + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); /* First we check if js module has already been loaded */ - if (gjs_object_has_property(context, in_object, GJS_STRING_MODULE_INIT, - &found) && found) { - JS::RootedValue module_obj_val(context); - if (gjs_object_get_property(context, in_object, - GJS_STRING_MODULE_INIT, - &module_obj_val)) { - return &module_obj_val.toObject(); - } + if (!JS_HasPropertyById(cx, in_object, atoms.module_init(), &found)) + return nullptr; + if (found) { + JS::RootedValue v_module(cx); + if (!JS_GetPropertyById(cx, in_object, atoms.module_init(), + &v_module)) + return nullptr; + if (v_module.isObject()) + return &v_module.toObject(); + + gjs_throw(cx, "Unexpected non-object module __init__ imported from %s", + full_path); + return nullptr; } - JS::RootedObject module_obj(context, JS_NewPlainObject(context)); + JS::RootedObject module_obj(cx, JS_NewPlainObject(cx)); + if (!module_obj) + return nullptr; + GjsAutoUnref file = g_file_new_for_commandline_arg(full_path); - if (!import_module_init(context, file, module_obj)) + if (!import_module_init(cx, file, module_obj)) { + JS_ClearPendingException(cx); return module_obj; + } - gjs_object_define_property(context, in_object, - GJS_STRING_MODULE_INIT, module_obj, - GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT); + if (!JS_DefinePropertyById(cx, in_object, atoms.module_init(), module_obj, + GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT)) + return nullptr; return module_obj; } -static void -load_module_elements(JSContext *cx, - JS::HandleObject in_object, - JS::AutoIdVector& prop_ids, - const char *init_path) -{ - size_t ix, length; +GJS_JSAPI_RETURN_CONVENTION +static bool load_module_elements(JSContext* cx, JS::HandleObject in_object, + JS::MutableHandleIdVector prop_ids, + const char* init_path) { JS::RootedObject module_obj(cx, load_module_init(cx, in_object, init_path)); - if (!module_obj) - return; + return false; JS::Rooted ids(cx, cx); if (!JS_Enumerate(cx, module_obj, &ids)) - return; + return false; - for (ix = 0, length = ids.length(); ix < length; ix++) - if (!prop_ids.append(ids[ix])) - g_error("Unable to append to vector"); + if (!prop_ids.appendAll(ids)) { + JS_ReportOutOfMemory(cx); + return false; + } + + return true; } /* If error, returns false. If not found, returns true but does not touch * the value at *result. If found, returns true and sets *result = true. */ +GJS_JSAPI_RETURN_CONVENTION static bool import_symbol_from_init_js(JSContext *cx, JS::HandleObject importer, @@ -414,6 +435,23 @@ import_symbol_from_init_js(JSContext *cx, return true; } +GJS_JSAPI_RETURN_CONVENTION +static bool attempt_import(JSContext* cx, JS::HandleObject obj, + JS::HandleId module_id, const char* module_name, + GFile* file) { + JS::RootedObject module_obj( + cx, gjs_module_import(cx, obj, module_id, module_name, file)); + if (!module_obj) + return false; + + GjsAutoChar full_path = g_file_get_parse_name(file); + + return define_meta_properties(cx, module_obj, full_path, module_name, + obj) && + seal_import(cx, obj, module_id, module_name); +} + +GJS_JSAPI_RETURN_CONVENTION static bool import_file_on_module(JSContext *context, JS::HandleObject obj, @@ -421,74 +459,62 @@ import_file_on_module(JSContext *context, const char *name, GFile *file) { - bool retval = false; - char *full_path = NULL; - - JS::RootedObject module_obj(context, - gjs_module_import(context, obj, id, name, file)); - if (!module_obj) - goto out; - - full_path = g_file_get_parse_name (file); - if (!define_meta_properties(context, module_obj, full_path, name, obj)) - goto out; - - if (!seal_import(context, obj, id, name)) - goto out; - - retval = true; - - out: - if (!retval) + if (!attempt_import(context, obj, id, name, file)) { cancel_import(context, obj, name); + return false; + } - g_free (full_path); - return retval; + return true; } -static bool -do_import(JSContext *context, - JS::HandleObject obj, - Importer *priv, - JS::HandleId id, - const char *name) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool do_import(JSContext* context, JS::HandleObject obj, Importer* priv, + JS::HandleId id) { JS::RootedObject search_path(context); guint32 search_path_len; guint32 i; bool exists, is_array; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); if (!gjs_object_require_property(context, obj, "importer", - GJS_STRING_SEARCH_PATH, &search_path)) + atoms.search_path(), &search_path)) return false; - if (!JS_IsArrayObject(context, search_path, &is_array)) + if (!JS::IsArrayObject(context, search_path, &is_array)) return false; if (!is_array) { gjs_throw(context, "searchPath property on importer is not an array"); return false; } - if (!JS_GetArrayLength(context, search_path, &search_path_len)) { + if (!JS::GetArrayLength(context, search_path, &search_path_len)) { gjs_throw(context, "searchPath array has no length"); return false; } - GjsAutoChar filename = g_strdup_printf("%s.js", name); - std::vector directories; - JS::RootedValue elem(context); - JS::RootedString str(context); + JS::UniqueChars name; + if (!gjs_get_string_id(context, id, &name)) + return false; + if (!name) { + gjs_throw(context, "Importing invalid module name"); + return false; + } - /* First try importing an internal module like byteArray */ - if (priv->is_root && gjs_is_registered_native_module(context, obj, name)) { - if (!gjs_import_native_module(context, obj, name)) + /* First try importing an internal module like gi */ + if (priv->is_root && gjs_is_registered_native_module(name.get())) { + if (!gjs_import_native_module(context, obj, name.get())) return false; - gjs_debug(GJS_DEBUG_IMPORTER, - "successfully imported module '%s'", name); + gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'", + name.get()); return true; } + GjsAutoChar filename = g_strdup_printf("%s.js", name.get()); + std::vector directories; + JS::RootedValue elem(context); + JS::RootedString str(context); + for (i = 0; i < search_path_len; ++i) { elem.setUndefined(); if (!JS_GetElement(context, search_path, i, &elem)) { @@ -507,7 +533,7 @@ do_import(JSContext *context, } str = elem.toString(); - GjsAutoJSChar dirname = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars dirname(JS_EncodeStringToUTF8(context, str)); if (!dirname) return false; @@ -517,20 +543,22 @@ do_import(JSContext *context, /* Try importing __init__.js and loading the symbol from it */ bool found = false; - if (!import_symbol_from_init_js(context, obj, dirname, name, &found)) + if (!import_symbol_from_init_js(context, obj, dirname.get(), name.get(), + &found)) return false; if (found) return true; /* Second try importing a directory (a sub-importer) */ - GjsAutoChar full_path = g_build_filename(dirname, name, nullptr); + GjsAutoChar full_path = + g_build_filename(dirname.get(), name.get(), 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.get(), name); - directories.push_back(std::move(full_path)); + full_path.get(), name.get()); + directories.push_back(full_path.get()); } /* If we just added to directories, we know we don't need to @@ -543,20 +571,19 @@ do_import(JSContext *context, continue; /* Third, if it's not a directory, try importing a file */ - full_path = g_build_filename(dirname, filename.get(), nullptr); + full_path = g_build_filename(dirname.get(), filename.get(), nullptr); gfile = g_file_new_for_commandline_arg(full_path); exists = g_file_query_exists(gfile, NULL); if (!exists) { - gjs_debug(GJS_DEBUG_IMPORTER, - "JS import '%s' not found in %s", - name, dirname.get()); + gjs_debug(GJS_DEBUG_IMPORTER, "JS import '%s' not found in %s", + name.get(), dirname.get()); continue; } - if (import_file_on_module(context, obj, id, name, gfile)) { - gjs_debug(GJS_DEBUG_IMPORTER, - "successfully imported module '%s'", name); + if (import_file_on_module(context, obj, id, name.get(), gfile)) { + gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported module '%s'", + name.get()); return true; } @@ -568,18 +595,11 @@ do_import(JSContext *context, } if (!directories.empty()) { - /* NULL-terminate the char** */ - 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(); - - bool result = import_directory(context, obj, name, full_paths); - g_free(full_paths); - if (!result) + if (!import_directory(context, obj, name.get(), directories)) return false; - gjs_debug(GJS_DEBUG_IMPORTER, - "successfully imported directory '%s'", name); + gjs_debug(GJS_DEBUG_IMPORTER, "successfully imported directory '%s'", + name.get()); return true; } @@ -587,23 +607,22 @@ do_import(JSContext *context, * 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); + "No JS module '%s' found in search path", name.get()); return false; } /* Note that in a for ... in loop, this will be called first on the object, * then on its prototype. */ -static bool -importer_enumerate(JSContext *context, - JS::HandleObject object, - JS::AutoIdVector& properties, - bool enumerable_only) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool importer_new_enumerate(JSContext* context, JS::HandleObject object, + JS::MutableHandleIdVector properties, + bool enumerable_only [[maybe_unused]]) { Importer *priv; guint32 search_path_len; guint32 i; bool is_array; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); priv = priv_from_js(context, object); @@ -613,18 +632,17 @@ importer_enumerate(JSContext *context, JS::RootedObject search_path(context); if (!gjs_object_require_property(context, object, "importer", - GJS_STRING_SEARCH_PATH, - &search_path)) + atoms.search_path(), &search_path)) return false; - if (!JS_IsArrayObject(context, search_path, &is_array)) + if (!JS::IsArrayObject(context, search_path, &is_array)) return false; if (!is_array) { gjs_throw(context, "searchPath property on importer is not an array"); return false; } - if (!JS_GetArrayLength(context, search_path, &search_path_len)) { + if (!JS::GetArrayLength(context, search_path, &search_path_len)) { gjs_throw(context, "searchPath array has no length"); return false; } @@ -651,18 +669,20 @@ importer_enumerate(JSContext *context, } str = elem.toString(); - GjsAutoJSChar dirname = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars dirname(JS_EncodeStringToUTF8(context, str)); if (!dirname) return false; - init_path = g_build_filename(dirname, MODULE_INIT_FILENAME, NULL); + init_path = + g_build_filename(dirname.get(), MODULE_INIT_FILENAME, nullptr); - load_module_elements(context, object, properties, init_path); + if (!load_module_elements(context, object, properties, init_path)) + return false; g_free(init_path); /* new_for_commandline_arg handles resource:/// paths */ - GjsAutoUnref dir = g_file_new_for_commandline_arg(dirname); + GjsAutoUnref dir = g_file_new_for_commandline_arg(dirname.get()); GjsAutoUnref direnum = g_file_enumerate_children(dir, "standard::name,standard::type", G_FILE_QUERY_INFO_NONE, NULL, NULL); @@ -678,7 +698,7 @@ importer_enumerate(JSContext *context, GjsAutoChar filename = g_file_get_basename(file); /* skip hidden files and directories (.svn, .git, ...) */ - if (filename[0] == '.') + if (filename.get()[0] == '.') continue; /* skip module init file */ @@ -686,14 +706,24 @@ importer_enumerate(JSContext *context, continue; if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) { - if (!properties.append(gjs_intern_string_to_id(context, filename))) - g_error("Unable to append to vector"); + jsid id = gjs_intern_string_to_id(context, filename); + if (id == JSID_VOID) + return false; + if (!properties.append(id)) { + JS_ReportOutOfMemory(context); + return false; + } } else if (g_str_has_suffix(filename, "." G_MODULE_SUFFIX) || g_str_has_suffix(filename, ".js")) { GjsAutoChar filename_noext = g_strndup(filename, strlen(filename) - 3); - if (!properties.append(gjs_intern_string_to_id(context, filename_noext))) - g_error("Unable to append to vector"); + jsid id = gjs_intern_string_to_id(context, filename_noext); + if (id == JSID_VOID) + return false; + if (!properties.append(id)) { + JS_ReportOutOfMemory(context); + return false; + } } } } @@ -702,6 +732,7 @@ importer_enumerate(JSContext *context, /* The *resolved out parameter, on success, should be false to indicate that id * was not resolved; and true if id was resolved. */ +GJS_JSAPI_RETURN_CONVENTION static bool importer_resolve(JSContext *context, JS::HandleObject obj, @@ -709,24 +740,15 @@ importer_resolve(JSContext *context, bool *resolved) { Importer *priv; - jsid module_init_name; 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) { - *resolved = false; - return true; - } - - /* let Object.prototype resolve these */ - JSFlatString *str = JSID_TO_FLAT_STRING(id); - if (JS_FlatStringEqualsAscii(str, "valueOf") || - JS_FlatStringEqualsAscii(str, "toString") || - JS_FlatStringEqualsAscii(str, "__iterator__")) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (id == atoms.module_init() || id == atoms.to_string() || + id == atoms.value_of()) { *resolved = false; return true; } @@ -742,15 +764,12 @@ importer_resolve(JSContext *context, return true; } - JSAutoRequest ar(context); - - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { + if (!JSID_IS_STRING(id)) { *resolved = false; return true; } - if (!do_import(context, obj, priv, id, name)) + if (!do_import(context, obj, priv, id)) return false; *resolved = true; @@ -759,10 +778,7 @@ importer_resolve(JSContext *context, GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(importer) -static void -importer_finalize(js::FreeOp *fop, - JSObject *obj) -{ +static void importer_finalize(JSFreeOp*, JSObject* obj) { Importer *priv; priv = (Importer*) JS_GetPrivate(obj); @@ -779,50 +795,35 @@ importer_finalize(js::FreeOp *fop, * instances of the object, and to the prototype that instances of the * class have. */ -static const js::ClassOps gjs_importer_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate (see below) */ +static const JSClassOps gjs_importer_class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + importer_new_enumerate, importer_resolve, - nullptr, /* mayResolve */ + nullptr, // mayResolve importer_finalize }; -static const js::ObjectOps gjs_importer_object_ops = { - NULL, /* lookupProperty */ - NULL, /* defineProperty */ - NULL, /* hasProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* getOwnPropertyDescriptor */ - NULL, /* deleteProperty */ - NULL, /* watch */ - NULL, /* unwatch */ - NULL, /* getElements */ - importer_enumerate -}; - -const js::Class gjs_importer_real_class = { +const JSClass gjs_importer_class = { "GjsFileImporter", JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, &gjs_importer_class_ops, - nullptr, - nullptr, - &gjs_importer_object_ops }; -static JSPropertySpec *gjs_importer_proto_props = nullptr; +static const JSPropertySpec gjs_importer_proto_props[] = { + JS_STRING_SYM_PS(toStringTag, "GjsFileImporter", JSPROP_READONLY), + JS_PS_END}; + static JSFunctionSpec *gjs_importer_static_funcs = nullptr; JSFunctionSpec gjs_importer_proto_funcs[] = { - JS_FS("toString", importer_to_string, 0, 0), - JS_FS_END -}; + JS_FN("toString", importer_to_string, 0, 0), + JS_FS_END}; GJS_DEFINE_PROTO_FUNCS(importer) +GJS_JSAPI_RETURN_CONVENTION static JSObject* importer_new(JSContext *context, bool is_root) @@ -831,12 +832,12 @@ importer_new(JSContext *context, JS::RootedObject proto(context); if (!gjs_importer_define_proto(context, nullptr, &proto)) - g_error("Error creating importer prototype"); + return nullptr; JS::RootedObject importer(context, JS_NewObjectWithGivenProto(context, &gjs_importer_class, proto)); if (!importer) - g_error("No memory to create importer"); + return nullptr; priv = g_slice_new0(Importer); priv->is_root = is_root; @@ -853,21 +854,17 @@ importer_new(JSContext *context, return importer; } -static G_CONST_RETURN char * G_CONST_RETURN * -gjs_get_search_path(void) -{ - char **search_path; +[[nodiscard]] static const std::vector& gjs_get_search_path() { + static std::vector gjs_search_path; + static bool search_path_initialized = false; /* not thread safe */ - if (!gjs_search_path) { - G_CONST_RETURN gchar* G_CONST_RETURN * system_data_dirs; + if (!search_path_initialized) { + const char* const* system_data_dirs; const char *envstr; - GPtrArray *path; gsize i; - path = g_ptr_array_new(); - /* in order of priority */ /* $GJS_PATH */ @@ -876,96 +873,78 @@ gjs_get_search_path(void) char **dirs, **d; dirs = g_strsplit(envstr, G_SEARCHPATH_SEPARATOR_S, 0); for (d = dirs; *d != NULL; d++) - g_ptr_array_add(path, *d); + gjs_search_path.push_back(*d); /* we assume the array and strings are allocated separately */ g_free(dirs); } - g_ptr_array_add(path, g_strdup("resource:///org/cinnamon/cjs/modules/")); + gjs_search_path.push_back("resource:///org/gnome/gjs/modules/script/"); + gjs_search_path.push_back("resource:///org/gnome/gjs/modules/core/"); /* $XDG_DATA_DIRS /gjs-1.0 */ system_data_dirs = g_get_system_data_dirs(); for (i = 0; system_data_dirs[i] != NULL; ++i) { - char *s; - s = g_build_filename(system_data_dirs[i], "gjs-1.0", NULL); - g_ptr_array_add(path, s); + GjsAutoChar s = + g_build_filename(system_data_dirs[i], "gjs-1.0", nullptr); + gjs_search_path.push_back(s.get()); } /* ${datadir}/share/gjs-1.0 */ #ifdef G_OS_WIN32 extern HMODULE gjs_dll; char *basedir = g_win32_get_package_installation_directory_of_module (gjs_dll); - char *gjs_data_dir = g_build_filename (basedir, "share", "gjs-1.0", NULL); - g_ptr_array_add(path, g_strdup(gjs_data_dir)); - g_free (gjs_data_dir); + GjsAutoChar gjs_data_dir = + g_build_filename(basedir, "share", "gjs-1.0", nullptr); + gjs_search_path.push_back(gjs_data_dir.get()); g_free (basedir); #else - g_ptr_array_add(path, g_strdup(GJS_JS_DIR)); + gjs_search_path.push_back(GJS_JS_DIR); #endif - - g_ptr_array_add(path, NULL); - - search_path = (char**)g_ptr_array_free(path, false); - - gjs_search_path = search_path; - } else { - search_path = gjs_search_path; } - return (G_CONST_RETURN char * G_CONST_RETURN *)search_path; + return gjs_search_path; } -static JSObject* -gjs_create_importer(JSContext *context, - const char *importer_name, - const char * const *initial_search_path, - bool add_standard_search_path, - bool is_root, - JS::HandleObject in_object) -{ - char **paths[2] = {0}; - char **search_path; - - paths[0] = (char**)initial_search_path; +GJS_JSAPI_RETURN_CONVENTION +static JSObject* gjs_create_importer( + JSContext* context, const char* importer_name, + const std::vector& initial_search_path, + bool add_standard_search_path, bool is_root, JS::HandleObject in_object) { + std::vector search_paths = initial_search_path; if (add_standard_search_path) { /* Stick the "standard" shared search path after the provided one. */ - paths[1] = (char**)gjs_get_search_path(); + const std::vector& gjs_search_path = gjs_get_search_path(); + search_paths.insert(search_paths.end(), gjs_search_path.begin(), + gjs_search_path.end()); } - search_path = gjs_g_strv_concat(paths, 2); - JS::RootedObject importer(context, importer_new(context, is_root)); /* API users can replace this property from JS, is the idea */ - if (!gjs_define_string_array(context, importer, - "searchPath", -1, (const char **)search_path, - /* settable (no READONLY) but not deleteable (PERMANENT) */ - JSPROP_PERMANENT | JSPROP_RESOLVING)) - g_error("no memory to define importer search path prop"); - - g_strfreev(search_path); + if (!gjs_define_string_array( + context, importer, "searchPath", search_paths, + /* settable (no READONLY) but not deleteable (PERMANENT) */ + JSPROP_PERMANENT | JSPROP_RESOLVING)) + return nullptr; if (!define_meta_properties(context, importer, NULL, importer_name, in_object)) - g_error("failed to define meta properties on importer"); + return nullptr; return importer; } -static JSObject * -gjs_define_importer(JSContext *context, - JS::HandleObject in_object, - const char *importer_name, - const char * const *initial_search_path, - bool add_standard_search_path) - -{ +GJS_JSAPI_RETURN_CONVENTION +static JSObject* gjs_define_importer( + JSContext* context, JS::HandleObject in_object, const char* importer_name, + const std::vector& initial_search_path, + bool add_standard_search_path) { JS::RootedObject importer(context, gjs_create_importer(context, importer_name, initial_search_path, add_standard_search_path, false, in_object)); if (!JS_DefineProperty(context, in_object, importer_name, importer, GJS_MODULE_PROP_FLAGS)) - g_error("no memory to define importer property"); + return nullptr; gjs_debug(GJS_DEBUG_IMPORTER, "Defined importer '%s' %p in %p", importer_name, importer.get(), @@ -974,9 +953,7 @@ gjs_define_importer(JSContext *context, return importer; } -JSObject * -gjs_create_root_importer(JSContext *cx, - const char * const *search_path) -{ +JSObject* gjs_create_root_importer( + JSContext* cx, const std::vector& search_path) { return gjs_create_importer(cx, "imports", search_path, true, true, nullptr); -} \ No newline at end of file +} diff --git a/cjs/importer.h b/cjs/importer.h index 7e7419f..eaa4aba 100644 --- a/cjs/importer.h +++ b/cjs/importer.h @@ -21,22 +21,25 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_IMPORTER_H__ -#define __GJS_IMPORTER_H__ +#ifndef GJS_IMPORTER_H_ +#define GJS_IMPORTER_H_ -#include -#include -#include "cjs/jsapi-util.h" +#include -G_BEGIN_DECLS +#include +#include -JSObject *gjs_create_root_importer(JSContext *cx, - const char * const *search_path); +#include +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_create_root_importer(JSContext* cx, + const std::vector& search_path); + +GJS_JSAPI_RETURN_CONVENTION bool gjs_import_native_module(JSContext *cx, JS::HandleObject importer, const char *name); -G_END_DECLS - -#endif /* __GJS_IMPORTER_H__ */ +#endif // GJS_IMPORTER_H_ diff --git a/cjs/jsapi-class.h b/cjs/jsapi-class.h index c3c8fa9..dfd91c9 100644 --- a/cjs/jsapi-class.h +++ b/cjs/jsapi-class.h @@ -21,40 +21,48 @@ * IN THE SOFTWARE. */ -#ifndef GJS_JSAPI_CLASS_H -#define GJS_JSAPI_CLASS_H +#ifndef GJS_JSAPI_CLASS_H_ +#define GJS_JSAPI_CLASS_H_ -#include "global.h" -#include "jsapi-util.h" -#include "jsapi-wrapper.h" -#include "util/log.h" +#include -G_BEGIN_DECLS +#include +#include -bool gjs_init_class_dynamic(JSContext *cx, - JS::HandleObject in_object, - JS::HandleObject parent_proto, - const char *ns_name, - const char *class_name, - JSClass *clasp, - JSNative constructor_native, - unsigned nargs, - JSPropertySpec *ps, - JSFunctionSpec *fs, - JSPropertySpec *static_ps, - JSFunctionSpec *static_fs, - JS::MutableHandleObject prototype, - JS::MutableHandleObject constructor); +#include -bool gjs_typecheck_instance(JSContext *cx, - JS::HandleObject obj, - const JSClass *static_clasp, - bool throw_error); +#include "gi/wrapperutils.h" // IWYU pragma: keep +#include "cjs/global.h" // IWYU pragma: keep +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +GJS_JSAPI_RETURN_CONVENTION +bool gjs_init_class_dynamic( + JSContext* cx, JS::HandleObject in_object, JS::HandleObject parent_proto, + const char* ns_name, const char* class_name, const JSClass* clasp, + JSNative constructor_native, unsigned nargs, JSPropertySpec* ps, + JSFunctionSpec* fs, JSPropertySpec* static_ps, JSFunctionSpec* static_fs, + JS::MutableHandleObject prototype, JS::MutableHandleObject constructor); + +[[nodiscard]] bool gjs_typecheck_instance(JSContext* cx, JS::HandleObject obj, + const JSClass* static_clasp, + bool throw_error); + +GJS_JSAPI_RETURN_CONVENTION JSObject *gjs_construct_object_dynamic(JSContext *cx, JS::HandleObject proto, const JS::HandleValueArray& args); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_property_dynamic(JSContext *cx, + JS::HandleObject proto, + const char *prop_name, + const char *func_namespace, + JSNative getter, + JSNative setter, + JS::HandleValue private_slot, + unsigned flags); + /* * Helper methods to access private data: * @@ -68,35 +76,22 @@ JSObject *gjs_construct_object_dynamic(JSContext *cx, * priv_from_js_with_typecheck: a convenience function to call * do_base_typecheck and priv_from_js */ -#define GJS_DEFINE_PRIV_FROM_JS(type, klass) \ - GJS_ALWAYS_INLINE G_GNUC_UNUSED \ - static inline bool \ - do_base_typecheck(JSContext *context, \ - JS::HandleObject object, \ - bool throw_error) \ - { \ - return gjs_typecheck_instance(context, object, &klass, throw_error); \ - } \ - GJS_ALWAYS_INLINE \ - static inline type * \ - priv_from_js(JSContext *context, \ - JS::HandleObject object) \ - { \ - type *priv; \ - JS_BeginRequest(context); \ - priv = (type*) JS_GetInstancePrivate(context, object, &klass, NULL); \ - JS_EndRequest(context); \ - return priv; \ - } \ - G_GNUC_UNUSED static bool \ - priv_from_js_with_typecheck(JSContext *context, \ - JS::HandleObject object, \ - type **out) \ - { \ - if (!do_base_typecheck(context, object, false)) \ - return false; \ - *out = priv_from_js(context, object); \ - return true; \ +#define GJS_DEFINE_PRIV_FROM_JS(type, klass) \ + GJS_ALWAYS_INLINE [[nodiscard]] [[maybe_unused]] static inline bool \ + do_base_typecheck(JSContext* cx, JS::HandleObject obj, bool throw_error) { \ + return gjs_typecheck_instance(cx, obj, &klass, throw_error); \ + } \ + GJS_ALWAYS_INLINE [[nodiscard]] static inline type* priv_from_js( \ + JSContext* cx, JS::HandleObject obj) { \ + return static_cast( \ + JS_GetInstancePrivate(cx, obj, &klass, nullptr)); \ + } \ + [[nodiscard]] [[maybe_unused]] static bool priv_from_js_with_typecheck( \ + JSContext* cx, JS::HandleObject obj, type** out) { \ + if (!do_base_typecheck(cx, obj, false)) \ + return false; \ + *out = priv_from_js(cx, obj); \ + return true; \ } /* @@ -121,11 +116,7 @@ JSObject *gjs_construct_object_dynamic(JSContext *cx, type *priv = priv_from_js(cx, to) /* Helper for GJS_DEFINE_PROTO_* macros with no parent */ -static inline JSObject * -gjs_no_parent_get_proto(JSContext *cx) -{ - return nullptr; -} +static inline JSObject* gjs_no_parent_get_proto(JSContext*) { return nullptr; } /** * GJS_DEFINE_PROTO: @@ -168,133 +159,138 @@ _GJS_DEFINE_PROTO_FULL(tn, cn, parent_cn, gjs_##cn##_constructor, \ #define GJS_DEFINE_PROTO_ABSTRACT_WITH_PARENT(tn, cn, parent_cn, flags) \ _GJS_DEFINE_PROTO_FULL(tn, cn, parent_cn, nullptr, G_TYPE_NONE, flags) -#define _GJS_DEFINE_PROTO_FULL(type_name, cname, parent_cname, ctor, gtype, jsclass_flags) \ -extern JSPropertySpec gjs_##cname##_proto_props[]; \ -extern JSFunctionSpec gjs_##cname##_proto_funcs[]; \ -extern JSFunctionSpec gjs_##cname##_static_funcs[]; \ -static void gjs_##cname##_finalize(JSFreeOp *fop, JSObject *obj); \ -static const struct JSClassOps gjs_##cname##_class_ops = { \ - nullptr, /* addProperty */ \ - nullptr, /* deleteProperty */ \ - nullptr, /* getProperty */ \ - nullptr, /* setProperty */ \ - nullptr, /* enumerate */ \ - nullptr, /* resolve */ \ - nullptr, /* mayResolve */ \ - gjs_##cname##_finalize \ -}; \ -static struct JSClass gjs_##cname##_class = { \ - type_name, \ - JSCLASS_HAS_PRIVATE | jsclass_flags, \ - &gjs_##cname##_class_ops \ -}; \ -_GJS_DEFINE_GET_PROTO(cname) \ -_GJS_DEFINE_DEFINE_PROTO(cname, parent_cname, ctor, gtype) +// clang-format off +#define _GJS_DEFINE_PROTO_FULL(type_name, cname, parent_cname, ctor, gtype, \ + jsclass_flags) \ + extern JSPropertySpec gjs_##cname##_proto_props[]; \ + extern JSFunctionSpec gjs_##cname##_proto_funcs[]; \ + extern JSFunctionSpec gjs_##cname##_static_funcs[]; \ + static void gjs_##cname##_finalize(JSFreeOp* fop, JSObject* obj); \ + static const struct JSClassOps gjs_##cname##_class_ops = { \ + nullptr, /* addProperty */ \ + nullptr, /* deleteProperty */ \ + nullptr, /* enumerate */ \ + nullptr, /* newEnumerate */ \ + nullptr, /* resolve */ \ + nullptr, /* mayResolve */ \ + gjs_##cname##_finalize \ + }; \ + static struct JSClass gjs_##cname##_class = { \ + type_name, \ + JSCLASS_HAS_PRIVATE | jsclass_flags, \ + &gjs_##cname##_class_ops \ + }; \ + [[maybe_unused]] [[nodiscard]] _GJS_DEFINE_GET_PROTO(cname) \ + [[maybe_unused]] GJS_JSAPI_RETURN_CONVENTION _GJS_DEFINE_DEFINE_PROTO(cname, parent_cname, ctor, gtype) +// clang-format on -#define GJS_DEFINE_PROTO_FUNCS_WITH_PARENT(cname, parent_cname) \ -G_GNUC_UNUSED static \ -_GJS_DEFINE_GET_PROTO(cname) \ -G_GNUC_UNUSED static \ -_GJS_DEFINE_DEFINE_PROTO(cname, parent_cname, \ - gjs_##cname##_constructor, G_TYPE_NONE) +#define GJS_DEFINE_PROTO_FUNCS_WITH_PARENT(cname, parent_cname) \ + [[maybe_unused]] [[nodiscard]] static _GJS_DEFINE_GET_PROTO(cname); \ + [[maybe_unused]] GJS_JSAPI_RETURN_CONVENTION static _GJS_DEFINE_DEFINE_PROTO( \ + cname, parent_cname, gjs_##cname##_constructor, G_TYPE_NONE); #define GJS_DEFINE_PROTO_FUNCS(cname) \ GJS_DEFINE_PROTO_FUNCS_WITH_PARENT(cname, no_parent) -#define _GJS_DEFINE_GET_PROTO(cname) \ -JSObject * \ -gjs_##cname##_get_proto(JSContext *cx) \ -{ \ - JS::RootedValue v_proto(cx, \ - gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_PROTOTYPE_##cname)); \ - g_assert(((void) "gjs_" #cname "_define_proto() must be called before " \ - "gjs_" #cname "_get_proto()", !v_proto.isUndefined())); \ - g_assert(((void) "Someone stored some weird value in a global slot", \ - v_proto.isObject())); \ - return &v_proto.toObject(); \ -} +#define _GJS_DEFINE_GET_PROTO(cname) \ + JSObject* gjs_##cname##_get_proto(JSContext* cx) { \ + JSObject* global = JS::CurrentGlobalOrNull(cx); \ + g_assert(global); \ + \ + JSAutoRealm ar(cx, global); \ + JS::RootedValue v_proto( \ + cx, \ + gjs_get_global_slot(global, GjsGlobalSlot::PROTOTYPE_##cname)); \ + g_assert(((void)"gjs_" #cname "_define_proto() must be called before " \ + "gjs_" #cname "_get_proto()", \ + !v_proto.isUndefined())); \ + g_assert(((void)"Someone stored some weird value in a global slot", \ + v_proto.isObject())); \ + return &v_proto.toObject(); \ + } -#define _GJS_DEFINE_DEFINE_PROTO(cname, parent_cname, ctor, gtype) \ -bool \ -gjs_##cname##_define_proto(JSContext *cx, \ - JS::HandleObject module, \ - JS::MutableHandleObject proto) \ -{ \ - /* If we've been here more than once, we already have the proto */ \ - JS::RootedValue v_proto(cx, \ - gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_PROTOTYPE_##cname)); \ - if (!v_proto.isUndefined()) { \ - g_assert(((void) "Someone stored some weird value in a global slot", \ - v_proto.isObject())); \ - proto.set(&v_proto.toObject()); \ - return true; \ - } \ - \ - /* If module is not given, we are defining a global class */ \ - JS::RootedObject in_obj(cx, module); \ - if (!in_obj) \ - in_obj = gjs_get_import_global(cx); \ - \ - /* Create the class, prototype, and constructor */ \ - JS::RootedObject parent_proto(cx, gjs_##parent_cname##_get_proto(cx)); \ - proto.set(JS_InitClass(cx, in_obj, parent_proto, &gjs_##cname##_class, \ - ctor, 0, gjs_##cname##_proto_props, \ - gjs_##cname##_proto_funcs, nullptr, \ - gjs_##cname##_static_funcs)); \ - if (!proto) \ - g_error("Can't init class %s", gjs_##cname##_class.name); \ - gjs_set_global_slot(cx, GJS_GLOBAL_SLOT_PROTOTYPE_##cname, \ - JS::ObjectValue(*proto)); \ - \ - /* Look up the constructor */ \ - JS::RootedObject ctor_obj(cx); \ - JS::RootedId class_name(cx, \ - gjs_intern_string_to_id(cx, gjs_##cname##_class.name)); \ - if (!gjs_object_require_property(cx, in_obj, #cname " constructor", \ - class_name, &ctor_obj)) \ - return false; \ - \ - /* JS_InitClass defines the constructor as a property on the given \ - * "global" object. If it's a module and not the real global object, \ - * redefine it with different flags so it's enumerable; cairo copies \ - * properties from cairoNative, for example */ \ - if (module) { \ - if (!JS_DefinePropertyById(cx, module, class_name, ctor_obj, \ - GJS_MODULE_PROP_FLAGS)) \ - return false; \ - } \ - \ - /* Define the GType value as a "$gtype" property on the constructor */ \ - if (gtype != G_TYPE_NONE) { \ - JS::RootedObject gtype_obj(cx, \ - gjs_gtype_create_gtype_wrapper(cx, gtype)); \ - if (!JS_DefineProperty(cx, ctor_obj, "$gtype", gtype_obj, \ - JSPROP_PERMANENT)) \ - return false; \ - } \ - gjs_debug(GJS_DEBUG_CONTEXT, "Initialized class %s prototype %p", \ - gjs_##cname##_class.name, proto.get()); \ - return true; \ -} +#define _GJS_DEFINE_DEFINE_PROTO(cname, parent_cname, ctor, type) \ + bool gjs_##cname##_define_proto(JSContext* cx, JS::HandleObject module, \ + JS::MutableHandleObject proto) { \ + /* If we've been here more than once, we already have the proto */ \ + JSObject* global = JS::CurrentGlobalOrNull(cx); \ + g_assert(global); \ + \ + JSAutoRealm ar(cx, global); \ + JS::RootedValue v_proto( \ + cx, \ + gjs_get_global_slot(global, GjsGlobalSlot::PROTOTYPE_##cname)); \ + \ + if (!v_proto.isUndefined()) { \ + g_assert( \ + ((void)"Someone stored some weird value in a global slot", \ + v_proto.isObject())); \ + proto.set(&v_proto.toObject()); \ + return true; \ + } \ + \ + /* If module is not given, we are defining a global class */ \ + JS::RootedObject in_obj(cx, module); \ + if (!in_obj) \ + in_obj = global; \ + \ + /* Create the class, prototype, and constructor */ \ + JS::RootedObject parent_proto(cx, gjs_##parent_cname##_get_proto(cx)); \ + proto.set(JS_InitClass(cx, in_obj, parent_proto, &gjs_##cname##_class, \ + ctor, 0, gjs_##cname##_proto_props, \ + gjs_##cname##_proto_funcs, nullptr, \ + gjs_##cname##_static_funcs)); \ + if (!proto) \ + return false; \ + gjs_set_global_slot(global, GjsGlobalSlot::PROTOTYPE_##cname, \ + JS::ObjectValue(*proto)); \ + \ + /* Look up the constructor */ \ + JS::RootedObject ctor_obj(cx); \ + JS::RootedId class_name( \ + cx, gjs_intern_string_to_id(cx, gjs_##cname##_class.name)); \ + if (class_name == JSID_VOID) \ + return false; \ + if (!gjs_object_require_property(cx, in_obj, #cname " constructor", \ + class_name, &ctor_obj)) \ + return false; \ + \ + /* JS_InitClass defines the constructor as a property on the given \ + * "global" object. If it's a module and not the real global object, \ + * redefine it with different flags so it's enumerable; cairo copies \ + * properties from cairoNative, for example */ \ + if (module) { \ + if (!JS_DefinePropertyById(cx, module, class_name, ctor_obj, \ + GJS_MODULE_PROP_FLAGS)) \ + return false; \ + } \ + \ + /* Define the GType value as a "$gtype" property on the constructor */ \ + if (type != G_TYPE_NONE) { \ + if (!gjs_wrapper_define_gtype_prop(cx, ctor_obj, type)) \ + return false; \ + } \ + gjs_debug(GJS_DEBUG_CONTEXT, "Initialized class %s prototype %p", \ + gjs_##cname##_class.name, proto.get()); \ + return true; \ + } /** * GJS_NATIVE_CONSTRUCTOR_DECLARE: * Prototype a constructor. */ -#define GJS_NATIVE_CONSTRUCTOR_DECLARE(name) \ -static bool \ -gjs_##name##_constructor(JSContext *context, \ - unsigned argc, \ - JS::Value *vp) +#define GJS_NATIVE_CONSTRUCTOR_DECLARE(name) \ + GJS_JSAPI_RETURN_CONVENTION static bool gjs_##name##_constructor( \ + JSContext* context, unsigned argc, JS::Value* vp) /** * GJS_NATIVE_CONSTRUCTOR_VARIABLES: * Declare variables necessary for the constructor; should * be at the very top. */ -#define GJS_NATIVE_CONSTRUCTOR_VARIABLES(name) \ - JS::RootedObject object(context); \ - JS::CallArgs argv G_GNUC_UNUSED = JS::CallArgsFromVp(argc, vp); +#define GJS_NATIVE_CONSTRUCTOR_VARIABLES(name) \ + JS::RootedObject object(context); \ + [[maybe_unused]] JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); /** * GJS_NATIVE_CONSTRUCTOR_PRELUDE: @@ -333,6 +329,12 @@ gjs_##name##_constructor(JSContext *context, \ return false; \ } -G_END_DECLS +[[nodiscard]] JS::Value gjs_dynamic_property_private_slot( + JSObject* accessor_obj); + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_object_in_prototype_chain(JSContext* cx, JS::HandleObject proto, + JS::HandleObject check_obj, + bool* is_in_chain); -#endif /* GJS_JSAPI_CLASS_H */ +#endif // GJS_JSAPI_CLASS_H_ diff --git a/cjs/jsapi-dynamic-class.cpp b/cjs/jsapi-dynamic-class.cpp index cb34349..540d4e4 100644 --- a/cjs/jsapi-dynamic-class.cpp +++ b/cjs/jsapi-dynamic-class.cpp @@ -24,39 +24,45 @@ #include -#include -#include -#include +#include // for strlen -#include "jsapi-class.h" -#include "jsapi-util.h" -#include "jsapi-wrapper.h" +#include -#include -#include +#include // for JSNative +#include +#include +#include // for JSPROP_GETTER +#include // for GetRealmObjectPrototype +#include +#include +#include +#include +#include // for JS_DefineFunctions, JS_DefineProp... +#include // for GetFunctionNativeReserved, NewFun... +#include // for JSProto_TypeError -bool -gjs_init_class_dynamic(JSContext *context, - JS::HandleObject in_object, - JS::HandleObject parent_proto, - const char *ns_name, - const char *class_name, - JSClass *clasp, - JSNative constructor_native, - unsigned nargs, - JSPropertySpec *proto_ps, - JSFunctionSpec *proto_fs, - JSPropertySpec *static_ps, - JSFunctionSpec *static_fs, - JS::MutableHandleObject prototype, - JS::MutableHandleObject constructor) -{ - /* Force these variables on the stack, so the conservative GC will - find them */ - JSFunction * volatile constructor_fun; - char *full_function_name = NULL; - bool res = false; +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +struct JSFunctionSpec; +struct JSPropertySpec; + +/* Reserved slots of JSNative accessor wrappers */ +enum { + DYNAMIC_PROPERTY_PRIVATE_SLOT, +}; +bool gjs_init_class_dynamic(JSContext* context, JS::HandleObject in_object, + JS::HandleObject parent_proto, const char* ns_name, + const char* class_name, const JSClass* clasp, + JSNative constructor_native, unsigned nargs, + JSPropertySpec* proto_ps, JSFunctionSpec* proto_fs, + JSPropertySpec* static_ps, + JSFunctionSpec* static_fs, + JS::MutableHandleObject prototype, + JS::MutableHandleObject constructor) { /* Without a name, JS_NewObject fails */ g_assert (clasp->name != NULL); @@ -64,9 +70,7 @@ gjs_init_class_dynamic(JSContext *context, use JS_InitClass for static classes like Math */ g_assert (constructor_native != NULL); - JS_BeginRequest(context); - - /* Class initalization consists of three parts: + /* Class initalization consists of five parts: - building a prototype - defining prototype properties and functions - building a constructor and definining it on the right object @@ -84,72 +88,38 @@ gjs_init_class_dynamic(JSContext *context, prototype.set(JS_NewObject(context, clasp)); } if (!prototype) - goto out; - - /* Bypass resolve hooks when defining the initial properties */ - if (clasp->cOps->resolve) { - JSPropertySpec *ps_iter; - JSFunctionSpec *fs_iter; - for (ps_iter = proto_ps; ps_iter && ps_iter->name; ps_iter++) - ps_iter->flags |= JSPROP_RESOLVING; - for (fs_iter = proto_fs; fs_iter && fs_iter->name; fs_iter++) - fs_iter->flags |= JSPROP_RESOLVING; - } + return false; if (proto_ps && !JS_DefineProperties(context, prototype, proto_ps)) - goto out; + return false; if (proto_fs && !JS_DefineFunctions(context, prototype, proto_fs)) - goto out; + return false; - full_function_name = g_strdup_printf("%s_%s", ns_name, class_name); - constructor_fun = JS_NewFunction(context, constructor_native, nargs, JSFUN_CONSTRUCTOR, - full_function_name); + GjsAutoChar full_function_name = + g_strdup_printf("%s_%s", ns_name, class_name); + JSFunction* constructor_fun = + JS_NewFunction(context, constructor_native, nargs, JSFUN_CONSTRUCTOR, + full_function_name); if (!constructor_fun) - goto out; + return false; constructor.set(JS_GetFunctionObject(constructor_fun)); if (static_ps && !JS_DefineProperties(context, constructor, static_ps)) - goto out; + return false; if (static_fs && !JS_DefineFunctions(context, constructor, static_fs)) - goto out; + return false; - if (!clasp->cOps->resolve) { - if (!JS_LinkConstructorAndPrototype(context, constructor, prototype)) - goto out; - } else { - /* Have to fake it with JSPROP_RESOLVING, otherwise it will trigger - * the resolve hook */ - if (!gjs_object_define_property(context, constructor, - GJS_STRING_PROTOTYPE, prototype, - JSPROP_PERMANENT | JSPROP_READONLY | JSPROP_RESOLVING)) - goto out; - if (!gjs_object_define_property(context, prototype, - GJS_STRING_CONSTRUCTOR, constructor, - JSPROP_RESOLVING)) - goto out; - } + if (!JS_LinkConstructorAndPrototype(context, constructor, prototype)) + return false; /* The constructor defined by JS_InitClass has no property attributes, but this is a more useful default for gjs */ - if (!JS_DefineProperty(context, in_object, class_name, constructor, - GJS_MODULE_PROP_FLAGS)) - goto out; - - res = true; - - constructor_fun = NULL; - - out: - JS_EndRequest(context); - g_free(full_function_name); - - return res; + return JS_DefineProperty(context, in_object, class_name, constructor, + GJS_MODULE_PROP_FLAGS); } -static const char* -format_dynamic_class_name (const char *name) -{ +[[nodiscard]] static const char* format_dynamic_class_name(const char* name) { if (g_str_has_prefix(name, "_private_")) return name + strlen("_private_"); else @@ -183,14 +153,134 @@ gjs_construct_object_dynamic(JSContext *context, JS::HandleObject proto, const JS::HandleValueArray& args) { - JSAutoRequest ar(context); - + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject constructor(context); if (!gjs_object_require_property(context, proto, "prototype", - GJS_STRING_CONSTRUCTOR, - &constructor)) + atoms.constructor(), &constructor)) return NULL; return JS_New(context, constructor, args); } + +GJS_JSAPI_RETURN_CONVENTION +static JSObject * +define_native_accessor_wrapper(JSContext *cx, + JSNative call, + unsigned nargs, + const char *func_name, + JS::HandleValue private_slot) +{ + JSFunction *func = js::NewFunctionWithReserved(cx, call, nargs, 0, func_name); + if (!func) + return nullptr; + + JSObject *func_obj = JS_GetFunctionObject(func); + js::SetFunctionNativeReserved(func_obj, DYNAMIC_PROPERTY_PRIVATE_SLOT, + private_slot); + return func_obj; +} + +/** + * gjs_define_property_dynamic: + * @cx: the #JSContext + * @proto: the prototype of the object, on which to define the property + * @prop_name: name of the property or field in GObject, visible to JS code + * @func_namespace: string from which the internal names for the getter and + * setter functions are built, not visible to JS code + * @getter: getter function + * @setter: setter function + * @private_slot: private data in the form of a #JS::Value that the getter and + * setter will have access to + * @flags: additional flags to define the property with (other than the ones + * required for a property with native getter/setter) + * + * When defining properties in a GBoxed or GObject, we can't have a separate + * getter and setter for each one, since the properties are defined dynamically. + * Therefore we must have one getter and setter for all the properties we define + * on all the types. In order to have that, we must provide the getter and + * setter with private data, e.g. the field index for GBoxed, in a "reserved + * slot" for which we must unfortunately use the jsfriendapi. + * + * Returns: %true on success, %false if an exception is pending on @cx. + */ +bool +gjs_define_property_dynamic(JSContext *cx, + JS::HandleObject proto, + const char *prop_name, + const char *func_namespace, + JSNative getter, + JSNative setter, + JS::HandleValue private_slot, + unsigned flags) +{ + GjsAutoChar getter_name = g_strconcat(func_namespace, "_get::", prop_name, nullptr); + GjsAutoChar setter_name = g_strconcat(func_namespace, "_set::", prop_name, nullptr); + + JS::RootedObject getter_obj(cx, + define_native_accessor_wrapper(cx, getter, 0, getter_name, private_slot)); + if (!getter_obj) + return false; + + JS::RootedObject setter_obj(cx, + define_native_accessor_wrapper(cx, setter, 1, setter_name, private_slot)); + if (!setter_obj) + return false; + + flags |= JSPROP_GETTER | JSPROP_SETTER; + + return JS_DefineProperty(cx, proto, prop_name, getter_obj, setter_obj, + flags); +} + +/** + * gjs_dynamic_property_private_slot: + * @accessor_obj: the getter or setter as a function object, i.e. + * `&args.callee()` in the #JSNative function + * + * For use in dynamic property getters and setters (see + * gjs_define_property_dynamic()) to retrieve the private data passed there. + * + * Returns: the JS::Value that was passed to gjs_define_property_dynamic(). + */ +JS::Value +gjs_dynamic_property_private_slot(JSObject *accessor_obj) +{ + return js::GetFunctionNativeReserved(accessor_obj, + DYNAMIC_PROPERTY_PRIVATE_SLOT); +} + +/** + * gjs_object_in_prototype_chain: + * @cx: + * @proto: The prototype which we are checking if @check_obj has in its chain + * @check_obj: The object to check + * @is_in_chain: (out): Whether @check_obj has @proto in its prototype chain + * + * Similar to JS_HasInstance() but takes into account abstract classes defined + * with JS_InitClass(), which JS_HasInstance() does not. Abstract classes don't + * have constructors, and JS_HasInstance() requires a constructor. + * + * Returns: false if an exception was thrown, true otherwise. + */ +bool gjs_object_in_prototype_chain(JSContext* cx, JS::HandleObject proto, + JS::HandleObject check_obj, + bool* is_in_chain) { + JS::RootedObject object_prototype(cx, JS::GetRealmObjectPrototype(cx)); + if (!object_prototype) + return false; + + JS::RootedObject proto_iter(cx); + if (!JS_GetPrototype(cx, check_obj, &proto_iter)) + return false; + while (proto_iter != object_prototype) { + if (proto_iter == proto) { + *is_in_chain = true; + return true; + } + if (!JS_GetPrototype(cx, proto_iter, &proto_iter)) + return false; + } + *is_in_chain = false; + return true; +} diff --git a/cjs/jsapi-util-args.h b/cjs/jsapi-util-args.h index e723919..ff86290 100644 --- a/cjs/jsapi-util-args.h +++ b/cjs/jsapi-util-args.h @@ -23,18 +23,28 @@ * Authored by: Philip Chimento */ -#include +#ifndef GJS_JSAPI_UTIL_ARGS_H_ +#define GJS_JSAPI_UTIL_ARGS_H_ + +#include + +#include + +#include // for enable_if, is_enum, is_same +#include // for move #include -#include "jsapi-util.h" -#include "jsapi-wrapper.h" +#include +#include +#include +#include // for JS_ClearPendingException -GJS_ALWAYS_INLINE -static inline bool -check_nullable(const char*& fchar, - const char*& fmt_string) -{ +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +GJS_ALWAYS_INLINE [[nodiscard]] static inline bool check_nullable( + const char*& fchar, const char*& fmt_string) { if (*fchar != '?') return false; @@ -48,13 +58,8 @@ check_nullable(const char*& fchar, /* This preserves the previous behaviour of gjs_parse_args(), but maybe we want * to use JS::ToBoolean instead? */ GJS_ALWAYS_INLINE -static inline void -assign(JSContext *cx, - char c, - bool nullable, - JS::HandleValue value, - bool *ref) -{ +static inline void assign(JSContext*, char c, bool nullable, + JS::HandleValue value, bool* ref) { if (c != 'b') throw g_strdup_printf("Wrong type for %c, got bool*", c); if (!value.isBoolean()) @@ -67,17 +72,12 @@ assign(JSContext *cx, /* This preserves the previous behaviour of gjs_parse_args(), but maybe we want * to box primitive types instead of throwing? */ GJS_ALWAYS_INLINE -static inline void -assign(JSContext *cx, - char c, - bool nullable, - JS::HandleValue value, - JS::MutableHandleObject ref) -{ +static inline void assign(JSContext*, char c, bool nullable, + JS::HandleValue value, JS::MutableHandleObject ref) { if (c != 'o') throw g_strdup_printf("Wrong type for %c, got JS::MutableHandleObject", c); if (nullable && value.isNull()) { - ref.set(NULL); + ref.set(nullptr); return; } if (!value.isObject()) @@ -86,21 +86,18 @@ assign(JSContext *cx, } GJS_ALWAYS_INLINE -static inline void -assign(JSContext *cx, - char c, - bool nullable, - JS::HandleValue value, - GjsAutoJSChar *ref) -{ +static inline void assign(JSContext* cx, char c, bool nullable, + JS::HandleValue value, JS::UniqueChars* ref) { if (c != 's') - throw g_strdup_printf("Wrong type for %c, got GjsAutoJSChar*", c); + throw g_strdup_printf("Wrong type for %c, got JS::UniqueChars*", c); if (nullable && value.isNull()) { ref->reset(); return; } - if (!gjs_string_to_utf8(cx, value, ref)) + JS::UniqueChars tmp = gjs_string_to_utf8(cx, value); + if (!tmp) throw g_strdup("Couldn't convert to string"); + *ref = std::move(tmp); } GJS_ALWAYS_INLINE @@ -192,16 +189,10 @@ assign(JSContext *cx, /* Special case: treat pointer-to-enum as pointer-to-int, but use enable_if to * prevent instantiation for any other types besides pointer-to-enum */ -template::value, int>::type = 0> -GJS_ALWAYS_INLINE -static inline void -assign(JSContext *cx, - char c, - bool nullable, - JS::HandleValue value, - T *ref) -{ +template , int> = 0> +GJS_ALWAYS_INLINE static inline void assign(JSContext* cx, char c, + bool nullable, + JS::HandleValue value, T* ref) { /* Sadly, we cannot use std::underlying_type here; the underlying type of * an enum is implementation-defined, so it would not be clear what letter * to use in the format string. For the same reason, we can only support @@ -215,10 +206,9 @@ assign(JSContext *cx, /* Force JS::RootedObject * to be converted to JS::MutableHandleObject, * see overload in jsapi-util-args.cpp */ -template::value, int>::type = 0> -static inline void -free_if_necessary(T param_ref) {} +template , int> = 0> +static inline void free_if_necessary(T param_ref [[maybe_unused]]) {} GJS_ALWAYS_INLINE static inline void @@ -227,25 +217,18 @@ free_if_necessary(JS::MutableHandleObject param_ref) /* This is not exactly right, since before we consumed a JS::ObjectValue * there may have been something different inside the handle. But it has * already been clobbered at this point anyhow */ - param_ref.set(NULL); + param_ref.set(nullptr); } -template -static bool -parse_call_args_helper(JSContext *cx, - const char *function_name, - JS::CallArgs& args, - bool ignore_trailing_args, - const char*& fmt_required, - const char*& fmt_optional, - unsigned param_ix, - const char *param_name, - T param_ref) -{ +template +GJS_JSAPI_RETURN_CONVENTION static bool parse_call_args_helper( + JSContext* cx, const char* function_name, const JS::CallArgs& args, + const char*& fmt_required, const char*& fmt_optional, unsigned param_ix, + const char* param_name, T param_ref) { bool nullable = false; const char *fchar = fmt_required; - g_return_val_if_fail (param_name != NULL, false); + g_return_val_if_fail(param_name, false); if (*fchar != '\0') { nullable = check_nullable(fchar, fmt_required); @@ -277,30 +260,17 @@ parse_call_args_helper(JSContext *cx, return true; } -template -static bool -parse_call_args_helper(JSContext *cx, - const char *function_name, - JS::CallArgs& args, - bool ignore_trailing_args, - const char*& fmt_required, - const char*& fmt_optional, - unsigned param_ix, - const char *param_name, - T param_ref, - Args ...params) -{ - bool retval; - - if (!parse_call_args_helper(cx, function_name, args, ignore_trailing_args, - fmt_required, fmt_optional, param_ix, - param_name, param_ref)) +template +GJS_JSAPI_RETURN_CONVENTION static bool parse_call_args_helper( + JSContext* cx, const char* function_name, const JS::CallArgs& args, + const char*& fmt_required, const char*& fmt_optional, unsigned param_ix, + const char* param_name, T param_ref, Args... params) { + if (!parse_call_args_helper(cx, function_name, args, fmt_required, + fmt_optional, param_ix, param_name, param_ref)) return false; - retval = parse_call_args_helper(cx, function_name, args, - ignore_trailing_args, - fmt_required, fmt_optional, ++param_ix, - params...); + bool retval = parse_call_args_helper(cx, function_name, args, fmt_required, + fmt_optional, ++param_ix, params...); /* We still own the strings in the error case, free any we converted */ if (!retval) @@ -309,13 +279,9 @@ parse_call_args_helper(JSContext *cx, } /* Empty-args version of the template */ -G_GNUC_UNUSED -static bool -gjs_parse_call_args(JSContext *cx, - const char *function_name, - JS::CallArgs& args, - const char *format) -{ +GJS_JSAPI_RETURN_CONVENTION [[maybe_unused]] static bool gjs_parse_call_args( + JSContext* cx, const char* function_name, const JS::CallArgs& args, + const char* format) { bool ignore_trailing_args = false; if (*format == '!') { @@ -327,7 +293,6 @@ gjs_parse_call_args(JSContext *cx, *format == '\0')); if (!ignore_trailing_args && args.length() > 0) { - JSAutoRequest ar(cx); gjs_throw(cx, "Error invoking %s: Expected 0 arguments, got %d", function_name, args.length()); return false; @@ -352,7 +317,7 @@ gjs_parse_call_args(JSContext *cx, * value location pairs. The currently accepted format specifiers are: * * b: A boolean (pass a bool *) - * s: A string, converted into UTF-8 (pass a GjsAutoJSChar *) + * s: A string, converted into UTF-8 (pass a JS::UniqueChars*) * F: A string, converted into "filename encoding" (i.e. active locale) (pass * a GjsAutoChar *) * i: A number, will be converted to a 32-bit int (pass an int32_t * or a @@ -373,18 +338,13 @@ gjs_parse_call_args(JSContext *cx, * may be null. For 's' or 'F' a null pointer is returned, for 'o' the handle is * set to null. */ -template -static bool -gjs_parse_call_args(JSContext *cx, - const char *function_name, - JS::CallArgs& args, - const char *format, - Args ...params) -{ +template +GJS_JSAPI_RETURN_CONVENTION static bool gjs_parse_call_args( + JSContext* cx, const char* function_name, const JS::CallArgs& args, + const char* format, Args... params) { const char *fmt_iter, *fmt_required, *fmt_optional; unsigned n_required = 0, n_total = 0; - bool optional_args = false, ignore_trailing_args = false, retval; - char **parts; + bool optional_args = false, ignore_trailing_args = false; if (*format == '!') { ignore_trailing_args = true; @@ -410,12 +370,9 @@ gjs_parse_call_args(JSContext *cx, g_assert(((void) "Wrong number of parameters passed to gjs_parse_call_args()", sizeof...(Args) / 2 == n_total)); - JSAutoRequest ar(cx); - - /* COMPAT: In future, use args.requireAtLeast() - * https://bugzilla.mozilla.org/show_bug.cgi?id=1334338 */ - if (args.length() < n_required || - (args.length() > n_total && !ignore_trailing_args)) { + if (!args.requireAtLeast(cx, function_name, n_required)) + return false; + if (!ignore_trailing_args && args.length() > n_total) { if (n_required == n_total) { gjs_throw(cx, "Error invoking %s: Expected %d arguments, got %d", function_name, n_required, args.length()); @@ -428,14 +385,12 @@ gjs_parse_call_args(JSContext *cx, return false; } - parts = g_strsplit(format, "|", 2); - fmt_required = parts[0]; - fmt_optional = parts[1]; /* may be NULL */ - - retval = parse_call_args_helper(cx, function_name, args, - ignore_trailing_args, fmt_required, - fmt_optional, 0, params...); + GjsAutoStrv parts = g_strsplit(format, "|", 2); + fmt_required = parts.get()[0]; + fmt_optional = parts.get()[1]; // may be null - g_strfreev(parts); - return retval; + return parse_call_args_helper(cx, function_name, args, fmt_required, + fmt_optional, 0, params...); } + +#endif // GJS_JSAPI_UTIL_ARGS_H_ diff --git a/cjs/jsapi-util-error.cpp b/cjs/jsapi-util-error.cpp index f4f6714..606f6df 100644 --- a/cjs/jsapi-util-error.cpp +++ b/cjs/jsapi-util-error.cpp @@ -23,15 +23,25 @@ #include -#include "jsapi-util.h" -#include "jsapi-wrapper.h" -#include "gi/gerror.h" +#include + +#include + +#include +#include +#include +#include +#include // for UniqueChars +#include +#include // for JS_ReportErrorUTF8, BuildStackString +#include // for JSProtoKey, JSProto_Error, JSProto... + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" +#include "util/log.h" #include "util/misc.h" -#include - -#include - /* * See: * https://bugzilla.mozilla.org/show_bug.cgi?id=166436 @@ -55,10 +65,6 @@ gjs_throw_valist(JSContext *context, s = g_strdup_vprintf(format, args); - JSAutoCompartment compartment(context, gjs_get_import_global(context)); - - JS_BeginRequest(context); - if (JS_IsExceptionPending(context)) { /* Often it's unclear whether a given jsapi.h function * will throw an exception, so we will throw ourselves @@ -72,15 +78,13 @@ gjs_throw_valist(JSContext *context, "Ignoring second exception: '%s'", s); g_free(s); - JS_EndRequest(context); return; } JS::RootedObject constructor(context); - JS::RootedObject global(context, JS::CurrentGlobalOrNull(context)); JS::RootedValue v_constructor(context), exc_val(context); JS::RootedObject new_exc(context); - JS::AutoValueArray<1> error_args(context); + JS::RootedValueArray<1> error_args(context); result = false; if (!gjs_string_from_utf8(context, s, error_args[0])) { @@ -97,11 +101,11 @@ gjs_throw_valist(JSContext *context, if (!new_exc) goto out; - if (error_name != NULL) { + if (error_name) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue name_value(context); if (!gjs_string_from_utf8(context, error_name, &name_value) || - !gjs_object_set_property(context, new_exc, GJS_STRING_NAME, - name_value)) + !JS_SetPropertyById(context, new_exc, atoms.name(), name_value)) goto out; } @@ -119,8 +123,6 @@ gjs_throw_valist(JSContext *context, JS_ReportErrorUTF8(context, "Failed to throw exception '%s'", s); } g_free(s); - - JS_EndRequest(context); } /* Throws an exception, like "throw new Error(message)" @@ -156,10 +158,10 @@ gjs_throw_custom(JSContext *cx, { 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); + kind == JSProto_EvalError || kind == JSProto_RangeError || + kind == JSProto_ReferenceError || + kind == JSProto_SyntaxError || kind == JSProto_TypeError || + kind == JSProto_URIError); va_start(args, format); gjs_throw_valist(cx, kind, error_name, format, args); @@ -180,29 +182,23 @@ gjs_throw_literal(JSContext *context, } /** - * gjs_throw_g_error: + * gjs_throw_gerror_message: + * + * Similar to gjs_throw_gerror(), but does not marshal the GError structure into + * JavaScript. Instead, it creates a regular JavaScript Error object and copies + * the GError's message into it. * - * Convert a GError into a JavaScript Exception, and - * frees the GError. Differently from gjs_throw(), it - * will overwrite an existing exception, as it is used - * to report errors from C functions. + * Use this when handling a GError in an internal function, where the error code + * and domain don't matter. So, for example, don't use it to throw errors + * around calling from JS into C code. + * + * Frees the GError. */ -void -gjs_throw_g_error (JSContext *context, - GError *error) -{ - if (error == NULL) - return; - - JS_BeginRequest(context); - - JS::RootedValue err(context, - JS::ObjectOrNullValue(gjs_error_from_gerror(context, error, true))); - g_error_free (error); - if (!err.isNull()) - JS_SetPendingException(context, err); - - JS_EndRequest(context); +bool gjs_throw_gerror_message(JSContext* cx, GError* error) { + g_return_val_if_fail(error, false); + gjs_throw_literal(cx, error->message); + g_error_free(error); + return false; } /** @@ -222,8 +218,8 @@ gjs_format_stack_trace(JSContext *cx, JS::AutoSaveExceptionState saved_exc(cx); JS::RootedString stack_trace(cx); - GjsAutoJSChar stack_utf8; - if (JS::BuildStackString(cx, saved_frame, &stack_trace, 2)) + JS::UniqueChars stack_utf8; + if (JS::BuildStackString(cx, nullptr, saved_frame, &stack_trace, 2)) stack_utf8 = JS_EncodeStringToUTF8(cx, stack_trace); saved_exc.restore(); @@ -231,28 +227,25 @@ gjs_format_stack_trace(JSContext *cx, if (!stack_utf8) return nullptr; - return g_filename_from_utf8(stack_utf8, -1, nullptr, nullptr, nullptr); + return g_filename_from_utf8(stack_utf8.get(), -1, nullptr, nullptr, + nullptr); } -void -gjs_warning_reporter(JSContext *context, - JSErrorReport *report) -{ +void gjs_warning_reporter(JSContext*, 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) { + !report->isWarning() && 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) { + if (report->isWarning()) { warning = "WARNING"; level = G_LOG_LEVEL_MESSAGE; diff --git a/cjs/jsapi-util-root.h b/cjs/jsapi-util-root.h index f78ec45..cded9e0 100644 --- a/cjs/jsapi-util-root.h +++ b/cjs/jsapi-util-root.h @@ -1,6 +1,7 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* * Copyright (c) 2017 Endless Mobile, Inc. + * Copyright (c) 2019 Canonical, Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -21,14 +22,27 @@ * IN THE SOFTWARE. */ -#ifndef GJS_JSAPI_UTIL_ROOT_H -#define GJS_JSAPI_UTIL_ROOT_H +#ifndef GJS_JSAPI_UTIL_ROOT_H_ +#define GJS_JSAPI_UTIL_ROOT_H_ + +#include + +#include // for uintptr_t + +#include // for nullptr_t +#include +#include +#include // for enable_if_t, is_pointer -#include #include +#include + +#include +#include +#include "cjs/context-private.h" #include "cjs/context.h" -#include "cjs/jsapi-wrapper.h" +#include "cjs/macros.h" #include "util/log.h" /* jsapi-util-root.h - Utilities for dealing with the lifetime and ownership of @@ -66,15 +80,13 @@ */ template struct GjsHeapOperation { - static bool update_after_gc(JS::Heap *location); + [[nodiscard]] static bool update_after_gc(JS::Heap* location); static void expose_to_js(JS::Heap& thing); }; template<> struct GjsHeapOperation { - static bool - update_after_gc(JS::Heap *location) - { + [[nodiscard]] static bool update_after_gc(JS::Heap* location) { JS_UpdateWeakPointerAfterGC(location); return (location->unbarrieredGet() == nullptr); } @@ -82,129 +94,124 @@ struct GjsHeapOperation { static void expose_to_js(JS::Heap& thing) { JSObject *obj = thing.unbarrieredGet(); /* If the object has been swept already, then the zone is nullptr */ - if (!obj || !js::gc::detail::GetGCThingZone(uintptr_t(obj))) + if (!obj || !JS::GetGCThingZone(JS::GCCellPtr(obj))) return; - /* COMPAT: Use JS::CurrentThreadIsHeapCollecting() in mozjs59 */ - JS::GCCellPtr ptr(obj, JS::TraceKind::Object); - JS::shadow::Runtime *rt = js::gc::detail::GetCellRuntime(ptr.asCell()); - if (!rt->isHeapCollecting()) + if (!JS::RuntimeHeapIsCollecting()) JS::ExposeObjectToActiveJS(obj); } }; -template<> -struct GjsHeapOperation {}; +template <> +struct GjsHeapOperation { + static void expose_to_js(const JS::Heap& thing) { + JSFunction* func = thing.unbarrieredGet(); + if (!func || !JS::GetGCThingZone(JS::GCCellPtr(func))) + return; + if (!JS::RuntimeHeapIsCollecting()) + js::gc::ExposeGCThingToActiveJS(JS::GCCellPtr(func)); + } +}; -/* GjsMaybeOwned is intended only for use in heap allocation. Do not allocate it - * on the stack, and do not allocate any instances of structures that have it as - * a member on the stack either. Unfortunately we cannot enforce this at compile - * time with a private constructor; that would prevent the intended usage as a - * member of a heap-allocated struct. */ +/* GjsMaybeOwned is intended for use as a member of classes that are allocated + * on the heap. Do not allocate GjsMaybeOwned on the stack, and do not allocate + * any instances of classes that have it as a member on the stack either. */ template class GjsMaybeOwned { -public: + public: typedef void (*DestroyNotify)(JS::Handle thing, void *data); -private: - bool m_rooted; /* wrapper is in rooted mode */ - bool m_has_weakref; /* we have a weak reference to the GjsContext */ + private: + /* m_root value controls which of these members we can access. When switching + * from one to the other, be careful to call the constructor and destructor + * of JS::Heap, since they use post barriers. */ + JS::Heap m_heap; + std::unique_ptr> m_root; + + struct Notifier { + Notifier(GjsMaybeOwned *parent, DestroyNotify func, void *data) + : m_parent(parent) + , m_func(func) + , m_data(data) + { + GjsContext* current = gjs_context_get_current(); + g_assert(GJS_IS_CONTEXT(current)); + g_object_weak_ref(G_OBJECT(current), on_context_destroy, this); + } + + ~Notifier() { disconnect(); } + + static void on_context_destroy(void* data, + GObject* ex_context [[maybe_unused]]) { + auto self = static_cast(data); + auto *parent = self->m_parent; + self->m_parent = nullptr; + self->m_func(parent->handle(), self->m_data); + } + + void disconnect() { + if (!m_parent) + return; - JSContext *m_cx; - JS::Heap m_heap; /* should be untouched if in rooted mode */ - JS::PersistentRooted *m_root; /* should be null if not in rooted mode */ + GjsContext* current = gjs_context_get_current(); + g_assert(GJS_IS_CONTEXT(current)); + g_object_weak_unref(G_OBJECT(current), on_context_destroy, this); + m_parent = nullptr; + } - DestroyNotify m_notify; - void *m_data; + private: + GjsMaybeOwned *m_parent; + DestroyNotify m_func; + void *m_data; + }; + std::unique_ptr m_notify; /* No-op unless GJS_VERBOSE_ENABLE_LIFECYCLE is defined to 1. */ - inline void - debug(const char *what) - { + inline void debug(const char* what GJS_USED_VERBOSE_LIFECYCLE) { gjs_debug_lifecycle(GJS_DEBUG_KEEP_ALIVE, "GjsMaybeOwned %p %s", this, what); } - static void - on_context_destroy(void *data, - GObject *ex_context) - { - auto self = static_cast *>(data); - self->invalidate(); - } - void - teardown_rooting(void) + teardown_rooting() { debug("teardown_rooting()"); - g_assert(m_rooted); - - delete m_root; - m_root = nullptr; - m_rooted = false; + g_assert(m_root); - if (!m_has_weakref) - return; - - auto gjs_cx = static_cast(JS_GetContextPrivate(m_cx)); - g_object_weak_unref(G_OBJECT(gjs_cx), on_context_destroy, this); - m_has_weakref = false; - } + m_root.reset(); + m_notify.reset(); - /* Called for a rooted wrapper when the JSContext is about to be destroyed. - * This calls the destroy-notify callback if one was passed to root(), and - * then removes all rooting from the object. */ - void - invalidate(void) - { - debug("invalidate()"); - g_assert(m_rooted); - - /* The weak ref is already gone because the context is dead, so no need - * to remove it. */ - m_has_weakref = false; - - /* The object is still live entering this callback. The callback - * must reset() this wrapper. */ - if (m_notify) - m_notify(handle(), m_data); - else - reset(); + new (&m_heap) JS::Heap(); } -public: - GjsMaybeOwned(void) : - m_rooted(false), - m_has_weakref(false), - m_cx(nullptr), - m_root(nullptr), - m_notify(nullptr), - m_data(nullptr) - { + public: + GjsMaybeOwned() { debug("created"); } - ~GjsMaybeOwned(void) - { + ~GjsMaybeOwned() { debug("destroyed"); - if (m_rooted) - teardown_rooting(); } /* To access the GC thing, call get(). In many cases you can just use the * GjsMaybeOwned wrapper in place of the GC thing itself due to the implicit * cast operator. But if you want to call methods on the GC thing, for * example if it's a JS::Value, you have to use get(). */ - const T - get(void) const - { - return m_rooted ? m_root->get() : m_heap.get(); + [[nodiscard]] const T get() const { + return m_root ? m_root->get() : m_heap.get(); + } + operator const T() const { return get(); } + + /* Use debug_addr() only for debug logging, because it is unbarriered. */ + template + [[nodiscard]] const void* debug_addr( + std::enable_if_t>* = nullptr) const { + return m_root ? m_root->get() : m_heap.unbarrieredGet(); } - operator const T(void) const { return get(); } bool operator==(const T& other) const { - if (m_rooted) + if (m_root) return m_root->get() == other; return m_heap == other; } @@ -215,19 +222,20 @@ public: bool operator==(std::nullptr_t) const { - if (m_rooted) + if (m_root) return m_root->get() == nullptr; return m_heap.unbarrieredGet() == nullptr; } inline bool operator!=(std::nullptr_t) const { return !(*this == nullptr); } + /* Likewise the truth value does not require a read barrier */ + inline explicit operator bool() 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 * JSContext can be destroyed while the Handle is live. */ - JS::Handle - handle(void) - { - g_assert(m_rooted); + [[nodiscard]] JS::Handle handle() { + g_assert(m_root); return *m_root; } @@ -240,20 +248,13 @@ public: void *data = nullptr) { debug("root()"); - g_assert(!m_rooted); - g_assert(m_heap.get() == JS::GCPolicy::initial()); - m_rooted = true; - m_cx = cx; - m_notify = notify; - m_data = data; - m_root = new JS::PersistentRooted(m_cx, thing); - - 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; - } + g_assert(!m_root); + g_assert(m_heap.get() == JS::SafelyInitialized()); + m_heap.~Heap(); + m_root = std::make_unique>(cx, thing); + + if (notify) + m_notify = std::make_unique(this, notify, data); } /* You can only assign directly to the GjsMaybeOwned wrapper in the @@ -261,34 +262,27 @@ public: void operator=(const T& thing) { - g_assert(!m_rooted); + g_assert(!m_root); m_heap = thing; } /* Marks an object as reachable for one GC with ExposeObjectToActiveJS(). * Use to avoid stopping tracing an object during GC. This makes no sense * in the rooted case. */ - void - prevent_collection(void) - { + void prevent_collection() { debug("prevent_collection()"); - g_assert(!m_rooted); + g_assert(!m_root); GjsHeapOperation::expose_to_js(m_heap); } - void - reset(void) - { + void reset() { debug("reset()"); - if (!m_rooted) { - m_heap = JS::GCPolicy::initial(); + if (!m_root) { + m_heap = JS::SafelyInitialized(); return; } teardown_rooting(); - m_cx = nullptr; - m_notify = nullptr; - m_data = nullptr; } void @@ -297,32 +291,28 @@ public: void *data = nullptr) { debug("switch to rooted"); - g_assert(!m_rooted); + g_assert(!m_root); /* Prevent the thing from being garbage collected while it is in neither * m_heap nor m_root */ - JSAutoRequest ar(cx); JS::Rooted thing(cx, m_heap); reset(); root(cx, thing, notify, data); - g_assert(m_rooted); + g_assert(m_root); } - void - switch_to_unrooted(void) - { + void switch_to_unrooted(JSContext* cx) { debug("switch to unrooted"); - g_assert(m_rooted); + g_assert(m_root); /* Prevent the thing from being garbage collected while it is in neither * m_heap nor m_root */ - JSAutoRequest ar(m_cx); - JS::Rooted thing(m_cx, *m_root); + JS::Rooted thing(cx, *m_root); reset(); m_heap = thing; - g_assert(!m_rooted); + g_assert(!m_root); } /* Tracing makes no sense in the rooted case, because JS::PersistentRooted @@ -332,22 +322,20 @@ public: const char *name) { debug("trace()"); - g_assert(!m_rooted); + g_assert(!m_root); JS::TraceEdge(tracer, &m_heap, name); } /* If not tracing, then you must call this method during GC in order to * update the object's location if it was moved, or null it out if it was * finalized. If the object was finalized, returns true. */ - bool - update_after_gc(void) - { + bool update_after_gc() { debug("update_after_gc()"); - g_assert(!m_rooted); + g_assert(!m_root); return GjsHeapOperation::update_after_gc(&m_heap); } - bool rooted(void) { return m_rooted; } + [[nodiscard]] bool rooted() const { return m_root != nullptr; } }; -#endif /* GJS_JSAPI_UTIL_ROOT_H */ +#endif // GJS_JSAPI_UTIL_ROOT_H_ diff --git a/cjs/jsapi-util-string.cpp b/cjs/jsapi-util-string.cpp index 03e8c4f..6d13141 100644 --- a/cjs/jsapi-util-string.cpp +++ b/cjs/jsapi-util-string.cpp @@ -23,20 +23,57 @@ #include -#include -#include -#include -#include -#include +#include +#include // for size_t, strlen +#include // for ssize_t + +#include // for copy +#include // for operator<<, setfill, setw +#include // for operator<<, basic_ostream, ostring... +#include // for allocator, char_traits + +#include + +#include +#include +#include +#include // for AutoCheckCannotGC +#include // for JSID_IS_STRING... +#include +#include +#include +#include // for UniqueChars +#include +#include // for JSID_TO_FLAT_STRING, JS_GetTwoByte... +#include // for FlatStringToLinearString, GetLatin... + +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} + +class JSLinearString; -#include "jsapi-util.h" -#include "jsapi-wrapper.h" +char* gjs_hyphen_to_underscore(const char* str) { + char *s = g_strdup(str); + char *retval = s; + while (*(s++) != '\0') { + if (*s == '-') + *s = '_'; + } + return retval; +} /** * 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. * @@ -44,22 +81,17 @@ * 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. + * + * Returns: Unique UTF8 chars, empty on exception throw. */ -bool -gjs_string_to_utf8(JSContext *cx, - const JS::Value value, - GjsAutoJSChar *utf8_string_p) -{ - JSAutoRequest ar(cx); - +JS::UniqueChars gjs_string_to_utf8(JSContext* cx, const JS::Value value) { if (!value.isString()) { gjs_throw(cx, "Value is not a string, cannot convert to UTF-8"); - return false; + return nullptr; } JS::RootedString str(cx, value.toString()); - utf8_string_p->reset(JS_EncodeStringToUTF8(cx, str)); - return !!*utf8_string_p; + return JS_EncodeStringToUTF8(cx, str); } bool @@ -67,14 +99,11 @@ gjs_string_from_utf8(JSContext *context, const char *utf8_string, JS::MutableHandleValue value_p) { - JS_BeginRequest(context); - JS::ConstUTF8CharsZ chars(utf8_string, strlen(utf8_string)); JS::RootedString str(context, JS_NewStringCopyUTF8Z(context, chars)); if (str) value_p.setString(str); - JS_EndRequest(context); return str != nullptr; } @@ -84,8 +113,6 @@ gjs_string_from_utf8_n(JSContext *cx, 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) @@ -100,21 +127,18 @@ gjs_string_to_filename(JSContext *context, GjsAutoChar *filename_string) { GError *error; - GjsAutoJSChar tmp; /* gjs_string_to_filename verifies that filename_val is a string */ - if (!gjs_string_to_utf8(context, filename_val, &tmp)) { - /* exception already set */ + JS::UniqueChars tmp = gjs_string_to_utf8(context, filename_val); + if (!tmp) return false; - } error = NULL; - *filename_string = g_filename_from_utf8(tmp, -1, NULL, NULL, &error); - if (!*filename_string) { - gjs_throw_g_error(context, error); - return false; - } + *filename_string = + g_filename_from_utf8(tmp.get(), -1, nullptr, nullptr, &error); + if (!*filename_string) + return gjs_throw_gerror_message(context, error); return true; } @@ -145,13 +169,10 @@ gjs_string_from_filename(JSContext *context, /* Converts a JSString's array of Latin-1 chars to an array of a wider integer * type, by what the compiler believes is the most efficient method possible */ -template -static bool -from_latin1(JSContext *cx, - JSString *str, - T **data_p, - size_t *len_p) -{ +template +GJS_JSAPI_RETURN_CONVENTION static bool from_latin1(JSContext* cx, + JSString* str, T** data_p, + size_t* len_p) { /* No garbage collection should be triggered while we are using the string's * chars. Crash if that happens. */ JS::AutoCheckCannotGC nogc; @@ -189,8 +210,6 @@ gjs_string_get_char16_data(JSContext *context, char16_t **data_p, size_t *len_p) { - JSAutoRequest ar(context); - if (JS_StringHasLatin1Chars(str)) return from_latin1(context, str, data_p, len_p); @@ -227,7 +246,6 @@ gjs_string_to_ucs4(JSContext *cx, if (ucs4_string_p == NULL) return true; - JSAutoRequest ar(cx); size_t len; GError *error = NULL; @@ -278,12 +296,17 @@ gjs_string_from_ucs4(JSContext *cx, ssize_t n_chars, JS::MutableHandleValue value_p) { + // a null array pointer takes precedence over whatever `n_chars` says + if (!ucs4_string) { + value_p.setString(JS_GetEmptyString(cx)); + return true; + } + long u16_string_length; GError *error = NULL; - char16_t *u16_string = - reinterpret_cast(g_ucs4_to_utf16(ucs4_string, n_chars, NULL, - &u16_string_length, &error)); + gunichar2* u16_string = g_ucs4_to_utf16(ucs4_string, n_chars, nullptr, + &u16_string_length, &error); if (!u16_string) { gjs_throw(cx, "Failed to convert UCS-4 string to UTF-16: %s", error->message); @@ -291,9 +314,13 @@ gjs_string_from_ucs4(JSContext *cx, return false; } - JSAutoRequest ar(cx); - /* Avoid a copy - assumes that g_malloc == js_malloc == malloc */ - JS::RootedString str(cx, JS_NewUCString(cx, u16_string, u16_string_length)); + // Sadly, must copy, because js::UniquePtr forces that chars passed to + // JS_NewUCString() must have been allocated by the JS engine. + JS::RootedString str( + cx, JS_NewUCStringCopyN(cx, reinterpret_cast(u16_string), + u16_string_length)); + + g_free(u16_string); if (!str) { gjs_throw(cx, "Failed to convert UCS-4 string to UTF-16"); @@ -306,32 +333,25 @@ gjs_string_from_ucs4(JSContext *cx, /** * gjs_get_string_id: - * @context: a #JSContext + * @cx: a #JSContext * @id: a jsid that is an object hash key (could be an int or string) * @name_p place to store ASCII string version of key * - * If the id is not a string ID, return false and set *name_p to %NULL. + * If the id is not a string ID, return true and set *name_p to nullptr. * Otherwise, return true and fill in *name_p with ASCII name of id. * - * Returns: true if *name_p is non-%NULL + * Returns: false on error, otherwise true **/ -bool -gjs_get_string_id (JSContext *context, - jsid id, - GjsAutoJSChar *name_p) -{ - JS::RootedValue id_val(context); - - if (!JS_IdToValue(context, id, &id_val)) - return false; - - if (id_val.isString()) { - JS::RootedString str(context, id_val.toString()); - name_p->reset(JS_EncodeStringToUTF8(context, str)); - return !!*name_p; - } else { - return false; +bool gjs_get_string_id(JSContext* cx, jsid id, JS::UniqueChars* name_p) { + if (!JSID_IS_STRING(id)) { + name_p->reset(); + return true; } + + JSLinearString* lstr = JSID_TO_LINEAR_STRING(id); + JS::RootedString s(cx, JS_FORGET_STRING_LINEARNESS(lstr)); + *name_p = JS_EncodeStringToUTF8(cx, s); + return !!*name_p; } /** @@ -351,9 +371,9 @@ gjs_unichar_from_string (JSContext *context, JS::Value value, gunichar *result) { - GjsAutoJSChar utf8_str; - if (gjs_string_to_utf8(context, value, &utf8_str)) { - *result = g_utf8_get_char(utf8_str); + JS::UniqueChars utf8_str = gjs_string_to_utf8(context, value); + if (utf8_str) { + *result = g_utf8_get_char(utf8_str.get()); return true; } return false; @@ -363,15 +383,13 @@ jsid gjs_intern_string_to_id(JSContext *cx, const char *string) { - JSAutoRequest ar(cx); JS::RootedString str(cx, JS_AtomizeAndPinString(cx, string)); - return INTERNED_STRING_TO_JSID(cx, str); + if (!str) + return JSID_VOID; + return JS::PropertyKey::fromPinnedString(str); } -static std::string -gjs_debug_flat_string(JSFlatString *fstr) -{ - JSLinearString *str = js::FlatStringToLinearString(fstr); +[[nodiscard]] static std::string gjs_debug_linear_string(JSLinearString* str) { size_t len = js::GetLinearStringLength(str); JS::AutoCheckCannotGC nogc; @@ -401,17 +419,22 @@ gjs_debug_flat_string(JSFlatString *fstr) std::string gjs_debug_string(JSString *str) { - if (!JS_StringIsFlat(str)) { + if (!str) + return ""; + if (!JS_StringIsLinear(str)) { std::ostringstream out("'; return out.str(); } - return gjs_debug_flat_string(JS_ASSERT_STRING_IS_FLAT(str)); + return gjs_debug_linear_string(JS_ASSERT_STRING_IS_LINEAR(str)); } std::string gjs_debug_symbol(JS::Symbol * const sym) { + if (!sym) + return ""; + /* This is OK because JS::GetSymbolCode() and JS::GetSymbolDescription() * can't cause a garbage collection */ JS::HandleSymbol handle = JS::HandleSymbol::fromMarkedLocation(&sym); @@ -446,6 +469,9 @@ gjs_debug_symbol(JS::Symbol * const sym) std::string gjs_debug_object(JSObject * const obj) { + if (!obj) + return ""; + std::ostringstream out; const JSClass* clasp = JS_GetClass(obj); out << "name << " at " << obj << '>'; @@ -501,6 +527,6 @@ 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_linear_string(JSID_TO_LINEAR_STRING(id)); return gjs_debug_value(js::IdToValue(id)); } diff --git a/cjs/jsapi-util.cpp b/cjs/jsapi-util.cpp index ce66146..18c7f10 100644 --- a/cjs/jsapi-util.cpp +++ b/cjs/jsapi-util.cpp @@ -24,110 +24,38 @@ #include -#include -#include -#include "jsapi-wrapper.h" -#include - -#include -#include -#include -#include - -#include "jsapi-class.h" -#include "jsapi-util.h" -#include "context-private.h" -#include - -#include -#include - -GQuark -gjs_util_error_quark (void) -{ - return g_quark_from_static_string ("gjs-util-error-quark"); -} - -bool -gjs_object_get_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::MutableHandleValue value_p) -{ - return JS_GetPropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value_p); -} - -bool -gjs_object_set_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleValue value) -{ - return JS_SetPropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value); -} - -bool -gjs_object_has_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - bool *found) -{ - return JS_HasPropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - found); -} - -bool -gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleValue value, - unsigned flags) -{ - return JS_DefinePropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value, flags); -} +#include // for sscanf +#include // for strlen -bool -gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleObject value, - unsigned flags) -{ - return JS_DefinePropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value, flags); -} - -bool -gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleString value, - unsigned flags) -{ - return JS_DefinePropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value, flags); -} +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#endif -bool -gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - uint32_t value, - unsigned flags) -{ - return JS_DefinePropertyById(cx, obj, - gjs_context_get_const_string(cx, property_name), - value, flags); -} +#include // for codecvt_utf8_utf16 +#include // for wstring_convert +#include +#include // for move +#include + +#include +#include +#include +#include +#include +#include +#include // for JS_MaybeGC, NonIncrementalGC, GCRe... +#include // for RootedVector +#include +#include +#include +#include +#include // for JS_GetPropertyById, JS_ClearPendin... +#include // for ProtoKeyToClass + +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" static void throw_property_lookup_error(JSContext *cx, @@ -155,8 +83,6 @@ throw_property_lookup_error(JSContext *cx, * * SpiderMonkey will emit a warning if the property is not present, so don't * use this if you expect the property not to be present some of the time. - * - * Requires request. */ bool gjs_object_require_property(JSContext *context, @@ -216,18 +142,19 @@ gjs_object_require_property(JSContext *cx, return false; } -/* Converts JS string value to UTF-8 string. value must be freed with JS_free. */ -bool -gjs_object_require_property(JSContext *cx, - JS::HandleObject obj, - const char *description, - JS::HandleId property_name, - GjsAutoJSChar *value) -{ +/* Converts JS string value to UTF-8 string. */ +bool gjs_object_require_property(JSContext* cx, JS::HandleObject obj, + const char* description, + JS::HandleId property_name, + JS::UniqueChars* value) { JS::RootedValue prop_value(cx); - if (JS_GetPropertyById(cx, obj, property_name, &prop_value) && - gjs_string_to_utf8(cx, prop_value, value)) - return true; + if (JS_GetPropertyById(cx, obj, property_name, &prop_value)) { + JS::UniqueChars tmp = gjs_string_to_utf8(cx, prop_value); + if (tmp) { + *value = std::move(tmp); + return true; + } + } throw_property_lookup_error(cx, obj, description, property_name, "it was not a valid string"); @@ -285,10 +212,10 @@ gjs_throw_abstract_constructor_error(JSContext *context, const JSClass *proto_class; const char *name = "anonymous"; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject callee(context, &args.callee()); JS::RootedValue prototype(context); - if (gjs_object_get_property(context, callee, GJS_STRING_PROTOTYPE, - &prototype)) { + if (JS_GetPropertyById(context, callee, atoms.prototype(), &prototype)) { proto_class = JS_GetClass(&prototype.toObject()); name = proto_class->name; } @@ -296,44 +223,30 @@ gjs_throw_abstract_constructor_error(JSContext *context, gjs_throw(context, "You cannot construct new instances of '%s'", name); } -JSObject * -gjs_build_string_array(JSContext *context, - gssize array_length, - char **array_values) -{ - int i; - - if (array_length == -1) - array_length = g_strv_length(array_values); - - JS::AutoValueVector elems(context); - if (!elems.reserve(array_length)) - g_error("Unable to reserve memory for vector"); +JSObject* gjs_build_string_array(JSContext* context, + const std::vector& strings) { + JS::RootedValueVector elems(context); + if (!elems.reserve(strings.size())) { + JS_ReportOutOfMemory(context); + return nullptr; + } - for (i = 0; i < array_length; ++i) { - JS::ConstUTF8CharsZ chars(array_values[i], strlen(array_values[i])); + for (const std::string& string : strings) { + JS::ConstUTF8CharsZ chars(string.c_str(), string.size()); JS::RootedValue element(context, JS::StringValue(JS_NewStringCopyUTF8Z(context, chars))); - if (!elems.append(element)) - g_error("Unable to append to vector"); + elems.infallibleAppend(element); } - return JS_NewArrayObject(context, elems); + return JS::NewArrayObject(context, elems); } -JSObject* -gjs_define_string_array(JSContext *context, - JS::HandleObject in_object, - const char *array_name, - ssize_t array_length, - const char **array_values, - unsigned attrs) -{ - JSAutoRequest ar(context); - - JS::RootedObject array(context, - gjs_build_string_array(context, array_length, (char **) array_values)); - +JSObject* gjs_define_string_array(JSContext* context, + JS::HandleObject in_object, + const char* array_name, + const std::vector& strings, + unsigned attrs) { + JS::RootedObject array(context, gjs_build_string_array(context, strings)); if (!array) return nullptr; @@ -352,17 +265,13 @@ gjs_define_string_array(JSContext *context, * are \x escaped. * */ -static char * -gjs_string_readable(JSContext *context, - JS::HandleString string) -{ +[[nodiscard]] static char* gjs_string_readable(JSContext* context, + JS::HandleString string) { GString *buf = g_string_new(""); - JS_BeginRequest(context); - g_string_append_c(buf, '"'); - GjsAutoJSChar chars = JS_EncodeStringToUTF8(context, string); + JS::UniqueChars 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 @@ -377,19 +286,15 @@ gjs_string_readable(JSContext *context, g_string_append(buf, escaped); g_free(escaped); } else { - g_string_append(buf, chars); + g_string_append(buf, chars.get()); } g_string_append_c(buf, '"'); - JS_EndRequest(context); - return g_string_free(buf, false); } -static char * -_gjs_g_utf8_make_valid (const char *name) -{ +[[nodiscard]] static char* _gjs_g_utf8_make_valid(const char* name) { GString *string; const char *remainder, *invalid; int remaining_bytes, valid_bytes; @@ -437,19 +342,19 @@ char* gjs_value_debug_string(JSContext *context, JS::HandleValue value) { - char *bytes; - char *debugstr; - /* Special case debug strings for strings */ if (value.isString()) { JS::RootedString str(context, value.toString()); return gjs_string_readable(context, str); } - JS_BeginRequest(context); - JS::RootedString str(context, JS::ToString(context, value)); + if (!str) { + JS_ClearPendingException(context); + str = JS_ValueToSource(context, value); + } + if (!str) { if (value.isObject()) { /* Specifically the Call object (see jsfun.c in spidermonkey) @@ -459,76 +364,67 @@ gjs_value_debug_string(JSContext *context, if (klass != NULL) { str = JS_NewStringCopyZ(context, klass->name); JS_ClearPendingException(context); - if (!str) { - JS_EndRequest(context); + if (!str) return g_strdup("[out of memory copying class name]"); - } } else { gjs_log_exception(context); - JS_EndRequest(context); return g_strdup("[unknown object]"); } } else { - JS_EndRequest(context); return g_strdup("[unknown non-object]"); } } g_assert(str); - bytes = JS_EncodeStringToUTF8(context, str); - JS_EndRequest(context); - - debugstr = _gjs_g_utf8_make_valid(bytes); - JS_free(context, bytes); - - return debugstr; + JS::UniqueChars bytes = JS_EncodeStringToUTF8(context, str); + return _gjs_g_utf8_make_valid(bytes.get()); } -static char * -utf8_exception_from_non_gerror_value(JSContext *cx, - JS::HandleValue exc) -{ - JS::RootedString exc_str(cx, JS::ToString(cx, exc)); - if (!exc_str) - return nullptr; - - GjsAutoJSChar utf8_exception = JS_EncodeStringToUTF8(cx, exc_str); - return utf8_exception.copy(); -} - -bool -gjs_log_exception_full(JSContext *context, - JS::HandleValue exc, - JS::HandleString message) -{ - char *utf8_exception; - bool is_syntax; +/** + * gjs_log_exception_full: + * @cx: the #JSContext + * @exc: the exception value to be logged + * @message: a string to prepend to the log message + * @level: the severity level at which to log the exception + * + * Currently, uses %G_LOG_LEVEL_WARNING if the exception is being printed after + * being caught, and %G_LOG_LEVEL_CRITICAL if it was not caught by user code. + * + * Returns: %true if an exception was logged, %false if there was none pending. + */ +bool gjs_log_exception_full(JSContext* context, JS::HandleValue exc, + JS::HandleString message, GLogLevelFlags level) { + JS::AutoSaveExceptionState saved_exc(context); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); - JS_BeginRequest(context); JS::RootedObject exc_obj(context); + JS::RootedString exc_str(context); + bool is_syntax = false, is_internal = false; + if (exc.isObject()) { + exc_obj = &exc.toObject(); + const JSClass* syntax_error = js::ProtoKeyToClass(JSProto_SyntaxError); + is_syntax = JS_InstanceOf(context, exc_obj, syntax_error, nullptr); - is_syntax = false; + const JSClass* internal_error = + js::ProtoKeyToClass(JSProto_InternalError); + is_internal = JS_InstanceOf(context, exc_obj, internal_error, nullptr); + } - if (!exc.isObject()) { - utf8_exception = utf8_exception_from_non_gerror_value(context, exc); + if (is_internal) { + JSErrorReport* report = JS_ErrorFromException(context, exc_obj); + if (!report->message()) + exc_str = JS_NewStringCopyZ(context, "(unknown internal error)"); + else + exc_str = JS_NewStringCopyUTF8Z(context, report->message()); } else { - exc_obj = &exc.toObject(); - if (gjs_typecheck_boxed(context, exc_obj, NULL, G_TYPE_ERROR, false)) { - GError *gerror = (GError *) gjs_c_struct_from_boxed(context, exc_obj); - utf8_exception = g_strdup_printf("GLib.Error %s: %s", - g_quark_to_string(gerror->domain), - gerror->message); - } else { - 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); - } + exc_str = JS::ToString(context, exc); } + JS::UniqueChars utf8_exception; + if (exc_str) + utf8_exception = JS_EncodeStringToUTF8(context, exc_str); - GjsAutoJSChar utf8_message; + JS::UniqueChars utf8_message; if (message) utf8_message = JS_EncodeStringToUTF8(context, message); @@ -541,57 +437,68 @@ gjs_log_exception_full(JSContext *context, JS::RootedValue js_lineNumber(context), js_fileName(context); unsigned lineNumber; - 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); + JS_GetPropertyById(context, exc_obj, atoms.line_number(), + &js_lineNumber); + JS_GetPropertyById(context, exc_obj, atoms.file_name(), &js_fileName); - GjsAutoJSChar utf8_filename; + JS::UniqueChars 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(); if (message) { - g_critical("JS ERROR: %s: %s @ %s:%u", utf8_message.get(), utf8_exception, - utf8_filename.get(), lineNumber); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s: %s @ %s:%u", + utf8_message.get(), utf8_exception.get(), + utf8_filename ? utf8_filename.get() : "unknown", lineNumber); } else { - g_critical("JS ERROR: %s @ %s:%u", utf8_exception, - utf8_filename.get(), lineNumber); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s @ %s:%u", + utf8_exception.get(), + utf8_filename ? utf8_filename.get() : "unknown", lineNumber); } } else { - GjsAutoJSChar utf8_stack; - JS::RootedValue stack(context); - - if (exc.isObject() && - gjs_object_get_property(context, exc_obj, GJS_STRING_STACK, - &stack) && - stack.isString()) { - JS::RootedString str(context, stack.toString()); - utf8_stack = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars utf8_stack; + if (exc.isObject()) { + // Check both the internal SavedFrame object and the stack property. + // GErrors will not have the former, and internal errors will not + // have the latter. + JS::RootedObject saved_frame(context, + JS::ExceptionStackOrNull(exc_obj)); + JS::RootedString str(context); + if (saved_frame) { + JS::BuildStackString(context, nullptr, saved_frame, &str, 0); + } else { + JS::RootedValue stack(context); + JS_GetPropertyById(context, exc_obj, atoms.stack(), &stack); + if (stack.isString()) + str = stack.toString(); + } + if (str) + utf8_stack = JS_EncodeStringToUTF8(context, str); } if (message) { if (utf8_stack) - g_warning("JS ERROR: %s: %s\n%s", utf8_message.get(), utf8_exception, utf8_stack.get()); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s: %s\n%s", + utf8_message.get(), utf8_exception.get(), + utf8_stack.get()); else - g_warning("JS ERROR: %s: %s", utf8_message.get(), utf8_exception); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s: %s", + utf8_message.get(), utf8_exception.get()); } else { if (utf8_stack) - g_warning("JS ERROR: %s\n%s", utf8_exception, utf8_stack.get()); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s\n%s", + utf8_exception.get(), utf8_stack.get()); else - g_warning("JS ERROR: %s", utf8_exception); + g_log(G_LOG_DOMAIN, level, "JS ERROR: %s", + utf8_exception.get()); } } - g_free(utf8_exception); - - JS_EndRequest(context); + saved_exc.restore(); return true; } @@ -599,44 +506,37 @@ gjs_log_exception_full(JSContext *context, bool gjs_log_exception(JSContext *context) { - bool retval = false; - - JS_BeginRequest(context); - JS::RootedValue exc(context); if (!JS_GetPendingException(context, &exc)) - goto out; + return false; JS_ClearPendingException(context); - gjs_log_exception_full(context, exc, nullptr); - - retval = true; - - out: - JS_EndRequest(context); - - return retval; + gjs_log_exception_full(context, exc, nullptr, G_LOG_LEVEL_WARNING); + return true; } -bool -gjs_call_function_value(JSContext *context, - JS::HandleObject obj, - JS::HandleValue fval, - const JS::HandleValueArray& args, - JS::MutableHandleValue rval) -{ - bool result; - - JS_BeginRequest(context); - - result = JS_CallFunctionValue(context, obj, fval, args, rval); +/** + * gjs_log_exception_uncaught: + * @cx: the #JSContext + * + * Logs the exception pending on @cx, if any, indicating an uncaught exception + * in the running JS program. + * (Currently, due to main loop boundaries, uncaught exceptions may not bubble + * all the way back up to the top level, so this doesn't necessarily mean the + * program exits with an error.) + * + * Returns: %true if an exception was logged, %false if there was none pending. + */ +bool gjs_log_exception_uncaught(JSContext* cx) { + JS::RootedValue exc(cx); + if (!JS_GetPendingException(cx, &exc)) + return false; - if (result) - gjs_schedule_gc_if_needed(context); + JS_ClearPendingException(cx); - JS_EndRequest(context); - return result; + gjs_log_exception_full(cx, exc, nullptr, G_LOG_LEVEL_CRITICAL); + return true; } #ifdef __linux__ @@ -644,31 +544,30 @@ static void _linux_get_self_process_size (gulong *vm_size, gulong *rss_size) { - char *contents; char *iter; gsize len; int i; *vm_size = *rss_size = 0; - if (!g_file_get_contents ("/proc/self/stat", &contents, &len, NULL)) + char* contents_unowned; + if (!g_file_get_contents("/proc/self/stat", &contents_unowned, &len, + nullptr)) return; + GjsAutoChar contents = contents_unowned; iter = contents; /* See "man proc" for where this 22 comes from */ for (i = 0; i < 22; i++) { iter = strchr (iter, ' '); if (!iter) - goto out; + return; iter++; } sscanf (iter, " %lu", vm_size); iter = strchr (iter, ' '); if (iter) sscanf (iter, " %lu", rss_size); - - out: - g_free (contents); } static gulong linux_rss_trigger; @@ -707,12 +606,14 @@ gjs_gc_if_needed (JSContext *context) */ if (rss_size > linux_rss_trigger) { linux_rss_trigger = (gulong) MIN(G_MAXULONG, rss_size * 1.25); - JS::GCForReason(context, GC_SHRINK, JS::gcreason::Reason::API); + JS::NonIncrementalGC(context, GC_SHRINK, JS::GCReason::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); } } +#else // !__linux__ + (void)context; #endif } @@ -728,130 +629,61 @@ gjs_maybe_gc (JSContext *context) gjs_gc_if_needed(context); } -void -gjs_schedule_gc_if_needed (JSContext *context) -{ - GjsContext *gjs_context; - - /* We call JS_MaybeGC immediately, but defer a check for a full - * GC cycle to an idle handler. - */ - JS_MaybeGC(context); - - gjs_context = (GjsContext *) JS_GetContextPrivate(context); - if (gjs_context) - _gjs_context_schedule_gc_if_needed(gjs_context); -} - /** - * gjs_strip_unix_shebang: + * gjs_get_import_global: + * @context: a #JSContext * - * @script: (in): A pointer to a JS script - * @script_len: (inout): A pointer to the script length. The - * pointer will be modified if a shebang is stripped. - * @new_start_line_number: (out) (allow-none): A pointer to - * write the start-line number to account for the offset - * as a result of stripping the shebang. + * Gets the "import global" for the context's runtime. The import + * global object is the global object for the context. It is used + * as the root object for the scope of modules loaded by GJS in this + * runtime, and should also be used as the globals 'obj' argument passed + * to JS_InitClass() and the parent argument passed to JS_ConstructObject() + * when creating a native classes that are shared between all contexts using + * the runtime. (The standard JS classes are not shared, but we share + * classes such as GObject proxy classes since objects of these classes can + * easily migrate between contexts and having different classes depending + * on the context where they were first accessed would be confusing.) * - * Returns a pointer to the beginning of a script with unix - * shebangs removed. The outparams are useful to know the - * new length of the script and on what line of the - * original script we're executing from, so that any relevant - * offsets can be applied to the results of an execution pass. + * Return value: the "import global" for the context's + * runtime. Will never return %NULL while GJS has an active context + * for the runtime. */ -const char * -gjs_strip_unix_shebang(const char *script, - size_t *script_len, - int *start_line_number_out) -{ - g_assert(script_len); - - /* handle scripts with UNIX shebangs */ - if (strncmp(script, "#!", 2) == 0) { - /* If we found a newline, advance the script by one line */ - const char *s = (const char *) strstr (script, "\n"); - if (s != NULL) { - if (*script_len > 0) - *script_len -= (s + 1 - script); - script = s + 1; - - if (start_line_number_out) - *start_line_number_out = 2; - - return script; - } else { - /* Just a shebang */ - if (start_line_number_out) - *start_line_number_out = -1; - - *script_len = 0; - - return NULL; - } - } - - /* No shebang, return the original script */ - if (start_line_number_out) - *start_line_number_out = 1; - - return script; +JSObject* gjs_get_import_global(JSContext* cx) { + return GjsContextPrivate::from_cx(cx)->global(); } -bool -gjs_eval_with_scope(JSContext *context, - JS::HandleObject object, - const char *script, - ssize_t script_len, - const char *filename, - JS::MutableHandleValue retval) -{ - int start_line_number = 1; - JSAutoRequest ar(context); - size_t real_len = script_len; - - if (script_len < 0) - real_len = strlen(script); - - script = gjs_strip_unix_shebang(script, - &real_len, - &start_line_number); - - /* log and clear exception if it's set (should not be, normally...) */ - if (JS_IsExceptionPending(context)) { - g_warning("gjs_eval_in_scope called with a pending exception"); - return false; - } +#if defined(G_OS_WIN32) && (defined(_MSC_VER) && (_MSC_VER >= 1900)) +/* Unfortunately Visual Studio's C++ .lib somehow did not contain the right + * codecvt stuff that we need to convert from utf8 to utf16 (char16_t), so we + * need to work around this Visual Studio bug. Use Windows API + * MultiByteToWideChar() and obtain the std::u16string on the std::wstring we + * obtain from MultiByteToWideChar(). See: + * https://social.msdn.microsoft.com/Forums/en-US/8f40dcd8-c67f-4eba-9134-a19b9178e481/vs-2015-rc-linker-stdcodecvt-error?forum=vcgeneral + */ +static std::wstring gjs_win32_vc140_utf8_to_utf16(const char* str, + ssize_t len) { + int bufsize = MultiByteToWideChar(CP_UTF8, 0, str, len, nullptr, 0); + if (bufsize == 0) + return nullptr; - JS::RootedObject eval_obj(context, object); - if (!eval_obj) - eval_obj = JS_NewPlainObject(context); + std::wstring wstr(bufsize, 0); + int result = MultiByteToWideChar(CP_UTF8, 0, str, len, &wstr[0], bufsize); + if (result == 0) + return nullptr; - JS::CompileOptions options(context); - options.setFileAndLine(filename, start_line_number) - .setSourceIsLazy(true); + wstr.resize(len < 0 ? strlen(str) : len); + return wstr; +} +#endif +std::u16string gjs_utf8_script_to_utf16(const char* script, ssize_t len) { +#if defined(G_OS_WIN32) && (defined(_MSC_VER) && (_MSC_VER >= 1900)) + std::wstring wscript = gjs_win32_vc140_utf8_to_utf16(script, len); + return std::u16string(reinterpret_cast(wscript.c_str())); +#else 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::Evaluate(context, scope_chain, options, buf, retval)) - return false; - - gjs_schedule_gc_if_needed(context); - - if (JS_IsExceptionPending(context)) { - g_warning("EvaluateScript returned true but exception was pending; " - "did somebody call gjs_throw() without returning false?"); - return false; - } - - gjs_debug(GJS_DEBUG_CONTEXT, - "Script evaluation succeeded"); - - return true; + if (len < 0) + return convert.from_bytes(script); + return convert.from_bytes(script, script + len); +#endif } diff --git a/cjs/jsapi-util.h b/cjs/jsapi-util.h index 2474f3b..e945b2c 100644 --- a/cjs/jsapi-util.h +++ b/cjs/jsapi-util.h @@ -21,88 +21,180 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_JSAPI_UTIL_H__ -#define __GJS_JSAPI_UTIL_H__ +#ifndef GJS_JSAPI_UTIL_H_ +#define GJS_JSAPI_UTIL_H_ -#include -#include -#include +#include +#include // for size_t +#include +#include // for ssize_t + +#include // for unique_ptr +#include // for string, u16string +#include + +#include #include +#include -#include "jsapi-wrapper.h" -#include "gi/gtype.h" +#include // for IgnoreGCPolicy +#include +#include +#include // for UniqueChars +#include // for JSProtoKey -#ifdef __GNUC__ -#define GJS_ALWAYS_INLINE __attribute__((always_inline)) -#else -#define GJS_ALWAYS_INLINE -#endif +#include "cjs/macros.h" -class GjsAutoChar : public std::unique_ptr { -public: - GjsAutoChar(char *str = nullptr) : unique_ptr(str, g_free) {} +class JSErrorReport; +namespace JS { +class CallArgs; +} - operator const char *() { - return get(); +struct GjsAutoTakeOwnership {}; + +template +struct GjsAutoPointer : std::unique_ptr { + GjsAutoPointer(T* ptr = nullptr) // NOLINT(runtime/explicit) + : GjsAutoPointer::unique_ptr(ptr, *free_func) {} + GjsAutoPointer(T* ptr, const GjsAutoTakeOwnership&) + : GjsAutoPointer(nullptr) { + // FIXME: should use if constexpr (...), but that doesn't work with + // ubsan, which generates a null pointer check making it not a constexpr + // anymore: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71962 - Also a + // bogus warning, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94554 + auto ref = ref_func; + this->reset(ptr && ref ? reinterpret_cast(ref(ptr)) : ptr); } - void operator= (char *str) { - reset(str); + operator T*() const { return this->get(); } + T& operator[](size_t i) const { return static_cast(*this)[i]; } + + [[nodiscard]] T* copy() const { + return reinterpret_cast(ref_func(this->get())); } - void operator= (const char *str) { - reset(g_strdup(str)); + template + [[nodiscard]] C* as() const { + return const_cast(reinterpret_cast(this->get())); } }; template -class GjsAutoUnref : public std::unique_ptr { -public: - GjsAutoUnref(T *ptr = nullptr) : GjsAutoUnref::unique_ptr(ptr, g_object_unref) {} +using GjsAutoFree = GjsAutoPointer; - operator T *() { - return GjsAutoUnref::unique_ptr::get(); - } +struct GjsAutoCharFuncs { + static char* dup(char* str) { return g_strdup(str); } + static void free(char* str) { g_free(str); } }; +using GjsAutoChar = + GjsAutoPointer; -struct GjsJSFreeArgs { - void operator() (char *str) { - JS_free(nullptr, str); - } +using GjsAutoStrv = GjsAutoPointer; + +template +using GjsAutoUnref = GjsAutoPointer; + +template +struct GjsAutoTypeClass : GjsAutoPointer { + GjsAutoTypeClass(gpointer ptr = nullptr) // NOLINT(runtime/explicit) + : GjsAutoPointer(static_cast(ptr)) {} + explicit GjsAutoTypeClass(GType gtype) + : GjsAutoTypeClass(g_type_class_ref(gtype)) {} }; -class GjsAutoJSChar : public std::unique_ptr { -public: - GjsAutoJSChar(char *str = nullptr) : unique_ptr(str, GjsJSFreeArgs()) { } +// Use this class for owning a GIBaseInfo* of indeterminate type. Any type (e.g. +// GIFunctionInfo*, GIObjectInfo*) will fit. If you know that the info is of a +// certain type (e.g. you are storing the return value of a function that +// returns GIFunctionInfo*,) use one of the derived classes below. +struct GjsAutoBaseInfo : GjsAutoPointer { + GjsAutoBaseInfo(GIBaseInfo* ptr = nullptr) // NOLINT(runtime/explicit) + : GjsAutoPointer(ptr) {} + + [[nodiscard]] const char* name() const { + return g_base_info_get_name(*this); + } + [[nodiscard]] const char* ns() const { + return g_base_info_get_namespace(*this); + } + [[nodiscard]] GIInfoType type() const { + return g_base_info_get_type(*this); + } +}; - operator const char*() { - return get(); +// Use GjsAutoInfo, preferably its typedefs below, when you know for sure that +// the info is either of a certain type or null. +template +struct GjsAutoInfo : GjsAutoBaseInfo { + // Normally one-argument constructors should be explicit, but we are trying + // to conform to the interface of std::unique_ptr here. + GjsAutoInfo(GIBaseInfo* ptr = nullptr) // NOLINT(runtime/explicit) + : GjsAutoBaseInfo(ptr) { + validate(); } - void operator=(char *str) { - reset(str); + void reset(GIBaseInfo* other = nullptr) { + GjsAutoBaseInfo::reset(other); + validate(); } - char* copy() { - /* Strings acquired by this should be g_free()'ed */ - return g_strdup(get()); + // You should not need this method, because you already know the answer. + GIInfoType type() = delete; + + private: + void validate() const { + if (GIBaseInfo* base = *this) + g_assert(g_base_info_get_type(base) == TAG); } }; -G_BEGIN_DECLS +using GjsAutoEnumInfo = GjsAutoInfo; +using GjsAutoFieldInfo = GjsAutoInfo; +using GjsAutoFunctionInfo = GjsAutoInfo; +using GjsAutoInterfaceInfo = GjsAutoInfo; +using GjsAutoObjectInfo = GjsAutoInfo; +using GjsAutoPropertyInfo = GjsAutoInfo; +using GjsAutoStructInfo = GjsAutoInfo; +using GjsAutoTypeInfo = GjsAutoInfo; +using GjsAutoValueInfo = GjsAutoInfo; +using GjsAutoVFuncInfo = GjsAutoInfo; + +// GICallableInfo can be one of several tags, so we have to have a separate +// class, and use GI_IS_CALLABLE_INFO() to validate. +struct GjsAutoCallableInfo : GjsAutoBaseInfo { + GjsAutoCallableInfo(GIBaseInfo* ptr = nullptr) // NOLINT(runtime/explicit) + : GjsAutoBaseInfo(ptr) { + validate(); + } + + void reset(GIBaseInfo* other = nullptr) { + GjsAutoBaseInfo::reset(other); + validate(); + } -#define GJS_UTIL_ERROR gjs_util_error_quark () -GQuark gjs_util_error_quark (void); -enum { - GJS_UTIL_ERROR_NONE, - GJS_UTIL_ERROR_ARGUMENT_INVALID, - GJS_UTIL_ERROR_ARGUMENT_UNDERFLOW, - GJS_UTIL_ERROR_ARGUMENT_OVERFLOW, - GJS_UTIL_ERROR_ARGUMENT_TYPE_MISMATCH + private: + void validate() const { + if (GIBaseInfo* base = *this) + g_assert(GI_IS_CALLABLE_INFO(base)); + } }; -typedef struct GjsRootedArray GjsRootedArray; +/* For use of GjsAutoInfo in GC hash maps */ +namespace JS { +template +struct GCPolicy> : public IgnoreGCPolicy> {}; +} // namespace JS + +using GjsAutoParam = GjsAutoPointer; + +/* For use of GjsAutoParam in GC hash maps */ +namespace JS { +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} // namespace JS /* Flags that should be set on properties exported from native code modules. * Basically set these on API, but do NOT set them on data. @@ -123,27 +215,28 @@ typedef struct GjsRootedArray GjsRootedArray; * A convenience macro for getting the 'this' object a function was called with. * Use in any JSNative function. */ -#define GJS_GET_THIS(cx, argc, vp, args, to) \ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ - JS::RootedObject to(cx, &args.computeThis(cx).toObject()) +#define GJS_GET_THIS(cx, argc, vp, args, to) \ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ + JS::RootedObject to(cx); \ + if (!args.computeThis(cx, &to)) \ + return false; -JSObject* gjs_get_import_global (JSContext *context); +[[nodiscard]] JSObject* gjs_get_import_global(JSContext* cx); void gjs_throw_constructor_error (JSContext *context); void gjs_throw_abstract_constructor_error(JSContext *context, JS::CallArgs& args); -JSObject* gjs_build_string_array (JSContext *context, - gssize array_length, - char **array_values); +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_build_string_array(JSContext* cx, + const std::vector& strings); -JSObject *gjs_define_string_array(JSContext *context, - JS::HandleObject obj, - const char *array_name, - ssize_t array_length, - const char **array_values, - unsigned attrs); +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_define_string_array(JSContext* cx, JS::HandleObject obj, + const char* array_name, + const std::vector& strings, + unsigned attrs); void gjs_throw (JSContext *context, const char *format, @@ -155,67 +248,67 @@ void gjs_throw_custom (JSContext *context, ...) G_GNUC_PRINTF (4, 5); void gjs_throw_literal (JSContext *context, const char *string); -void gjs_throw_g_error (JSContext *context, - GError *error); +bool gjs_throw_gerror_message(JSContext* cx, GError* error); bool gjs_log_exception (JSContext *context); -bool gjs_log_exception_full(JSContext *context, - JS::HandleValue exc, - JS::HandleString message); +bool gjs_log_exception_uncaught(JSContext* cx); -char *gjs_value_debug_string(JSContext *context, - JS::HandleValue value); +bool gjs_log_exception_full(JSContext* cx, JS::HandleValue exc, + JS::HandleString message, GLogLevelFlags level); -bool gjs_call_function_value(JSContext *context, - JS::HandleObject obj, - JS::HandleValue fval, - const JS::HandleValueArray& args, - JS::MutableHandleValue rval); +[[nodiscard]] char* gjs_value_debug_string(JSContext* cx, + JS::HandleValue value); -void gjs_warning_reporter(JSContext *cx, - JSErrorReport *report); +void gjs_warning_reporter(JSContext*, JSErrorReport* report); -bool gjs_string_to_utf8 (JSContext *context, - const JS::Value string_val, - GjsAutoJSChar *utf8_string_p); +GJS_JSAPI_RETURN_CONVENTION +JS::UniqueChars gjs_string_to_utf8(JSContext* cx, const JS::Value string_val); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_from_utf8(JSContext *context, const char *utf8_string, JS::MutableHandleValue value_p); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_from_utf8_n(JSContext *cx, const char *utf8_chars, size_t len, JS::MutableHandleValue out); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_to_filename(JSContext *cx, const JS::Value string_val, GjsAutoChar *filename_string); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_from_filename(JSContext *context, const char *filename_string, ssize_t n_bytes, JS::MutableHandleValue value_p); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_get_char16_data(JSContext *cx, JS::HandleString str, char16_t **data_p, size_t *len_p); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_to_ucs4(JSContext *cx, JS::HandleString value, gunichar **ucs4_string_p, size_t *len_p); +GJS_JSAPI_RETURN_CONVENTION bool gjs_string_from_ucs4(JSContext *cx, const gunichar *ucs4_string, ssize_t n_chars, JS::MutableHandleValue value_p); -bool gjs_get_string_id (JSContext *context, - jsid id, - GjsAutoJSChar *name_p); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_get_string_id(JSContext* cx, jsid id, JS::UniqueChars* name_p); +GJS_JSAPI_RETURN_CONVENTION jsid gjs_intern_string_to_id (JSContext *context, const char *string); +GJS_JSAPI_RETURN_CONVENTION bool gjs_unichar_from_string (JSContext *context, JS::Value string, gunichar *result); @@ -223,171 +316,69 @@ bool gjs_unichar_from_string (JSContext *context, /* 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, - const char *script, - ssize_t script_len, - const char *filename, - JS::MutableHandleValue retval); - -typedef enum { - GJS_STRING_CONSTRUCTOR, - GJS_STRING_PROTOTYPE, - GJS_STRING_LENGTH, - GJS_STRING_IMPORTS, - GJS_STRING_PARENT_MODULE, - GJS_STRING_MODULE_INIT, - GJS_STRING_SEARCH_PATH, - GJS_STRING_KEEP_ALIVE_MARKER, - GJS_STRING_PRIVATE_NS_MARKER, - GJS_STRING_GI_MODULE, - GJS_STRING_GI_VERSIONS, - GJS_STRING_GI_OVERRIDES, - GJS_STRING_GOBJECT_INIT, - GJS_STRING_INSTANCE_INIT, - GJS_STRING_NEW_INTERNAL, - GJS_STRING_NEW, - GJS_STRING_MESSAGE, - GJS_STRING_CODE, - GJS_STRING_STACK, - GJS_STRING_FILENAME, - GJS_STRING_LINE_NUMBER, - GJS_STRING_COLUMN_NUMBER, - GJS_STRING_NAME, - GJS_STRING_X, - GJS_STRING_Y, - GJS_STRING_WIDTH, - GJS_STRING_HEIGHT, - GJS_STRING_MODULE_PATH, - GJS_STRING_LAST -} GjsConstString; - -const char * gjs_strip_unix_shebang(const char *script, - size_t *script_len, - int *new_start_line_number); - -/* These four functions wrap JS_GetPropertyById(), etc., but with a - * GjsConstString constant instead of a jsid. */ - -bool gjs_object_get_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::MutableHandleValue value_p); - -bool gjs_object_set_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleValue value); - -bool gjs_object_has_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - bool *found); - -G_END_DECLS +[[nodiscard]] std::u16string gjs_utf8_script_to_utf16(const char* script, + ssize_t len); +GJS_JSAPI_RETURN_CONVENTION GjsAutoChar gjs_format_stack_trace(JSContext *cx, JS::HandleObject saved_frame); -bool gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleValue value, - unsigned flags); - -bool gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleObject value, - unsigned flags); - -bool gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - JS::HandleString value, - unsigned flags); - -bool gjs_object_define_property(JSContext *cx, - JS::HandleObject obj, - GjsConstString property_name, - uint32_t value, - unsigned flags); - -JS::HandleId gjs_context_get_const_string(JSContext *cx, - GjsConstString string); - /* Overloaded functions, must be outside G_DECLS. More types are intended to be * added as the opportunity arises. */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_object_require_property(JSContext *context, JS::HandleObject obj, const char *obj_description, JS::HandleId property_name, JS::MutableHandleValue value); +GJS_JSAPI_RETURN_CONVENTION bool gjs_object_require_property(JSContext *cx, JS::HandleObject obj, const char *description, JS::HandleId property_name, bool *value); +GJS_JSAPI_RETURN_CONVENTION bool gjs_object_require_property(JSContext *cx, JS::HandleObject obj, const char *description, JS::HandleId property_name, int32_t *value); -bool gjs_object_require_property(JSContext *cx, - JS::HandleObject obj, - const char *description, - JS::HandleId property_name, - GjsAutoJSChar *value); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_object_require_property(JSContext* cx, JS::HandleObject obj, + const char* description, + JS::HandleId property_name, + JS::UniqueChars* value); +GJS_JSAPI_RETURN_CONVENTION bool gjs_object_require_property(JSContext *cx, JS::HandleObject obj, const char *description, JS::HandleId property_name, JS::MutableHandleObject value); +GJS_JSAPI_RETURN_CONVENTION bool gjs_object_require_converted_property(JSContext *context, JS::HandleObject obj, const char *description, JS::HandleId property_name, uint32_t *value); -/* Here, too, we have wrappers that take a GjsConstString. */ - -template -bool gjs_object_require_property(JSContext *cx, - JS::HandleObject obj, - const char *description, - GjsConstString property_name, - T value) -{ - return gjs_object_require_property(cx, obj, description, - gjs_context_get_const_string(cx, property_name), - value); -} +[[nodiscard]] std::string gjs_debug_string(JSString* str); +[[nodiscard]] std::string gjs_debug_symbol(JS::Symbol* const sym); +[[nodiscard]] std::string gjs_debug_object(JSObject* obj); +[[nodiscard]] std::string gjs_debug_value(JS::Value v); +[[nodiscard]] std::string gjs_debug_id(jsid id); -template -bool gjs_object_require_converted_property(JSContext *cx, - JS::HandleObject obj, - const char *description, - GjsConstString property_name, - T value) -{ - return gjs_object_require_converted_property(cx, obj, description, - gjs_context_get_const_string(cx, property_name), - value); -} +[[nodiscard]] char* gjs_hyphen_to_underscore(const char* str); -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); +#if defined(G_OS_WIN32) && (defined(_MSC_VER) && (_MSC_VER >= 1900)) +[[nodiscard]] std::wstring gjs_win32_vc140_utf8_to_utf16(const char* str); +#endif -#endif /* __GJS_JSAPI_UTIL_H__ */ +#endif // GJS_JSAPI_UTIL_H_ diff --git a/cjs/macros.h b/cjs/macros.h index 05457da..9411da8 100644 --- a/cjs/macros.h +++ b/cjs/macros.h @@ -21,10 +21,10 @@ * IN THE SOFTWARE. */ -#ifndef GJS_MACROS_H -#define GJS_MACROS_H +#ifndef GJS_MACROS_H_ +#define GJS_MACROS_H_ -#include +#include /* IWYU pragma: keep */ #ifdef G_OS_WIN32 # ifdef GJS_COMPILATION @@ -32,8 +32,47 @@ # else # define GJS_EXPORT __declspec(dllimport) # endif +# define siginfo_t void #else -# define GJS_EXPORT +# define GJS_EXPORT __attribute__((visibility("default"))) #endif -#endif /* GJS_MACROS_H */ +/** + * GJS_USE: + * + * Indicates a return value must be used, or the compiler should log a warning. + * Equivalent to [[nodiscard]], but this macro is for use in external headers + * which are not necessarily compiled with a C++ compiler. + */ +#if defined(__GNUC__) || defined(__clang__) +# define GJS_USE __attribute__((warn_unused_result)) +#else +# define GJS_USE +#endif + +/** + * GJS_JSAPI_RETURN_CONVENTION: + * + * Same as [[nodiscard]], but indicates that a return value of true or non-null + * means that no exception must be pending on the passed-in #JSContext. + * Conversely, a return value of false or nullptr means that an exception must + * be pending, or else an uncatchable exception has been thrown. + * + * It's intended for use by static analysis tools to do better consistency + * checks. If not using them, then it has the same effect as [[nodiscard]]. + * It's also intended as documentation for the programmer. + */ +#ifdef __clang_analyzer__ +# define GJS_JSAPI_RETURN_CONVENTION \ + [[nodiscard]] __attribute__((annotate("jsapi_return_convention"))) +#else +# define GJS_JSAPI_RETURN_CONVENTION [[nodiscard]] +#endif + +#ifdef __GNUC__ +# define GJS_ALWAYS_INLINE __attribute__((always_inline)) +#else +# define GJS_ALWAYS_INLINE +#endif + +#endif /* GJS_MACROS_H_ */ diff --git a/cjs/mem-private.h b/cjs/mem-private.h new file mode 100644 index 0000000..14b739a --- /dev/null +++ b/cjs/mem-private.h @@ -0,0 +1,75 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2008 litl, LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef GJS_MEM_PRIVATE_H_ +#define GJS_MEM_PRIVATE_H_ + +#include + +typedef struct { + volatile int value; + const char* name; +} GjsMemCounter; + +// clang-format off +#define GJS_FOR_EACH_COUNTER(macro) \ + macro(boxed_instance) \ + macro(boxed_prototype) \ + macro(closure) \ + macro(function) \ + macro(fundamental_instance) \ + macro(fundamental_prototype) \ + macro(gerror_instance) \ + macro(gerror_prototype) \ + macro(importer) \ + macro(interface) \ + macro(module) \ + macro(ns) \ + macro(object_instance) \ + macro(object_prototype) \ + macro(param) \ + macro(repo) \ + macro(union_instance) \ + macro(union_prototype) +// clang-format on + +#define GJS_DECLARE_COUNTER(name) extern GjsMemCounter gjs_counter_##name; + +GJS_DECLARE_COUNTER(everything) +GJS_FOR_EACH_COUNTER(GJS_DECLARE_COUNTER) + +#define GJS_INC_COUNTER(name) \ + do { \ + g_atomic_int_add(&gjs_counter_everything.value, 1); \ + g_atomic_int_add(&gjs_counter_##name.value, 1); \ + } while (0) + +#define GJS_DEC_COUNTER(name) \ + do { \ + g_atomic_int_add(&gjs_counter_everything.value, -1); \ + g_atomic_int_add(&gjs_counter_##name.value, -1); \ + } while (0) + +#define GJS_GET_COUNTER(name) g_atomic_int_get(&gjs_counter_##name.value) + +#endif // GJS_MEM_PRIVATE_H_ diff --git a/cjs/mem.cpp b/cjs/mem.cpp index 3fff516..3d6f36e 100644 --- a/cjs/mem.cpp +++ b/cjs/mem.cpp @@ -21,10 +21,11 @@ * IN THE SOFTWARE. */ -#include +#include -#include "mem.h" -#include +#include "cjs/mem-private.h" +#include "cjs/mem.h" +#include "util/log.h" #define GJS_DEFINE_COUNTER(name) \ GjsMemCounter gjs_counter_ ## name = { \ @@ -33,35 +34,11 @@ GJS_DEFINE_COUNTER(everything) +GJS_FOR_EACH_COUNTER(GJS_DEFINE_COUNTER) -GJS_DEFINE_COUNTER(boxed) -GJS_DEFINE_COUNTER(closure) -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) +#define GJS_LIST_COUNTER(name) &gjs_counter_##name, -#define GJS_LIST_COUNTER(name) \ - & gjs_counter_ ## name - -static GjsMemCounter* counters[] = { - GJS_LIST_COUNTER(boxed), - GJS_LIST_COUNTER(closure), - 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), -}; +static GjsMemCounter* counters[] = {GJS_FOR_EACH_COUNTER(GJS_LIST_COUNTER)}; void gjs_memory_report(const char *where, @@ -91,14 +68,13 @@ gjs_memory_report(const char *where, " %d objects currently alive", GJS_GET_COUNTER(everything)); - for (i = 0; i < n_counters; ++i) { - gjs_debug(GJS_DEBUG_MEMORY, - " %12s = %d", - counters[i]->name, - counters[i]->value); - } + if (GJS_GET_COUNTER(everything) != 0) { + for (i = 0; i < n_counters; ++i) { + gjs_debug(GJS_DEBUG_MEMORY, " %24s = %d", counters[i]->name, + counters[i]->value); + } - if (die_if_leaks && GJS_GET_COUNTER(everything) > 0) { - g_error("%s: JavaScript objects were leaked.", where); + if (die_if_leaks) + g_error("%s: JavaScript objects were leaked.", where); } } diff --git a/cjs/mem.h b/cjs/mem.h index c85ff54..14b9b14 100644 --- a/cjs/mem.h +++ b/cjs/mem.h @@ -21,55 +21,25 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_MEM_H__ -#define __GJS_MEM_H__ +#ifndef GJS_MEM_H_ +#define GJS_MEM_H_ -#include -#include -#include "cjs/jsapi-util.h" - -G_BEGIN_DECLS - -typedef struct { - volatile int value; - const char *name; -} GjsMemCounter; +#if !defined(INSIDE_GJS_H) && !defined(GJS_COMPILATION) +# error "Only can be included directly." +#endif -#define GJS_DECLARE_COUNTER(name) \ - extern GjsMemCounter gjs_counter_ ## name ; +#include /* IWYU pragma: keep */ -GJS_DECLARE_COUNTER(everything) - -GJS_DECLARE_COUNTER(boxed) -GJS_DECLARE_COUNTER(closure) -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) - -#define GJS_INC_COUNTER(name) \ - do { \ - g_atomic_int_add(&gjs_counter_everything.value, 1); \ - g_atomic_int_add(&gjs_counter_ ## name .value, 1); \ - } while (0) +#include -#define GJS_DEC_COUNTER(name) \ - do { \ - g_atomic_int_add(&gjs_counter_everything.value, -1); \ - g_atomic_int_add(&gjs_counter_ ## name .value, -1); \ - } while (0) +#include "cjs/macros.h" -#define GJS_GET_COUNTER(name) \ - g_atomic_int_get(&gjs_counter_ ## name .value) +G_BEGIN_DECLS +GJS_EXPORT void gjs_memory_report(const char *where, bool die_if_leaks); G_END_DECLS -#endif /* __GJS_MEM_H__ */ +#endif // GJS_MEM_H_ diff --git a/cjs/module.cpp b/cjs/module.cpp index 474bba4..e9e3ada 100644 --- a/cjs/module.cpp +++ b/cjs/module.cpp @@ -21,53 +21,65 @@ * IN THE SOFTWARE. */ -#include -#include +#include -#include +#include // for size_t +#include // for ssize_t + +#include // for u16string -#include "jsapi-util.h" -#include "jsapi-wrapper.h" -#include "module.h" +#include +#include + +#include +#include +#include +#include // for RootedVector +#include +#include +#include +#include +#include // for JS_DefinePropertyById, ... + +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "cjs/module.h" #include "util/log.h" -class GjsModule { +class GjsScriptModule { char *m_name; - GjsModule(const char *name) - { + GjsScriptModule(const char* name) { m_name = g_strdup(name); + GJS_INC_COUNTER(module); } - ~GjsModule() - { + ~GjsScriptModule() { g_free(m_name); + GJS_DEC_COUNTER(module); } /* Private data accessors */ - static inline GjsModule * - priv(JSObject *module) - { - return static_cast(JS_GetPrivate(module)); + [[nodiscard]] static inline GjsScriptModule* priv(JSObject* module) { + return static_cast(JS_GetPrivate(module)); } /* Creates a JS module object. Use instead of the class's constructor */ - static JSObject * - create(JSContext *cx, - const char *name) - { - JSObject *module = JS_NewObject(cx, &GjsModule::klass); - JS_SetPrivate(module, new GjsModule(name)); + [[nodiscard]] static JSObject* create(JSContext* cx, const char* name) { + JSObject* module = JS_NewObject(cx, &GjsScriptModule::klass); + JS_SetPrivate(module, new GjsScriptModule(name)); return module; } /* Defines the empty module as a property on the importer */ + GJS_JSAPI_RETURN_CONVENTION bool define_import(JSContext *cx, JS::HandleObject module, JS::HandleObject importer, - JS::HandleId name) + JS::HandleId name) const { if (!JS_DefinePropertyById(cx, importer, name, module, GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT)) { @@ -80,32 +92,35 @@ class GjsModule { } /* Carries out the actual execution of the module code */ - bool - evaluate_import(JSContext *cx, - JS::HandleObject module, - const char *script, - size_t script_len, - const char *filename, - int line_number) - { - JS::CompileOptions options(cx); - options.setFileAndLine(filename, line_number) - .setSourceIsLazy(true); + GJS_JSAPI_RETURN_CONVENTION + bool evaluate_import(JSContext* cx, JS::HandleObject module, + const char* script, ssize_t script_len, + const char* filename) { + std::u16string utf16_string = + gjs_utf8_script_to_utf16(script, script_len); + // COMPAT: This could use JS::SourceText directly, + // but that messes up code coverage. See bug + // https://bugzilla.mozilla.org/show_bug.cgi?id=1404784 + JS::SourceText buf; + if (!buf.init(cx, utf16_string.c_str(), utf16_string.size(), + JS::SourceOwnership::Borrowed)) + 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::RootedObjectVector scope_chain(cx); + if (!scope_chain.append(module)) { + JS_ReportOutOfMemory(cx); + return false; + } - JS::AutoObjectVector scope_chain(cx); - if (!scope_chain.append(module)) - g_error("Unable to append to vector"); + JS::CompileOptions options(cx); + options.setFileAndLine(filename, 1); JS::RootedValue ignored_retval(cx); if (!JS::Evaluate(cx, scope_chain, options, buf, &ignored_retval)) return false; - gjs_schedule_gc_if_needed(cx); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + gjs->schedule_gc_if_needed(); gjs_debug(GJS_DEBUG_IMPORTER, "Importing module %s succeeded", m_name); @@ -113,6 +128,7 @@ class GjsModule { } /* Loads JS code from a file and imports it */ + GJS_JSAPI_RETURN_CONVENTION bool import_file(JSContext *cx, JS::HandleObject module, @@ -121,27 +137,21 @@ class GjsModule { GError *error = nullptr; char *unowned_script; size_t script_len = 0; - int start_line_number = 1; if (!(g_file_load_contents(file, nullptr, &unowned_script, &script_len, - nullptr, &error))) { - gjs_throw_g_error(cx, error); - return false; - } + nullptr, &error))) + return gjs_throw_gerror_message(cx, error); GjsAutoChar script = unowned_script; /* steals ownership */ - g_assert(script != nullptr); - - const char *stripped_script = - gjs_strip_unix_shebang(script, &script_len, &start_line_number); + g_assert(script); GjsAutoChar full_path = g_file_get_parse_name(file); - return evaluate_import(cx, module, stripped_script, script_len, - full_path, start_line_number); + return evaluate_import(cx, module, script, script_len, full_path); } /* JSClass operations */ + GJS_JSAPI_RETURN_CONVENTION bool resolve_impl(JSContext *cx, JS::HandleObject module, @@ -177,6 +187,7 @@ class GjsModule { JS_DefinePropertyById(cx, module, id, desc); } + GJS_JSAPI_RETURN_CONVENTION static bool resolve(JSContext *cx, JS::HandleObject module, @@ -186,33 +197,27 @@ class GjsModule { return priv(module)->resolve_impl(cx, module, id, resolved); } - static void - finalize(JSFreeOp *op, - JSObject *module) - { - delete priv(module); - } + static void finalize(JSFreeOp*, JSObject* module) { delete priv(module); } static constexpr JSClassOps class_ops = { - nullptr, /* addProperty */ - nullptr, /* deleteProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - &GjsModule::resolve, - nullptr, /* mayResolve */ - &GjsModule::finalize, + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + &GjsScriptModule::resolve, + nullptr, // mayResolve + &GjsScriptModule::finalize, }; static constexpr JSClass klass = { - "GjsModule", + "GjsScriptModule", JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE, - &GjsModule::class_ops, + &GjsScriptModule::class_ops, }; -public: - + public: /* Carries out the import operation */ + GJS_JSAPI_RETURN_CONVENTION static JSObject * import(JSContext *cx, JS::HandleObject importer, @@ -220,7 +225,7 @@ public: const char *name, GFile *file) { - JS::RootedObject module(cx, GjsModule::create(cx, name)); + JS::RootedObject module(cx, GjsScriptModule::create(cx, name)); if (!module || !priv(module)->define_import(cx, module, importer, id) || !priv(module)->import_file(cx, module, file)) @@ -253,8 +258,8 @@ gjs_module_import(JSContext *cx, const char *name, GFile *file) { - return GjsModule::import(cx, importer, id, name, file); + return GjsScriptModule::import(cx, importer, id, name, file); } -decltype(GjsModule::klass) constexpr GjsModule::klass; -decltype(GjsModule::class_ops) constexpr GjsModule::class_ops; +decltype(GjsScriptModule::klass) constexpr GjsScriptModule::klass; +decltype(GjsScriptModule::class_ops) constexpr GjsScriptModule::class_ops; diff --git a/cjs/module.h b/cjs/module.h index 9041391..b580222 100644 --- a/cjs/module.h +++ b/cjs/module.h @@ -21,15 +21,18 @@ * IN THE SOFTWARE. */ -#ifndef GJS_MODULE_H -#define GJS_MODULE_H +#ifndef GJS_MODULE_H_ +#define GJS_MODULE_H_ + +#include #include -#include "jsapi-wrapper.h" +#include -G_BEGIN_DECLS +#include "cjs/macros.h" +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_module_import(JSContext *cx, JS::HandleObject importer, @@ -37,6 +40,4 @@ gjs_module_import(JSContext *cx, const char *name, GFile *file); -G_END_DECLS - -#endif /* GJS_MODULE_H */ +#endif // GJS_MODULE_H_ diff --git a/cjs/native.cpp b/cjs/native.cpp index c7f6dcb..949d441 100644 --- a/cjs/native.cpp +++ b/cjs/native.cpp @@ -23,31 +23,34 @@ #include -#include +#include +#include // for tie +#include +#include // for pair -#include +#include -#include "native.h" -#include "jsapi-wrapper.h" -#include "jsapi-util.h" +#include +#include -static GHashTable *modules = NULL; +#include "cjs/jsapi-util.h" +#include "cjs/native.h" +#include "util/log.h" + +static std::unordered_map modules; void gjs_register_native_module (const char *module_id, GjsDefineModuleFunc func) { - if (modules == NULL) - modules = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); - - if (g_hash_table_lookup(modules, module_id) != NULL) { + bool inserted; + std::tie(std::ignore, inserted) = modules.insert({module_id, func}); + if (!inserted) { g_warning("A second native module tried to register the same id '%s'", module_id); return; } - g_hash_table_replace(modules, g_strdup(module_id), (void*) func); - gjs_debug(GJS_DEBUG_NATIVE, "Registered native JS module '%s'", module_id); @@ -55,30 +58,22 @@ gjs_register_native_module (const char *module_id, /** * gjs_is_registered_native_module: - * @context: - * @parent: the parent object defining the namespace * @name: name of the module * * Checks if a native module corresponding to @name has already * been registered. This is used to check to see if a name is a * builtin module without starting to try and load it. */ -bool -gjs_is_registered_native_module(JSContext *context, - JSObject *parent, - const char *name) -{ - if (modules == NULL) - return false; - - return g_hash_table_lookup(modules, name) != NULL; +bool gjs_is_registered_native_module(const char* name) { + return modules.count(name) > 0; } /** * gjs_load_native_module: * @context: the #JSContext - * @name: Name under which the module was registered with - * gjs_register_native_module() + * @parse_name: Name under which the module was registered with + * gjs_register_native_module(), should be in the format as returned by + * g_file_get_parse_name() * @module_out: Return location for a #JSObject * * Loads a builtin native-code module called @name into @module_out. @@ -87,26 +82,21 @@ gjs_is_registered_native_module(JSContext *context, */ bool gjs_load_native_module(JSContext *context, - const char *name, + const char *parse_name, JS::MutableHandleObject module_out) { - GjsDefineModuleFunc func; - gjs_debug(GJS_DEBUG_NATIVE, "Defining native module '%s'", - name); + parse_name); - if (modules != NULL) - func = (GjsDefineModuleFunc) g_hash_table_lookup(modules, name); - else - func = NULL; + const auto& iter = modules.find(parse_name); - if (!func) { + if (iter == modules.end()) { gjs_throw(context, "No native module '%s' has registered itself", - name); + parse_name); return false; } - return func (context, module_out); + return iter->second(context, module_out); } diff --git a/cjs/native.h b/cjs/native.h index 8443d2e..cddfa99 100644 --- a/cjs/native.h +++ b/cjs/native.h @@ -21,14 +21,15 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_NATIVE_H__ -#define __GJS_NATIVE_H__ +#ifndef GJS_NATIVE_H_ +#define GJS_NATIVE_H_ -#include -#include -#include "cjs/jsapi-util.h" +#include -G_BEGIN_DECLS +#include +#include + +#include "cjs/macros.h" typedef bool (* GjsDefineModuleFunc) (JSContext *context, JS::MutableHandleObject module_out); @@ -38,15 +39,12 @@ void gjs_register_native_module (const char *module_id, GjsDefineModuleFunc func); /* called by importer.c to to check for already loaded modules */ -bool gjs_is_registered_native_module(JSContext *context, - JSObject *parent, - const char *name); +[[nodiscard]] bool gjs_is_registered_native_module(const char* name); /* called by importer.cpp to load a statically linked native module */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_load_native_module(JSContext *cx, const char *name, JS::MutableHandleObject module_out); -G_END_DECLS - -#endif /* __GJS_NATIVE_H__ */ +#endif // GJS_NATIVE_H_ diff --git a/cjs/profiler-private.h b/cjs/profiler-private.h index 98bb722..0f03f11 100644 --- a/cjs/profiler-private.h +++ b/cjs/profiler-private.h @@ -21,21 +21,24 @@ * IN THE SOFTWARE. */ -#ifndef GJS_PROFILER_PRIVATE_H -#define GJS_PROFILER_PRIVATE_H +#ifndef GJS_PROFILER_PRIVATE_H_ +#define GJS_PROFILER_PRIVATE_H_ -#include "context.h" -#include "profiler.h" +#include -G_BEGIN_DECLS +#include "cjs/context.h" +#include "cjs/macros.h" +#include "cjs/profiler.h" GjsProfiler *_gjs_profiler_new(GjsContext *context); void _gjs_profiler_free(GjsProfiler *self); -bool _gjs_profiler_is_running(GjsProfiler *self); +void _gjs_profiler_add_mark(GjsProfiler* self, int64_t time, int64_t duration, + const char* group, const char* name, + const char* message); -void _gjs_profiler_setup_signals(GjsProfiler *self, GjsContext *context); +[[nodiscard]] bool _gjs_profiler_is_running(GjsProfiler* self); -G_END_DECLS +void _gjs_profiler_setup_signals(GjsProfiler *self, GjsContext *context); -#endif /* GJS_PROFILER_PRIVATE_H */ +#endif // GJS_PROFILER_PRIVATE_H_ diff --git a/cjs/profiler.cpp b/cjs/profiler.cpp index 1b6817d..aceb8e6 100644 --- a/cjs/profiler.cpp +++ b/cjs/profiler.cpp @@ -21,29 +21,43 @@ * IN THE SOFTWARE. */ -#include +#include // for ENABLE_PROFILER, HAVE_SYS_SYSCALL_H, HAVE_UNISTD_H -#include -#include -#include -#include -#include -#include -#include - -#include +#ifndef HAVE_SIGNAL_H +# include // for siginfo_t, sigevent, sigaction, SIGPROF, ... +#endif -#include "jsapi-wrapper.h" -#include +#include +#include -#include "context.h" -#include "jsapi-util.h" -#include "profiler-private.h" #ifdef ENABLE_PROFILER -# include -# include "util/sp-capture-writer.h" +# include +# include +# include // for size_t +# include +# include // for sscanf +# include // for memcpy, strlen +# include // for CLOCK_MONOTONIC +# include // for timer_t +# include // for __NR_gettid +# include // for itimerspec, timer_delete, ... +# ifdef HAVE_UNISTD_H +# include // for getpid, syscall +# endif +# ifdef G_OS_UNIX +# include +# endif +# include #endif +#include // for EnableContextProfilingStack, ... + +#include "cjs/context.h" +#include "cjs/jsapi-util.h" +#include "cjs/profiler.h" + +#define FLUSH_DELAY_SECONDS 3 + /* * This is mostly non-exciting code wrapping the builtin Profiler in * mozjs. In particular, the profiler consumer is required to "bring your @@ -83,28 +97,26 @@ struct _GjsProfiler { * information while executing. We will look into this during our * SIGPROF handler. */ - js::ProfileEntry stack[1024]; + ProfilingStack stack; /* The context being profiled */ JSContext *cx; /* Buffers and writes our sampled stacks */ - SpCaptureWriter *capture; + SysprofCaptureWriter* capture; + GSource* periodic_flush; #endif /* ENABLE_PROFILER */ /* The filename to write to */ char *filename; + /* An FD to capture to */ + int fd; + #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; @@ -129,11 +141,7 @@ static GjsContext *profiling_context; * Returns: %TRUE if successful; otherwise %FALSE and the profile * should abort. */ -static bool -gjs_profiler_extract_maps(GjsProfiler *self) -{ - using AutoStrv = std::unique_ptr; - +[[nodiscard]] static bool gjs_profiler_extract_maps(GjsProfiler* self) { int64_t now = g_get_monotonic_time() * 1000L; g_assert(((void) "Profiler must be set up before extracting maps", self)); @@ -146,9 +154,9 @@ gjs_profiler_extract_maps(GjsProfiler *self) return false; GjsAutoChar content = content_tmp; - AutoStrv lines(g_strsplit(content, "\n", 0), g_strfreev); + GjsAutoStrv lines = g_strsplit(content, "\n", 0); - for (size_t ix = 0; lines.get()[ix]; ix++) { + for (size_t ix = 0; lines[ix]; ix++) { char file[256]; unsigned long start; unsigned long end; @@ -157,7 +165,7 @@ gjs_profiler_extract_maps(GjsProfiler *self) file[sizeof file - 1] = '\0'; - int r = sscanf(lines.get()[ix], "%lx-%lx %*15s %lx %*x:%*x %lu %255s", + int r = sscanf(lines[ix], "%lx-%lx %*15s %lx %*x:%*x %lu %255s", &start, &end, &offset, &inode, file); if (r != 5) continue; @@ -167,8 +175,8 @@ gjs_profiler_extract_maps(GjsProfiler *self) inode = 0; } - if (!sp_capture_writer_add_map(self->capture, now, -1, self->pid, start, - end, offset, inode, file)) + if (!sysprof_capture_writer_add_map(self->capture, now, -1, self->pid, + start, end, offset, inode, file)) return false; } @@ -220,6 +228,7 @@ _gjs_profiler_new(GjsContext *context) self->cx = static_cast(gjs_context_get_native_context(context)); self->pid = getpid(); #endif + self->fd = -1; profiling_context = context; @@ -248,7 +257,13 @@ _gjs_profiler_free(GjsProfiler *self) g_clear_pointer(&self->filename, g_free); #ifdef ENABLE_PROFILER - g_clear_pointer(&self->capture, sp_capture_writer_unref); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); + + if (self->fd != -1) + close(self->fd); + + self->stack.~ProfilingStack(); #endif g_free(self); } @@ -272,26 +287,8 @@ _gjs_profiler_is_running(GjsProfiler *self) #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) -{ +static void gjs_profiler_sigprof(int signum [[maybe_unused]], siginfo_t* info, + void*) { GjsProfiler *self = gjs_context_get_profiler(profiling_context); g_assert(((void) "SIGPROF handler called with invalid signal info", info)); @@ -309,46 +306,96 @@ gjs_profiler_sigprof(int signum, if (!self || info->si_code != SI_TIMER) return; - size_t depth = gjs_profiler_get_stack_size(self); + uint32_t depth = self->stack.stackSize(); 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)); + SysprofCaptureAddress* addrs = + // cppcheck-suppress allocaCalled + static_cast(alloca(sizeof *addrs * depth)); - for (size_t ix = 0; ix < depth; ix++) { - js::ProfileEntry& entry = self->stack[ix]; + for (uint32_t ix = 0; ix < depth; ix++) { + js::ProfilingStackFrame& entry = self->stack.frames[ix]; const char *label = entry.label(); - size_t flipped = depth - 1 - ix; + const char *dynamic_string = entry.dynamicString(); + uint32_t flipped = depth - 1 - ix; + size_t label_length = strlen(label); + + /* + * 512 is an arbitrarily large size, very likely to be enough to + * hold the final string. + */ + char final_string[512]; + char *position = final_string; + size_t available_length = sizeof (final_string) - 1; + + if (label_length > 0) { + label_length = MIN(label_length, available_length); + + /* Start copying the label to the final string */ + memcpy(position, label, label_length); + available_length -= label_length; + position += label_length; + + /* + * Add a space in between the label and the dynamic string, + * if there is one. + */ + if (dynamic_string && available_length > 0) { + *position++ = ' '; + available_length--; + } + } + + /* Now append the dynamic string at the end of the final string. + * The string is cut in case it doesn't fit the remaining space. + */ + if (dynamic_string) { + size_t dynamic_string_length = strlen(dynamic_string); + + if (dynamic_string_length > 0) { + size_t remaining_length = MIN(available_length, dynamic_string_length); + memcpy(position, dynamic_string, remaining_length); + position += remaining_length; + } + } + + *position = 0; /* - * SPSProfiler will put "js::RunScript" on the stack, but it has + * GeckoProfiler 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); + if (final_string[0] != '\0') + addrs[flipped] = + sysprof_capture_writer_add_jitmap(self->capture, final_string); else - addrs[flipped] = SpCaptureAddress(entry.stackAddress()); + addrs[flipped] = SysprofCaptureAddress(entry.stackAddress()); } - if (!sp_capture_writer_add_sample(self->capture, now, -1, self->pid, addrs, depth)) + if (!sysprof_capture_writer_add_sample(self->capture, now, -1, self->pid, + -1, addrs, depth)) gjs_profiler_stop(self); } +static gboolean profiler_auto_flush_cb(void* user_data) { + auto* self = static_cast(user_data); + + if (!self->running) + return G_SOURCE_REMOVE; + + sysprof_capture_writer_flush(self->capture); + + return G_SOURCE_CONTINUE; +} + #endif /* ENABLE_PROFILER */ /** @@ -382,25 +429,43 @@ gjs_profiler_start(GjsProfiler *self) 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)); + if (self->fd != -1) { + self->capture = sysprof_capture_writer_new_from_fd(self->fd, 0); + self->fd = -1; + } else { + 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); + self->capture = sysprof_capture_writer_new(path, 0); + } if (!self->capture) { g_warning("Failed to open profile capture"); return; } + /* Automatically flush to be resilient against SIGINT, etc */ + if (!self->periodic_flush) { + self->periodic_flush = + g_timeout_source_new_seconds(FLUSH_DELAY_SECONDS); + g_source_set_name(self->periodic_flush, + "[sysprof-capture-writer-flush]"); + g_source_set_priority(self->periodic_flush, G_PRIORITY_LOW + 100); + g_source_set_callback(self->periodic_flush, + (GSourceFunc)profiler_auto_flush_cb, self, + nullptr); + g_source_attach(self->periodic_flush, + g_main_context_get_thread_default()); + } + if (!gjs_profiler_extract_maps(self)) { g_warning("Failed to extract proc maps"); - g_clear_pointer(&self->capture, sp_capture_writer_unref); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); 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; @@ -408,7 +473,8 @@ gjs_profiler_start(GjsProfiler *self) 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); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); return; } @@ -428,7 +494,8 @@ gjs_profiler_start(GjsProfiler *self) 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); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); return; } @@ -442,15 +509,15 @@ gjs_profiler_start(GjsProfiler *self) 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); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); 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)); + js::SetContextProfilingStack(self->cx, &self->stack); /* Start recording stack info */ js::EnableContextProfilingStack(self->cx, true); @@ -460,7 +527,7 @@ gjs_profiler_start(GjsProfiler *self) #else /* !ENABLE_PROFILER */ self->running = true; - g_message("Profiler is disabled. Recompile with --enable-profiler to use."); + g_message("Profiler is disabled. Recompile with it enabled to use."); #endif /* ENABLE_PROFILER */ } @@ -496,13 +563,13 @@ gjs_profiler_stop(GjsProfiler *self) timer_delete(self->timer); js::EnableContextProfilingStack(self->cx, false); - js::SetContextProfilingStack(self->cx, nullptr, nullptr, 0); + js::SetContextProfilingStack(self->cx, nullptr); - sp_capture_writer_flush(self->capture); + sysprof_capture_writer_flush(self->capture); - g_clear_pointer(&self->capture, sp_capture_writer_unref); + g_clear_pointer(&self->capture, sysprof_capture_writer_unref); + g_clear_pointer(&self->periodic_flush, g_source_destroy); - self->stack_depth = 0; g_message("Profiler stopped"); #endif /* ENABLE_PROFILER */ @@ -559,6 +626,7 @@ _gjs_profiler_setup_signals(GjsProfiler *self, #else /* !ENABLE_PROFILER */ g_message("Profiler is disabled. Not setting up signals."); + (void)self; #endif /* ENABLE_PROFILER */ } @@ -594,6 +662,11 @@ gjs_profiler_chain_signal(GjsContext *context, } } +#else // !ENABLE_PROFILER + + (void)context; + (void)info; + #endif /* ENABLE_PROFILER */ return false; @@ -617,3 +690,39 @@ gjs_profiler_set_filename(GjsProfiler *self, g_free(self->filename); self->filename = g_strdup(filename); } + +void _gjs_profiler_add_mark(GjsProfiler* self, gint64 time_nsec, + gint64 duration_nsec, const char* group, + const char* name, const char* message) { + g_return_if_fail(self); + g_return_if_fail(group); + g_return_if_fail(name); + +#ifdef ENABLE_PROFILER + if (self->running && self->capture != nullptr) { + sysprof_capture_writer_add_mark(self->capture, time_nsec, -1, self->pid, + duration_nsec, group, name, message); + } +#else + // Unused in the no-profiler case + (void)time_nsec; + (void)duration_nsec; + (void)message; +#endif +} + +void gjs_profiler_set_fd(GjsProfiler* self, int fd) { + g_return_if_fail(self); + g_return_if_fail(!self->filename); + g_return_if_fail(!self->running); + +#ifdef ENABLE_PROFILER + if (self->fd != fd) { + if (self->fd != -1) + close(self->fd); + self->fd = fd; + } +#else + (void)fd; // Unused in the no-profiler case +#endif +} diff --git a/cjs/profiler.h b/cjs/profiler.h index 498487e..417ccc8 100644 --- a/cjs/profiler.h +++ b/cjs/profiler.h @@ -21,12 +21,17 @@ * IN THE SOFTWARE. */ -#ifndef GJS_PROFILER_H -#define GJS_PROFILER_H +#ifndef GJS_PROFILER_H_ +#define GJS_PROFILER_H_ +#if !defined(INSIDE_GJS_H) && !defined(GJS_COMPILATION) +# error "Only can be included directly." +#endif + +#include #include -#include +#include G_BEGIN_DECLS @@ -40,6 +45,8 @@ GType gjs_profiler_get_type(void); GJS_EXPORT void gjs_profiler_set_filename(GjsProfiler *self, const char *filename); +GJS_EXPORT +void gjs_profiler_set_fd(GjsProfiler* self, int fd); GJS_EXPORT void gjs_profiler_start(GjsProfiler *self); @@ -49,4 +56,4 @@ void gjs_profiler_stop(GjsProfiler *self); G_END_DECLS -#endif /* GJS_PROFILER_H */ +#endif // GJS_PROFILER_H_ diff --git a/cjs/stack.cpp b/cjs/stack.cpp index c2c6c0f..e51f4d5 100644 --- a/cjs/stack.cpp +++ b/cjs/stack.cpp @@ -41,11 +41,24 @@ * ***** END LICENSE BLOCK ***** */ #include + +#include // for stderr + +#include #include -#include -#include "context.h" -#include "jsapi-util.h" -#include "jsapi-wrapper.h" + +#include +#include // for DumpBacktrace + +#include "cjs/context.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} void gjs_context_print_stack_stderr(GjsContext *context) diff --git a/cjs/type-module.cpp b/cjs/type-module.cpp deleted file mode 100644 index 4457d94..0000000 --- a/cjs/type-module.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright (c) 2012 Giovanni Campagna - * - * 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 "type-module.h" - -struct _GjsTypeModule { - GTypeModule parent; -}; - -struct _GjsTypeModuleClass { - GTypeModuleClass parent_class; -}; - -G_DEFINE_TYPE (GjsTypeModule, gjs_type_module, G_TYPE_TYPE_MODULE) - -static GjsTypeModule *global_type_module; - -GjsTypeModule * -gjs_type_module_get () -{ - if (global_type_module == NULL) { - global_type_module = (GjsTypeModule *) g_object_new (GJS_TYPE_TYPE_MODULE, NULL); - } - - return global_type_module; -} - -static gboolean -gjs_type_module_load (GTypeModule *self) -{ - return true; -} - -G_GNUC_NORETURN -static void -gjs_type_module_unload (GTypeModule *self) -{ - g_assert_not_reached (); -} - -static void -gjs_type_module_class_init (GjsTypeModuleClass *klass) -{ - GTypeModuleClass *type_module_class; - - type_module_class = G_TYPE_MODULE_CLASS (klass); - type_module_class->load = gjs_type_module_load; - type_module_class->unload = gjs_type_module_unload; -} - -static void -gjs_type_module_init (GjsTypeModule *self) -{ - /* Prevent the use count from ever dropping to zero */ - g_type_module_use (G_TYPE_MODULE (self)); -} diff --git a/cjs/type-module.h b/cjs/type-module.h deleted file mode 100644 index 9d6fb45..0000000 --- a/cjs/type-module.h +++ /dev/null @@ -1,43 +0,0 @@ -/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright (c) 2012 Giovanni Campagna - * - * 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. - */ - -#ifndef GJS_TYPE_MODULE_H -#define GJS_TYPE_MODULE_H - -#include - -typedef struct _GjsTypeModule GjsTypeModule; -typedef struct _GjsTypeModuleClass GjsTypeModuleClass; - -#define GJS_TYPE_TYPE_MODULE (gjs_type_module_get_type ()) -#define GJS_TYPE_MODULE(module) (G_TYPE_CHECK_INSTANCE_CAST ((module), GJS_TYPE_TYPE_MODULE, GjsTypeModule)) -#define GJS_TYPE_MODULE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GJS_TYPE_TYPE_MODULE, GjsTypeModuleClass)) -#define GJS_IS_TYPE_MODULE(module) (G_TYPE_CHECK_INSTANCE_TYPE ((module), GJS_TYPE_TYPE_MODULE)) -#define GJS_IS_TYPE_MODULE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GJS_TYPE_TYPE_MODULE)) -#define GJS_TYPE_MODULE_GET_CLASS(module) (G_TYPE_INSTANCE_GET_CLASS ((module), GJS_TYPE_TYPE_MODULE, GjsTypeModuleClass)) - -GType gjs_type_module_get_type (void) G_GNUC_CONST; - -GjsTypeModule *gjs_type_module_get (void); - -#endif diff --git a/configure.ac b/configure.ac deleted file mode 100644 index 10cded7..0000000 --- a/configure.ac +++ /dev/null @@ -1,328 +0,0 @@ -# -*- Autoconf -*- -# Process this file with autoconf to produce a configure script. - -m4_define(pkg_major_version, 4) -m4_define(pkg_minor_version, 6) -m4_define(pkg_micro_version, 0) -m4_define(pkg_version, pkg_major_version.pkg_minor_version.pkg_micro_version) -m4_define(pkg_int_version, (pkg_major_version * 100 + pkg_minor_version) * 100 + pkg_micro_version) - -AC_PREREQ([2.64]) -AC_INIT([cjs],[pkg_version],[https://github.com/linuxmint/cjs/issues/new],[cjs],[https://github.com/linuxmint/cjs]) -AM_INIT_AUTOMAKE([1.11.1 subdir-objects dist-xz no-dist-gzip tar-ustar -Wno-portability]) -m4_ifdef([AX_IS_RELEASE], - [AX_IS_RELEASE([git-directory])]) -AC_CONFIG_SRCDIR([cjs/console.cpp]) -AC_CONFIG_MACRO_DIR([m4]) -AC_CONFIG_HEADERS([config.h]) -AC_REQUIRE_AUX_FILE([tap-driver.sh]) - -AC_SUBST(GJS_VERSION, m4_eval(pkg_int_version)) -AC_DEFINE([GJS_VERSION], pkg_int_version, [The gjs version as an integer]) - -AC_SUBST([GETTEXT_PACKAGE], [cjs]) -AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["$GETTEXT_PACKAGE"], [The name of the gettext domain]) - -AM_MAINTAINER_MODE([enable]) -AM_SILENT_RULES([yes]) - -# our first pkg-config invocation is conditional, ensure macros still work -PKG_PROG_PKG_CONFIG -PKG_INSTALLDIR - -AC_LANG([C++]) -AC_USE_SYSTEM_EXTENSIONS -AC_PROG_CXX -AX_CXX_COMPILE_STDCXX_11 - -LT_PREREQ([2.2.0]) -# no stupid static libraries -LT_INIT([disable-static]) - -# Other programs -AC_PROG_MKDIR_P -AC_PROG_LN_S -AC_PROG_SED -AC_PROG_AWK - -AX_COMPILER_FLAGS - -AX_CODE_COVERAGE -dnl Clang does not need to link with -lgcov -AX_CHECK_LINK_FLAG([-lgcov],, [ - CODE_COVERAGE_LIBS= - CODE_COVERAGE_LDFLAGS= -]) - -# Checks for libraries. -m4_define(glib_required_version, 2.42.0) - -GOBJECT_INTROSPECTION_REQUIRE([1.41.4]) - -GOBJECT_REQUIREMENT="gobject-2.0 >= glib_required_version" -gjs_base_packages="$GOBJECT_REQUIREMENT gio-2.0" -common_packages="gthread-2.0 gio-2.0 >= glib_required_version mozjs-52" -gjs_packages="gobject-introspection-1.0 libffi $common_packages" -gjs_cairo_packages="cairo cairo-gobject $common_packages" -gjs_gtk_packages="gtk+-3.0 >= 3.14.0" -# gjs-tests links against everything -gjstests_packages="gio-unix-2.0 $gjs_packages" - -CJS_PACKAGE_REQUIRES="$GOBJECT_REQUIREMENT" -CJS_PACKAGE_REQUIRES_PRIVATE="$gjs_packages" - -PKG_CHECK_MODULES([GJS], [$GOBJECT_REQUIREMENT]) -PKG_CHECK_MODULES([GJS_PRIVATE], [$gjs_packages]) -PKG_CHECK_MODULES([GJS_GDBUS], [$gjs_base_packages]) -PKG_CHECK_MODULES([GJS_CONSOLE], [$gjs_base_packages]) -PKG_CHECK_MODULES([GJSTESTS], [$gjstests_packages]) - -# Optional cairo dep (enabled by default) -AC_ARG_WITH(cairo, - [AS_HELP_STRING([--without-cairo], [Don't build cairo module])]) -AS_IF([test "x$with_cairo" != "xno"], - [PKG_CHECK_MODULES([GJS_CAIRO], [$gjs_cairo_packages], - [have_cairo=yes], [have_cairo=no]) - CJS_PACKAGE_REQUIRES_PRIVATE="$CJS_PACKAGE_REQUIRES_PRIVATE $gjs_cairo_packages"], - [have_cairo=no]) -AM_CONDITIONAL(ENABLE_CAIRO, test x$have_cairo = xyes) -AS_IF([test x$have_cairo = xyes], [ - AC_DEFINE([ENABLE_CAIRO],[1],[Define if you want to build with cairo support]) - PKG_CHECK_MODULES([GJS_CAIRO_XLIB], [cairo-xlib], [], - [AC_MSG_WARN([Cairo-xlib support not found]) - CJS_PACKAGE_REQUIRES_PRIVATE="$CJS_PACKAGE_REQUIRES_PRIVATE cairo-xlib"]) -], [AS_IF([test "x$with_cairo" = "xyes"], - [AC_MSG_ERROR([Cairo requested but not found])])]) - -# Optional GTK+ dep (enabled by default) -AC_ARG_WITH(gtk, - [AS_HELP_STRING([--without-gtk], [Don't build GTK-related code])]) -AS_IF([test "x$with_gtk" != "xno"], - [PKG_CHECK_MODULES([GJS_GTK], [$gjs_gtk_packages], - [have_gtk=yes], [have_gtk=no]) - CJS_PACKAGE_REQUIRES_PRIVATE="$CJS_PACKAGE_REQUIRES_PRIVATE $gjs_gtk_packages"], - [have_gtk=no]) -AM_CONDITIONAL(ENABLE_GTK, test x$have_gtk = xyes) -AS_IF([test x$have_gtk = xyes], [ - AC_DEFINE([ENABLE_GTK],[1],[Define if you want to build with GTK+ support]) -], [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]) -AC_SUBST([CJS_PACKAGE_REQUIRES_PRIVATE]) - -# readline -LIBS_no_readline=$LIBS - -# On some systems we need to link readline to a termcap compatible -# library. -gjs_cv_lib_readline=no -AC_MSG_CHECKING([how to link readline libs]) -for gjs_libtermcap in "" ncursesw ncurses curses termcap; do - if test -z "$gjs_libtermcap"; then - READLINE_LIBS="-lreadline" - else - READLINE_LIBS="-lreadline -l$gjs_libtermcap" - fi - LIBS="$READLINE_LIBS $LIBS_no_readline" - AC_LINK_IFELSE( - [AC_LANG_CALL([],[readline])], - [gjs_cv_lib_readline=yes]) - if test $gjs_cv_lib_readline = yes; then - break - fi -done -if test $gjs_cv_lib_readline = no; then - AC_MSG_RESULT([none]) - READLINE_LIBS="" - ac_cv_header_readline_readline_h=no -else - AC_MSG_RESULT([$READLINE_LIBS]) - AC_CHECK_HEADERS([readline/readline.h]) -fi - -AC_SUBST([READLINE_LIBS]) - -# End of readline checks: restore LIBS -LIBS=$LIBS_no_readline - -AC_MSG_CHECKING([whether printf() accepts '%Id' for alternative integer output]) -CXXFLAGS_save="$CXXFLAGS" -CXXFLAGS="-Werror -Wformat -pedantic-errors" -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include ]], - [[printf("%Id", (int)0);]])], [ - AC_MSG_RESULT([yes]) - AC_DEFINE([HAVE_PRINTF_ALTERNATIVE_INT], [1], - [Define to 1 if printf() accepts '%Id' for alternative integer output]) - ], - [AC_MSG_RESULT([no])]) -CXXFLAGS="$CXXFLAGS_save" - -AC_ARG_ENABLE(installed_tests, - [AS_HELP_STRING([--enable-installed-tests], - [Install test programs @<:@default: no@:>@])]) -AM_CONDITIONAL(BUILDOPT_INSTALL_TESTS, test x$enable_installed_tests = xyes) - -dnl -dnl Tracing -dnl - -AC_ARG_ENABLE([dtrace], - [AS_HELP_STRING([--enable-dtrace], - [Include dtrace trace support @<:@default: no@:>@])]) - -AC_ARG_ENABLE([systemtap], - [AS_HELP_STRING([--enable-systemtap], - [Include systemtap trace support (implies --enable-dtrace) @<:@default: no@:>@])]) -AS_IF([test "x$enable_systemtap" = "xyes"], [enable_dtrace=yes]) -AM_CONDITIONAL([ENABLE_SYSTEMTAP], [test "x$enable_systemtap" = "xyes"]) - -AS_IF([test "x$enable_dtrace" = "xyes"], [ - AC_PATH_PROG([DTRACE], [dtrace]) - AS_IF([test -z "$DTRACE"], - [AC_MSG_ERROR([dtrace is required for --enable-dtrace or --enable-systemtap])]) - AC_CHECK_HEADER([sys/sdt.h],, - [AC_MSG_ERROR([sys/sdt.h header is required for --enable-dtrace or --enable-systemtap])]) - AC_DEFINE([HAVE_DTRACE], [1], [Define to 1 if using dtrace probes.]) -]) -AM_CONDITIONAL([ENABLE_DTRACE], [test "x$enable_dtrace" = "xyes"]) - -dnl -dnl Check for -Bsymbolic-functions linker flag used to avoid -dnl intra-library PLT jumps, if available. -dnl -AC_ARG_ENABLE([Bsymbolic], - [AS_HELP_STRING([--disable-Bsymbolic], [avoid linking with -Bsymbolic])]) -AS_IF([test "x$enable_Bsymbolic" != "xno"], - [AX_APPEND_LINK_FLAGS([-Bsymbolic-functions])]) - -dnl If SpiderMonkey was compiled with --enable-debug, then we need to define -dnl -DDEBUG before including js-config.h. -AC_MSG_CHECKING([whether SpiderMonkey was configured with --enable-debug]) -CPPFLAGS_save="$CPPFLAGS" -CPPFLAGS="$GJS_CFLAGS" -AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ -#include -#ifdef JS_DEBUG -#error debug yes, if we didn't already error out due to DEBUG not being defined -#endif -]])], [AC_MSG_RESULT([no])], [ - AC_DEFINE([HAVE_DEBUG_SPIDERMONKEY], [1], - [Define to 1 if SpiderMonkey was compiled with --enable-debug]) - AC_MSG_RESULT([yes]) -]) -CPPFLAGS="$CPPFLAGS_save" - -dnl -dnl Check for -fsanitize=address and -fsanitize=undefined support -dnl -AC_ARG_ENABLE([asan], - [AS_HELP_STRING([--enable-asan], [Build with address sanitizer support @<:@default: no@:>@])]) - -AS_IF([test "x$enable_asan" = "xyes"], [ - AX_CHECK_COMPILE_FLAG([-fsanitize=address -fno-omit-frame-pointer -g], [ - AX_CHECK_LINK_FLAG([-fsanitize=address], [SAN_FLAGS="-fsanitize=address"]) - ]) -]) - -AC_ARG_ENABLE([ubsan], - [AS_HELP_STRING([--enable-ubsan], [Build with undefined behavior sanitizer support @<:@default: no@:>@])]) - -AS_IF([test "x$enable_ubsan" = "xyes"], [ - AX_CHECK_COMPILE_FLAG([-fsanitize=undefined -fno-sanitize=vptr -fno-omit-frame-pointer -g], [ - AX_CHECK_LINK_FLAG([-fsanitize=undefined -fno-sanitize=vptr], - [SAN_FLAGS="$SAN_FLAGS -fsanitize=undefined -fno-sanitize=vptr"]) - ]) -]) - -# According to https://github.com/google/sanitizers/issues/380, asan is not -# compatible with -no-undefined. -NO_UNDEFINED_FLAG=-no-undefined -AS_IF([test -n "${SAN_FLAGS}"], [ - GJS_CFLAGS="$GJS_CFLAGS $SAN_FLAGS -fno-omit-frame-pointer -g" - # We have to clobber LDFLAGS here and not use AM_LDFLAGS, or else - # g-ir-scanner will not pick it up. - LDFLAGS="$LDFLAGS $SAN_FLAGS" - NO_UNDEFINED_FLAG= -]) -AC_SUBST([NO_UNDEFINED_FLAG]) - -AC_ARG_WITH([xvfb-tests], - [AS_HELP_STRING([--with-xvfb-tests], - [Run all tests under an XVFB server @<:@default=no@:>@])]) -AS_IF([test "x$with_xvfb_tests" = "xyes"], [ - AC_PATH_PROG([XVFB], [Xvfb]) - AS_IF([test -z "$XVFB"], - [AC_MSG_ERROR([Xvfb is required for --with-xvfb-tests])]) -]) -AM_CONDITIONAL([XVFB_TESTS], [test "x$with_xvfb_tests" = "xyes"]) - -AC_ARG_WITH([dbus-tests], - [AS_HELP_STRING([--without-dbus-tests], - [Don't try to use DBus during make check @<:@default=use@:>@])]) -AS_IF([test "x$with_dbus_tests" != "xno"], [ - AC_PATH_PROG([DBUS_RUN_SESSION], [dbus-run-session]) - AS_IF([test -z "$DBUS_RUN_SESSION"], - [AC_MSG_ERROR([dbus-run-session is required for --with-dbus-tests])]) -]) -AM_CONDITIONAL([DBUS_TESTS], [test "x$with_dbus_tests" != "xno"]) - -AC_SUBST([gjsjsdir], [\${datadir}/cjs-1.0]) - -dnl automake 1.11/1.12 defines this but does not substitute it -AC_SUBST([pkglibexecdir], ["${libexecdir}/${PACKAGE}"]) - -AC_CONFIG_FILES([Makefile cjs-1.0.pc win32/config.h.win32]) -dnl Symlink the files from gobject-introspection's test libraries into the tree; -dnl Automake plans to drop support for compiling them in-place. -AC_CONFIG_LINKS([ - installed-tests/gitestmacros.h:$GI_DATADIR/tests/gitestmacros.h - installed-tests/regress.c:$GI_DATADIR/tests/regress.c - installed-tests/regress.h:$GI_DATADIR/tests/regress.h - installed-tests/warnlib.c:$GI_DATADIR/tests/warnlib.c - installed-tests/warnlib.h:$GI_DATADIR/tests/warnlib.h - installed-tests/gimarshallingtests.c:$GI_DATADIR/tests/gimarshallingtests.c - installed-tests/gimarshallingtests.h:$GI_DATADIR/tests/gimarshallingtests.h -]) -AC_OUTPUT - -# Warn about conditions that affect runtime -PKG_CHECK_EXISTS([gobject-introspection-1.0 >= 1.46.0], [], [ - AC_MSG_WARN([You do not have a new enough version of -gobject-introspection to run the tests. You can still build CJS, but some -tests will fail.])]) - -TEST_MSG= -AM_COND_IF([XVFB_TESTS], [TEST_MSG="xvfb "]) -AM_COND_IF([DBUS_TESTS], [TEST_MSG="${TEST_MSG}dbus"]) -AS_IF([test -z "$TEST_MSG"], [TEST_MSG="nothing special"]) - -AC_MSG_RESULT([ - $PACKAGE_NAME $VERSION - - GJS_CFLAGS: ${GJS_CFLAGS} - GJS_LIBS: ${GJS_LIBS} - - cairo: ${have_cairo} - GTK+: ${have_gtk} - 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/debian/changelog b/debian/changelog index 6f1dbe0..5ffa497 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +cjs (4.7.0) UNRELEASED; urgency=medium + + * Bump for moz78 + + -- Clement Lefebvre Wed, 7 Oct 2020 13:45:37 +0100 + cjs (4.6.0) ulyana; urgency=medium [ Garry Filakhtov ] @@ -837,4 +843,3 @@ cjs (1.9.1) petra; urgency=low * Based on 1.34.0-0ubuntu1 quantal -- Clement Lefebvre Mon, 30 Sep 2013 13:31:38 +0100 - diff --git a/debian/cjs.docs b/debian/cjs.docs index 50bd824..8913f46 100644 --- a/debian/cjs.docs +++ b/debian/cjs.docs @@ -1,2 +1,2 @@ NEWS -README +README.md diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..4d16147 --- /dev/null +++ b/debian/clean @@ -0,0 +1 @@ +debian/temp-home/ diff --git a/debian/compat b/debian/compat index f599e28..3cacc0b 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -10 +12 \ No newline at end of file diff --git a/debian/control b/debian/control index bcbafcd..4379030 100644 --- a/debian/control +++ b/debian/control @@ -1,34 +1,39 @@ # This file is autogenerated. DO NOT EDIT! -# +# # Modifications should be made to debian/control.in instead. # This file is regenerated automatically in the clean target. Source: cjs Section: interpreters Priority: optional Maintainer: Linux Mint -Build-Depends: debhelper (>= 10), - dpkg-dev (>= 1.17.14), - gnome-common, - gnome-pkg-tools, - pkg-config, - libglib2.0-dev (>= 2.42.0), - libgirepository1.0-dev (>= 1.46.0), - gobject-introspection (>= 1.46.0), - libmozjs-52-dev, +Build-Depends: debhelper (>= 12.0), + dh-sequence-gir, + dh-sequence-gnome, + pkg-config (>= 0.28), + libcairo2-dev, + libffi-dev (>= 3.2.1), + libglib2.0-dev (>= 2.58.0), + libgirepository1.0-dev (>= 1.58.3), + gir1.2-gtk-3.0, + gobject-introspection (>= 1.58.3), + libmozjs-78-dev, libreadline-dev, - libgtk-3-dev, libcairo2-dev, - dbus, - dbus-x11, - at-spi2-core, - xvfb, - libxml2-utils -Standards-Version: 3.9.8 + meson (>= 0.49.2), + dbus , + dbus-x11 , + at-spi2-core , + xauth , + xvfb +Rules-Requires-Root: no +Standards-Version: 4.5.0 +Vcs-Git: https://github.com/linuxmint/cjs.git Package: cjs Architecture: any Depends: ${shlibs:Depends}, - ${misc:Depends} + ${misc:Depends}, + gir1.2-gtk-3.0 Description: Mozilla-based javascript bindings for the Cinnamon platform Makes it possible for applications to use all of Cinnamon's platform libraries using the JavaScript language. It's mainly based on the @@ -38,6 +43,7 @@ Description: Mozilla-based javascript bindings for the Cinnamon platform Package: libcjs0f Architecture: any +Multi-Arch: same Section: libs Depends: ${shlibs:Depends}, ${misc:Depends}, @@ -58,9 +64,9 @@ Section: libdevel Depends: ${misc:Depends}, cjs, libcjs0f (= ${binary:Version}), - libgirepository1.0-dev (>= 1.41.4), - libgtk-3-dev, - libmozjs-52-dev + libgirepository1.0-dev (>= 1.58.3), + libcairo2-dev, + libmozjs-78-dev Description: Mozilla-based javascript bindings for the Cinnamon platform Makes it possible for applications to use all of Cinnamon's platform libraries using the JavaScript language. It's mainly based on the diff --git a/debian/control.in b/debian/control.in index 9990cc7..f585c93 100644 --- a/debian/control.in +++ b/debian/control.in @@ -2,28 +2,34 @@ Source: cjs Section: interpreters Priority: optional Maintainer: Linux Mint -Build-Depends: debhelper (>= 10), - dpkg-dev (>= 1.17.14), - gnome-common, - gnome-pkg-tools, - pkg-config, - libglib2.0-dev (>= 2.42.0), - libgirepository1.0-dev (>= 1.46.0), - gobject-introspection (>= 1.46.0), - libmozjs-52-dev, +Build-Depends: debhelper (>= 12.0), + dh-sequence-gir, + dh-sequence-gnome, + pkg-config (>= 0.28), + libcairo2-dev, + libffi-dev (>= 3.2.1), + libglib2.0-dev (>= 2.58.0), + libgirepository1.0-dev (>= 1.58.3), + gir1.2-gtk-3.0, + gobject-introspection (>= 1.58.3), + libmozjs-78-dev, libreadline-dev, - libgtk-3-dev, libcairo2-dev, - dbus, - dbus-x11, - at-spi2-core, - xvfb -Standards-Version: 3.9.8 + meson (>= 0.49.2), + dbus , + dbus-x11 , + at-spi2-core , + xauth , + xvfb +Rules-Requires-Root: no +Standards-Version: 4.5.0 +Vcs-Git: https://github.com/linuxmint/cjs.git Package: cjs Architecture: any Depends: ${shlibs:Depends}, - ${misc:Depends} + ${misc:Depends}, + gir1.2-gtk-3.0 Description: Mozilla-based javascript bindings for the Cinnamon platform Makes it possible for applications to use all of Cinnamon's platform libraries using the JavaScript language. It's mainly based on the @@ -33,6 +39,7 @@ Description: Mozilla-based javascript bindings for the Cinnamon platform Package: libcjs0f Architecture: any +Multi-Arch: same Section: libs Depends: ${shlibs:Depends}, ${misc:Depends}, @@ -53,9 +60,9 @@ Section: libdevel Depends: ${misc:Depends}, cjs, libcjs0f (= ${binary:Version}), - libgirepository1.0-dev (>= 1.41.4), - libgtk-3-dev, - libmozjs-52-dev + libgirepository1.0-dev (>= 1.58.3), + libcairo2-dev, + libmozjs-78-dev Description: Mozilla-based javascript bindings for the Cinnamon platform Makes it possible for applications to use all of Cinnamon's platform libraries using the JavaScript language. It's mainly based on the diff --git a/debian/copyright b/debian/copyright index 7aa9e4a..92b108f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,46 +1,33 @@ -This work was packaged for Debian by: - +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://download.gnome.org/sources/gjs/ +Comment: + This work was packaged for Debian by: + . Gustavo Noronha Silva on Mon, 12 Oct 2009 18:38:36 -0300 - -It was downloaded from https://download.gnome.org/sources/gjs/ - -Upstream Author: - + . + Upstream Author: + . litl, LLC +Files: * Copyright: + 2008 litl, LLC + 2009 Red Hat, Inc. + 2006-2007 Zeh Fernando and Nate Chatellier + 2018 Philip Chimento +License: Expat - Copyright © 2008 litl, LLC - -For the following files: - - gjs/jsapi-util.c - modules/gettext-native.c - modules/gettext.js - modules/jsUnit.js - modules/gettext-native.h - - Copyright © 2009 Red Hat, Inc. - -For the following files: - - modules/tweener/tweener.js - modules/tweener/tweenList.js - - Copyright © 2006-2007 Zeh Fernando and Nate Chatellier - -License: - +License: Expat 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 @@ -49,40 +36,35 @@ License: FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -The Debian packaging is: - - Copyright © 2009 Collabora Ltd. - -and is licensed under the same license as the upstream source. - -For the following files, the information bellow applies: +Files: debian/* +Copyright: + 2009 Collabora Ltd. +License: Expat +Files: modules/console.c gjs/stack.c - -License: - Version: MPL 1.1/GPL 2.0/LGPL 2.1 - +License: MPL-1.1 or GPL-2.0+ or LGPL-2.1+ The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.mozilla.org/MPL/ - + https://www.mozilla.org/MPL/ + . Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. - + . The Original Code is Mozilla Communicator client code, released March 31, 1998. - + . The Initial Developer of the Original Code is Netscape Communications Corporation. Portions created by the Initial Developer are Copyright (C) 1998 the Initial Developer. All Rights Reserved. - + . Contributor(s): - + . Alternatively, the contents of this file may be used under the terms of either of the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), @@ -94,20 +76,36 @@ License: and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. - +Copyright: Copyright © 2008 litl, LLC © 1998 Netscape Communications Corporation +Comment: + The GPLv2, LGPLv2.1 and MPLv1.1 files can be obtained, on Debian systems, + at /usr/share/common-licenses/GPL-2, /usr/share-common-licenses/LGPL-2.1 + and /usr/share/common-licenses/MPL-1.1, respectively. -The GPLv2 and LGPLv2.1 files can be obtained, on Debian systems, at -/usr/share/common-licenses/GPL-2, and -/usr/share-common-licenses/LGPL-2.1, respectively. - -For the following files the information bellow applies: +Files: + modules/_bootstrap/debugger.js +Copyright: + Unspecified, assumed to be: + 1998 Netscape Communications Corporation + 1998-2018 Mozilla Project + 2018 Philip Chimento +License: MPL-2.0 + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +Comment: + Based on js/examples/jorendb.js from mozjs60. + . + On Debian systems a copy of the MPLv2.0 can be found in + /usr/share/common-licenses/MPL-2.0 +Files: modules/tweener/equations.js - - Copyright © 2001 Robert Penner - License: BSD +Copyright: + 2001 Robert Penner +License: BSD-3-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . diff --git a/debian/docs b/debian/docs index 50bd824..8913f46 100644 --- a/debian/docs +++ b/debian/docs @@ -1,2 +1,2 @@ NEWS -README +README.md diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..b24011a --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,17 @@ +[DEFAULT] +pristine-tar = True +debian-branch = debian/master +upstream-branch = upstream/latest +upstream-vcs-tag = %(version)s + +[buildpackage] +sign-tags = True + +[dch] +multimaint-merge = True + +[import-orig] +postimport = dch -v%(version)s New upstream release; git add debian/changelog; debcommit + +[pq] +patch-numbers = False diff --git a/debian/libcjs-dev.install b/debian/libcjs-dev.install index 3b2eee9..a122e0b 100644 --- a/debian/libcjs-dev.install +++ b/debian/libcjs-dev.install @@ -1,3 +1,4 @@ usr/include -usr/lib/libcjs*.so -usr/lib/pkgconfig/ +usr/lib/*/libcjs*.so +usr/lib/*/pkgconfig/ +usr/share/cjs-1.0/ diff --git a/debian/libcjs0f.install b/debian/libcjs0f.install index 9fee0d4..9f7589e 100644 --- a/debian/libcjs0f.install +++ b/debian/libcjs0f.install @@ -1,2 +1,2 @@ -usr/lib/libcjs*.so.* -usr/lib/cjs/girepository-1.0/*.typelib +usr/lib/*/cjs/girepository-1.0/*.typelib +usr/lib/*/libcjs*.so.* diff --git a/debian/libcjs0f.symbols b/debian/libcjs0f.symbols new file mode 100644 index 0000000..eea676d --- /dev/null +++ b/debian/libcjs0f.symbols @@ -0,0 +1,45 @@ +libcjs.so.0 libcjs0f #MINVER# +* Build-Depends-Package: libcjs-dev + gjs_bindtextdomain@Base 1.63.90 + gjs_context_define_string_array@Base 1.63.90 + gjs_context_eval@Base 1.63.90 + gjs_context_eval_file@Base 1.63.90 + gjs_context_gc@Base 1.63.90 + gjs_context_get_all@Base 1.63.90 + gjs_context_get_current@Base 1.63.90 + gjs_context_get_native_context@Base 1.63.90 + gjs_context_get_profiler@Base 1.63.90 + gjs_context_get_type@Base 1.63.90 + gjs_context_make_current@Base 1.63.90 + gjs_context_maybe_gc@Base 1.63.90 + gjs_context_new@Base 1.63.90 + gjs_context_new_with_search_path@Base 1.63.90 + gjs_context_print_stack_stderr@Base 1.63.90 + gjs_context_setup_debugger_console@Base 1.63.90 + gjs_coverage_enable@Base 1.65.90 + gjs_coverage_new@Base 1.63.90 + gjs_coverage_write_statistics@Base 1.63.90 + gjs_dbus_implementation_emit_property_changed@Base 1.63.90 + gjs_dbus_implementation_emit_signal@Base 1.63.90 + gjs_dbus_implementation_get_type@Base 1.63.90 + gjs_dumpstack@Base 1.63.90 + gjs_error_quark@Base 1.63.90 + gjs_format_int_alternative_output@Base 1.63.90 + gjs_get_js_version@Base 1.63.90 + gjs_gtk_container_child_set_property@Base 1.63.90 + gjs_js_error_get_type@Base 1.63.90 + gjs_js_error_quark@Base 1.63.90 + gjs_locale_category_get_type@Base 1.63.90 + gjs_memory_report@Base 1.63.90 + gjs_open_bytes@Base 1.63.90 + gjs_param_spec_get_flags@Base 1.63.90 + gjs_param_spec_get_owner_type@Base 1.63.90 + gjs_param_spec_get_value_type@Base 1.63.90 + gjs_profiler_chain_signal@Base 1.63.90 + gjs_profiler_get_type@Base 1.63.90 + gjs_profiler_set_fd@Base 1.63.90 + gjs_profiler_set_filename@Base 1.63.90 + gjs_profiler_start@Base 1.63.90 + gjs_profiler_stop@Base 1.63.90 + gjs_setlocale@Base 1.63.90 + gjs_textdomain@Base 1.63.90 diff --git a/debian/rules b/debian/rules index 63da18b..f14171e 100755 --- a/debian/rules +++ b/debian/rules @@ -1,39 +1,34 @@ #!/usr/bin/make -f -include /usr/share/gnome-pkg-tools/1/rules/gnome-get-source.mk +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export DEB_LDFLAGS_MAINT_APPEND = -Wl,-z,defs -Wl,--as-needed +export DEB_BUILD_OPTIONS = nocheck -# Use recursive variables since this variable must not be expanded until -# files have been installed. -#LIBMOZJS = $(shell objdump -p debian/tmp/usr/lib/libgjs.so | awk '$$1=="NEEDED" && $$2~/^libmozjs/ { print $$2 }' | sed s/\\.so\\./-/ ) -# Hard code the mozjs version here since the above line didn't seem to work -LIBMOZJS = libmozjs-52-0 +include /usr/share/dpkg/default.mk + +export DH_VERBOSE=1 %: - dh $@ --with gnome,gir + dh $@ override_dh_auto_configure: dh_auto_configure -- \ - --libdir=\$${prefix}/lib \ - --libexecdir=\$${prefix}/lib/cjs \ - --disable-Werror \ - --with-xvfb-tests \ - --with-dbus-tests + -Dauto_features=enabled \ + -Dprofiler=disabled \ + -Dinstalled_tests=false \ + $(NULL) override_dh_girepository: - dh_girepository -l $(CURDIR) /usr/lib/cjs/girepository-1.0 - -override_dh_install: - dh_install --fail-missing -X.la + dh_girepository -l $(BUILDDIR) /usr/lib/$(DEB_HOST_MULTIARCH)/cjs/girepository-1.0 override_dh_auto_test: - dh_auto_test || true - -override_dh_strip: - dh_strip --dbg-package=libcjs-dbg +ifeq (, $(filter nocheck, $(DEB_BUILD_OPTIONS))) + obj-$(DEB_HOST_GNU_TYPE)/cjs-console -c 'print("Smoke-test OK")' + env \ + TZ=UTC \ + xvfb-run -a \ + dh_auto_test -- -v -C $(CURDIR)/obj-$(DEB_HOST_GNU_TYPE) +endif override_dh_makeshlibs: - dh_makeshlibs -Xusr/lib/cjs-1.0/ -V'libcjs0f (>= $(DEB_UPSTREAM_VERSION)), libcjs0-$(LIBMOZJS)' -- -c4 - -override_dh_gencontrol: - echo cjs:Provides=libcjs0-$(LIBMOZJS) >> debian/libcjs0f.substvars - dh_gencontrol + dh_makeshlibs -Xusr/lib/$(DEB_HOST_MULTIARCH)/cjs-1.0 -- -c4 diff --git a/debian/tests/build b/debian/tests/build old mode 100644 new mode 100755 index ea2532f..85f84fe --- a/debian/tests/build +++ b/debian/tests/build @@ -6,11 +6,18 @@ set -e -WORKDIR=$(mktemp -d) -trap "rm -rf $WORKDIR" 0 INT QUIT ABRT PIPE TERM -cd $WORKDIR +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' 0 INT QUIT ABRT PIPE TERM +cd "$WORKDIR" + +if [ -n "${DEB_HOST_GNU_TYPE:-}" ]; then + CROSS_COMPILE="$DEB_HOST_GNU_TYPE-" +else + CROSS_COMPILE= +fi + cat < gjstest.c -#include +#include int main() { @@ -20,7 +27,9 @@ int main() } EOF -gcc -o gjstest gjstest.c `pkg-config --cflags --libs cjs-1.0` +# Deliberately word-splitting, that's how pkg-config works: +# shellcheck disable=SC2046 +"${CROSS_COMPILE}gcc" -o gjstest gjstest.c $("${CROSS_COMPILE}pkg-config" --cflags --libs gjs-1.0) echo "build: OK" [ -x gjstest ] ./gjstest diff --git a/debian/tests/control b/debian/tests/control index 5838e01..d3d2840 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,4 +1,5 @@ Tests: build +Restrictions: superficial Depends: libgjs-dev, build-essential Tests: installed-tests diff --git a/debian/tests/installed-tests b/debian/tests/installed-tests old mode 100644 new mode 100755 index 740ffaa..22f9906 --- a/debian/tests/installed-tests +++ b/debian/tests/installed-tests @@ -5,7 +5,7 @@ set -e -export XDG_RUNTIME_DIR=$ADTTMP +export XDG_RUNTIME_DIR=$AUTOPKGTEST_TMP export LC_ALL=C # dbus outputs activation messages to stderr which fails the test diff --git a/debian/watch b/debian/watch deleted file mode 100644 index 3f36e6d..0000000 --- a/debian/watch +++ /dev/null @@ -1,3 +0,0 @@ -version=4 -https://download.gnome.org/sources/@PACKAGE@/([0-9.]+)/ \ - @PACKAGE@@ANY_VERSION@\.tar\.xz diff --git a/doc/ByteArray.md b/doc/ByteArray.md index 3c5dbf9..53b65fe 100644 --- a/doc/ByteArray.md +++ b/doc/ByteArray.md @@ -1,16 +1,10 @@ -The ByteArray type in the `imports.byteArray` module is based on an -ECMAScript 4 proposal that was never adopted. This can be found at: -http://wiki.ecmascript.org/doku.php?id=proposals:bytearray -and the wikitext of that page is appended to this file. +The `imports.byteArray` module was originally based on an +ECMAScript 4 proposal that was never adopted. +Now that ES6 has typed arrays, we use `Uint8Array` to represent byte +arrays in GJS and add some extra functions for conversion to and from +strings and `GLib.Bytes`. -The main difference from the ECMA proposal is that gjs's ByteArray is -inside a module, and `toString()`/`fromString()` default to UTF-8 and take -optional encoding arguments. - -There are a number of more elaborate byte array proposals in the -Common JS project at http://wiki.commonjs.org/wiki/Binary - -We went with the ECMA proposal because it seems simplest, and the main +Unlike the old custom `ByteArray`, `Uint8Array` is not resizable. The main goal for most gjs users will be to shovel bytes between various C APIs, for example reading from an IO stream and then pushing the bytes into a parser. Actually manipulating bytes in JS is likely to be @@ -20,138 +14,30 @@ messing with bytes, can be done in C. --- -ECMAScript proposal follows; remember it's almost but not quite like -gjs ByteArray, in particular we use UTF-8 instead of busted Latin-1 as -default encoding. - ---- - -# ByteArray # - -(Also see the [discussion page][1] for this proposal) - -## Overview ## - -In previous versions of ECMAScript, there wasn't a good way to efficiently represent a packed array of arbitrary bytes. The predefined core object Array is inefficient for this purpose; some developers have (mis)used character strings for this purpose, which may be slightly more efficient for some implementations, but still a misuse of the string type and either a less efficient use of memory (if one byte per character was stored) or cycles (if two bytes per char). - -Edition 4 will add a new predefined core object, ByteArray. A ByteArray can be thought of as similar to an Array of uint ([uint]) with each element truncated to the integer range of 0..255. - -## Creating a ByteArray ## - -To create a ByteArray object: - -```js -byteArrayObject = new ByteArray(byteArrayLength:uint) -``` - -byteArrayLength is the initial length of the ByteArray, in bytes. If omitted, the initial length is zero. - -All elements in a ByteArray are initialized to the value of zero. - -Unlike Array, there is no special form that allows you to list the initial values for the ByteArray's elements. However, the ByteArray class has an `intrinsic::to` static method that can convert an Array to a ByteArray, and implementations are free to optimize away the Array instance if it is used exclusively to initialize a ByteArray: - -```js -var values:ByteArray = [1, 2, 3, 4]; // legal by virtue of ByteArray.intrinsic::to -``` - -## Populating a ByteArray ## - -You can populate a ByteArray by assigning values to its elements. Each element can hold only an unsigned integer in the range 0..255 (inclusive). Values will be converted to unsigned integer (if necessary), then truncated to the least-significant 8 bits. - -For example, - -```js -var e = new ByteArray(3); - -e[0] = 0x12; // stores 18 -e[1] = Math.PI; // stores 3 -e[2] = "foo"; // stores 0 (generates compile error in strict mode) -e[2] = "42"; // stores 42 (generates compile error in strict mode) -e[2] = null; // stores 0 -e[2] = undefined; // stores 0 -``` - -## Referring to ByteArray Elements ## - -You refer to a ByteArray's elements by using the element's ordinal number; you refer to the first element of the array as `myArray[0]` and the second element of the array as `myArray[1]`, etc. The index of the elements begins with zero (0), but the length of array (for example, `myArray.length`) reflects the number of elements in the array. - -## ByteArray Methods ## - -The ByteArray object has the follow methods: - -### `static function fromString(s:String):ByteArray` ### - -Convert a String into newly constructed ByteArray; this creates a new ByteArray of the same length as the String, then assigns each ByteArray entry the corresponding charCodeAt() value of the String. As with other ByteArray assignments, only the lower 8 bits of the charCode value will be retained. - -```js -class ByteArray { - ... - static function fromString(s:String):ByteArray - { - var a:ByteArray = new Bytearray(s.length); - for (var i:int = 0; i < s.length; ++i) - a[i] = s.charCodeAt(i); - return a; - } - ... -} -``` - -### `static function fromArray(s:Array):ByteArray` ### - -Converts an Array into a newly constructed ByteArray; this creates a new ByteArray of the same length as the input Array, then assigns each ByteArray entry the corresponding entry value of the Array. As with other ByteArray assignments, only the lower 8 bits of the charCode value will be retained. - -```js -class ByteArray { - ... - static function fromArray(s:Array):ByteArray - { - var a:ByteArray = new Bytearray(s.length); - for (var i:int = 0; i < s.length; ++i) - a[i] = s[i]; - return a; - ... -} -``` - -### `function toString():String` ### - -Converts the ByteArray into a literal string, with each character entry set to the value of the corresponding ByteArray entry. - -The resulting string is guaranteed to round-trip back into an identical ByteArray by passing the result to `ByteArray.fromString()`, i.e., `b === ByteArray.fromString(b.toString())`. (Note that the reverse is not guaranteed to be true: `ByteArray.fromString(s).toString != s` unless all character codes in `s` are <= 255) - -```js -class ByteArray { - ... - function toString():String - { - // XXX return String.fromCharCode.apply(String, this); - var s:String = ""; - for (var i:int = 0; i < this.length; ++i) - s += String.fromCharCode(this[i]); - return s; - } - ... -} -``` +## ByteArray Functions ## -## ByteArray Properties ## +The ByteArray module has the following functions: -The ByteArray object has the following properties: +### `fromString(s:String, encoding:String):Uint8Array` ### -### `length:uint` ### +Convert a String into a newly constructed `Uint8Array`; this creates a +new `Uint8Array` of the same length as the String, then assigns each +`Uint8Array` entry the corresponding byte value of the String encoded +according to the given encoding (or UTF-8 if not given). -This property contains the number of bytes in the ByteArray. Setting the length property to a smaller value decreases the size of the ByteArray, discarding the unused bytes. Setting the length property to a larger value increases the size of the ByteArray, initializing all newly-allocated elements to zero. +### `toString(a:Uint8Array, encoding:String):String` ### -### `prototype:Object` ### +Converts the `Uint8Array` into a literal string. The bytes are +interpreted according to the given encoding (or UTF-8 if not given). -This property contains the methods of ByteArray. +The resulting string is guaranteed to round-trip back into an identical ByteArray by passing the result to `ByteArray.fromString()`, i.e., `b === ByteArray.fromString(ByteArray.toString(b, encoding), encoding)`. -## Prior Art ## +### `fromGBytes(b:GLib.Bytes):Uint8Array` ### -Adobe's ActionScript 3.0 provides [`flash.utils.ByteArray`][2], which was the primary inspiration for this proposal. Note that the ActionScript version of ByteArray includes additional functionality which has been omitted here for the sake of allowing small implementations; it is anticipated that the extra functionality can be written in ES4 itself and provided as a standard library. +Convert a `GLib.Bytes` instance into a newly constructed `Uint8Array`. +The contents are copied. -[Synopsis of ActionScript's implementation too detailed and moved to [discussion][1] page] +### `toGBytes(a:Uint8Array):GLib.Bytes` ### -[1] http://wiki.ecmascript.org/doku.php?id=discussion:bytearray -[2] http://livedocs.macromedia.com/flex/2/langref/flash/utils/ByteArray.html +Converts the `Uint8Array` into a `GLib.Bytes` instance. +The contents are copied. diff --git a/doc/CPP_Style_Guide.md b/doc/CPP_Style_Guide.md new file mode 100644 index 0000000..beac17d --- /dev/null +++ b/doc/CPP_Style_Guide.md @@ -0,0 +1,1026 @@ +# C++ Coding Standards # + +## Introduction ## + +This guide attempts to describe a few coding standards that are being +used in GJS. + +For formatting we follow the [Google C++ Style Guide][google]. +This guide won't repeat all the rules that you can read there. +Instead, it covers rules that can't be checked "mechanically" with an +automated style checker. +It is not meant to be exhaustive. + +This guide is based on the [LLVM coding standards][llvm] (source code +[here][llvm-source].) +No coding standards should be regarded as absolute requirements to be +followed in all instances, but they are important to keep a large +complicated codebase readable. + +Many of these rules are not uniformly followed in the code base. +This is because most of GJS was written before they were put in place. +Our long term goal is for the entire codebase to follow the conventions, +but we explicitly *do not* want patches that do large-scale reformatting +of existing code. +On the other hand, it is reasonable to rename the methods of a class if +you're about to change it in some other way. +Just do the reformatting as a separate commit from the functionality +change. + +The ultimate goal of these guidelines is to increase the readability and +maintainability of our code base. +If you have suggestions for topics to be included, please open an issue +at . + +[google]: https://google.github.io/styleguide/cppguide.html +[llvm]: https://llvm.org/docs/CodingStandards.html +[llvm-source]: https://raw.githubusercontent.com/llvm-mirror/llvm/master/docs/CodingStandards.rst + +## Languages, Libraries, and Standards ## + +Most source code in GJS using these coding standards is C++ code. +There are some places where C code is used due to environment +restrictions or historical reasons. +Generally, our preference is for standards conforming, modern, and +portable C++ code as the implementation language of choice. + +### C++ Standard Versions ### + +GJS is currently written using C++17 conforming code, although we +restrict ourselves to features which are available in the major +toolchains. + +Regardless of the supported features, code is expected to (when +reasonable) be standard, portable, and modern C++17 code. +We avoid unnecessary vendor-specific extensions, etc., including +`g_autoptr()` and friends. + +### C++ Standard Library ### + +Use the C++ standard library facilities whenever they are available for +a particular task. +In particular, use STL containers rather than `GList*` and `GHashTable*` +and friends, for their type safety and memory management. + +There are some exceptions such as the standard I/O streams library which +is avoided, and use in space-constrained situations. + +### Supported C++17 Language and Library Features ### + +While GJS and SpiderMonkey use C++17, not all features are available in +all of the toolchains which we support. +A good rule of thumb is to check whether SpiderMonkey uses the feature. +If so, it's okay to use in GJS. + +### Other Languages ### + +Any code written in JavaScript is not subject to the formatting rules +below. +Instead, we adopt the formatting rules enforced by the +[`eslint`][eslint] tool. + +[eslint]: https://eslint.org/ + +## Mechanical Source Issues ## + +All source code formatting should follow the +[Google C++ Style Guide][google] with a few exceptions: + +* We use four-space indentation, to match the previous GJS coding style + so that the auto-formatter doesn't make a huge mess. +* Likewise we keep short return statements on separate lines instead of + allowing them on single lines. + +Our tools (clang-format and cpplint) have the last word on acceptable +formatting. +It may happen that the tools are not configured correctly, or contradict +each other. +In that case we accept merge requests to fix that, rather than code that +the tools reject. + +[google]: https://google.github.io/styleguide/cppguide.html + +### Source Code Formatting ### + +#### Commenting #### + +Comments are one critical part of readability and maintainability. +Everyone knows they should comment their code, and so should you. +When writing comments, write them as English prose, which means they +should use proper capitalization, punctuation, etc. +Aim to describe what the code is trying to do and why, not *how* it does +it at a micro level. +Here are a few critical things to document: + +##### File Headers ###### + +Every source file should have a header on it that describes the basic +purpose of the file. +If a file does not have a header, it should not be checked into the +tree. +The standard header looks like this: + +```c++ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) YEARS NAME + * + * 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 + +/* gi/private.cpp - private "imports._gi" module with operations that we need + * to use from JS in order to create GObject classes, but should not be exposed + * to client code. + */ +``` + +A few things to note about this particular format: The "`-*-`" string on +the first line is there to tell editors that the source file is a C++ +file, not a C file (since C++ and C headers both share the `.h` +extension.) +This is originally an Emacs convention, but other editors use it too. + +The next section in the file is a concise note that describes the file's +copyright and the license that the file is released under. +This makes it perfectly clear what terms the source code can be +distributed under and should not be modified. +Names can be added to the copyright when making a substantial +contribution to the file, not just a function or two. + +After the header includes comes a paragraph or two about what code the +file contains. +If an algorithm is being implemented or something tricky is going on, +this should be explained here, as well as any notes or *gotchas* in the +code to watch out for. + +##### Class overviews ###### + +Classes are one fundamental part of a good object oriented design. +As such, a class definition should have a comment block that explains +what the class is used for and how it works. +Every non-trivial class is expected to have such a comment block. + +##### Method information ###### + +Methods defined in a class (as well as any global functions) should also +be documented properly. +A quick note about what it does and a description of the borderline +behaviour is all that is necessary here (unless something particularly +tricky or insidious is going on). +The hope is that people can figure out how to use your interfaces +without reading the code itself. + +#### Comment Formatting #### + +Either C++ style comments (`//`) or C style (`/* */`) comments are +acceptable. +However, when documenting a method or function, use [gtk-doc style] +comments which are based on C style (`/** */`). +When C style comments take more than one line, put an asterisk (`*`) at +the beginning of each line: + +```c++ +/* a list of all GClosures installed on this object (from + * signals, trampolines and explicit GClosures), used when tracing */ +``` + +Commenting out large blocks of code is discouraged, but if you really +have to do this (for documentation purposes or as a suggestion for debug printing), use `#if 0` and `#endif`. +These nest properly and are better behaved in general than C style +comments. + +[gtk-doc style]: https://developer.gnome.org/gtk-doc-manual/unstable/documenting.html.en + +### Language and Compiler Issues ### + +#### Treat Compiler Warnings Like Errors #### + +If your code has compiler warnings in it, something is wrong — you +aren't casting values correctly, you have questionable constructs in +your code, or you are doing something legitimately wrong. +Compiler warnings can cover up legitimate errors in output and make +dealing with a translation unit difficult. + +It is not possible to prevent all warnings from all compilers, nor is it +desirable. +Instead, pick a standard compiler (like GCC) that provides a good +thorough set of warnings, and stick to it. +Currently we use GCC and the set of warnings defined by the +[`ax_compiler_flags`][ax-compiler-flags] macro. +In the future, we will use Meson's highest `warning_level` setting as +the arbiter. + +[ax-compiler-flags]: https://www.gnu.org/software/autoconf-archive/ax_compiler_flags.html#ax_compiler_flags + +#### Write Portable Code #### + +In almost all cases, it is possible and within reason to write +completely portable code. +If there are cases where it isn't possible to write portable code, +isolate it behind a well defined (and well documented) interface. + +In practice, this means that you shouldn't assume much about the host +compiler (and Visual Studio tends to be the lowest common denominator). + +#### Use of `class` and `struct` Keywords #### + +In C++, the `class` and `struct` keywords can be used almost +interchangeably. +The only difference is when they are used to declare a class: `class` +makes all members private by default while `struct` makes all members +public by default. + +Unfortunately, not all compilers follow the rules and some will generate +different symbols based on whether `class` or `struct` was used to +declare the symbol (e.g., MSVC). +This can lead to problems at link time. + +* All declarations and definitions of a given `class` or `struct` must + use the same keyword. For example: + + ```c++ + class Foo; + + // Breaks mangling in MSVC. + struct Foo { + int data; + }; + ``` + +* As a rule of thumb, `struct` should be kept to structures where *all* + members are declared public. + + ```c++ + // Foo feels like a class... this is strange. + struct Foo { + private: + int m_data; + + public: + Foo() : m_data(0) {} + int getData() const { return m_data; } + void setData(int d) { m_data = d; } + }; + + // Bar isn't POD, but it does look like a struct. + struct Bar { + int m_data; + Bar() : m_data(0) {} + }; + ``` + +#### Use `auto` Type Deduction to Make Code More Readable #### + +Some are advocating a policy of "almost always `auto`" in C++11 and +later, but GJS uses a more moderate stance. +Use `auto` only if it makes the code more readable or easier to +maintain. +Don't "almost always" use `auto`, but do use `auto` with initializers +like `cast(...)` or other places where the type is already obvious +from the context. +Another time when `auto` works well for these purposes is when the type +would have been abstracted away anyway, often behind a container's +typedef such as `std::vector::iterator`. + +#### Beware unnecessary copies with ``auto`` #### + +The convenience of `auto` makes it easy to forget that its default +behaviour is a copy. +Particularly in range-based `for` loops, careless copies are expensive. + +As a rule of thumb, use `auto&` unless you need to copy the result, +and use `auto*` when copying pointers. + +```c++ +// Typically there's no reason to copy. +for (const auto& val : container) + observe(val); +for (auto& val : container) + val.change(); + +// Remove the reference if you really want a new copy. +for (auto val : container) { + val.change(); + save_somewhere(val); +} + +// Copy pointers, but make it clear that they're pointers. +for (const auto* ptr : container) + observe(*ptr); +for (auto* ptr : container) + ptr->change(); +``` + +#### Beware of non-determinism due to ordering of pointers #### + +In general, there is no relative ordering among pointers. +As a result, when unordered containers like sets and maps are used with +pointer keys the iteration order is undefined. +Hence, iterating such containers may result in non-deterministic code +generation. +While the generated code might not necessarily be "wrong code", this +non-determinism might result in unexpected runtime crashes or simply +hard to reproduce bugs on the customer side making it harder to debug +and fix. + +As a rule of thumb, in case an ordered result is expected, remember to +sort an unordered container before iteration. +Or use ordered containers like `std::vector` if you want to iterate +pointer keys. + +#### Beware of non-deterministic sorting order of equal elements #### + +`std::sort` uses a non-stable sorting algorithm in which the order of +equal elements is not guaranteed to be preserved. +Thus using `std::sort` for a container having equal elements may result +in non-determinstic behaviour. + +## Style Issues ## + +### The High-Level Issues ### + +#### Self-contained Headers #### + +Header files should be self-contained (compile on their own) and end in +`.h`. +Non-header files that are meant for inclusion should end in `.inc` and +be used sparingly. + +All header files should be self-contained. +Users and refactoring tools should not have to adhere to special +conditions to include the header. +Specifically, a header should have header guards and include all other +headers it needs. + +There are rare cases where a file designed to be included is not +self-contained. +These are typically intended to be included at unusual locations, such +as the middle of another file. +They might not use header guards, and might not include their +prerequisites. +Name such files with the `.inc` extension. +Use sparingly, and prefer self-contained headers when possible. + +#### `#include` as Little as Possible #### + +`#include` hurts compile time performance. +Don't do it unless you have to, especially in header files. + +But wait! Sometimes you need to have the definition of a class to use +it, or to inherit from it. +In these cases go ahead and `#include` that header file. +Be aware however that there are many cases where you don't need to have +the full definition of a class. +If you are using a pointer or reference to a class, you don't need the +header file. +If you are simply returning a class instance from a prototyped function +or method, you don't need it. +In fact, for most cases, you simply don't need the definition of a +class. +And not `#include`ing speeds up compilation. + +It is easy to try to go too overboard on this recommendation, however. +You **must** include all of the header files that you are using — you +can include them either directly or indirectly through another header +file. +To make sure that you don't accidentally forget to include a header file +in your module header, make sure to include your module header **first** +in the implementation file (as mentioned above). +This way there won't be any hidden dependencies that you'll find out +about later. + +The tool [IWYU][iwyu] can help with this, but it generates a lot of +false positives, so we don't automate it. + +In many cases, header files with SpiderMonkey types will only need to +include one SpiderMonkey header, ``, unless they have +inline functions or SpiderMonkey member types. +This header file contains a number of forward declarations and nothing +else. + +[iwyu]: https://include-what-you-use.org/ + +#### Header inclusion order #### + +Headers should be included in the following order: + +- `` +- C system headers +- C++ system headers +- GNOME library headers +- SpiderMonkey library headers +- GJS headers + +Each of these groups must be separated by blank lines. +Within each group, all the headers should be alphabetized. +The first five groups should use angle brackets for the includes. + +Note that the header `` must be included before any +SpiderMonkey headers. + +GJS headers should use quotes, _except_ in public header files (any +header file included from ``.) + +If you need to include headers conditionally, add the conditional +after the group that it belongs to, separated by a blank line. + +If it is not obvious, you may add a comment after the include, +explaining what this header is included for. +This makes it easier to figure out whether to remove a header later if +its functionality is no longer used in the file. + +Here is an example of all of the above rules together: + +```c++ +#include // for ENABLE_CAIRO + +#include // for strlen + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#endif + +#include // for codecvt_utf8_utf16 +#include // for wstring_convert +#include + +#include +#include + +#include // for GCHashMap +#include // for JS_New, JSAutoRealm, JS_GetProperty +#include + +#include "gjs/atoms.h" +#include "gjs/context-private.h" +#include "gjs/jsapi-util.h" +``` + +#### Keep "Internal" Headers Private #### + +Many modules have a complex implementation that causes them to use more +than one implementation (`.cpp`) file. It is often tempting to put +the internal communication interface (helper classes, extra functions, +etc.) in the public module header file. +Don't do this! + +If you really need to do something like this, put a private header file +in the same directory as the source files, and include it locally. +This ensures that your private interface remains private and undisturbed +by outsiders. + +It's okay to put extra implementation methods in a public class itself. +Just make them private (or protected) and all is well. + +#### Use Early Exits and `continue` to Simplify Code #### + +When reading code, keep in mind how much state and how many previous +decisions have to be remembered by the reader to understand a block of +code. +Aim to reduce indentation where possible when it doesn't make it more +difficult to understand the code. +One great way to do this is by making use of early exits and the +`continue` keyword in long loops. +As an example of using an early exit from a function, consider this +"bad" code: + +```c++ +Value* do_something(Instruction* in) { + if (!is_a(in) && in->has_one_use() && do_other_thing(in)) { + ... some long code.... + } + + return nullptr; +} +``` + +This code has several problems if the body of the `if` is large. +When you're looking at the top of the function, it isn't immediately +clear that this *only* does interesting things with non-terminator +instructions, and only applies to things with the other predicates. +Second, it is relatively difficult to describe (in comments) why these +predicates are important because the `if` statement makes it difficult +to lay out the comments. +Third, when you're deep within the body of the code, it is indented an +extra level. +Finally, when reading the top of the function, it isn't clear what the +result is if the predicate isn't true; you have to read to the end of +the function to know that it returns null. + +It is much preferred to format the code like this: + +```c++ +Value* do_something(Instruction* in) { + // Terminators never need 'something' done to them because ... + if (is_a(in)) + return nullptr; + + // We conservatively avoid transforming instructions with multiple uses + // because goats like cheese. + if (!in->has_one_use()) + return nullptr; + + // This is really just here for example. + if (!do_other_thing(in)) + return nullptr; + + ... some long code.... +} +``` + +This fixes these problems. +A similar problem frequently happens in `for` loops. +A silly example is something like this: + +```c++ +for (Instruction& in : bb) { + if (auto* bo = dyn_cast(&in)) { + Value* lhs = bo->get_operand(0); + Value* rhs = bo->get_operand(1); + if (lhs != rhs) { + ... + } + } +} +``` + +When you have very small loops, this sort of structure is fine. +But if it exceeds more than 10-15 lines, it becomes difficult for people +to read and understand at a glance. +The problem with this sort of code is that it gets very nested very +quickly, meaning that the reader of the code has to keep a lot of +context in their brain to remember what is going immediately on in the +loop, because they don't know if/when the `if` conditions will have +`else`s etc. +It is strongly preferred to structure the loop like this: + +```c++ +for (Instruction& in : bb) { + auto* bo = dyn_cast(&in); + if (!bo) + continue; + + Value* lhs = bo->get_operand(0); + Value* rhs = bo->get_operand(1); + if (lhs == rhs) + continue; + + ... +} +``` + +This has all the benefits of using early exits for functions: it reduces +nesting of the loop, it makes it easier to describe why the conditions +are true, and it makes it obvious to the reader that there is no `else` +coming up that they have to push context into their brain for. +If a loop is large, this can be a big understandability win. + +#### Don't use `else` after a `return` #### + +For similar reasons above (reduction of indentation and easier reading), +please do not use `else` or `else if` after something that interrupts +control flow — like `return`, `break`, `continue`, `goto`, etc. +For example, this is *bad*: + +```c++ +case 'J': { + if (is_signed) { + type = cx.getsigjmp_buf_type(); + if (type.is_null()) { + error = ASTContext::ge_missing_sigjmp_buf; + return QualType(); + } else { + break; + } + } else { + type = cx.getjmp_buf_type(); + if (type.is_null()) { + error = ASTContext::ge_missing_jmp_buf; + return QualType(); + } else { + break; + } + } +} +``` +It is better to write it like this: + +```c++ +case 'J': + if (is_signed) { + type = cx.getsigjmp_buf_type(); + if (type.is_null()) { + error = ASTContext::ge_missing_sigjmp_buf; + return QualType(); + } + } else { + type = cx.getjmp_buf_type(); + if (type.is_null()) { + error = ASTContext::ge_missing_jmp_buf; + return QualType(); + } + } + break; +``` + +Or better yet (in this case) as: + +```c++ +case 'J': + if (is_signed) + type = cx.getsigjmp_buf_type(); + else + type = cx.getjmp_buf_type(); + + if (type.is_null()) { + error = is_signed ? ASTContext::ge_missing_sigjmp_buf + : ASTContext::ge_missing_jmp_buf; + return QualType(); + } + break; +``` + +The idea is to reduce indentation and the amount of code you have to +keep track of when reading the code. + +#### Turn Predicate Loops into Predicate Functions ##### + +It is very common to write small loops that just compute a boolean +value. +There are a number of ways that people commonly write these, but an +example of this sort of thing is: + +```c++ +bool found_foo = false; +for (unsigned ix = 0, len = bar_list.size(); ix != len; ++ix) + if (bar_list[ix]->is_foo()) { + found_foo = true; + break; + } + +if (found_foo) { + ... +} +``` + +This sort of code is awkward to write, and is almost always a bad sign. +Instead of this sort of loop, we strongly prefer to use a predicate +function (which may be `static`) that uses early exits to compute the +predicate. +We prefer the code to be structured like this: + +```c++ +/* Helper function: returns true if the specified list has an element that is + * a foo. */ +static bool contains_foo(const std::vector &list) { + for (unsigned ix = 0, len = list.size(); ix != len; ++ix) + if (list[ix]->is_foo()) + return true; + return false; +} +... + +if (contains_foo(bar_list)) { + ... +} +``` + +There are many reasons for doing this: it reduces indentation and +factors out code which can often be shared by other code that checks for +the same predicate. +More importantly, it *forces you to pick a name* for the function, and +forces you to write a comment for it. +In this silly example, this doesn't add much value. +However, if the condition is complex, this can make it a lot easier for +the reader to understand the code that queries for this predicate. +Instead of being faced with the in-line details of how we check to see +if the `bar_list` contains a foo, we can trust the function name and +continue reading with better locality. + +### The Low-Level Issues ### + +#### Name Types, Functions, Variables, and Enumerators Properly #### + +Poorly-chosen names can mislead the reader and cause bugs. +We cannot stress enough how important it is to use *descriptive* names. +Pick names that match the semantics and role of the underlying entities, +within reason. +Avoid abbreviations unless they are well known. +After picking a good name, make sure to use consistent capitalization +for the name, as inconsistency requires clients to either memorize the +APIs or to look it up to find the exact spelling. + +Different kinds of declarations have different rules: + +* **Type names** (including classes, structs, enums, typedefs, etc.) + should be nouns and should be named in camel case, starting with an + upper-case letter (e.g. `ObjectInstance`). + +* **Variable names** should be nouns (as they represent state). + The name should be snake case (e.g. `count` or `new_param`). + Private member variables should start with `m_` to distinguish them + from local variables representing the same thing. + +* **Function names** should be verb phrases (as they represent actions), + and command-like function should be imperative. + The name should be snake case (e.g. `open_file()` or `is_foo()`). + +* **Enum declarations** (e.g. `enum Foo {...}`) are types, so they + should follow the naming conventions for types. + A common use for enums is as a discriminator for a union, or an + indicator of a subclass. + When an enum is used for something like this, it should have a `Kind` + suffix (e.g. `ValueKind`). + +* **Enumerators** (e.g. `enum { Foo, Bar }`) and **public member + variables** should start with an upper-case letter, just like types. + Unless the enumerators are defined in their own small namespace or + inside a class, enumerators should have a prefix corresponding to the + enum declaration name. + For example, `enum ValueKind { ... };` may contain enumerators like + `VK_Argument`, `VK_BasicBlock`, etc. + Enumerators that are just convenience constants are exempt from the + requirement for a prefix. + For instance: + + ```c++ + enum { + MaxSize = 42, + Density = 12 + }; + ``` + +Here are some examples of good and bad names: + +```c++ +class VehicleMaker { + ... + Factory m_f; // Bad -- abbreviation and non-descriptive. + Factory m_factory; // Better. + Factory m_tire_factory; // Even better -- if VehicleMaker has more + // than one kind of factories. +}; + +Vehicle make_vehicle(VehicleType Type) { + VehicleMaker m; // Might be OK if having a short life-span. + Tire tmp1 = m.make_tire(); // Bad -- 'Tmp1' provides no information. + Light headlight = m.make_light("head"); // Good -- descriptive. + ... +} +``` + +#### Assert Liberally #### + +Use the `g_assert()` macro to its fullest. +Check all of your preconditions and assumptions, you never know when a +bug (not necessarily even yours) might be caught early by an assertion, +which reduces debugging time dramatically. + +To further assist with debugging, usually you should put some kind of +error message in the assertion statement, which is printed if the +assertion is tripped. +This helps the poor debugger make sense of why an assertion is being +made and enforced, and hopefully what to do about it. +Here is one complete example: + +```c++ +inline Value* get_operand(unsigned ix) { + g_assert(ix < operands.size() && "get_operand() out of range!"); + return operands[ix]; +} +``` + +To indicate a piece of code that should not be reached, use +`g_assert_not_reached()`. +When assertions are enabled, this will print the message if it's ever +reached and then exit the program. +When assertions are disabled (i.e. in release builds), +`g_assert_not_reached()` becomes a hint to compilers to skip generating +code for this branch. +If the compiler does not support this, it will fall back to the +`abort()` implementation. + +Neither assertions or `g_assert_not_reached()` will abort the program on +a release build. +If the error condition can be triggered by user input then the +recoverable error mechanism of `GError*` should be used instead. +In cases where this is not practical, either use `g_critical()` and +continue execution as best as possible, or use `g_error()` to abort with +a fatal error. + +Another issue is that values used only by assertions will produce an +"unused value" warning when assertions are disabled. +For example, this code will warn: + +```c++ +unsigned size = v.size(); +g_assert(size > 42 && "Vector smaller than it should be"); + +bool new_to_set = my_set.insert(value); +g_assert(new_to_set && "The value shouldn't be in the set yet"); +``` + +These are two interesting different cases. +In the first case, the call to `v.size()` is only useful for the assert, +and we don't want it executed when assertions are disabled. +Code like this should move the call into the assert itself. +In the second case, the side effects of the call must happen whether the +assert is enabled or not. +In this case, the value should be cast to void to disable the warning. +To be specific, it is preferred to write the code like this: + +```c++ +g_assert(v.size() > 42 && "Vector smaller than it should be"); + +bool new_to_set = my_set.insert(value); +(void)new_to_set; +g_assert(new_to_set && "The value shouldn't be in the set yet"); +``` + +#### Do Not Use `using namespace std` #### + +In GJS, we prefer to explicitly prefix all identifiers from the standard +namespace with an `std::` prefix, rather than rely on `using namespace +std;`. + +In header files, adding a `using namespace XXX` directive pollutes the +namespace of any source file that `#include`s the header. +This is clearly a bad thing. + +In implementation files (e.g. `.cpp` files), the rule is more of a +stylistic rule, but is still important. +Basically, using explicit namespace prefixes makes the code **clearer**, because it is immediately obvious what facilities are being used and +where they are coming from. +And **more portable**, because namespace clashes cannot occur between +LLVM code and other namespaces. +The portability rule is important because different standard library +implementations expose different symbols (potentially ones they +shouldn't), and future revisions to the C++ standard will add more +symbols to the `std` namespace. +As such, we never use `using namespace std;` in GJS. + +The exception to the general rule (i.e. it's not an exception for the +`std` namespace) is for implementation files. +For example, in the future we might decide to put GJS code inside a +`Gjs` namespace. +In that case, it is OK, and actually clearer, for the `.cpp` files to +have a `using namespace Gjs;` directive at the top, after the +`#include`s. +This reduces indentation in the body of the file for source editors that +indent based on braces, and keeps the conceptual context cleaner. +The general form of this rule is that any `.cpp` file that implements +code in any namespace may use that namespace (and its parents'), but +should not use any others. + +#### Provide a Virtual Method Anchor for Classes in Headers #### + +If a class is defined in a header file and has a vtable (either it has +virtual methods or it derives from classes with virtual methods), it +must always have at least one out-of-line virtual method in the class. +Without this, the compiler will copy the vtable and RTTI into every `.o` +file that `#include`s the header, bloating `.o` file sizes and +increasing link times. + +#### Don't use default labels in fully covered switches over enumerations #### + +`-Wswitch` warns if a switch, without a default label, over an +enumeration, does not cover every enumeration value. +If you write a default label on a fully covered switch over an +enumeration then the `-Wswitch` warning won't fire when new elements are +added to that enumeration. +To help avoid adding these kinds of defaults, Clang has the warning `-Wcovered-switch-default`. + +A knock-on effect of this stylistic requirement is that when building +GJS with GCC you may get warnings related to "control may reach end of +non-void function" if you return from each case of a covered +switch-over-enum because GCC assumes that the enum expression may take +any representable value, not just those of individual enumerators. +To suppress this warning, use `g_assert_not_reached()` after the switch. + +#### Use range-based `for` loops wherever possible #### + +The introduction of range-based `for` loops in C++11 means that explicit +manipulation of iterators is rarely necessary. We use range-based `for` +loops wherever possible for all newly added code. For example: + +```c++ +for (GClosure* closure : m_closures) + ... use closure ...; +``` + +#### Don't evaluate `end()` every time through a loop #### + +In cases where range-based `for` loops can't be used and it is necessary +to write an explicit iterator-based loop, pay close attention to whether +`end()` is re-evaluted on each loop iteration. +One common mistake is to write a loop in this style: + +```c++ +for (auto* closure = m_closures->begin(); closure != m_closures->end(); + ++closure) + ... use closure ... +``` + +The problem with this construct is that it evaluates `m_closures->end()` +every time through the loop. +Instead of writing the loop like this, we strongly prefer loops to be +written so that they evaluate it once before the loop starts. +A convenient way to do this is like so: + +```c++ +for (auto* closure = m_closures->begin(), end = m_closures->end(); + closure != end; ++closure) + ... use closure ... +``` + +The observant may quickly point out that these two loops may have +different semantics: if the container is being mutated, then +`m_closures->end()` may change its value every time through the loop and +the second loop may not in fact be correct. +If you actually do depend on this behavior, please write the loop in the +first form and add a comment indicating that you did it intentionally. + +Why do we prefer the second form (when correct)? +Writing the loop in the first form has two problems. +First it may be less efficient than evaluating it at the start of the +loop. +In this case, the cost is probably minor — a few extra loads every time +through the loop. +However, if the base expression is more complex, then the cost can rise +quickly. +If the end expression was actually something like `some_map[x]->end()`, +map lookups really aren't cheap. +By writing it in the second form consistently, you eliminate the issue +entirely and don't even have to think about it. + +The second (even bigger) issue is that writing the loop in the first +form hints to the reader that the loop is mutating the container (which +a comment would handily confirm!) +If you write the loop in the second form, it is immediately obvious +without even looking at the body of the loop that the container isn't +being modified, which makes it easier to read the code and understand +what it does. + +While the second form of the loop is a few extra keystrokes, we do +strongly prefer it. + +#### Avoid `std::endl` #### + +The `std::endl` modifier, when used with `iostreams`, outputs a newline +to the output stream specified. +In addition to doing this, however, it also flushes the output stream. +In other words, these are equivalent: + +```c++ +std::cout << std::endl; +std::cout << '\n' << std::flush; +``` + +Most of the time, you probably have no reason to flush the output +stream, so it's better to use a literal `'\n'`. + +#### Don't use `inline` when defining a function in a class definition #### + +A member function defined in a class definition is implicitly inline, so +don't put the `inline` keyword in this case. + +Don't: + +```c++ +class Foo { + public: + inline void bar() { + // ... + } +}; +``` + +Do: + +```c++ +class Foo { + public: + void bar() { + // ... + } +}; +``` diff --git a/doc/Environment.md b/doc/Environment.md new file mode 100644 index 0000000..68ac0fd --- /dev/null +++ b/doc/Environment.md @@ -0,0 +1,104 @@ +## Environment + +GJS allows runtime configuration with a number of environment variables. + +### General + +* `GJS_PATH` + + Set this variable to a list of colon-separated (`:`) paths (just like `PATH`), + to add them to the search path for the importer. Use of the `--include-path` + command-line option is preferred over this variable. + +* `GJS_ABORT_ON_OOM` + + > NOTE: This feature is not well tested. + + Setting this variable to any value causes GJS to exit when an out-of-memory + condition is encountered, instead of just printing a warning. + +### JavaScript Engine + +* `JS_GC_ZEAL` + + Enable GC zeal, a testing and debugging feature that helps find GC-related + bugs in JSAPI applications. See the [Hacking][hacking-gczeal] and the + [JSAPI Documentation][mdn-gczeal] for more information about this variable. + +* `GJS_DISABLE_JIT` + + Setting this variable to any value will disable JIT compiling in the + JavaScript engine. + + +### Debugging + +* `GJS_DEBUG_HEAP_OUTPUT` + + In addition to `System.dumpHeap()`, you can dump a heap from a running program + by starting it with this environment variable set to a path and sending it the + `SIGUSR1` signal. + +* `GJS_DEBUG_OUTPUT` + + Set this to "stderr" to log to `stderr` or a file path to save to. + +* `GJS_DEBUG_TOPICS` + + Set this to a semi-colon delimited (`;`) list of prefixes to allow to be + logged. Prefixes include: + + * "JS GI USE" + * "JS MEMORY" + * "JS CTX" + * "JS IMPORT" + * "JS NATIVE" + * "JS KP ALV" + * "JS G REPO" + * "JS G NS" + * "JS G OBJ" + * "JS G FUNC" + * "JS G FNDMTL" + * "JS G CLSR" + * "JS G BXD" + * "JS G ENUM" + * "JS G PRM" + * "JS G ERR" + * "JS G IFACE" + +* `GJS_DEBUG_THREAD` + + Set this variable to print the thread number when logging. + +* `GJS_DEBUG_TIMESTAMP` + + Set this variable to print a timestamp when logging. + + +### Testing + +* `GJS_COVERAGE_OUTPUT` + + Set this variable to define an output path for code coverage information. Use + of the `--coverage-output` command-line option is preferred over this + variable. + +* `GJS_COVERAGE_PREFIXES` + + Set this variable to define a colon-separated (`:`) list of prefixes to output + code coverage information for. Use of the `--coverage-prefix` command-line + option is preferred over this variable. + +* `GJS_ENABLE_PROFILER` + + Set this variable to `1` to enable or `0` to disable the profiler. Use of the + `--profile` command-line option is preferred over this variable. + +* `GJS_TRACE_FD` + + The GJS profiler is integrated directly into Sysprof via this variable. It not + typically useful to set this manually. + + +[hacking-gczeal]: https://gitlab.gnome.org/GNOME/gjs/blob/master/doc/Hacking.md#gc-zeal +[mdn-gczeal]: https://developer.mozilla.org/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/JS_SetGCZeal diff --git a/doc/Hacking.md b/doc/Hacking.md index 2987b22..7c62cfa 100644 --- a/doc/Hacking.md +++ b/doc/Hacking.md @@ -2,53 +2,89 @@ ## Setting up ## -For the time being, we recommend using JHBuild to develop GJS. -Follow the [instructions from GNOME][jhbuild]. - -Even if your system includes a development package for mozjs, we -recommend building it on JHBuild so that you can enable the debugging -features. Add this to your JHBuild configuration file: -```python -module_autogenargs['mozjs52'] = '--enable-debug' +First of all, if you are contributing C++ code, install the handy git +commit hook that will autoformat your code when you commit it. +In your GJS checkout directory, run +`tools/git-pre-commit-format install`. +For more information, see +. +(You can skip this step if it doesn't work for you, but in that case +you'll need to manually format your code before it gets merged. +You can also skip this step if you are not writing any C++ code.) + +GJS requires four other libraries to be installed: GLib, libffi, +gobject-introspection, and SpiderMonkey (also called "mozjs78" on some +systems.) +The readline library is not required, but strongly recommended. +We recommend installing your system's development packages for GLib, +libffi, gobject-introspection, and readline. +(For example, on Ubuntu you would run +`sudo apt-get install libglib2.0-dev libffi-dev libreadline-dev libgirepository1.0-dev libreadline-dev`.) +But, if your system's versions of these packages aren't new enough, then +the build process will download and build sufficient versions. + +SpiderMonkey cannot be auto-installed, so you will need to install it +either through your system's package manager, or building it yourself. +Even if your system includes a development package for SpiderMonkey, we +still recommend building it if you are going to do any development on +GJS so that you can enable the debugging features. +These debugging features reduce performance by quite a lot, but they +will help catch mistakes in the API that could otherwise go unnoticed +and cause crashes in gnome-shell later on. + +To build SpiderMonkey, follow the instructions on [this page](https://github.com/mozilla-spidermonkey/spidermonkey-embedding-examples/blob/esr78/docs/Building%20SpiderMonkey.md) to download the source code and build the library. +If you are using `-Dprefix` to build GJS into a different path, then +make sure to use the same build prefix for SpiderMonkey with `--prefix`. + +## First build ## + +To build GJS, change to your checkout directory, and run: +```sh +meson _build +ninja -C _build ``` -Make sure it is built first with `jhbuild build mozjs52`, otherwise -`jhbuild build gjs` will skip it if you have the system package -installed. +Add any options with `-D` arguments to the `meson _build` command. +For a list of available options, run `meson configure`. -Debugging features in mozjs reduce performance by quite a lot, in -exchange for performing many runtime checks that can alert you when -you're not using the JS API correctly. +To install GJS into the path you chose with `-Dprefix`, (or into +`/usr/local` if you didn't choose a path), run +`ninja -C _build install`, adding `sudo` if necessary. ## Making Sure Your Stuff Doesn't Break Anything Else ## +Make your changes in your GJS checkout directory, then run +`ninja -C _build` to build a modified copy of GJS. + Each changeset should ensure that the test suite still passes. In fact, each commit should ensure that the test suite still passes, though there are some exceptions to this rule. -You can run the test suite with `jhbuild make check`. +You can run the test suite with `meson test -C _build`. + +For some contributions, it's a good idea to test your modified version +of GJS with GNOME Shell. +For this, you might want to use JHBuild to build GJS instead, and run +it with `jhbuild run gnome-shell --replace`. +You need to be logged into an Xorg session, not Wayland, for this to +work. ## Debugging ## Mozilla has some pretty-printers that make debugging JSAPI code easier. Unfortunately they're not included in most packaged distributions of -mozjs, but you can grab them from your JHBuild copy of mozjs. +mozjs, but you can grab them from your built copy of mozjs. After reaching a breakpoint in your program, type this to activate the pretty-printers: -``` -source ~/.cache/jhbuild/build/mozjs-52.Y.Z/js/src/shell/js-gdb.py +```sh +source /path/to/spidermonkey/js/src/_build/js/src/shell/js-gdb.py ``` -(replace `Y.Z` with mozjs's minor and micro version numbers) +(replace `/path/to/spidermonkey` with the path to your SpiderMonkey +sources) ## Checking Things More Thoroughly Before A Release ## -### Distcheck ### - -Run `jhbuild make distcheck` once before every release to make sure the -package builds and the tests run from a clean release tarball. -If there are any errors, they must be fixed before making the release. - ### GC Zeal ### Run the test suite with "GC zeal" to make non-deterministic GC errors @@ -56,21 +92,21 @@ more likely to show up. To see which GC zeal options are available: ```sh -JS_GC_ZEAL=-1 jhbuild run gjs +JS_GC_ZEAL=-1 js78 ``` -To run the test suite under it: +We include three test setups, `extra_gc`, `pre_verify`, and +`post_verify`, for the most useful modes: `2,1`, `4`, and `11` +respectively. +Run them like this (replace `extra_gc` with any of the other names): ```sh -JS_GC_ZEAL=... jhbuild make check +meson test -C _build --setup=extra_gc ``` -Good parameters for `...` are `1`, `2,100`, `2,1` (really slow and can -cause the tests to time out), `4`, `6`, `7`, `11`. - -Failures in mode 4 (pre barrier verification) usually point to a GC -thing not being traced when it should have been. Failures in mode 11 -(post barrier verification) usually point to a weak pointer's location -not being updated after GC moved it. +Failures in mode `pre_verify` usually point to a GC thing not being +traced when it should have been. +Failures in mode `post_verify` usually point to a weak pointer's +location not being updated after GC moved it. ### Valgrind ### @@ -78,12 +114,13 @@ Valgrind catches memory leak errors in the C++ code. It's a good idea to run the test suite under Valgrind before each release. -To run the test suite under a succession of Valgrind tools: +To run the test suite under Valgrind's memcheck tool: ```sh -jhbuild make check-valgrind +meson test -C _build --setup=valgrind ``` -The logs from each run will be in `~/.cache/jhbuild/build/gjs/test-suite-.log`, where `` is `drd`, `helgrind`, `memcheck`, and `sgcheck`. +The logs from each run will be in +`_build/meson-logs/testlog-valgrind.txt`. Note that LeakSanitizer, part of ASan (see below) can catch many, but not all, errors that Valgrind can catch. @@ -93,7 +130,7 @@ LSan executes faster than Valgrind, however. To execute cppcheck, a static code analysis tool for the C and C++, run: ```sh -jhbuild make cppcheck +tools/run_cppcheck.sh ``` It is a versatile tool that can check non-standard code, including: variable checking, bounds checking, leaks, etc. It can detect the types of bugs that @@ -101,36 +138,22 @@ the compilers normally fail to detect. ### Sanitizers ### -To add instrumentation code to gjs, put this (both, or any one of them) in -your JHBuild configuration file: -```python -module_autogenargs['gjs'] = '--enable-asan --enable-ubsan' +To build GJS with support for the ASan and UBSan sanitizers, configure +meson like this: +```sh +meson _build -Db_sanitize=address,undefined ``` - -Sanitizers are based on compile-time instrumentation. They are available -in gcc and clang for a range of supported operating systems and -platforms. - -Please, keep in mind that instrumentation is limited by execution coverage. So, -if your "testing" session never reaches a particular point of execution, then -instrumentation at that point collects no data. +and then run the tests as normal. ### Test Coverage ### -To generate a test coverage report, put this in your JHBuild -configuration file: -```python -module_autogenargs['gjs'] = '--enable-code-coverage' -``` - -Then run: +To generate a test coverage report, run this script: ```sh -jhbuild cleanone gjs -jhbuild buildone gjs -jhbuild make check-code-coverage -xdg-open ~/.cache/jhbuild/build/gjs/gjs-X.Y.Z-coverage/index.html +tools/run_coverage.sh +gio open _coverage/html/index.html ``` +This will build GJS into a separate build directory with code coverage +instrumentation enabled, run the test suite to collect the coverage +data, and open the generated HTML report. -(replace `X.Y.Z` with the version number, e.g. `1.48.0`) - -[jhbuild] https://wiki.gnome.org/HowDoI/Jhbuild +[embedder](https://github.com/spidermonkey-embedders/spidermonkey-embedding-examples/blob/esr78/docs/Building%20SpiderMonkey.md) \ No newline at end of file diff --git a/doc/Home.md b/doc/Home.md new file mode 100644 index 0000000..4b10ed9 --- /dev/null +++ b/doc/Home.md @@ -0,0 +1,61 @@ +# GJS: Javascript Bindings for GNOME + +The current stable series is built on Mozilla's SpiderMonkey 78 featuring **ECMAScript 2019** and GObjectIntrospection making most of the **GNOME API library** available. + +To find out when a language feature was implemented in GJS, review [NEWS][gjs-news] in the GitLab repository. In many cases older versions of GJS can be supported using [polyfills][mdn-polyfills] and [legacy-style GJS classes](Modules.md#lang). + +GJS includes some built-in modules like Cairo and Gettext, as well as helpers for some core APIs like DBus and GVariants. See the [Modules](Modules.md) page for an overview of the built-in modules and their usage. + +[gjs-news]: https://gitlab.gnome.org/GNOME/gjs/raw/master/NEWS +[mdn-polyfills]: https://developer.mozilla.org/docs/Glossary/Polyfill + +## GNOME API Documentation + +There is now official [GNOME API Documentation][gjs-docs] for GJS, including everything from GLib and Gtk to Soup and WebKit2. + +The [Mapping](Mapping.md) page has an overview of GNOME API usage in GJS such as subclassing, constants and flags, functions with multiple return values, and more. + +There are also a growing number of [examples][gjs-examples] and thorough tests of language features in the [test suite][gjs-tests]. + +[gjs-docs]: https://gjs-docs.gnome.org/ +[gjs-examples]: https://gitlab.gnome.org/GNOME/gjs/tree/master/examples +[gjs-tests]: https://gitlab.gnome.org/GNOME/gjs/blob/master/installed-tests/js + + +## Standalone Applications + +It's possible to write standalone applications with GJS for the GNOME Desktop, and infrastructure for Gettext, GSettings and GResources via the `package` import. There is a package specification, template repository available and plans for an in depth tutorial. + +* [GJS Package Specification](https://wiki.gnome.org/Projects/Gjs/Package/Specification.md) +* [GJS Package Template](https://github.com/gcampax/gtk-js-app) + +GNOME Applications written in GJS: + +* [GNOME Characters](https://gitlab.gnome.org/GNOME/gnome-characters) +* [GNOME Documents](https://gitlab.gnome.org/GNOME/gnome-documents) +* [GNOME Maps](https://gitlab.gnome.org/GNOME/gnome-maps) +* [GNOME Sound Recorder](https://gitlab.gnome.org/GNOME/gnome-sound-recorder) +* [GNOME Weather](https://gitlab.gnome.org/GNOME/gnome-weather) +* [GNOME Books](https://gitlab.gnome.org/GNOME/gnome-books) +* [Polari](https://gitlab.gnome.org/GNOME/polari) IRC Client + +Third party applications written in GJS: + +* [Tangram](https://github.com/sonnyp/Tangram) +* [Quick Lookup](https://github.com/johnfactotum/quick-lookup) +* [Foliate](https://github.com/johnfactotum/foliate) +* [Marker](https://github.com/fabiocolacio/Marker) +* [Gnomit](https://github.com/small-tech/gnomit) + +## Getting Help + +* Mailing List: http://mail.gnome.org/mailman/listinfo/javascript-list +* IRC: irc://irc.gnome.org/#javascript +* Issue/Bug Tracker: https://gitlab.gnome.org/GNOME/gjs/issues +* StackOverflow: https://stackoverflow.com/questions/tagged/gjs + +## External Links + +* [GObjectIntrospection](https://wiki.gnome.org/action/show/Projects/GObjectIntrospection) +* [GNOME Developer Platform Demo](https://developer.gnome.org/gnome-devel-demos/stable/js.html) (Some older examples that still might be informative) +* [Writing GNOME Shell Extensions](https://wiki.gnome.org/Projects/GNOMEShell/Extensions/Writing) \ No newline at end of file diff --git a/doc/Logging.md b/doc/Logging.md new file mode 100644 index 0000000..9e58981 --- /dev/null +++ b/doc/Logging.md @@ -0,0 +1,116 @@ +GJS includes a number of built-in funtions for logging and aiding debugging, in +addition to those available as a part of the GNOME APIs. + +# Built-in Functions + +GJS includes four built-in logging functions: `log()`, `logError()`, `print()` +and `printerr()`. These functions are available globally (ie. without import) +and the source for these is found in [global.cpp][global-cpp]. + +### log() + +`log()` is the most basic function available, taking a single argument as a +`String` or other object which can be coerced to a `String`. The string, or +object coerced to a string, is logged with `g_message()` from GLib. + +```js +// expected output: JS LOG: Some message +log('Some message'); + +// expected output: JS LOG: [object Object] +log(new Object()); +``` + +### logError() + +`logError()` is a more useful function for debugging that logs the stack trace of +a JavaScript `Error()` object, with an optional prefix. + +It is commonly used in conjunction with `try...catch` blocks to log errors while +still trapping the exception. An example in `gjs-console` with a backtrace: + +```js +$ gjs +gjs> try { +.... throw new Error('Some error occured'); +.... } catch (e) { +.... logError(e, 'FooError'); +.... } + +(gjs:28115): Gjs-WARNING **: 19:28:13.334: JS ERROR: FooError: Error: Some error occured +@typein:2:16 +@:1:34 +``` + + +### print() & printerr() + +`print()` takes any number of string (or coercable) arguments, joins them with a +space and appends a newline (`\n`). The resulting message will be printed +directly to `stdout` of the current process using `g_print()`. + +`printerr()` is exactly like `print()`, except the resulting message is printed +to `stderr` with `g_printerr()`. + +These functions are generally less useful for debugging code in programs that +embed GJS like GNOME Shell, where it is less convenient to access the `stdout` +and `stderr` pipes. + +```js +$ gjs +gjs> print('some', 'string', 42); +some string 42$ +gjs> printerr('some text 42'); +some text +``` + + +# GLib Functions + +Aside from the built-in functions in GJS, many functions from GLib can be used +to log messages at different severity levels and assist in debugging. + +Below is a common pattern for defining a series of logging functions as used in +[Polari][polari] and some other GJS applications: + +```js +const GLib = imports.gi.GLib; + +var LOG_DOMAIN = 'Polari'; + +function _makeLogFunction(level) { + return message => { + let stack = (new Error()).stack; + let caller = stack.split('\n')[1]; + + // Map from resource- to source location + caller = caller.replace('resource:///org/gnome/Polari/js', 'src'); + + let [code, line] = caller.split(':'); + let [func, file] = code.split(/\W*@/); + GLib.log_structured(LOG_DOMAIN, level, { + 'MESSAGE': `${message}`, + 'SYSLOG_IDENTIFIER': 'org.gnome.Polari', + 'CODE_FILE': file, + 'CODE_FUNC': func, + 'CODE_LINE': line + }); + }; +} + +// `window` is the global object in GJS, for historical reasons +window.log = _makeLogFunction(GLib.LogLevelFlags.LEVEL_MESSAGE); +window.debug = _makeLogFunction(GLib.LogLevelFlags.LEVEL_DEBUG); +window.info = _makeLogFunction(GLib.LogLevelFlags.LEVEL_INFO); +window.warning = _makeLogFunction(GLib.LogLevelFlags.LEVEL_WARNING); +window.critical = _makeLogFunction(GLib.LogLevelFlags.LEVEL_CRITICAL); +window.error = _makeLogFunction(GLib.LogLevelFlags.LEVEL_ERROR); + +// Log all messages when connected to the journal +if (GLib.log_writer_is_journald(2)) + GLib.setenv('G_MESSAGES_DEBUG', LOG_DOMAIN, false); +``` + +[global-cpp]: https://gitlab.gnome.org/GNOME/gjs/blob/master/gjs/global.cpp +[polari]: https://gitlab.gnome.org/GNOME/polari/blob/master/src/main.js + diff --git a/doc/Mapping.md b/doc/Mapping.md new file mode 100644 index 0000000..e5f80bd --- /dev/null +++ b/doc/Mapping.md @@ -0,0 +1,244 @@ +## GObject Construction, Subclassing, Templates and GType + +### Constructing GObjects + +GObjects can be constructed with the `new` operator, just like JavaScript objects, and usually take an Object map of properties. + +The object that you pass to `new` (e.g. `Gtk.Label` in `let label = new Gtk.Label()`) is the **constructor object**, that contains constructor methods and static methods such as `Gio.File.new_for_path()`. +It's different from the **prototype object** containing instance methods. +For more information on JavaScript's prototypal inheritance, this [blog post][understanding-javascript-prototypes] is a good resource. + +```js +let label = new Gtk.Label({ + label: 'gnome.org', + halign: Gtk.Align.CENTER, + hexpand: true, + use_markup: true, + visible: true +}); + +let file = Gio.File.new_for_path('/proc/cpuinfo'); +``` + +[understanding-javascript-prototypes]: https://javascriptweblog.wordpress.com/2010/06/07/understanding-javascript-prototypes/ + +### Subclassing GObjects + +GObjects have facilities for defining properties, signals and implemented interfaces. Additionally, Gtk objects support defining a CSS name and composite template. + +The **constructor object** is also passed to the `extends` keyword in class declarations when subclassing GObjects. + +```js +var MyLabel = GObject.registerClass({ + // GObject + GTypeName: 'Gjs_MyLabel', // GType name (see below) + Implements: [ Gtk.Orientable ], // Interfaces the subclass implements + Properties: {}, // More below on custom properties + Signals: {}, // More below on custom signals + // Gtk + CssName: '', // CSS name + Template: 'resource:///path/example.ui', // Builder template + Children: [ 'label-child' ], // Template children + InternalChildren: [ 'internal-box' ] // Template internal (private) children +}, class MyLabel extends Gtk.Label { + // Currently GObjects use _init, not construct + _init(params) { + // Chaining up + super._init(params); + } +}); +``` + +### GType Objects + +This is the object that represents a type in the GObject type system. Internally a GType is an integer, but you can't access that integer in GJS. + +The `$gtype` property gives the GType object for the given type. This is the proper way to find the GType given an object or a class. For a class, `GObject.type_from_name('GtkLabel')` would work too if you know the GType name, but only if you had previously constructed a Gtk.Label object. + +```js +log(Gtk.Label.$gtype); +log(labelInstance.constructor.$gtype); +// expected output: [object GType for 'GtkLabel'] +``` + +The `name` property of GType objects gives the GType name as a string ('GtkLabel'). This is the proper way to find the type name given an object or a class. + +User defined subclasses' GType name will be the class name prefixed with `Gjs_`by default. +If you want to specify your own name, you can pass it as the value for the `GTypeName` property to `GObject.registerClass()`. +This will be relevant in situations such as defining a composite template for a GtkWidget subclass. + +```js +log(Gtk.Label.$gtype.name); +log(labelInstance.constructor.$gtype.name); +// expected output: GtkLabel + +log(MyLabel.$gtype.name); +// expected output: Gjs_MyLabel +``` + +[`instanceof`][mdn-instanceof] can be used to compare an object instance to a **constructor object**. + +```js +log(typeof labelInstance); +// expected output: object + +log(labelInstance instanceof Gtk.Label); +// expected output: true +``` + +[mdn-instanceof]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/instanceof + +## Properties + +GObject properties may be retrieved and set using native property style access or GObject get/set methods. Note that variables in JavaScript can't contain hyphens (-) so when a property name is *unquoted* use an underscore (_). + +```js +if (!label.use_markup) { + label.connect('notify::use-markup', () => { ... }); + label.use_markup = true; + label['use-markup'] = true; + label.set_use_markup(true); +} +``` + +GObject subclasses can register properties, which is necessary if you want to use `GObject.notify()` or `GObject.bind_property()`. + +**NOTE:** Never use underscores in property names in the ParamSpec, because of the conversion between underscores and hyphens mentioned above. + +```js +var MyLabel = GObject.registerClass({ + Properties: { + 'example-prop': GObject.ParamSpec.string( + 'example-prop', // property name + 'ExampleProperty', // nickname + 'An example read write property', // description + GObject.ParamFlags.READWRITE, // READABLE/READWRITE/CONSTRUCT/etc + 'A default' // default value if omitting getter/setter + ) + } +}, class MyLabel extends Gtk.Label { + get example_prop() { + if (!('_example_prop' in this) + return 'A default'; + return this._example_prop; + } + + set example_prop(value) { + if (this._example_prop !== value) { + this._example_prop = value; + this.notify('example-prop'); + } + } +}); +``` + +If you just want a simple property that you can get change notifications from, you can leave out the getter and setter and GJS will attempt to do the right thing. +However, if you define one, you have to define both (unless the property is read-only or write-only). + +The 'default value' parameter passed to `GObject.ParamSpec` will be taken into account if you omit the getter and setter. +If you write your own getter and setter, you have to implement the default value yourself, as in the above example. + +## Signals + +Every object inherited from GObject has `connect()`, `connect_after()`, `disconnect()` and `emit()` methods. + +```js +let handlerId = label.connect('activate-link', (label, uri) => { + Gtk.show_uri_on_window( + label.get_toplevel(), + uri, + Gdk.get_current_time() + ); + return true; +}); + +label.emit('activate-link', 'https://www.gnome.org'); + +label.disconnect(handlerId); +``` + +GObject subclasses can also register their own signals. + +```js +var MyLabel = GObject.registerClass({ + Signals: { + 'my-signal': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [ GObject.TYPE_STRING ] + } + } +}, class ExampleApplication extends GObject.Object { + _init() { + super._init(); + this.emit('my-signal', 'a string parameter'); + } +}); +``` + +**NOTE:** GJS also includes a built-in [`signals`](Modules#signals) module for applying signals to native JavaScript classes. + +## Enumerations and Flags + +Both enumerations and flags appear as entries under the namespace, with associated member properties. These are available in the official GJS [GNOME API documentation][gjs-docs]. + +```js +// enum GtkAlign, member GTK_ALIGN_CENTER +Gtk.Align.CENTER; +// enum GtkWindowType, member GTK_WINDOW_TOPLEVEL +Gtk.WindowType.TOPLEVEL; +// enum GApplicationFlags, member G_APPLICATION_FLAGS_NONE +Gio.ApplicationFlags.FLAGS_NONE +``` + +Flags can be manipulated using native [bitwise operators][mdn-bitwise]. + +```js +let myApp = new Gio.Application({ + flags: Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.HANDLES_COMMAND_LINE +}); + +if (myApp.flags & Gio.ApplicationFlags.HANDLES_OPEN) { + myApp.flags &= ~Gio.ApplicationFlags.HANDLES_OPEN; +} +``` + +[gjs-docs]: http://devdocs.baznga.org/ +[mdn-bitwise]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators + +## Structs and Unions + +C structures and unions are documented in the [GNOME API documentation][gjs-docs] (e.g. [Gdk.Event][gdk-event]) and generally have either JavaScript properties or getter methods for each member. Results may vary when trying to modify structs or unions. + +```js +widget.connect("key-press-event", (widget, event) => { + log(event); + // expected output: [union instance proxy GIName:Gdk.Event jsobj@0x7f19a00b6400 native@0x5620c6a7c6e0] + log(event.get_event_type() === Gdk.EventType.KEY_PRESS); + // expected output: true + let [ok, keyval] = event.get_keyval(); + log(keyval); + // example output: 65507 +}); +``` + +[gdk-event]: http://devdocs.baznga.org/gdk30~3.22p/gdk.event + +## Multiple return values (caller-allocates) + +In GJS caller-allocates (variables passed into a function) and functions with multiple out parameters are returned as an array of return values. If the function has a return value, it will be the first element of that array. + +```js +let [minimumSize, naturalSize] = label.get_preferred_size(); + +// Functions with boolean 'success' returns often still throw an Error on failure +try { + let file = new Gio.File({ path: '/proc/cpuinfo' }); + let [ok, contents, etag_out] = file.load_contents(null); + // "ok" is actually useless in this scenario, since if it is false, + // an exception will have been thrown. You can skip return values + // you don't want with array elision: + let [, contents2] = file.load_contents(null); +} catch(e) { + log('Failed to read file: ' + e.message); +} +``` \ No newline at end of file diff --git a/doc/Modules.md b/doc/Modules.md new file mode 100644 index 0000000..6c90607 --- /dev/null +++ b/doc/Modules.md @@ -0,0 +1,283 @@ +GJS includes some built-in modules, as well as helpers for some core APIs like DBus like Variants. The headings below are links to the JavaScript source, which are decently documented and informative of usage. + +## [Gio](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gio.js) + +**Import with `const Gio = imports.gi.Gio;`** + +The `Gio` override includes a number of utilities for DBus that will be documented further at a later date. Below is a reasonable overview. + +* `Gio.DBus.session`, `Gio.DBus.system` + + Convenience properties that wrap `Gio.bus_get_sync()` to return a DBus connection +* `Gio.DBusNodeInfo.new_for_xml(xmlString)` + + Return a new `Gio.DBusNodeInfo` for xmlString +* `Gio.DBusInterfaceInfo.new_for_xml(xmlString)` + + Return a new `Gio.DBusInterfaceInfo` for the first interface node of xmlString +* `Gio.DBusProxy.makeProxyWrapper(xmlString)` + + Returns a `function(busConnection, busName, objectPath, asyncCallback, cancellable)` which can be called to return a new `Gio.DBusProxy` for the first interface node of `xmlString`. See [here][old-dbus-example] for the original example. +* `Gio.DBusExportedObject.wrapJSObject(Gio.DbusInterfaceInfo, jsObj)` + + Takes `jsObj`, an object instance implementing the interface described by `Gio.DbusInterfaceInfo`, and returns an implementation object with these methods: + + * `export(busConnection, objectPath)` + * `unexport()` + * `flush()` + * `emit_signal(name, variant)` + * `emit_property_changed(name, variant)` + +[old-dbus-example]: https://wiki.gnome.org/Gjs/Examples/DBusClient + +## [GLib](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GLib.js) + +**Import with `const GLib = imports.gi.GLib;`** + +Mostly GVariant and GBytes compatibility. + +* `GLib.log_structured()`: Wrapper for g_log_variant() +* `GLib.Bytes.toArray()`: Convert a GBytes object to a ByteArray object +* `GLib.Variant.unpack()`: Unpack a variant to a native type +* `GLib.Variant.deep_unpack()`: Deep unpack a variant. + +## [GObject](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/GObject.js) + +**Import with `const GObject = imports.gi.GObject;`** + +Mostly GObject implementation (properties, signals, GType mapping). May be useful as a reference. + +## [Gtk](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/core/overrides/Gtk.js) + +**Import with `const Gtk = imports.gi.Gtk;`** + +Mostly GtkBuilder/composite template implementation. May be useful as a reference. + +>>> +**REMINDER:** You should specify a version prior to importing a library with multiple versions: + +```js +imports.gi.versions.Gtk = "3.0"; +const Gtk = imports.gi.Gtk; +``` +>>> + +## Cairo + +**Import with `const Cairo = imports.cairo;`** + +Mostly API compatible with [cairo](https://www.cairographics.org/documentation/), but using camelCase function names. There is list of constants in [cairo.js][cairo-const] and functions for each object in its corresponding C++ file (eg. [cairo-context.cpp][cairo-func]). A simple example drawing a 32x32 red circle: + +```js +imports.gi.versions.Gtk = "3.0"; +const Gtk = imports.gi.Gtk; +const Cairo = imports.cairo; + +let drawingArea = new Gtk.DrawingArea({ + height_request: 32, + width_request: 32 +}); + +drawingArea.connect("draw", (widget, cr) => { + // Cairo in GJS uses camelCase function names + cr.setSourceRGB(1.0, 0.0, 0.0); + cr.setOperator(Cairo.Operator.DEST_OVER); + cr.arc(16, 16, 16, 0, 2*Math.PI); + cr.fill(); + // currently when you connect to a draw signal you have to call + // cr.$dispose() on the Cairo context or the memory will be leaked. + cr.$dispose(); + return false; +}); +``` + +[cairo-const]: https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/cairo.js +[cairo-func]: https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/cairo-context.cpp#L825 + +## [Format](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/format.js) + +**Import with `const Format = imports.format;`** + +The format import is mostly obsolete, providing `vprintf()`, `printf()` and `format()`. Native [template literals][template-literals] should be preferred now, except in few situations like Gettext (See [Bug #50920][bug-50920]). + +```js +let foo = "Pi"; +let bar = 1; +let baz = Math.PI; + +// Using native template literals (Output: Pi to 2 decimal points: 3.14) +`${foo} to ${bar*2} decimal points: ${baz.toFixed(bar*2)}` + +// Applying format() to the string prototype +const Format = imports.format; +String.prototype.format = Format.format; + +// Using format() (Output: Pi to 2 decimal points: 3.14) +"%s to %d decimal points: %.2f".format(foo, bar*2, baz); + +// Using format() with Gettext +_("%d:%d").format(11, 59); +Gettext.ngettext("I have %d apple", "I have %d apples", num).format(num); + +``` + +[template-literals]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals +[bug-50920]: https://savannah.gnu.org/bugs/?50920 + +## [Gettext](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/gettext.js) + +**Import with `const Gettext = imports.gettext;`** + +Helper functions for gettext. See also [examples/gettext.js][example-gettext] for usage. + +[example-gettext]: https://gitlab.gnome.org/GNOME/gjs/blob/master/examples/gettext.js + +## [jsUnit](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/jsUnit.js) + +**DEPRECATED** + +Deprecated unit test functions. [Jasmine][jasmine-gjs] for GJS should now be preferred, as demonstrated in the GJS [test suite][gjs-tests]. + +[jasmine-gjs]: https://github.com/ptomato/jasmine-gjs +[gjs-tests]: https://gitlab.gnome.org/GNOME/gjs/blob/master/installed-tests/js + +## [`Lang`](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/lang.js) + +**DEPRECATED** + +Lang is a mostly obsolete library, that should only be used in cases where older versions of GJS must be supported. For example, `Lang.bind()` was necessary to bind `this` to the function context before the availability of arrow functions: + +```js +const Lang = imports.lang; +const FnorbLib = imports.fborbLib; + +const MyLegacyClass = new Lang.Class({ + _init: function() { + let fnorb = new FnorbLib.Fnorb(); + fnorb.connect('frobate', Lang.bind(this, this._onFnorbFrobate)); + }, + + _onFnorbFrobate: function(fnorb) { + this._updateFnorb(); + } +}); + +var MyNewClass = class { + constructor() { + let fnorb = new FnorbLib.Fnorb(); + fnorb.connect('frobate', fnorb => this._onFnorbFrobate); + } + + _onFnorbFrobate(fnorb) { + this._updateFnorb(); + } +} +``` + +## [Mainloop](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/mainloop.js) + +**DEPRECATED** + +Mainloop is simply a layer of convenience and backwards-compatibility over some GLib functions (such as [`GLib.timeout_add()`][gjs-timeoutadd] which in GJS is mapped to [`g_timeout_add_full()`][c-timeoutaddfull]). It's use is not generally recommended anymore. + +[c-timeoutaddfull]: https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#g-timeout-add-full +[gjs-timeoutadd]: http://devdocs.baznga.org/glib20~2.50.0/glib.timeout_add + +## [Package](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/package.js) + +Infrastructure and utilities for [standalone applications](Home#standalone-applications). + +## [Signals](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/signals.js) + +**Import with `const Signals = imports.signals;`** + +A GObject-like signal framework for native Javascript objects. + +**NOTE:** Unlike [GObject signals](Mapping#signals), `this` within a signal callback will refer to the global object (ie. `window`). + +```js +const Signals = imports.signals; + +var MyJSClass = class { + testSignalEmission () { + this.emit("exampleSignal", "stringArg", 42); + } +} +Signals.addSignalMethods(MyJSClass.prototype); + +let obj = new MyJSObject(); + +// Connect and disconnect like standard GObject signals +let handlerId = obj.connect("exampleSignal", (obj, stringArg, intArg) => { + // Remember 'this' === 'window' +}); +obj.disconnect(handlerId); + +// A convenience function not in GObject +obj.disconnectAll(); +``` + +## [System](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/system.cpp) + +**Import with `const System = imports.system;`** + +The System module offers a number of useful functions and properties for debugging and shell interaction (eg. ARGV): + + * `addressOf(object)` + + Return the memory address of any object as a string in hexadecimal, e.g. `0xb4f170f0`. + Caution, don't use this as a unique identifier! + JavaScript's garbage collector can move objects around in memory, or deduplicate identical objects, so this value may change during the execution of a program. + + * `refcount(gobject)` + + Return the reference count of any GObject-derived type (almost any class from GTK, Clutter, GLib, Gio, etc.). When an object's reference count is zero, it is cleaned up and erased from memory. + + * `breakpoint()` + + This is the real gem of the System module! It allows just the tiniest amount of decent debugging practice in GJS. Put `System.breakpoint()` in your code and run it under GDB like so: + + ``` + gdb --args gjs my_program.js + ``` + + When GJS reaches the breakpoint, it will stop executing and return you to the GDB prompt, where you can examine the stack or other things, or type `cont` to continue running. Note that if you run the program outside of GDB, it will abort at the breakpoint, so make sure to remove the breakpoint when you're done debugging. + + * `gc()` + + Run the garbage collector. + + * `exit(error_code)` + + This works the same as C's `exit()` function; exits the program, passing a certain error code to the shell. The shell expects the error code to be zero if there was no error, or non-zero (any value you please) to indicate an error. This value is used by other tools such as `make`; if `make` calls a program that returns a non-zero error code, then `make` aborts the build. + + * `version` + + This property contains version information about GJS. + + * `programInvocationName` + + This property contains the name of the script as it was invoked from the command line. In C and other languages, this information is contained in the first element of the platform's equivalent of `argv`, but GJS's `ARGV` only contains the subsequent command-line arguments, so `ARGV[0]` in GJS is the same as `argv[1]` in C. + + For example, passing ARGV to a `Gio.Application`/`Gtk.Application` (See also: + [examples/gtk-application.js][example-application]): + + ```js + imports.gi.versions.Gtk = "3.0"; + const Gtk = imports.gi.Gtk; + const System = imports.system; + + let myApp = new Gtk.Application(); + myApp.connect("activate", () => log("activated")); + myApp.run([System.programInvocationName].concat(ARGV)); + ``` + +[example-application]: https://gitlab.gnome.org/GNOME/gjs/blob/master/examples/gtk-application.js + +## [Tweener](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/tweener/) + +**Import with `const Tweener = imports.tweener.tweener;`** + +Built-in version of the well-known [Tweener][tweener-www] animation/property transition library. + +[tweener-www]: http://hosted.zeh.com.br/tweener/docs/ \ No newline at end of file diff --git a/doc/Package/Specification.md b/doc/Package/Specification.md new file mode 100644 index 0000000..39c92dd --- /dev/null +++ b/doc/Package/Specification.md @@ -0,0 +1,102 @@ +This document aims to build a set of conventions for JS applications using GJS and GObjectIntrospection. + +## Rationale + +It is believed that the current deployment facilities for GJS apps, ie autotools, [bash wrapper scripts](https://git.gnome.org/browse/gnome-documents/tree/src/gnome-documents.in) and [sed invocations](https://git.gnome.org/browse/gnome-documents/tree/src/Makefile.am#n26) represent a huge obstacle in making the GJS application platform palatable for newcomers. Additionally, the lack of standardization on the build system hinders the diffusion of pure JS utility / convenience modules. + +The goal is to create a standard packaging method for GJS, similar to Python's . + +The choice of keeping the autotools stems from the desire of integration with GNOME submodules such as libgd and egg-list-box. While those are temporary and will enter GTK in due time, it is still worthy for free software applications to be able to share submodules easily. + +Moreover, so far the autotools have the best support for generating GObjectIntrospection information, and it is sometimes necessary for JS apps to use a private helper library in a compiled language. + +## Requirements + +* Implementation details, whenever exposed to the app developers because of limitations of the underlying tools, must be copy-pastable between packages. +* The application must be fully functional when run uninstalled. In particular, it must not fail because it lacks GtkBuilder files, images, CSS or GSettings. +* The application must honor `--prefix` and `--libdir` (which must be a subdirectory of `--prefix`) at configure time. +* The application must not require more than `--prefix` and `--libdir` to work. +* The application must be installable by a regular user, provided he has write permission in `--prefix` +* The format must allow the application to be comprised of one or more JS entry points, and one or more introspection based libraries + +## Prior Art + +* [setuptools](https://pypi.python.org/pypi/setuptools) and [distutils-extra](https://launchpad.net/python-distutils-extra) (for Python) + * [Ubuntu Quickly](https://wiki.ubuntu.com/Quickly|Ubuntu Quickly) (again, for Python) + * [CommonJS package format](http://wiki.commonjs.org/wiki/Packages) (only describes the package layout, and does not provide runtime services) + * https://live.gnome.org/BuilDj (build system only) + +## Specification + +The following meta variable are used throughout this document: + +* **${package-name}**: the fully qualified ID of the package, in DBus name format. Example: org.gnome.Weather. +* **${entry-point-name}**: the fully qualified ID of an entry point, in DBus name format. Example: org.gnome.Weather.Application. This must be a sub ID of **${package-name}** +* **${entry-point-path}**: the entry point ID, converted to a DBus path in the same way GApplication does it (prepend /, replace . with /) +* **${package-tarname}**: the short, but unambiguous, short name of the package, such as gnome-weather +* **${package-version}**: the version of the package + +This specification is an addition to the Gjs style guide, and it inherits all requirements. + +## Package layout + +* The application package is expected to use autotools, or a compatible build system. In particular, it must optionally support recursive configure and recursive make. +* The following directories and files in the toplevel package must exist: + + * **src/**: contains JS modules + * **src/${entry-point-name}.src.gresource.xml**: the GResource XML for JS files for the named entry point (see below) + * **src/${entry-point-name}.src.gresource**: the compiled GResource for JS files + * **data/**: contains misc application data (CSS, GtkBuilder definitions, images...) + + * **data/${entry-point-name}.desktop**: contains the primary desktop file for the application + * *(OPTIONAL)* **data/${entry-point-name}.data.gresource**: contains the primary application resource + * *(OPTIONAL)* **data/${entry-point-name}.gschema.xml**: contains the primary GSettings schema + * *(OPTIONAL)* **data/gschemas.compiled**: compiled version of GSettings schemas in data/, for uninstalled use + * *(OPTIONAL)* **lib/**: contains sources and .la files of private shared libraries + * *(OPTIONAL)* **lib/.libs**: contains the compiled (.so) version of private libraries + * *(OPTIONAL)* another toplevel directory such as libgd or egg-list-box: same as lib/, but for shared submodules + * **po/**: contains intltool PO files and templates; the translation domain must be ${package-name} + +* The package must be installed as following: + * **${datadir}** must be configured as **${prefix}/share** + * Arch-independent private data (CSS, GtkBuilder, GResource) must be installed in **${datadir}/${package-name}**, aka **${pkgdatadir}** + + * Source files must be compiled in a GResource with path **${entry-point-path}/js**, in a bundle called **${entry-point-name}.src.gresource** installed in **${pkgdatadir}** + * Private libraries must be **${libdir}/${package-name}**, aka ${pkglibdir} + * Typelib for private libraries must be in **${pkglibdir}/girepository-1.0** + * Translations must be in **${datadir}/locale/** + * Other files (launches, GSettings schemas, icons, etc) must be in their specified locations, relative to **${prefix}** and **${datadir}** + +## Usage + +Applications complying with this specification will have one application script, installed in **${prefix}/share/${package-name}** (aka **${pkgdatadir}**), and named as **${entry-point-name}**, without any extension or mangling. + +Optionally, one or more symlinks will be placed in ${bindir}, pointing to the appropriate script in ${pkgdatadir} and named in a fashion more suitable for command line usage (usually ${package-tarname}). Alternatively, a script that calls "gapplication launch ${package-name}" can be used. + +The application itself will be DBus activated from a script called **src/${entry-point-name}**, generated from configure substitution of the following **${entry-point-name}.in**: + +```sh +#!@GJS@ +imports.package.init({ name: "${package-name}", version: "@PACKAGE_VERSION@", prefix: "@prefix@" }); +imports.package.run(${main-module}) +``` + +Where **${main-module}** is a module containing the `main()` function that will be invoked to start the process. This function should accept a single argument, an array of command line args. The first element in the array will be the full resolved path to the entry point itself (unlike the global ARGV variable for gjs). Also unlike ARGV, it is safe to modify this array. + +This `main()` function should initialize a GApplication whose id is **${entry-point-name}**, and do all the work inside the GApplication `vfunc_*` handlers. + +> **`[!]`** Users should refer to https://github.com/gcampax/gtk-js-app for a full example of the build environment. + +## Runtime support + +The following API will be available to applications, through the [`package.js`](https://gitlab.gnome.org/GNOME/gjs/blob/master/modules/script/package.js) module. + +* `window.pkg` (ie `pkg` on the global object) will provide access to the package module +* `pkg.name` and `pkg.version` will return the package name and version, as passed to `pkg.init()` +* `pkg.prefix`, `pkg.datadir`, `pkg.libdir` will return the installed locations of those folders +* `pkg.pkgdatadir`, `pkg.moduledir`, `pkg.pkglibdir`, `pkg.localedir` will return the respective directories, or the appropriate subdirectory of the current directory if running uninstalled +* `pkg.initGettext()` will initialize gettext. After calling `window._`, `window.C_` and `window.N_` will be available +* `pkg.initFormat()` will initialize the format module. After calling, String.prototype.format will be available +* `pkg.initSubmodule(name)` will initialize a submodule named @name. It must be called before accessing the typelibs installed by that submodule +* `pkg.loadResource(name)` will load and register a GResource named @name. @name is optional and defaults to ${package-name} +* `pkg.require(deps)` will mark a set of dependencies on GI and standard JS modules. **@deps** is a object whose keys are repository names and whose values are API versions. If the dependencies are not satisfied, `pkg.require()` will print an error message and quit. \ No newline at end of file diff --git a/doc/SpiderMonkey_Memory.md b/doc/SpiderMonkey_Memory.md index 5d4d072..3e30a7c 100644 --- a/doc/SpiderMonkey_Memory.md +++ b/doc/SpiderMonkey_Memory.md @@ -14,17 +14,19 @@ This is a good approach for "embeddable" interpreters, because unlike say the Bo An object has two forms. * `JS::Value` is a type-tagged version, think of `GValue` (though it is much more efficient) -* inside a `JS::Value` can be one of: a 32-bit integer, a boolean, a double, a `JSString*`, or a `JSObject*`. +* inside a `JS::Value` can be one of: a 32-bit integer, a boolean, a double, a `JSString*`, a `JS::Symbol*`, or a `JSObject*`. `JS::Value` is a 64 bits-wide union. Some of the bits are a type tag. However, don't rely on the layout of `JS::Value`, as it may change between API versions. -You check the type tag with the methods `val.isObject()`, `val.isInt32()`, `val.isDouble()`, `val.isString()`, `val.isBoolean()`. Use `val.isNull()` and `val.isUndefined()` rather than comparing `val == JSVAL_NULL` and `val == JSVAL_VOID` to avoid an extra memory access. +You check the type tag with the methods `val.isObject()`, `val.isInt32()`, `val.isDouble()`, `val.isString()`, `val.isBoolean()`, `val.isSymbol()`. +Use `val.isNull()` and `val.isUndefined()` rather than comparing `val == JSVAL_NULL` and `val == JSVAL_VOID` to avoid an extra memory access. null does not count as an object, so `val.isObject()` does not return true for null. This contrasts with the behavior of `JSVAL_IS_OBJECT(val)`, which was the previous API, but this was changed because the object-or-null behavior was a source of bugs. If you still want this behaviour use `val.isObjectOrNull()`. The methods `val.toObject()`, `val.toInt32()`, etc. are just accessing the appropriate members of the union. -The jsapi.h header is pretty readable, if you want to learn more. Types you see in there not mentioned above, such as `JSFunction*`, would show up as an object - `val.isObject()` would return true. From a `JS::Value` perspective, everything is one of object, string, double, int, boolean, null, or undefined. +The jsapi.h header is pretty readable, if you want to learn more. Types you see in there not mentioned above, such as `JSFunction*`, would show up as an object - `val.isObject()` would return true. +From a `JS::Value` perspective, everything is one of object, string, symbol, double, int, boolean, null, or undefined. ## Value types vs. allocated types; "gcthing" ## @@ -32,17 +34,21 @@ For integers, booleans, doubles, null, and undefined there is no pointer. The va The importance is: these types just get ignored by the garbage collector. -However, strings and objects are all allocated pointers that get finalized eventually. These are what garbage collection applies to. +However, strings, symbols, and objects are all allocated pointers that get finalized eventually. +These are what garbage collection applies to. -The API refers to these allocated types as "GC things." The macro `val.toGCThing()` returns the value part of the union as a pointer. `val.isGCThing()` returns true for string, object, null; and false for void, boolean, double, integer. +The API refers to these allocated types as "GC things." +The macro `val.toGCThing()` returns the value part of the union as a pointer. +`val.isGCThing()` returns true for string, object, symbol, null; and false for void, boolean, double, integer. ## Tracing ## The general rule is that SpiderMonkey has a set of GC roots. To do the garbage collection, it finds all objects accessible from those roots, and finalizes all objects that are not. -So if you have a `JS::Value` or `JSObject*`/`JSString*`/`JSFunction*` somewhere that is not reachable from one of SpiderMonkey's GC roots - say, declared on the stack or in the private data of an object - that will not be found. SpiderMonkey may try to finalize this object even though you have a reference to it. +So if you have a `JS::Value` or `JSObject*`/`JSString*`/`JSFunction*`/`JS::Symbol*` somewhere that is not reachable from one of SpiderMonkey's GC roots - say, declared on the stack or in the private data of an object - that will not be found. +SpiderMonkey may try to finalize this object even though you have a reference to it. -If you reference JavaScript objects from your custom object, you have to use `JS::Heap` and set the `JSCLASS_MARK_IS_TRACE` flag in your JSClass, and define a trace function in the class struct. A trace function just invokes `JS_CallHeapValueTracer()`, `JS_CallHeapObjectTracer()`, etc. to tell SpiderMonkey about any objects you reference. See [JSTraceOp docs][2]. +If you reference JavaScript objects from your custom object, you have to use `JS::Heap` and set the `JSCLASS_MARK_IS_TRACE` flag in your JSClass, and define a trace function in the class struct. A trace function just invokes `JS::TraceEdge()` to tell SpiderMonkey about any objects you reference. See [JSTraceOp docs][2]. Tracing doesn't add a GC thing to the GC root set! It just notifies the interpreter that a thing is reachable from another thing. diff --git a/doc/Style_Guide.md b/doc/Style_Guide.md index 1a91457..908fefc 100644 --- a/doc/Style_Guide.md +++ b/doc/Style_Guide.md @@ -4,15 +4,22 @@ Our goal is to have all JavaScript code in GNOME follow a consistent style. In a JavaScript, it is essential to be rigorous about style (and unit tests), or you rapidly end up with a spaghetti-code mess. -## Semicolons ## +## Linter ## -JavaScript allows omitting semicolons at the end of lines, but don't. Always end -statements with a semicolon. +GJS includes an eslint configuration file, `.eslintrc.yml`. +There is an additional one that applies to test code in +`installed-tests/js/.eslintrc.yml`. +We recommend using this for your project, with any modifications you +need that are particular to your project. -## js2-mode ## +In most editors you can set up eslint to run on your code as you type. +Or you can set it up as a git commit hook. +In any case if you contribute code to GJS, eslint will check the code in +your merge request. -If using Emacs, try js2-mode. It functions as a "lint" by highlighting missing semicolons -and the like. +The style guide for JS code in GJS is, by definition, the eslint config +file. +This file only contains conventions that the linter can't catch. ## Imports ## @@ -20,65 +27,68 @@ Use CamelCase when importing modules to distinguish them from ordinary variables ```js const Big = imports.big; -const GLib = imports.gi.GLib; +const {GLib} = imports.gi; ``` ## Variable declaration ## -Always use one of `const`, `var`, or `let` when defining a variable. Always use `let` when block scope is intended; in particular, inside `for()` and `while()` loops, `let` is almost always correct. +Always use `const` or `let` when block scope is intended. +In almost all cases `const` is correct if you don't reassign the +variable, and otherwise `let`. +In general `var` is only needed for variables that you are exporting +from a module. ```js // Iterating over an array for (let i = 0; i < 10; ++i) { - let foo = bar(i); + let foo = bar(i); } // Iterating over an object's properties for (let prop in someobj) { - ... + ... } ``` -If you don't use `let` then the variable is added to function scope, not the for loop block scope. +If you don't use `let` or `const` then the variable is added to function +scope, not the for loop block scope. See [What's new in JavaScript 1.7][1] A common case where this matters is when you have a closure inside a loop: ```js for (let i = 0; i < 10; ++i) { - mainloop.idle_add(function() { log("number is: " + i); }); + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, function () { + log(`number is: ${i}`); + }); } ``` If you used `var` instead of `let` it would print "10" a bunch of times. -Inside functions, `let` is always correct instead of `var` as far as we know. `var` is useful when you want to add something to the `with()` object, though... in particular we think you need `var` to define module variables, since our module system loads modules with the equivalent of `with (moduleObject)` - ## `this` in closures ## `this` will not be captured in a closure; `this` is relative to how the closure is invoked, not to the value of this where the closure is created, because `this` is a keyword with a value passed in at function invocation time, it is not a variable that can be captured in closures. -To solve this, use `Function.bind()`, or fat arrow functions, e.g.: +To solve this, use `Function.bind()`, or arrow functions, e.g.: ```js -let closure = function() { this._fnorbate() }.bind(this); +const closure = () => { this._fnorbate(); }; // or -let closure = () => { this._fnorbate(); }; +const closure = function() { this._fnorbate() }.bind(this); ``` A more realistic example would be connecting to a signal on a method of a prototype: ```js -const Lang = imports.lang; - -MyPrototype = { - _init : function() { - fnorb.connect('frobate', this._onFnorbFrobate.bind(this)); +const MyPrototype = { + _init() { + fnorb.connect('frobate', this._onFnorbFrobate.bind(this)); }, - _onFnorbFrobate : function(fnorb) { - this._updateFnorb(); + _onFnorbFrobate(fnorb) { + this._updateFnorb(); }, }; ``` @@ -87,18 +97,18 @@ MyPrototype = { JavaScript allows equivalently: ```js -foo = { 'bar' : 42 }; -foo = { bar: 42 }; +const foo = {'bar': 42}; +const foo = {bar: 42}; ``` and ```js -var b = foo['bar']; -var b = foo.bar; +const b = foo['bar']; +const b = foo.bar; ``` -If your usage of an object is like an object, then you're defining "member variables." For member variables, use the no-quotes no-brackets syntax, that is, `{ bar: 42 }` and `foo.bar`. +If your usage of an object is like an object, then you're defining "member variables." For member variables, use the no-quotes no-brackets syntax, that is, `{bar: 42}` and `foo.bar`. -If your usage of an object is like a hash table (and thus conceptually the keys can have special chars in them), don't use quotes, but use brackets, `{ bar: 42 }`, `foo['bar']`. +If your usage of an object is like a hash table (and thus conceptually the keys can have special chars in them), don't use quotes, but use brackets, `{bar: 42}`, `foo['bar']`. ## Variable naming ## @@ -107,30 +117,5 @@ If your usage of an object is like a hash table (and thus conceptually the keys - True global variables (in the global or 'window' object) should be avoided whenever possible. If you do create them, the variable name should have a namespace in it, like `BigFoo` - When you assign a module to an alias to avoid typing `imports.foo.bar` all the time, the alias should be `const TitleCase` so `const Bar = imports.foo.bar;` - If you need to name a variable something weird to avoid a namespace collision, add a trailing `_` (not leading, leading `_` means private). -- For GObject constructors, always use the `lowerCamelCase` style for property names instead of dashes or underscores. - -## Whitespace ## - -* 4-space indentation (the Java style) -* No trailing whitespace. -* No tabs. -* If you `chmod +x .git/hooks/pre-commit` it will not let you commit with messed-up whitespace (well, it doesn't catch tabs. turn off tabs in your text editor.) - -## JavaScript attributes ## - -Don't use the getter/setter syntax when getting and setting has side effects, that is, the code: -```js -foo.bar = 10; -``` -should not do anything other than save "10" as a property of `foo`. It's obfuscated otherwise; if the setting has side effects, it's better if it looks like a method. - -In practice this means the only use of attributes is to create read-only properties: -```js -get bar() { - return this._bar; -} -``` - -If the property requires a setter, or if getting it has side effects, methods are probably clearer. [1] http://developer.mozilla.org/en/docs/index.php?title=New_in_JavaScript_1.7&printable=yes#Block_scope_with_let diff --git a/doc/Understanding-SpiderMonkey-code.md b/doc/Understanding-SpiderMonkey-code.md new file mode 100644 index 0000000..2742cba --- /dev/null +++ b/doc/Understanding-SpiderMonkey-code.md @@ -0,0 +1,23 @@ +Basics +------ + +- SpiderMonkey is the Javascript engine from Mozilla Firefox. It's also known as "mozjs" in most Linux distributions, and sometimes as "JSAPI" in code. +- Like most browsers' JS engines, SpiderMonkey works standalone, which is what allows GJS to work. In Mozilla terminology, this is known as "embedding", and GJS is an "embedder." +- Functions that start with `JS_` or `JS::`, or types that start with `JS`, are part of the SpiderMonkey API. +- Functions that start with `js_` or `js::` are part of the "JS Friend" API, which is a section of the SpiderMonkey API which is supposedly less stable. (Although SpiderMonkey isn't API stable in the first place.) +- We use the SpiderMonkey from the ESR (Extended Support Release) of Firefox. These ESRs are released approximately once a year. +- Since ESR 24, the SpiderMonkey team has gotten sloppy about making official releases of standalone SpiderMonkey. (Arguably, that was because nobody, including us, was using them.) We had high hopes for an official release of ESR 52, but there were some problems that couldn't be fixed on the ESR branch. The SpiderMonkey team will likely make an official release of ESR 60, but they may need some reminders when the time comes. +- When reading GJS code, to quickly find out what a SpiderMonkey API function does, you can go to https://searchfox.org/ and search for it. This is literally faster than opening `jsapi.h` in your editor, and you can click through to other functions, and find everywhere a function is used. +- Don't trust the wiki on MDN as documentation for SpiderMonkey, as it is mostly out of date and can be quite misleading. + +Coding conventions +------------------ + +- Most API functions take a `JSContext *` as their first parameter. This context contains the state of the JS engine. +- `cx` stands for "context." +- Many API functions return a `bool`. As in many other APIs, these should return `true` for success and `false` for failure. +- Specific to SpiderMonkey is the convention that if an API function returns `false`, an exception should have been thrown (a JS exception, not a C++ exception, which would terminate the program!) This is also described as "an exception should be _pending_ on `cx`". Likewise, if the function returns `true`, an exception should not be pending. +- There are two ways to violate that condition: + - Returning `false` with no exception pending. This is interpreted as an "uncatchable" exception, and it's used for out-of-memory and killing scripts within Firefox, for example. In GJS we use it to implement `System.exit()`. + - Returning `true` while an exception is pending. This can easily happen by forgetting to check the return value of a SpiderMonkey function, and is a programmer error but not too serious. It will probably cause some warnings. +- Likewise if an API function returns a pointer such as `JSObject*` (this is less common), the convention is that it should return `nullptr` on failure, in which case an exception should be pending. \ No newline at end of file diff --git a/examples/README b/examples/README index 44143e6..9fa5166 100644 --- a/examples/README +++ b/examples/README @@ -1,3 +1,3 @@ In order to run those example scripts, do: - gjs-console script-filename.js + cjs-console script-filename.js diff --git a/examples/calc.js b/examples/calc.js new file mode 100644 index 0000000..87e21b6 --- /dev/null +++ b/examples/calc.js @@ -0,0 +1,143 @@ +#!/usr/bin/env gjs + +imports.gi.versions.Gtk = '3.0'; +const {Gtk} = imports.gi; + +Gtk.init(null); + +var calcVal = ''; + +function updateDisplay() { + label.set_markup(`${calcVal}`); + + if (calcVal === '') + label.set_markup("0"); +} + +function clear() { + calcVal = ''; + updateDisplay(); +} + +function backspace() { + calcVal = calcVal.substring(0, calcVal.length - 1); + updateDisplay(); +} + +function pressedEquals() { + calcVal = calcVal.replace('sin', 'Math.sin'); + calcVal = calcVal.replace('cos', 'Math.cos'); + calcVal = calcVal.replace('tan', 'Math.tan'); + calcVal = eval(calcVal); + // Avoid ridiculous amounts of precision from toString. + if (calcVal === Math.floor(calcVal)) + calcVal = Math.floor(calcVal); + else // bizarrely gjs loses str.toFixed() somehow?! + calcVal = Math.floor(calcVal * 10000) / 10000; + label.set_markup(`${calcVal}`); +} + +function pressedOperator(button) { + calcVal += button.label; + updateDisplay(); +} + +function pressedNumber(button) { + calcVal = (calcVal === 0 ? '' : calcVal) + button.label; + updateDisplay(); +} + +function swapSign() { + calcVal = calcVal[0] === '-' ? calcVal.substring(1) : `-${calcVal}`; + updateDisplay(); +} + +function randomNum() { + calcVal = `${Math.floor(Math.random() * 1000)}`; + updateDisplay(); +} + +function packButtons(buttons, vbox) { + var hbox = new Gtk.HBox(); + + hbox.homogeneous = true; + + vbox.pack_start(hbox, true, true, 2); + + for (let i = 0; i <= 4; i++) + hbox.pack_start(buttons[i], true, true, 1); +} + +function createButton(str, func) { + var btn = new Gtk.Button({label: str}); + btn.connect('clicked', func); + return btn; +} + +function createButtons() { + var vbox = new Gtk.VBox({homogeneous: true}); + + packButtons([ + createButton('(', pressedNumber), + createButton('←', backspace), + createButton('↻', randomNum), + createButton('Clr', clear), + createButton('±', swapSign), + ], vbox); + + packButtons([ + createButton(')', pressedNumber), + createButton('7', pressedNumber), + createButton('8', pressedNumber), + createButton('9', pressedNumber), + createButton('/', pressedOperator), + ], vbox); + + packButtons([ + createButton('sin(', pressedNumber), + createButton('4', pressedNumber), + createButton('5', pressedNumber), + createButton('6', pressedNumber), + createButton('*', pressedOperator), + ], vbox); + + packButtons([ + createButton('cos(', pressedNumber), + createButton('1', pressedNumber), + createButton('2', pressedNumber), + createButton('3', pressedNumber), + createButton('-', pressedOperator), + ], vbox); + + packButtons([ + createButton('tan(', pressedNumber), + createButton('0', pressedNumber), + createButton('.', pressedNumber), + createButton('=', pressedEquals), + createButton('+', pressedOperator), + ], vbox); + + return vbox; +} + +var win = new Gtk.Window({ + title: 'Calculator', + resizable: false, + opacity: 0.6, +}); + +win.resize(250, 250); +win.connect('destroy', () => Gtk.main_quit()); + +var label = new Gtk.Label({label: ''}); +label.set_alignment(1, 0); +updateDisplay(); + +var mainvbox = new Gtk.VBox(); +mainvbox.pack_start(label, false, true, 1); +mainvbox.pack_start(new Gtk.HSeparator(), false, true, 5); +mainvbox.pack_start(createButtons(), true, true, 2); + +win.add(mainvbox); +win.show_all(); +Gtk.main(); diff --git a/examples/clutter.js b/examples/clutter.js index a6b50ed..4a15669 100644 --- a/examples/clutter.js +++ b/examples/clutter.js @@ -4,14 +4,15 @@ Clutter.init(null); let stage = new Clutter.Stage(); -let texture = new Clutter.Texture({ filename: 'test.jpg', - reactive: true }); - -texture.connect('button-press-event', - function(o, event) { - log('Clicked!'); - return true; - }); +let texture = new Clutter.Texture({ + filename: 'test.jpg', + reactive: true, +}); + +texture.connect('button-press-event', () => { + log('Clicked!'); + return Clutter.EVENT_STOP; +}); let color = new Clutter.Color(); color.from_string('Black'); diff --git a/examples/dbus-client.js b/examples/dbus-client.js new file mode 100644 index 0000000..7cedf69 --- /dev/null +++ b/examples/dbus-client.js @@ -0,0 +1,167 @@ +'use strict'; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + + +/* + * An XML DBus Interface + */ +const ifaceXml = ` + + + + + + + + + + + + + + +`; + + + +// Pass the XML string to make a re-usable proxy class for an interface proxies. +const TestProxy = Gio.DBusProxy.makeProxyWrapper(ifaceXml); + + +let proxy = null; +let proxySignalId = 0; +let proxyPropId = 0; + + +// Watching a name on DBus. Another option is to create a proxy with the +// `Gio.DBusProxyFlags.DO_NOT_AUTO_START` flag and watch the `g-name-owner` +// property. +function onNameAppeared(connection, name, _owner) { + print(`"${name}" appeared on the session bus`); + + // If creating a proxy synchronously, errors will be thrown as normal + try { + proxy = new TestProxy( + Gio.DBus.session, + 'org.gnome.gjs.Test', + '/org/gnome/gjs/Test' + ); + } catch (e) { + logError(e); + return; + } + + + // Proxy wrapper signals use the special functions `connectSignal()` and + // `disconnectSignal()` to avoid conflicting with regular GObject signals. + proxySignalId = proxy.connectSignal('TestSignal', (proxy_, name_, args) => { + print(`TestSignal: ${args[0]}, ${args[1]}`); + }); + + + // To watch property changes, you can connect to the `g-properties-changed` + // GObject signal with `connect()` + proxyPropId = proxy.connect('g-properties-changed', (proxy_, changed, invalidated) => { + for (let [prop, value] of Object.entries(changed.deepUnpack())) + print(`Property '${prop}' changed to '${value.deepUnpack()}'`); + + for (let prop of invalidated) + print(`Property '${prop}' invalidated`); + }); + + + // Reading and writing properties is straight-forward + print(`ReadOnlyProperty: ${proxy.ReadOnlyProperty}`); + + print(`ReadWriteProperty: ${proxy.ReadWriteProperty}`); + + proxy.ReadWriteProperty = !proxy.ReadWriteProperty; + print(`ReadWriteProperty: ${proxy.ReadWriteProperty}`); + + + // Both synchronous and asynchronous functions will be generated + try { + let value = proxy.SimpleMethodSync(); + + print(`SimpleMethod: ${value}`); + } catch (e) { + logError(`SimpleMethod: ${e.message}`); + } + + proxy.ComplexMethodRemote('input string', (value, error, fdList) => { + + // If @error is not `null`, then an error occurred + if (error !== null) { + logError(error); + return; + } + + print(`ComplexMethod: ${value}`); + + // Methods that return file descriptors are fairly rare, so you should + // know to expect one or not. + if (fdList !== null) { + // + } + }); +} + +function onNameVanished(connection, name) { + print(`"${name}" vanished from the session bus`); + + if (proxy !== null) { + proxy.disconnectSignal(proxySignalId); + proxy.disconnect(proxyPropId); + proxy = null; + } +} + +let busWatchId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.gjs.Test', + Gio.BusNameWatcherFlags.NONE, + onNameAppeared, + onNameVanished +); + +// Start an event loop +let loop = GLib.MainLoop.new(null, false); +loop.run(); + +// Unwatching names works just like disconnecting signal handlers. +Gio.bus_unown_name(busWatchId); + + +/* Asynchronous Usage + * + * Below is the alternative, asynchronous usage of proxy wrappers. If creating + * a proxy asynchronously, you should not consider the proxy ready to use until + * the callback is invoked without error. + */ +proxy = null; + +new TestProxy( + Gio.DBus.session, + 'org.gnome.gjs.Test', + '/org/gnome/gjs/Test', + (sourceObj, error) => { + // If @error is not `null` it will be an Error object indicating the + // failure. @proxy will be `null` in this case. + if (error !== null) { + logError(error); + return; + } + + // At this point the proxy is initialized and you can start calling + // functions, using properties and so on. + proxy = sourceObj; + print(`ReadOnlyProperty: ${proxy.ReadOnlyProperty}`); + }, + // Optional Gio.Cancellable object. Pass `null` if you need to pass flags. + null, + // Optional flags passed to the Gio.DBusProxy constructor + Gio.DBusProxyFlags.NONE +); + diff --git a/examples/dbus-service.js b/examples/dbus-service.js new file mode 100644 index 0000000..493ebe5 --- /dev/null +++ b/examples/dbus-service.js @@ -0,0 +1,137 @@ +'use strict'; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + + +/* + * An XML DBus Interface + */ +const ifaceXml = ` + + + + + + + + + + + + + + +`; + + +// An example of the service-side implementation of the above interface. +class Service { + + constructor() { + this.dbus = Gio.DBusExportedObject.wrapJSObject(ifaceXml, this); + } + + // Properties + get ReadOnlyProperty() { + return 'a string'; + } + + get ReadWriteProperty() { + if (this._readWriteProperty === undefined) + return false; + + return this._readWriteProperty; + } + + set ReadWriteProperty(value) { + if (this.ReadWriteProperty !== value) { + this._readWriteProperty = value; + + // Emitting property changes over DBus + this.dbus.emit_property_changed( + 'ReadWriteProperty', + new GLib.Variant('b', value) + ); + } + } + + // Methods + SimpleMethod() { + print('SimpleMethod() invoked'); + } + + ComplexMethod(input) { + print(`ComplexMethod() invoked with "${input}"`); + + return input.length; + } + + // Signals + emitTestSignal() { + this.dbus.emit_signal( + 'TestSignal', + new GLib.Variant('(sb)', ['string', false]) + ); + } +} + + +// Once you've created an instance of your service, you will want to own a name +// on the bus so clients can connect to it. +let serviceObj = new Service(); +let serviceSignalId = 0; + + +function onBusAcquired(connection, _name) { + // At this point you have acquired a connection to the bus, and you should + // export your interfaces now. + serviceObj.dbus.export(connection, '/org/gnome/gjs/Test'); +} + +function onNameAcquired(_connection, _name) { + // Clients will typically start connecting and using your interface now. + + // Emit the TestSignal every few seconds + serviceSignalId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => { + serviceObj.emitTestSignal(); + + return GLib.SOURCE_CONTINUE; + }); +} + +function onNameLost(_connection, _name) { + // Clients will know not to call methods on your interface now. Usually this + // callback will only be invoked if you try to own a name on DBus that + // already has an owner. + + // Stop emitting the test signal + if (serviceSignalId > 0) { + GLib.Source.remove(serviceSignalId); + serviceSignalId = 0; + } +} + +let ownerId = Gio.bus_own_name( + Gio.BusType.SESSION, + 'org.gnome.gjs.Test', + Gio.BusNameOwnerFlags.NONE, + onBusAcquired, + onNameAcquired, + onNameLost +); + + +// Start an event loop +let loop = GLib.MainLoop.new(null, false); +loop.run(); + +// Unowning names works just like disconnecting, but note that `onNameLost()` +// will not be invoked in this case. +Gio.bus_unown_name(ownerId); + +if (serviceSignalId > 0) { + GLib.Source.remove(serviceSignalId); + serviceSignalId = 0; +} + diff --git a/examples/gettext.js b/examples/gettext.js index 3c67be7..25236eb 100644 --- a/examples/gettext.js +++ b/examples/gettext.js @@ -2,13 +2,13 @@ imports.gi.versions.Gtk = '3.0'; const Gettext = imports.gettext; const Gtk = imports.gi.Gtk; -Gettext.bindtextdomain("gnome-panel-3.0", "/usr/share/locale"); -Gettext.textdomain("gnome-panel-3.0"); +Gettext.bindtextdomain('gnome-panel-3.0', '/usr/share/locale'); +Gettext.textdomain('gnome-panel-3.0'); Gtk.init(null); -let w = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL }); -w.add(new Gtk.Label({ label: Gettext.gettext("Panel") })); +let w = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); +w.add(new Gtk.Label({label: Gettext.gettext('Panel')})); w.show_all(); Gtk.main(); diff --git a/examples/gio-cat.js b/examples/gio-cat.js index f26e710..b80af3b 100644 --- a/examples/gio-cat.js +++ b/examples/gio-cat.js @@ -1,4 +1,5 @@ +const ByteArray = imports.byteArray; const GLib = imports.gi.GLib; const Gio = imports.gi.Gio; @@ -6,24 +7,23 @@ let loop = GLib.MainLoop.new(null, false); function cat(filename) { let f = Gio.file_new_for_path(filename); - f.load_contents_async(null, function(f, res) { + f.load_contents_async(null, (obj, res) => { let contents; try { - contents = f.load_contents_finish(res)[1]; + contents = obj.load_contents_finish(res)[1]; } catch (e) { - log("*** ERROR: " + e.message); + logError(e); loop.quit(); return; } - print(contents); + print(ByteArray.toString(contents)); loop.quit(); }); loop.run(); } -if (ARGV.length != 1) { - printerr("Usage: gio-cat.js filename"); -} else { +if (ARGV.length !== 1) + printerr('Usage: gio-cat.js filename'); +else cat(ARGV[0]); -} diff --git a/examples/glistmodel.js b/examples/glistmodel.js new file mode 100644 index 0000000..4a04e77 --- /dev/null +++ b/examples/glistmodel.js @@ -0,0 +1,137 @@ +/* exported GjsListStore */ + +'use strict'; + +const GObject = imports.gi.GObject; +const Gio = imports.gi.Gio; + + + +/** + * An example of implementing the GListModel interface in GJS. The only real + * requirement here is that the class be derived from some GObject. + */ +var GjsListStore = GObject.registerClass({ + GTypeName: 'GjsListStore', + Implements: [Gio.ListModel], +}, class MyList extends GObject.Object { + _init() { + super._init(); + + /* We'll use a native Array as internal storage for the list model */ + this._items = []; + } + + /* Implementing this function amounts to returning a GType. This could be a + * more specific GType, but must be a subclass of GObject. */ + vfunc_get_item_type() { + return GObject.Object.$gtype; + } + + /* Implementing this function just requires returning the GObject at + * @position or %null if out-of-range. This must explicitly return %null, + * not `undefined`. */ + vfunc_get_item(position) { + return this._items[position] || null; + } + + /* Implementing this function is as simple as return the length of the + * storage object, in this case an Array. */ + vfunc_get_n_items() { + return this._items.length; + } + + /** + * Insert an item in the list. If @position is greater than the number of + * items in the list or less than `0` it will be appended to the end of the + * list. + * + * @param {GObject.Object} item - the item to add + * @param {number} position - the position to add the item + */ + insertItem(item, position) { + if (!(item instanceof GObject.Object)) + throw new TypeError('not a GObject'); + + if (position < 0 || position > this._items.length) + position = this._items.length; + + this._items.splice(position, 0, item); + this.items_changed(position, 0, 1); + } + + /** + * Append an item to the list. + * + * @param {GObject.Object} item - the item to add + */ + appendItem(item) { + if (!(item instanceof GObject.Object)) + throw new TypeError('not a GObject'); + + let position = this._items.length; + + this._items.push(item); + this.items_changed(position, 0, 1); + } + + /** + * Prepend an item to the list. + * + * @param {GObject.Object} item - the item to add + */ + prependItem(item) { + if (!(item instanceof GObject.Object)) + throw new TypeError('not a GObject'); + + this._items.unshift(item); + this.items_changed(0, 0, 1); + } + + /** + * Remove @item from the list. If @item is not in the list, this function + * does nothing. + * + * @param {GObject.Object} item - the item to remove + */ + removeItem(item) { + if (!(item instanceof GObject.Object)) + throw new TypeError('not a GObject'); + + let position = this._items.indexOf(item); + + if (position === -1) + return; + + this._items.splice(position, 1); + this.items_changed(position, 1, 0); + } + + /** + * Remove the item at @position. If @position is outside the length of the + * list, this function does nothing. + * + * @param {number} position - the position of the item to remove + */ + removePosition(position) { + if (position < 0 || position >= this._items.length) + return; + + this._items.splice(position, 1); + this.items_changed(position, 1, 0); + } + + /** + * Clear the list of all items. + */ + clear() { + let length = this._items.length; + + if (length === 0) + return; + + this._items = []; + this.items_changed(0, length, 0); + } +}); + diff --git a/examples/gtk-application.js b/examples/gtk-application.js index 65084c1..e8ea05a 100644 --- a/examples/gtk-application.js +++ b/examples/gtk-application.js @@ -3,7 +3,7 @@ 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"; +imports.gi.versions.Gtk = '3.0'; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; @@ -15,109 +15,92 @@ const Gtk = imports.gi.Gtk; // 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 + 'exampleprop': GObject.ParamSpec.string( + 'exampleprop', // property name + 'ExampleProperty', // nickname + 'An example read write property', // description GObject.ParamFlags.READWRITE, // read/write/construct... - "" // implement defaults manually - ) + 'a default value' + ), }, - Signals: { "examplesig": { param_types: [ GObject.TYPE_INT ] } }, + 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 + 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); + emitExamplesig(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") + name: 'exampleAction', + parameter_type: new GLib.VariantType('s'), }); - - exampleAction.connect("activate", (action, param) => { - param = param.deep_unpack().toString(); - - if (param === "exampleParameter") { - log("Yes!"); - } + + exampleAction.connect('activate', (action, param) => { + param = param.deepUnpack().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", + title: 'Example Application Window', default_width: 300, - default_height: 200 + default_height: 200, }); - - let label = new Gtk.Label({ label: this.exampleprop }); + + let label = new Gtk.Label({label: this.exampleprop}); window.add(label); - - window.connect("delete-event", () => { + + 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_title('Example Notification'); + notif.set_body('Example Body'); notif.set_icon( - new Gio.ThemedIcon({ name: "dialog-information-symbolic" }) + 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", + '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); - + this.send_notification('example-notification', notif); + // Withdraw - this.withdraw_notification("example-notification"); + this.withdraw_notification('example-notification'); } }); diff --git a/examples/gtk.js b/examples/gtk.js index ec3a29d..e420574 100644 --- a/examples/gtk.js +++ b/examples/gtk.js @@ -1,30 +1,30 @@ // Include this in case both GTK3 and GTK4 installed, otherwise an exception // will be thrown -imports.gi.versions.Gtk = "3.0"; +imports.gi.versions.Gtk = '3.0'; const Gtk = imports.gi.Gtk; // Initialize Gtk before you start calling anything from the import Gtk.init(null); // Construct a top-level window -let window = new Gtk.Window ({ +let win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL, - title: "A default title", + 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 + 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!"; +win.title = 'Hello World!'; // This is a callback function -function onDeleteEvent(widget, event) { - log("delete-event emitted"); +function onDeleteEvent() { + log('delete-event emitted'); // If you return false in the "delete_event" signal handler, Gtk will emit // the "destroy" signal. // @@ -36,37 +36,37 @@ function onDeleteEvent(widget, event) { // 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); +win.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", () => { +win.connect('destroy', () => { Gtk.main_quit(); }); // Create a button to close the window let button = new Gtk.Button({ - label: "Close the Window", + 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 + halign: Gtk.Align.CENTER, }); // Connect to the 'clicked' signal, using another way to call an arrow function -button.connect("clicked", () => window.destroy()); +button.connect('clicked', () => win.destroy()); // Add the button to the window -window.add(button); +win.add(button); // Show the window -window.show(); +win.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 diff --git a/examples/http-client.js b/examples/http-client.js new file mode 100644 index 0000000..d6f863e --- /dev/null +++ b/examples/http-client.js @@ -0,0 +1,53 @@ +// This is a simple example of a HTTP client in Gjs using libsoup +// https://developer.gnome.org/libsoup/stable/libsoup-client-howto.html + +const Soup = imports.gi.Soup; +const GLib = imports.gi.GLib; +const byteArray = imports.byteArray; + +const loop = GLib.MainLoop.new(null, false); + +const session = new Soup.Session(); +const message = new Soup.Message({ + method: 'GET', + uri: Soup.URI.new('http://localhost:1080/hello?myname=gjs'), +}); + +session.send_async(message, null, send_async_callback); + +function read_bytes_async_callback(inputStream, res) { + let data; + + try { + data = inputStream.read_bytes_finish(res); + } catch (e) { + logError(e); + loop.quit(); + return; + } + + log(`body:\n${byteArray.toString(byteArray.fromGBytes(data))}`); + + loop.quit(); +} + +function send_async_callback(self, res) { + let inputStream; + + try { + inputStream = session.send_finish(res); + } catch (e) { + logError(e); + loop.quit(); + return; + } + + log(`status: ${message.status_code} - ${message.reason_phrase}`); + message.response_headers.foreach((name, value) => { + log(`${name}: ${value}`); + }); + + inputStream.read_bytes_async(message.response_headers.get('content-length'), null, null, read_bytes_async_callback); +} + +loop.run(); diff --git a/examples/http-server.js b/examples/http-server.js index 36c73f0..0e8aede 100644 --- a/examples/http-server.js +++ b/examples/http-server.js @@ -2,24 +2,42 @@ const Soup = imports.gi.Soup; -function main() { - let handler = function(server, msg, path, query, client) { - msg.status_code = 200; - msg.response_headers.set_content_type('text/html', {}); - msg.response_body.append('Greetings, visitor from ' + client.get_host() + '
What is your name?
\n'); - }; - let helloHandler = function(server, msg, path, query, client) { - if (!query) { - msg.set_redirect(302, '/'); - return; - } +function handler(server, msg, path, query, client) { + msg.status_code = 200; + msg.response_headers.set_content_type('text/html', {}); + msg.response_body.append(` + + + Greetings, visitor from ${client.get_host()}
+ What is your name? +
+ +
+ + + `); +} - msg.status_code = 200; - msg.response_headers.set_content_type('text/html', { charset: 'UTF-8' }); - msg.response_body.append('Hello, ' + query.myname + '! \u263A
Go back'); - }; +function helloHandler(server, msg, path, query) { + if (!query) { + msg.set_redirect(302, '/'); + return; + } - let server = new Soup.Server({ port: 1080 }); + msg.status_code = 200; + msg.response_headers.set_content_type('text/html', {charset: 'UTF-8'}); + msg.response_body.append(` + + + Hello, ${query.myname}! ☺
+ Go back + + + `); +} + +function main() { + let server = new Soup.Server({port: 1080}); server.add_handler('/', handler); server.add_handler('/hello', helloHandler); server.run(); diff --git a/examples/webkit.js b/examples/webkit.js index 04e61d7..f3c117a 100644 --- a/examples/webkit.js +++ b/examples/webkit.js @@ -8,9 +8,13 @@ Gtk.init(null); let win = new Gtk.Window(); let view = new WebKit.WebView(); -view.load_uri("http://www.google.com/"); +view.load_uri('http://www.google.com/'); win.add(view); +win.connect('destroy', () => { + Gtk.main_quit(); +}); + win.set_size_request(640, 480); win.show_all(); diff --git a/examples/websocket-client.js b/examples/websocket-client.js new file mode 100644 index 0000000..84ca48a --- /dev/null +++ b/examples/websocket-client.js @@ -0,0 +1,52 @@ +// This is an example of a WebSocket client in Gjs using libsoup +// https://developer.gnome.org/libsoup/stable/libsoup-2.4-WebSockets.html + +const Soup = imports.gi.Soup; +const GLib = imports.gi.GLib; +const byteArray = imports.byteArray; + +const loop = GLib.MainLoop.new(null, false); + +const session = new Soup.Session(); +const message = new Soup.Message({ + method: 'GET', + uri: Soup.URI.new('wss://echo.websocket.org'), +}); + +session.websocket_connect_async(message, 'origin', [], null, websocket_connect_async_callback); + +function websocket_connect_async_callback(_session, res) { + let connection; + + try { + connection = session.websocket_connect_finish(res); + } catch (e) { + logError(e); + loop.quit(); + return; + } + + connection.connect('closed', () => { + log('closed'); + loop.quit(); + }); + + connection.connect('error', (self, err) => { + logError(err); + loop.quit(); + }); + + connection.connect('message', (self, type, data) => { + if (type !== Soup.WebsocketDataType.TEXT) + return; + + const str = byteArray.toString(byteArray.fromGBytes(data)); + log(`message: ${str}`); + connection.close(Soup.WebsocketCloseCode.NORMAL, null); + }); + + log('open'); + connection.send_text('hello'); +} + +loop.run(); diff --git a/gi/arg-cache.cpp b/gi/arg-cache.cpp new file mode 100644 index 0000000..d739a4f --- /dev/null +++ b/gi/arg-cache.cpp @@ -0,0 +1,1707 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2013 Giovanni Campagna + * + * 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 +#include +#include // for UniqueChars +#include +#include // for JS_TypeOfValue +#include // for JS_GetObjectFunction +#include // for JSTYPE_FUNCTION + +#include "gi/arg-cache.h" +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/boxed.h" +#include "gi/foreign.h" +#include "gi/function.h" +#include "gi/gerror.h" +#include "gi/gtype.h" +#include "gi/object.h" +#include "gi/param.h" +#include "gi/union.h" +#include "gi/value.h" +#include "cjs/byteArray.h" +#include "cjs/jsapi-util.h" + +enum ExpectedType { + OBJECT, + FUNCTION, + STRING, + LAST, +}; + +static const char* expected_type_names[] = {"object", "function", "string"}; +static_assert(G_N_ELEMENTS(expected_type_names) == ExpectedType::LAST, + "Names must match the values in ExpectedType"); + +// The global entry point for any invocations of GDestroyNotify; look up the +// callback through the user_data and then free it. +static void gjs_destroy_notify_callback(void* data) { + auto* trampoline = static_cast(data); + + g_assert(trampoline); + gjs_callback_trampoline_unref(trampoline); +} + +// A helper function to retrieve array lengths from a GIArgument (letting the +// compiler generate good instructions in case of big endian machines) +[[nodiscard]] static size_t gjs_g_argument_get_array_length(GITypeTag tag, + GIArgument* arg) { + if (tag == GI_TYPE_TAG_INT8) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_UINT8) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_INT16) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_UINT16) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_INT32) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_UINT32) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_INT64) + return gjs_arg_get(arg); + if (tag == GI_TYPE_TAG_UINT64) + return gjs_arg_get(arg); + g_assert_not_reached(); +} + +static void gjs_g_argument_set_array_length(GITypeTag tag, GIArgument* arg, + size_t value) { + switch (tag) { + case GI_TYPE_TAG_INT8: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_UINT8: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_INT16: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_UINT16: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_INT32: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_UINT32: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_INT64: + gjs_arg_set(arg, value); + break; + case GI_TYPE_TAG_UINT64: + gjs_arg_set(arg, value); + break; + default: + g_assert_not_reached(); + } +} + +GJS_JSAPI_RETURN_CONVENTION +static bool throw_not_introspectable_argument(JSContext* cx, + GICallableInfo* function, + const char* arg_name) { + gjs_throw(cx, + "Function %s.%s cannot be called: argument '%s' is not " + "introspectable.", + g_base_info_get_namespace(function), + g_base_info_get_name(function), arg_name); + return false; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool throw_not_introspectable_unboxed_type(JSContext* cx, + GICallableInfo* function, + const char* arg_name) { + gjs_throw(cx, + "Function %s.%s cannot be called: unexpected unregistered type " + "for argument '%s'.", + g_base_info_get_namespace(function), + g_base_info_get_name(function), arg_name); + return false; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool report_typeof_mismatch(JSContext* cx, const char* arg_name, + JS::HandleValue value, + ExpectedType expected) { + gjs_throw(cx, "Expected type %s for argument '%s' but got type %s", + expected_type_names[expected], arg_name, + JS::InformalValueTypeName(value)); + return false; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool report_gtype_mismatch(JSContext* cx, const char* arg_name, + JS::Value value, GType expected) { + gjs_throw( + cx, "Expected an object of type %s for argument '%s' but got type %s", + g_type_name(expected), arg_name, JS::InformalValueTypeName(value)); + return false; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool report_out_of_range(JSContext* cx, const char* arg_name, + GITypeTag tag) { + gjs_throw(cx, "Argument %s: value is out of range for %s", arg_name, + g_type_tag_to_string(tag)); + return false; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool report_invalid_null(JSContext* cx, const char* arg_name) { + gjs_throw(cx, "Argument %s may not be null", arg_name); + return false; +} + +// Marshallers: +// +// Each argument, irrespective of the direction, is processed in three phases: +// - before calling the C function [in] +// - after calling it, when converting the return value and out arguments [out] +// - at the end of the invocation, to release any allocated memory [release] +// +// The convention on the names is thus +// gjs_marshal_[argument type]_[direction]_[phase]. +// Some types don't have direction (for example, caller_allocates is only out, +// and callback is only in), in which case it is implied. + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_skipped_in(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, GIArgument*, + JS::HandleValue) { + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + return gjs_value_to_g_argument(cx, value, &self->type_info, self->arg_name, + self->is_return_value() + ? GJS_ARGUMENT_RETURN_VALUE + : GJS_ARGUMENT_ARGUMENT, + self->transfer, self->nullable, arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_inout_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, + JS::HandleValue value) { + if (!gjs_marshal_generic_in_in(cx, self, state, arg, value)) + return false; + + int ix = self->arg_pos; + state->out_cvalues[ix] = state->inout_original_cvalues[ix] = *arg; + gjs_arg_set(arg, &state->out_cvalues[ix]); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_in_in(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState* state, + GArgument* arg, + JS::HandleValue value) { + void* data; + size_t length; + + if (!gjs_array_to_explicit_array( + cx, value, &self->type_info, self->arg_name, GJS_ARGUMENT_ARGUMENT, + self->transfer, self->nullable, &data, &length)) + return false; + + uint8_t length_pos = self->contents.array.length_pos; + gjs_g_argument_set_array_length(self->contents.array.length_tag, + &state->in_cvalues[length_pos], length); + gjs_arg_set(arg, data); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_inout_in(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, + JS::HandleValue value) { + if (!gjs_marshal_explicit_array_in_in(cx, self, state, arg, value)) + return false; + + uint8_t length_pos = self->contents.array.length_pos; + uint8_t ix = self->arg_pos; + + if (!gjs_arg_get(arg)) { + // Special case where we were given JS null to also pass null for + // length, and not a pointer to an integer that derefs to 0. + gjs_arg_unset(&state->in_cvalues[length_pos]); + gjs_arg_unset(&state->out_cvalues[length_pos]); + gjs_arg_unset(&state->inout_original_cvalues[length_pos]); + + gjs_arg_unset(&state->out_cvalues[ix]); + gjs_arg_unset(&state->inout_original_cvalues[ix]); + } else { + state->out_cvalues[length_pos] = + state->inout_original_cvalues[length_pos] = + state->in_cvalues[length_pos]; + gjs_arg_set(&state->in_cvalues[length_pos], + &state->out_cvalues[length_pos]); + + state->out_cvalues[ix] = state->inout_original_cvalues[ix] = *arg; + gjs_arg_set(arg, &state->out_cvalues[ix]); + } + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_callback_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, JS::HandleValue value) { + GjsCallbackTrampoline* trampoline; + ffi_closure* closure; + + if (value.isNull() && self->nullable) { + closure = nullptr; + trampoline = nullptr; + } else { + if (JS_TypeOfValue(cx, value) != JSTYPE_FUNCTION) { + gjs_throw(cx, "Expected function for callback argument %s, got %s", + self->arg_name, JS::InformalValueTypeName(value)); + return false; + } + + JS::RootedFunction func(cx, JS_GetObjectFunction(&value.toObject())); + GjsAutoCallableInfo callable_info = + g_type_info_get_interface(&self->type_info); + bool is_object_method = !!state->instance_object; + trampoline = gjs_callback_trampoline_new(cx, func, callable_info, + self->contents.callback.scope, + is_object_method, false); + if (!trampoline) + return false; + if (self->contents.callback.scope == GI_SCOPE_TYPE_NOTIFIED && + is_object_method) { + auto* priv = ObjectInstance::for_js(cx, state->instance_object); + if (!priv) { + gjs_throw(cx, "Signal connected to wrong type of object"); + return false; + } + + priv->associate_closure(cx, trampoline->js_function); + } + closure = trampoline->closure; + } + + if (self->has_callback_destroy()) { + uint8_t destroy_pos = self->contents.callback.destroy_pos; + gjs_arg_set(&state->in_cvalues[destroy_pos], + trampoline ? gjs_destroy_notify_callback : nullptr); + } + if (self->has_callback_closure()) { + uint8_t closure_pos = self->contents.callback.closure_pos; + gjs_arg_set(&state->in_cvalues[closure_pos], trampoline); + } + + if (trampoline && self->contents.callback.scope != GI_SCOPE_TYPE_CALL) { + // Add an extra reference that will be cleared when collecting async + // calls, or when GDestroyNotify is called + gjs_callback_trampoline_ref(trampoline); + } + gjs_arg_set(arg, closure); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_out_in(JSContext*, GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, JS::HandleValue) { + // Default value in case a broken C function doesn't fill in the pointer + gjs_arg_unset(&state->out_cvalues[self->arg_pos]); + gjs_arg_set(arg, + &gjs_arg_member(&state->out_cvalues[self->arg_pos])); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_caller_allocates_in(JSContext*, GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, JS::HandleValue) { + void* blob = g_slice_alloc0(self->contents.caller_allocates_size); + gjs_arg_set(arg, blob); + gjs_arg_set(&state->out_cvalues[self->arg_pos], blob); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_null_in_in(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue) { + gjs_arg_unset(arg); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_boolean_in_in(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + gjs_arg_set(arg, JS::ToBoolean(value)); + return true; +} + +// Type tags are alternated, signed / unsigned +static int32_t min_max_ints[5][2] = {{G_MININT8, G_MAXINT8}, + {0, G_MAXUINT8}, + {G_MININT16, G_MAXINT16}, + {0, G_MAXUINT16}, + {G_MININT32, G_MAXINT32}}; + +[[nodiscard]] static inline bool value_in_range(int32_t number, GITypeTag tag) { + return (number >= min_max_ints[tag - GI_TYPE_TAG_INT8][0] && + number <= min_max_ints[tag - GI_TYPE_TAG_INT8][1]); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_integer_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + GITypeTag tag = self->contents.number.number_tag; + + if (self->is_unsigned) { + uint32_t number; + if (!JS::ToUint32(cx, value, &number)) + return false; + + if (!value_in_range(number, tag)) + return report_out_of_range(cx, self->arg_name, tag); + + gjs_g_argument_set_array_length(tag, arg, number); + } else { + int32_t number; + if (!JS::ToInt32(cx, value, &number)) + return false; + + if (!value_in_range(number, tag)) + return report_out_of_range(cx, self->arg_name, tag); + + gjs_g_argument_set_array_length(tag, arg, number); + } + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_number_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + double v; + if (!JS::ToNumber(cx, value, &v)) + return false; + + GITypeTag tag = self->contents.number.number_tag; + if (tag == GI_TYPE_TAG_DOUBLE) { + gjs_arg_set(arg, v); + } else if (tag == GI_TYPE_TAG_FLOAT) { + if (v < -G_MAXFLOAT || v > G_MAXFLOAT) + return report_out_of_range(cx, self->arg_name, GI_TYPE_TAG_FLOAT); + gjs_arg_set(arg, v); + } else if (tag == GI_TYPE_TAG_INT64) { + if (v < G_MININT64 || v > G_MAXINT64) + return report_out_of_range(cx, self->arg_name, GI_TYPE_TAG_INT64); + gjs_arg_set(arg, v); + } else if (tag == GI_TYPE_TAG_UINT64) { + if (v < 0 || v > G_MAXUINT64) + return report_out_of_range(cx, self->arg_name, GI_TYPE_TAG_UINT64); + gjs_arg_set(arg, v); + } else if (tag == GI_TYPE_TAG_UINT32) { + if (v < 0 || v > G_MAXUINT32) + return report_out_of_range(cx, self->arg_name, GI_TYPE_TAG_UINT32); + gjs_arg_set(arg, v); + } else { + g_assert_not_reached(); + } + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_unichar_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (!value.isString()) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::STRING); + + return gjs_unichar_from_string(cx, value, &gjs_arg_member(arg)); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_gtype_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return report_invalid_null(cx, self->arg_name); + if (!value.isObject()) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::OBJECT); + + JS::RootedObject gtype_obj(cx, &value.toObject()); + return gjs_gtype_get_actual_gtype( + cx, gtype_obj, &gjs_arg_member(arg)); +} + +// Common code for most types that are pointers on the C side +bool GjsArgumentCache::handle_nullable(JSContext* cx, GIArgument* arg) { + if (!nullable) + return report_invalid_null(cx, arg_name); + gjs_arg_unset(arg); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_string_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + if (!value.isString()) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::STRING); + + if (self->contents.string_is_filename) { + GjsAutoChar str; + if (!gjs_string_to_filename(cx, value, &str)) + return false; + gjs_arg_set(arg, str.release()); + return true; + } + + JS::UniqueChars str = gjs_string_to_utf8(cx, value); + if (!str) + return false; + gjs_arg_set(arg, g_strdup(str.get())); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_enum_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + int64_t number; + if (!JS::ToInt64(cx, value, &number)) + return false; + + // Unpack the values from their uint32_t bitfield. See note in + // gjs_arg_cache_build_enum_bounds(). + int64_t min, max; + if (self->is_unsigned) { + min = self->contents.enum_type.enum_min; + max = self->contents.enum_type.enum_max; + } else { + min = static_cast(self->contents.enum_type.enum_min); + max = static_cast(self->contents.enum_type.enum_max); + } + + if (number > max || number < min) { + gjs_throw(cx, "%" PRId64 " is not a valid value for enum argument %s", + number, self->arg_name); + return false; + } + + if (self->is_unsigned) + gjs_arg_set(arg, number); + else + gjs_arg_set(arg, number); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_flags_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + int64_t number; + if (!JS::ToInt64(cx, value, &number)) + return false; + + if ((uint64_t(number) & self->contents.flags_mask) != uint64_t(number)) { + gjs_throw(cx, "%" PRId64 " is not a valid value for flags argument %s", + number, self->arg_name); + return false; + } + + // We cast to unsigned because that's what makes sense, but then we + // put it in the v_int slot because that's what we use to unmarshal + // flags types at the moment. + gjs_arg_set(arg, static_cast(number)); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_foreign_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + GIStructInfo* foreign_info = g_type_info_get_interface(&self->type_info); + self->contents.tmp_foreign_info = foreign_info; + return gjs_struct_foreign_convert_to_g_argument( + cx, value, foreign_info, self->arg_name, GJS_ARGUMENT_ARGUMENT, + self->transfer, self->nullable, arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_gvalue_in_in(JSContext* cx, GjsArgumentCache*, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + GValue gvalue = G_VALUE_INIT; + + if (!gjs_value_to_g_value(cx, value, &gvalue)) + return false; + + gjs_arg_set(arg, g_boxed_copy(G_TYPE_VALUE, &gvalue)); + + g_value_unset(&gvalue); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_boxed_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + GType gtype = g_registered_type_info_get_g_type(self->contents.info); + + if (!value.isObject()) + return report_gtype_mismatch(cx, self->arg_name, value, gtype); + + JS::RootedObject object(cx, &value.toObject()); + if (gtype == G_TYPE_ERROR) { + return ErrorBase::transfer_to_gi_argument( + cx, object, arg, GI_DIRECTION_IN, self->transfer); + } + + return BoxedBase::transfer_to_gi_argument(cx, object, arg, GI_DIRECTION_IN, + self->transfer, gtype, + self->contents.info); +} + +// Unions include ClutterEvent and GdkEvent, which occur fairly often in an +// interactive application, so they're worth a special case in a different +// virtual function. +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_union_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + GType gtype = g_registered_type_info_get_g_type(self->contents.info); + g_assert(gtype != G_TYPE_NONE); + + if (!value.isObject()) + return report_gtype_mismatch(cx, self->arg_name, value, gtype); + + JS::RootedObject object(cx, &value.toObject()); + return UnionBase::transfer_to_gi_argument(cx, object, arg, GI_DIRECTION_IN, + self->transfer, gtype, + self->contents.info); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_gclosure_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + if (!(JS_TypeOfValue(cx, value) == JSTYPE_FUNCTION)) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::FUNCTION); + + JS::RootedFunction func(cx, JS_GetObjectFunction(&value.toObject())); + GClosure* closure = gjs_closure_new_marshaled(cx, func, "boxed"); + gjs_arg_set(arg, closure); + g_closure_ref(closure); + g_closure_sink(closure); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_gbytes_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + if (!value.isObject()) + return report_gtype_mismatch(cx, self->arg_name, value, G_TYPE_BYTES); + + JS::RootedObject object(cx, &value.toObject()); + if (JS_IsUint8Array(object)) { + gjs_arg_set(arg, gjs_byte_array_get_bytes(object)); + return true; + } + + // The bytearray path is taking an extra ref irrespective of transfer + // ownership, so we need to do the same here. + return BoxedBase::transfer_to_gi_argument( + cx, object, arg, GI_DIRECTION_IN, GI_TRANSFER_EVERYTHING, G_TYPE_BYTES, + self->contents.info); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_object_in_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::HandleValue value) { + if (value.isNull()) + return self->handle_nullable(cx, arg); + + GType gtype = g_registered_type_info_get_g_type(self->contents.info); + g_assert(gtype != G_TYPE_NONE); + + if (!value.isObject()) + return report_gtype_mismatch(cx, self->arg_name, value, gtype); + + JS::RootedObject object(cx, &value.toObject()); + return ObjectBase::transfer_to_gi_argument(cx, object, arg, GI_DIRECTION_IN, + self->transfer, gtype); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_gtype_struct_instance_in(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState*, + GIArgument* arg, + JS::HandleValue value) { + // Instance parameter is never nullable + if (!value.isObject()) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::OBJECT); + + JS::RootedObject obj(cx, &value.toObject()); + GType actual_gtype; + if (!gjs_gtype_get_actual_gtype(cx, obj, &actual_gtype)) + return false; + + if (actual_gtype == G_TYPE_NONE) { + gjs_throw(cx, "Invalid GType class passed for instance parameter"); + return false; + } + + // We use peek here to simplify reference counting (we just ignore transfer + // annotation, as GType classes are never really freed.) We know that the + // GType class is referenced at least once when the JS constructor is + // initialized. + if (g_type_is_a(actual_gtype, G_TYPE_INTERFACE)) + gjs_arg_set(arg, g_type_default_interface_peek(actual_gtype)); + else + gjs_arg_set(arg, g_type_class_peek(actual_gtype)); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_param_instance_in(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, + GIArgument* arg, + JS::HandleValue value) { + // Instance parameter is never nullable + if (!value.isObject()) + return report_typeof_mismatch(cx, self->arg_name, value, + ExpectedType::OBJECT); + + JS::RootedObject obj(cx, &value.toObject()); + if (!gjs_typecheck_param(cx, obj, G_TYPE_PARAM, true)) + return false; + gjs_arg_set(arg, gjs_g_param_from_param(cx, obj)); + + if (self->transfer == GI_TRANSFER_EVERYTHING) + g_param_spec_ref(gjs_arg_get(arg)); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_skipped_out(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, GIArgument*, + JS::MutableHandleValue) { + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_out_out(JSContext* cx, GjsArgumentCache* self, + GjsFunctionCallState*, GIArgument* arg, + JS::MutableHandleValue value) { + return gjs_value_from_g_argument(cx, value, &self->type_info, arg, true); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_out_out(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* arg, + JS::MutableHandleValue value) { + uint8_t length_pos = self->contents.array.length_pos; + GIArgument* length_arg = &(state->out_cvalues[length_pos]); + GITypeTag length_tag = self->contents.array.length_tag; + size_t length = gjs_g_argument_get_array_length(length_tag, length_arg); + + return gjs_value_from_explicit_array(cx, value, &self->type_info, arg, + length); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_skipped_release(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, + GIArgument* in_arg [[maybe_unused]], + GIArgument* out_arg [[maybe_unused]]) { + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_in_release( + JSContext* cx, GjsArgumentCache* self, GjsFunctionCallState* state, + GIArgument* in_arg, GIArgument* out_arg [[maybe_unused]]) { + GITransfer transfer = + state->call_completed ? self->transfer : GI_TRANSFER_NOTHING; + return gjs_g_argument_release_in_arg(cx, transfer, &self->type_info, + in_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_out_release(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState*, + GIArgument* in_arg [[maybe_unused]], + GIArgument* out_arg) { + return gjs_g_argument_release(cx, self->transfer, &self->type_info, + out_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_generic_inout_release(JSContext* cx, + GjsArgumentCache* self, + GjsFunctionCallState* state, + GIArgument* in_arg, + GIArgument* out_arg) { + // For inout, transfer refers to what we get back from the function; for + // the temporary C value we allocated, clearly we're responsible for + // freeing it. + + GIArgument* original_out_arg = + &(state->inout_original_cvalues[self->arg_pos]); + if (!gjs_g_argument_release_in_arg(cx, GI_TRANSFER_NOTHING, + &self->type_info, original_out_arg)) + return false; + + return gjs_marshal_generic_out_release(cx, self, state, in_arg, out_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_out_release( + JSContext* cx, GjsArgumentCache* self, GjsFunctionCallState* state, + GIArgument* in_arg [[maybe_unused]], GIArgument* out_arg) { + uint8_t length_pos = self->contents.array.length_pos; + GIArgument* length_arg = &(state->out_cvalues[length_pos]); + GITypeTag length_tag = self->contents.array.length_tag; + size_t length = gjs_g_argument_get_array_length(length_tag, length_arg); + + return gjs_g_argument_release_out_array(cx, self->transfer, + &self->type_info, length, out_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_in_release( + JSContext* cx, GjsArgumentCache* self, GjsFunctionCallState* state, + GIArgument* in_arg, GIArgument* out_arg [[maybe_unused]]) { + uint8_t length_pos = self->contents.array.length_pos; + GIArgument* length_arg = &(state->in_cvalues[length_pos]); + GITypeTag length_tag = self->contents.array.length_tag; + size_t length = gjs_g_argument_get_array_length(length_tag, length_arg); + + GITransfer transfer = + state->call_completed ? self->transfer : GI_TRANSFER_NOTHING; + + return gjs_g_argument_release_in_array(cx, transfer, &self->type_info, + length, in_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_explicit_array_inout_release( + JSContext* cx, GjsArgumentCache* self, GjsFunctionCallState* state, + GIArgument* in_arg [[maybe_unused]], GIArgument* out_arg) { + uint8_t length_pos = self->contents.array.length_pos; + GIArgument* length_arg = &(state->in_cvalues[length_pos]); + GITypeTag length_tag = self->contents.array.length_tag; + size_t length = gjs_g_argument_get_array_length(length_tag, length_arg); + + // For inout, transfer refers to what we get back from the function; for + // the temporary C value we allocated, clearly we're responsible for + // freeing it. + + GIArgument* original_out_arg = + &(state->inout_original_cvalues[self->arg_pos]); + if (gjs_arg_get(original_out_arg) != gjs_arg_get(out_arg) && + !gjs_g_argument_release_in_array(cx, GI_TRANSFER_NOTHING, + &self->type_info, length, + original_out_arg)) + return false; + + return gjs_g_argument_release_out_array(cx, self->transfer, + &self->type_info, length, out_arg); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_caller_allocates_release( + JSContext*, GjsArgumentCache* self, GjsFunctionCallState*, + GIArgument* in_arg, GIArgument* out_arg [[maybe_unused]]) { + g_slice_free1(self->contents.caller_allocates_size, + gjs_arg_get(in_arg)); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_callback_release(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, + GIArgument* in_arg, + GIArgument* out_arg [[maybe_unused]]) { + auto* closure = gjs_arg_get(in_arg); + if (!closure) + return true; + + auto trampoline = static_cast(closure->user_data); + // CallbackTrampolines are refcounted because for notified/async closures + // it is possible to destroy it while in call, and therefore we cannot + // check its scope at this point + gjs_callback_trampoline_unref(trampoline); + gjs_arg_unset(in_arg); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_string_in_release(JSContext*, GjsArgumentCache*, + GjsFunctionCallState*, + GIArgument* in_arg, + GIArgument* out_arg + [[maybe_unused]]) { + g_free(gjs_arg_get(in_arg)); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_foreign_in_release( + JSContext* cx, GjsArgumentCache* self, GjsFunctionCallState* state, + GIArgument* in_arg, GIArgument* out_arg [[maybe_unused]]) { + bool ok = true; + + GITransfer transfer = + state->call_completed ? self->transfer : GI_TRANSFER_NOTHING; + + if (transfer == GI_TRANSFER_NOTHING) + ok = gjs_struct_foreign_release_g_argument( + cx, self->transfer, self->contents.tmp_foreign_info, in_arg); + + g_base_info_unref(self->contents.tmp_foreign_info); + return ok; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_marshal_boxed_in_release(JSContext*, GjsArgumentCache* self, + GjsFunctionCallState*, + GIArgument* in_arg, + GIArgument* out_arg [[maybe_unused]]) { + GType gtype = g_registered_type_info_get_g_type(self->contents.info); + g_assert(g_type_is_a(gtype, G_TYPE_BOXED)); + + if (!gjs_arg_get(in_arg)) + return true; + + g_boxed_free(gtype, gjs_arg_get(in_arg)); + return true; +} + +static void gjs_arg_cache_interface_free(GjsArgumentCache* self) { + g_clear_pointer(&self->contents.info, g_base_info_unref); +} + +static const GjsArgumentMarshallers skip_all_marshallers = { + gjs_marshal_skipped_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +// .in is ignored for the return value +static const GjsArgumentMarshallers return_value_marshallers = { + nullptr, // no in + gjs_marshal_generic_out_out, // out + gjs_marshal_generic_out_release, // release +}; + +static const GjsArgumentMarshallers return_array_marshallers = { + gjs_marshal_generic_out_in, // in + gjs_marshal_explicit_array_out_out, // out + gjs_marshal_explicit_array_out_release, // release +}; + +static const GjsArgumentMarshallers array_length_out_marshallers = { + gjs_marshal_generic_out_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers fallback_in_marshallers = { + gjs_marshal_generic_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_generic_in_release, // release +}; + +static const GjsArgumentMarshallers fallback_interface_in_marshallers = { + gjs_marshal_generic_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_generic_in_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers fallback_inout_marshallers = { + gjs_marshal_generic_inout_in, // in + gjs_marshal_generic_out_out, // out + gjs_marshal_generic_inout_release, // release +}; + +static const GjsArgumentMarshallers fallback_out_marshallers = { + gjs_marshal_generic_out_in, // in + gjs_marshal_generic_out_out, // out + gjs_marshal_generic_out_release, // release +}; + +static const GjsArgumentMarshallers invalid_in_marshallers = { + nullptr, // no in, will cause the function invocation code to throw + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers enum_in_marshallers = { + gjs_marshal_enum_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers flags_in_marshallers = { + gjs_marshal_flags_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers foreign_struct_in_marshallers = { + gjs_marshal_foreign_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_foreign_in_release, // release +}; + +static const GjsArgumentMarshallers foreign_struct_instance_in_marshallers = { + gjs_marshal_foreign_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers gvalue_in_marshallers = { + gjs_marshal_gvalue_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers gvalue_in_transfer_none_marshallers = { + gjs_marshal_gvalue_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_boxed_in_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers gclosure_in_marshallers = { + gjs_marshal_gclosure_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers gclosure_in_transfer_none_marshallers = { + gjs_marshal_gclosure_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_boxed_in_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers gbytes_in_marshallers = { + gjs_marshal_gbytes_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers gbytes_in_transfer_none_marshallers = { + gjs_marshal_gbytes_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_boxed_in_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers object_in_marshallers = { + gjs_marshal_object_in_in, // in + gjs_marshal_skipped_out, // out + // This is a smart marshaller, no release needed + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers union_in_marshallers = { + gjs_marshal_union_in_in, // in + gjs_marshal_skipped_out, // out + // This is a smart marshaller, no release needed + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers boxed_in_marshallers = { + gjs_marshal_boxed_in_in, // in + gjs_marshal_skipped_out, // out + // This is a smart marshaller, no release needed + gjs_marshal_skipped_release, // release + gjs_arg_cache_interface_free, // free +}; + +static const GjsArgumentMarshallers null_in_marshallers = { + gjs_marshal_null_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers boolean_in_marshallers = { + gjs_marshal_boolean_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers integer_in_marshallers = { + gjs_marshal_integer_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers number_in_marshallers = { + gjs_marshal_number_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers unichar_in_marshallers = { + gjs_marshal_unichar_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers gtype_in_marshallers = { + gjs_marshal_gtype_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers string_in_marshallers = { + gjs_marshal_string_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers string_in_transfer_none_marshallers = { + gjs_marshal_string_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_string_in_release, // release +}; + +// .out is ignored for the instance parameter +static const GjsArgumentMarshallers gtype_struct_instance_in_marshallers = { + gjs_marshal_gtype_struct_instance_in, // in + nullptr, // no out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers param_instance_in_marshallers = { + gjs_marshal_param_instance_in, // in + nullptr, // no out + gjs_marshal_skipped_release, // release +}; + +static const GjsArgumentMarshallers callback_in_marshallers = { + gjs_marshal_callback_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_callback_release, // release +}; + +static const GjsArgumentMarshallers c_array_in_marshallers = { + gjs_marshal_explicit_array_in_in, // in + gjs_marshal_skipped_out, // out + gjs_marshal_explicit_array_in_release, // release +}; + +static const GjsArgumentMarshallers c_array_inout_marshallers = { + gjs_marshal_explicit_array_inout_in, // in + gjs_marshal_explicit_array_out_out, // out + gjs_marshal_explicit_array_inout_release, // release +}; + +static const GjsArgumentMarshallers c_array_out_marshallers = { + gjs_marshal_generic_out_in, // in + gjs_marshal_explicit_array_out_out, // out + gjs_marshal_explicit_array_out_release, // release +}; + +static const GjsArgumentMarshallers caller_allocates_out_marshallers = { + gjs_marshal_caller_allocates_in, // in + gjs_marshal_generic_out_out, // out + gjs_marshal_caller_allocates_release, // release +}; + +static inline void gjs_arg_cache_set_skip_all(GjsArgumentCache* self) { + self->marshallers = &skip_all_marshallers; + self->skip_in = self->skip_out = true; +} + +bool gjs_arg_cache_build_return(JSContext*, GjsArgumentCache* self, + GjsArgumentCache* arguments, + GICallableInfo* callable, + bool* inc_counter_out) { + g_assert(inc_counter_out && "forgot out parameter"); + + g_callable_info_load_return_type(callable, &self->type_info); + + if (g_type_info_get_tag(&self->type_info) == GI_TYPE_TAG_VOID) { + *inc_counter_out = false; + gjs_arg_cache_set_skip_all(self); + return true; + } + + *inc_counter_out = true; + self->set_return_value(); + self->transfer = g_callable_info_get_caller_owns(callable); + + if (g_type_info_get_tag(&self->type_info) == GI_TYPE_TAG_ARRAY) { + int length_pos = g_type_info_get_array_length(&self->type_info); + if (length_pos >= 0) { + gjs_arg_cache_set_skip_all(&arguments[length_pos]); + + // Even if we skip the length argument most of the time, we need to + // do some basic initialization here. + arguments[length_pos].set_arg_pos(length_pos); + arguments[length_pos].marshallers = &array_length_out_marshallers; + + self->marshallers = &return_array_marshallers; + + self->set_array_length_pos(length_pos); + + GIArgInfo length_arg; + g_callable_info_load_arg(callable, length_pos, &length_arg); + GITypeInfo length_type; + g_arg_info_load_type(&length_arg, &length_type); + self->contents.array.length_tag = g_type_info_get_tag(&length_type); + + return true; + } + } + + // marshal_in is ignored for the return value, but skip_in is not (it is + // used in the failure release path) + self->skip_in = true; + self->marshallers = &return_value_marshallers; + + return true; +} + +static void gjs_arg_cache_build_enum_bounds(GjsArgumentCache* self, + GIEnumInfo* enum_info) { + int64_t min = G_MAXINT64; + int64_t max = G_MININT64; + int n = g_enum_info_get_n_values(enum_info); + for (int i = 0; i < n; i++) { + GjsAutoValueInfo value_info = g_enum_info_get_value(enum_info, i); + int64_t value = g_value_info_get_value(value_info); + + if (value > max) + max = value; + if (value < min) + min = value; + } + + // From the docs for g_value_info_get_value(): "This will always be + // representable as a 32-bit signed or unsigned value. The use of gint64 as + // the return type is to allow both." + // We stuff them both into unsigned 32-bit fields, and use a flag to tell + // whether we have to compare them as signed. + self->contents.enum_type.enum_min = static_cast(min); + self->contents.enum_type.enum_max = static_cast(max); + self->is_unsigned = min >= 0 && max > G_MAXINT32; +} + +static void gjs_arg_cache_build_flags_mask(GjsArgumentCache* self, + GIEnumInfo* enum_info) { + uint64_t mask = 0; + int n = g_enum_info_get_n_values(enum_info); + for (int i = 0; i < n; i++) { + GjsAutoValueInfo value_info = g_enum_info_get_value(enum_info, i); + int64_t value = g_value_info_get_value(value_info); + // From the docs for g_value_info_get_value(): "This will always be + // representable as a 32-bit signed or unsigned value. The use of + // gint64 as the return type is to allow both." + // We stuff both into an unsigned, int-sized field, matching the + // internal representation of flags in GLib (which uses guint). + mask |= static_cast(value); + } + + self->contents.flags_mask = mask; +} + +[[nodiscard]] static inline 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 bool gjs_arg_cache_build_interface_in_arg(JSContext* cx, + GjsArgumentCache* self, + GICallableInfo* callable, + GIBaseInfo* interface_info, + bool is_instance_param) { + GIInfoType interface_type = g_base_info_get_type(interface_info); + + // We do some transfer magic later, so let's ensure we don't mess up. + // Should not happen in practice. + if (G_UNLIKELY(self->transfer == GI_TRANSFER_CONTAINER)) + return throw_not_introspectable_argument(cx, callable, self->arg_name); + + switch (interface_type) { + case GI_INFO_TYPE_ENUM: + gjs_arg_cache_build_enum_bounds(self, interface_info); + self->marshallers = &enum_in_marshallers; + return true; + + case GI_INFO_TYPE_FLAGS: + gjs_arg_cache_build_flags_mask(self, interface_info); + self->marshallers = &flags_in_marshallers; + return true; + + case GI_INFO_TYPE_STRUCT: + if (g_struct_info_is_foreign(interface_info)) { + if (is_instance_param) + self->marshallers = &foreign_struct_instance_in_marshallers; + else + self->marshallers = &foreign_struct_in_marshallers; + return true; + } + [[fallthrough]]; + case GI_INFO_TYPE_BOXED: + case GI_INFO_TYPE_OBJECT: + case GI_INFO_TYPE_INTERFACE: + case GI_INFO_TYPE_UNION: { + GType gtype = g_registered_type_info_get_g_type(interface_info); + self->contents.info = g_base_info_ref(interface_info); + + // Transfer handling is a bit complex here, because some of our _in + // marshallers know not to copy stuff if we don't need to. + + if (gtype == G_TYPE_VALUE) { + if (self->transfer == GI_TRANSFER_NOTHING && !is_instance_param) + self->marshallers = &gvalue_in_transfer_none_marshallers; + else + self->marshallers = &gvalue_in_marshallers; + return true; + } + + if (is_gdk_atom(interface_info)) { + // Fall back to the generic marshaller + self->marshallers = &fallback_interface_in_marshallers; + return true; + } + + if (gtype == G_TYPE_CLOSURE) { + if (self->transfer == GI_TRANSFER_NOTHING && !is_instance_param) + self->marshallers = &gclosure_in_transfer_none_marshallers; + else + self->marshallers = &gclosure_in_marshallers; + return true; + } + + if (gtype == G_TYPE_BYTES) { + if (self->transfer == GI_TRANSFER_NOTHING && !is_instance_param) + self->marshallers = &gbytes_in_transfer_none_marshallers; + else + self->marshallers = &gbytes_in_marshallers; + return true; + } + + if (g_type_is_a(gtype, G_TYPE_OBJECT) || + g_type_is_a(gtype, G_TYPE_INTERFACE)) { + self->marshallers = &object_in_marshallers; + return true; + } + + if (g_type_is_a(gtype, G_TYPE_PARAM)) { + // Fall back to the generic marshaller + self->marshallers = &fallback_interface_in_marshallers; + return true; + } + + if (interface_type == GI_INFO_TYPE_UNION) { + if (gtype == G_TYPE_NONE) { + // Can't handle unions without a GType + return throw_not_introspectable_unboxed_type( + cx, callable, self->arg_name); + } + + self->marshallers = &union_in_marshallers; + return true; + } + + // generic boxed type + if (gtype == G_TYPE_NONE && self->transfer != GI_TRANSFER_NOTHING) { + // Can't transfer ownership of a structure type not + // registered as a boxed + return throw_not_introspectable_unboxed_type(cx, callable, + self->arg_name); + } + + self->marshallers = &boxed_in_marshallers; + return true; + } break; + + case GI_INFO_TYPE_INVALID: + case GI_INFO_TYPE_FUNCTION: + case GI_INFO_TYPE_CALLBACK: + case GI_INFO_TYPE_CONSTANT: + case GI_INFO_TYPE_INVALID_0: + case GI_INFO_TYPE_VALUE: + case GI_INFO_TYPE_SIGNAL: + case GI_INFO_TYPE_VFUNC: + case GI_INFO_TYPE_PROPERTY: + case GI_INFO_TYPE_FIELD: + case GI_INFO_TYPE_ARG: + case GI_INFO_TYPE_TYPE: + case GI_INFO_TYPE_UNRESOLVED: + default: + // Don't know how to handle this interface type (should not happen + // in practice, for typelibs emitted by g-ir-compiler) + return throw_not_introspectable_argument(cx, callable, + self->arg_name); + } +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_arg_cache_build_normal_in_arg(JSContext* cx, + GjsArgumentCache* self, + GICallableInfo* callable, + GITypeTag tag) { + // "Normal" in arguments are those arguments that don't require special + // processing, and don't touch other arguments. + // Main categories are: + // - void* + // - small numbers (fit in 32bit) + // - big numbers (need a double) + // - strings + // - enums/flags (different from numbers in the way they're exposed in GI) + // - objects (GObjects, boxed, unions, etc.) + // - hashes + // - sequences (null-terminated arrays, lists, etc.) + + switch (tag) { + case GI_TYPE_TAG_VOID: + self->marshallers = &null_in_marshallers; + break; + + case GI_TYPE_TAG_BOOLEAN: + self->marshallers = &boolean_in_marshallers; + break; + + case GI_TYPE_TAG_INT8: + case GI_TYPE_TAG_INT16: + case GI_TYPE_TAG_INT32: + self->marshallers = &integer_in_marshallers; + self->contents.number.number_tag = tag; + self->is_unsigned = false; + break; + + case GI_TYPE_TAG_UINT8: + case GI_TYPE_TAG_UINT16: + self->marshallers = &integer_in_marshallers; + self->contents.number.number_tag = tag; + self->is_unsigned = true; + break; + + case GI_TYPE_TAG_UINT32: + case GI_TYPE_TAG_INT64: + case GI_TYPE_TAG_UINT64: + case GI_TYPE_TAG_FLOAT: + case GI_TYPE_TAG_DOUBLE: + self->marshallers = &number_in_marshallers; + self->contents.number.number_tag = tag; + break; + + case GI_TYPE_TAG_UNICHAR: + self->marshallers = &unichar_in_marshallers; + break; + + case GI_TYPE_TAG_GTYPE: + self->marshallers = >ype_in_marshallers; + break; + + case GI_TYPE_TAG_FILENAME: + if (self->transfer == GI_TRANSFER_NOTHING) + self->marshallers = &string_in_transfer_none_marshallers; + else + self->marshallers = &string_in_marshallers; + self->contents.string_is_filename = true; + break; + + case GI_TYPE_TAG_UTF8: + if (self->transfer == GI_TRANSFER_NOTHING) + self->marshallers = &string_in_transfer_none_marshallers; + else + self->marshallers = &string_in_marshallers; + self->contents.string_is_filename = false; + break; + + case GI_TYPE_TAG_INTERFACE: { + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(&self->type_info); + return gjs_arg_cache_build_interface_in_arg( + cx, self, callable, interface_info, + /* is_instance_param = */ false); + } + + case GI_TYPE_TAG_ARRAY: + case GI_TYPE_TAG_GLIST: + case GI_TYPE_TAG_GSLIST: + case GI_TYPE_TAG_GHASH: + case GI_TYPE_TAG_ERROR: + default: + // FIXME: Falling back to the generic marshaller + self->marshallers = &fallback_in_marshallers; + } + + return true; +} + +bool gjs_arg_cache_build_instance(JSContext* cx, GjsArgumentCache* self, + GICallableInfo* callable) { + GIBaseInfo* interface_info = g_base_info_get_container(callable); // !owned + + self->set_instance_parameter(); + self->transfer = g_callable_info_get_instance_ownership_transfer(callable); + + // These cases could be covered by the generic marshaller, except that + // there's no way to get GITypeInfo for a method's instance parameter. + // Instead, special-case the arguments here that would otherwise go through + // the generic marshaller. + // See: https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/334 + GIInfoType info_type = g_base_info_get_type(interface_info); + if (info_type == GI_INFO_TYPE_STRUCT && + g_struct_info_is_gtype_struct(interface_info)) { + self->marshallers = >ype_struct_instance_in_marshallers; + return true; + } + if (info_type == GI_INFO_TYPE_OBJECT) { + GType gtype = g_registered_type_info_get_g_type(interface_info); + + if (g_type_is_a(gtype, G_TYPE_PARAM)) { + self->marshallers = ¶m_instance_in_marshallers; + return true; + } + } + + return gjs_arg_cache_build_interface_in_arg(cx, self, callable, + interface_info, + /* is_instance_param = */ true); +} + +bool gjs_arg_cache_build_arg(JSContext* cx, GjsArgumentCache* self, + GjsArgumentCache* arguments, uint8_t gi_index, + GIDirection direction, GIArgInfo* arg, + GICallableInfo* callable, bool* inc_counter_out) { + g_assert(inc_counter_out && "forgot out parameter"); + + self->set_arg_pos(gi_index); + self->arg_name = g_base_info_get_name(arg); + g_arg_info_load_type(arg, &self->type_info); + self->transfer = g_arg_info_get_ownership_transfer(arg); + self->nullable = g_arg_info_may_be_null(arg); + + if (direction == GI_DIRECTION_IN) + self->skip_out = true; + else if (direction == GI_DIRECTION_OUT) + self->skip_in = true; + *inc_counter_out = true; + + GITypeTag type_tag = g_type_info_get_tag(&self->type_info); + if (direction == GI_DIRECTION_OUT && type_tag == GI_TYPE_TAG_INTERFACE && + g_arg_info_is_caller_allocates(arg)) { + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(&self->type_info); + g_assert(interface_info); + + GIInfoType interface_type = g_base_info_get_type(interface_info); + + size_t size; + if (interface_type == GI_INFO_TYPE_STRUCT) { + size = g_struct_info_get_size(interface_info); + } else if (interface_type == GI_INFO_TYPE_UNION) { + size = g_union_info_get_size(interface_info); + } else { + gjs_throw(cx, + "Unsupported type %s for argument %s with (out " + "caller-allocates)", + g_info_type_to_string(interface_type), self->arg_name); + return false; + } + + self->marshallers = &caller_allocates_out_marshallers; + self->contents.caller_allocates_size = size; + + return true; + } + + if (type_tag == GI_TYPE_TAG_INTERFACE) { + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(&self->type_info); + if (interface_info.type() == GI_INFO_TYPE_CALLBACK) { + if (direction != GI_DIRECTION_IN) { + // Can't do callbacks for out or inout + gjs_throw(cx, + "Function %s.%s has a callback out-argument %s, not " + "supported", + g_base_info_get_namespace(callable), + g_base_info_get_name(callable), self->arg_name); + return false; + } + + if (strcmp(interface_info.name(), "DestroyNotify") == 0 && + strcmp(interface_info.ns(), "GLib") == 0) { + // We don't know (yet) what to do with GDestroyNotify appearing + // before a callback. If the callback comes later in the + // argument list, then the invalid marshallers will be + // overwritten with the 'skipped' one. If no callback follows, + // then this is probably an unsupported function, so the + // function invocation code will check this and throw. + self->marshallers = &invalid_in_marshallers; + *inc_counter_out = false; + } else { + self->marshallers = &callback_in_marshallers; + + int destroy_pos = g_arg_info_get_destroy(arg); + int closure_pos = g_arg_info_get_closure(arg); + + if (destroy_pos >= 0) + gjs_arg_cache_set_skip_all(&arguments[destroy_pos]); + + if (closure_pos >= 0) + gjs_arg_cache_set_skip_all(&arguments[closure_pos]); + + if (destroy_pos >= 0 && closure_pos < 0) { + gjs_throw(cx, + "Function %s.%s has a GDestroyNotify but no " + "user_data, not supported", + g_base_info_get_namespace(callable), + g_base_info_get_name(callable)); + return false; + } + + self->contents.callback.scope = g_arg_info_get_scope(arg); + self->set_callback_destroy_pos(destroy_pos); + self->set_callback_closure_pos(closure_pos); + } + + return true; + } + } + + if (type_tag == GI_TYPE_TAG_ARRAY && + g_type_info_get_array_type(&self->type_info) == GI_ARRAY_TYPE_C) { + int length_pos = g_type_info_get_array_length(&self->type_info); + + if (length_pos >= 0) { + gjs_arg_cache_set_skip_all(&arguments[length_pos]); + + if (direction == GI_DIRECTION_IN) { + self->marshallers = &c_array_in_marshallers; + } else if (direction == GI_DIRECTION_INOUT) { + self->marshallers = &c_array_inout_marshallers; + } else { + // Even if we skip the length argument most of time, we need to + // do some basic initialization here. + arguments[length_pos].set_arg_pos(length_pos); + arguments[length_pos].marshallers = + &array_length_out_marshallers; + + self->marshallers = &c_array_out_marshallers; + } + + self->set_array_length_pos(length_pos); + + GIArgInfo length_arg; + g_callable_info_load_arg(callable, length_pos, &length_arg); + GITypeInfo length_type; + g_arg_info_load_type(&length_arg, &length_type); + self->contents.array.length_tag = g_type_info_get_tag(&length_type); + + if (length_pos < gi_index) { + // we already collected length_pos, remove it + *inc_counter_out = false; + } + + return true; + } + } + + if (direction == GI_DIRECTION_IN) + return gjs_arg_cache_build_normal_in_arg(cx, self, callable, type_tag); + + if (direction == GI_DIRECTION_INOUT) + self->marshallers = &fallback_inout_marshallers; + else + self->marshallers = &fallback_out_marshallers; + + return true; +} diff --git a/gi/arg-cache.h b/gi/arg-cache.h new file mode 100644 index 0000000..fab3aba --- /dev/null +++ b/gi/arg-cache.h @@ -0,0 +1,188 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2013 Giovanni Campagna + * + * 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. + */ + +#ifndef GI_ARG_CACHE_H_ +#define GI_ARG_CACHE_H_ + +#include + +#include +#include + +#include +#include // for g_assert + +#include +#include + +#include "cjs/macros.h" + +struct GjsFunctionCallState; +struct GjsArgumentCache; + +struct GjsArgumentMarshallers { + bool (*in)(JSContext* cx, GjsArgumentCache* cache, + GjsFunctionCallState* state, GIArgument* in_argument, + JS::HandleValue value); + bool (*out)(JSContext* cx, GjsArgumentCache* cache, + GjsFunctionCallState* state, GIArgument* out_argument, + JS::MutableHandleValue value); + bool (*release)(JSContext* cx, GjsArgumentCache* cache, + GjsFunctionCallState* state, GIArgument* in_argument, + GIArgument* out_argument); + void (*free)(GjsArgumentCache* cache); +}; + +struct GjsArgumentCache { + const GjsArgumentMarshallers* marshallers; + const char* arg_name; + GITypeInfo type_info; + + uint8_t arg_pos; + bool skip_in : 1; + bool skip_out : 1; + GITransfer transfer : 2; + bool nullable : 1; + bool is_unsigned : 1; // number and enum only + + union { + // for explicit array only + struct { + uint8_t length_pos; + GITypeTag length_tag : 5; + } array; + + struct { + uint8_t closure_pos; + uint8_t destroy_pos; + GIScopeType scope : 2; + } callback; + + struct { + GITypeTag number_tag : 5; + } number; + + // boxed / union / GObject + GIRegisteredTypeInfo* info; + + // foreign structures + GIStructInfo* tmp_foreign_info; + + // enum / flags + struct { + uint32_t enum_min; + uint32_t enum_max; + } enum_type; + unsigned flags_mask; + + // string / filename + bool string_is_filename : 1; + + // out caller allocates (FIXME: should be in object) + size_t caller_allocates_size; + } contents; + + GJS_JSAPI_RETURN_CONVENTION + bool handle_nullable(JSContext* cx, GIArgument* arg); + + // Introspected functions can have up to 253 arguments. 255 is a placeholder + // for the return value and 254 for the instance parameter. The callback + // closure or destroy notify parameter may have a value of 255 to indicate + // that it is absent. + static constexpr uint8_t MAX_ARGS = 253; + static constexpr uint8_t INSTANCE_PARAM = 254; + static constexpr uint8_t RETURN_VALUE = 255; + static constexpr uint8_t ABSENT = 255; + void set_arg_pos(int pos) { + g_assert(pos <= MAX_ARGS && "No more than 253 arguments allowed"); + arg_pos = pos; + } + void set_array_length_pos(int pos) { + g_assert(pos <= MAX_ARGS && "No more than 253 arguments allowed"); + contents.array.length_pos = pos; + } + void set_callback_destroy_pos(int pos) { + g_assert(pos <= MAX_ARGS && "No more than 253 arguments allowed"); + contents.callback.destroy_pos = pos < 0 ? ABSENT : pos; + } + [[nodiscard]] bool has_callback_destroy() { + return contents.callback.destroy_pos != ABSENT; + } + void set_callback_closure_pos(int pos) { + g_assert(pos <= MAX_ARGS && "No more than 253 arguments allowed"); + contents.callback.closure_pos = pos < 0 ? ABSENT : pos; + } + [[nodiscard]] bool has_callback_closure() { + return contents.callback.closure_pos != ABSENT; + } + + void set_instance_parameter() { + arg_pos = INSTANCE_PARAM; + arg_name = "instance parameter"; + // Some calls accept null for the instance, but generally in an object + // oriented language it's wrong to call a method on null + nullable = false; + skip_out = true; + } + + void set_return_value() { + arg_pos = RETURN_VALUE; + arg_name = "return value"; + nullable = false; // We don't really care for return values + } + [[nodiscard]] bool is_return_value() { return arg_pos == RETURN_VALUE; } +}; + +// This is a trick to print out the sizes of the structs at compile time, in +// an error message: +// template struct Measure; +// Measure arg_cache_size; + +#if defined(__x86_64__) && defined(__clang__) && !defined (_MSC_VER) +// This isn't meant to be comprehensive, but should trip on at least one CI job +// if sizeof(GjsArgumentCache) is increased. +// Note that this check is not applicable for clang-cl builds, as Windows is +// an LLP64 system +static_assert(sizeof(GjsArgumentCache) <= 104, + "Think very hard before increasing the size of GjsArgumentCache. " + "One is allocated for every argument to every introspected " + "function."); +#endif // x86-64 clang + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_arg_cache_build_arg(JSContext* cx, GjsArgumentCache* self, + GjsArgumentCache* arguments, uint8_t gi_index, + GIDirection direction, GIArgInfo* arg, + GICallableInfo* callable, bool* inc_counter_out); + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_arg_cache_build_return(JSContext* cx, GjsArgumentCache* self, + GjsArgumentCache* arguments, + GICallableInfo* callable, + bool* inc_counter_out); + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_arg_cache_build_instance(JSContext* cx, GjsArgumentCache* self, + GICallableInfo* callable); + +#endif // GI_ARG_CACHE_H_ diff --git a/gi/arg-inl.h b/gi/arg-inl.h new file mode 100644 index 0000000..0633088 --- /dev/null +++ b/gi/arg-inl.h @@ -0,0 +1,198 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later + * + * Copyright (c) 2020 Marco Trevisan + */ + +#pragma once + +#include + +#include // for nullptr_t +#include +#include // for to_string +#include + +#include +#include // for GType +#include // for gboolean + +#include "gi/utils-inl.h" + +// GIArgument accessor templates +// +// These are intended to make access to the GIArgument union more type-safe and +// reduce bugs that occur from assigning to one member and reading from another. +// (These bugs often work fine on one processor architecture but crash on +// another.) +// +// gjs_arg_member(GIArgument*) - returns a reference to the appropriate union +// member that would hold the type T. Rarely used, unless as a pointer to a +// return location. +// gjs_arg_get(GIArgument*) - returns the value of type T from the +// appropriate union member. +// gjs_arg_set(GIArgument*, T) - sets the appropriate union member for type T. +// gjs_arg_unset(GIArgument*) - sets the appropriate zero value in the +// appropriate union member for type T. + +template +[[nodiscard]] inline decltype(auto) gjs_arg_member(GIArgument* arg, + T GIArgument::*member) { + return (arg->*member); +} + +/* The tag is needed to disambiguate types such as gboolean and GType + * which are in fact typedef's of other generic types. + * Setting a tag for a type allows to perform proper specialization. */ +template +[[nodiscard]] inline decltype(auto) gjs_arg_member(GIArgument* arg) { + if constexpr (TAG == GI_TYPE_TAG_VOID) { + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_boolean); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_int8); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_uint8); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_int16); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_uint16); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_int32); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_uint32); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_int64); + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_uint64); + + // gunichar is stored in v_uint32 + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_uint32); + + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_float); + + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_double); + + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_string); + + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_pointer); + + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_pointer); + + if constexpr (std::is_pointer()) { + using NonconstPtrT = std::add_pointer_t< + std::remove_const_t>>; + return reinterpret_cast( + gjs_arg_member(arg, &GIArgument::v_pointer)); + } + } + + if constexpr (TAG == GI_TYPE_TAG_BOOLEAN && std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_boolean); + + if constexpr (TAG == GI_TYPE_TAG_GTYPE && std::is_same_v) { + // GType is defined differently on 32-bit vs. 64-bit architectures. + if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_size); + else if constexpr (std::is_same_v) + return gjs_arg_member(arg, &GIArgument::v_ulong); + } + + if constexpr (TAG == GI_TYPE_TAG_INTERFACE && std::is_integral_v) { + if constexpr (std::is_signed_v) + return gjs_arg_member(arg, &GIArgument::v_int); + else + return gjs_arg_member(arg, &GIArgument::v_uint); + } +} + +template +inline void gjs_arg_set(GIArgument* arg, T v) { + if constexpr (std::is_pointer_v) { + using NonconstPtrT = + std::add_pointer_t>>; + gjs_arg_member(arg) = const_cast(v); + } else { + if constexpr (std::is_same_v || (std::is_same_v && + TAG == GI_TYPE_TAG_BOOLEAN)) + v = !!v; + + gjs_arg_member(arg) = v; + } +} + +// Store function pointers as void*. It is a requirement of GLib that your +// compiler can do this +template +inline void gjs_arg_set(GIArgument* arg, ReturnT (*v)(Args...)) { + gjs_arg_member(arg) = reinterpret_cast(v); +} + +template +inline std::enable_if_t> gjs_arg_set(GIArgument* arg, + void *v) { + gjs_arg_set(arg, gjs_pointer_to_int(v)); +} + +template +[[nodiscard]] inline T gjs_arg_get(GIArgument* arg) { + if constexpr (std::is_same_v || + (std::is_same_v && TAG == GI_TYPE_TAG_BOOLEAN)) + return !!gjs_arg_member(arg); + + return gjs_arg_member(arg); +} + +template +[[nodiscard]] inline void* gjs_arg_get_as_pointer(GIArgument* arg) { + return gjs_int_to_pointer(gjs_arg_get(arg)); +} + +template +inline void gjs_arg_unset(GIArgument* arg) { + if constexpr (std::is_pointer_v) + gjs_arg_set(arg, nullptr); + else + gjs_arg_set(arg, static_cast(0)); +} + +// Implementation to store rounded (u)int64_t numbers into double + +template +[[nodiscard]] inline constexpr BigT max_safe_big_number() { + return BigT(1) << std::numeric_limits::digits; +} + +template +[[nodiscard]] inline constexpr BigT min_safe_big_number() { + if constexpr (std::is_signed_v) + return -(max_safe_big_number()) + 1; + + return std::numeric_limits::lowest(); +} + +template +[[nodiscard]] inline std::enable_if_t && + (std::numeric_limits::max() > + std::numeric_limits::max()), + double> +gjs_arg_get_maybe_rounded(GIArgument* arg) { + BigT val = gjs_arg_get(arg); + + if (val < min_safe_big_number() || + val > max_safe_big_number()) { + g_warning( + "Value %s cannot be safely stored in a JS Number " + "and may be rounded", + std::to_string(val).c_str()); + } + + return static_cast(val); +} diff --git a/gi/arg.cpp b/gi/arg.cpp index 5da7ca1..1de9f2c 100644 --- a/gi/arg.cpp +++ b/gi/arg.cpp @@ -1,6 +1,7 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* * Copyright (c) 2008 litl, LLC + * Copyright (c) 2020 Canonical, Ltd. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -23,38 +24,57 @@ #include -#include -#include - -#include "arg.h" -#include "gtype.h" -#include "object.h" -#include "interface.h" -#include "foreign.h" -#include "fundamental.h" -#include "boxed.h" -#include "union.h" -#include "param.h" -#include "value.h" -#include "gerror.h" +#include // for strcmp, strlen, memcpy + +#include // for numeric_limits +#include +#include + +#include +#include +#include + +#include +#include +#include +#include // for RootedVector, MutableWrappedPtrOp... +#include // for JSPROP_ENUMERATE +#include +#include +#include // for UniqueChars +#include +#include +#include // for JS_ReportOutOfMemory, JS_GetElement +#include // for JS_IsUint8Array, JS_GetObjectFunc... + +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/boxed.h" +#include "gi/foreign.h" +#include "gi/fundamental.h" +#include "gi/gerror.h" +#include "gi/gtype.h" +#include "gi/interface.h" +#include "gi/object.h" +#include "gi/param.h" +#include "gi/union.h" +#include "gi/value.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" #include "cjs/byteArray.h" -#include "cjs/jsapi-wrapper.h" -#include +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" +#include "util/log.h" -bool -_gjs_flags_value_is_valid(JSContext *context, - GType gtype, - gint64 value) -{ +bool _gjs_flags_value_is_valid(JSContext* context, GType gtype, int64_t value) { GFlagsValue *v; guint32 tmpval; - void *klass; /* FIXME: Do proper value check for flags with GType's */ if (gtype == G_TYPE_NONE) return true; - klass = g_type_class_ref(gtype); + GjsAutoTypeClass klass(gtype); /* check all bits are defined for flags.. not necessarily desired */ tmpval = (guint32)value; @@ -66,7 +86,7 @@ _gjs_flags_value_is_valid(JSContext *context, } while (tmpval) { - v = g_flags_get_first_value((GFlagsClass *) klass, tmpval); + v = g_flags_get_first_value(klass.as(), tmpval); if (!v) { gjs_throw(context, "0x%x is not a valid value for flags %s", @@ -76,16 +96,13 @@ _gjs_flags_value_is_valid(JSContext *context, tmpval &= ~v->value; } - g_type_class_unref(klass); return true; } -static bool -_gjs_enum_value_is_valid(JSContext *context, - GIEnumInfo *enum_info, - gint64 value) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool _gjs_enum_value_is_valid(JSContext* context, GIEnumInfo* enum_info, + int64_t value) { bool found; int n_values; int i; @@ -95,10 +112,9 @@ _gjs_enum_value_is_valid(JSContext *context, for (i = 0; i < n_values; ++i) { GIValueInfo *value_info; - gint64 enum_value; value_info = g_enum_info_get_value(enum_info, i); - enum_value = g_value_info_get_value(value_info); + int64_t enum_value = g_value_info_get_value(value_info); g_base_info_unref((GIBaseInfo *)value_info); if (enum_value == value) { @@ -116,9 +132,7 @@ _gjs_enum_value_is_valid(JSContext *context, return found; } -static bool -_gjs_enum_uses_signed_type (GIEnumInfo *enum_info) -{ +[[nodiscard]] static bool _gjs_enum_uses_signed_type(GIEnumInfo* enum_info) { GITypeTag storage = g_enum_info_get_storage_type(enum_info); return (storage == GI_TYPE_TAG_INT8 || storage == GI_TYPE_TAG_INT16 || @@ -126,38 +140,28 @@ _gjs_enum_uses_signed_type (GIEnumInfo *enum_info) storage == GI_TYPE_TAG_INT64); } -/* This is hacky - g_function_info_invoke() and g_field_info_get/set_field() expect - * arg->v_int to have the enum value in arg->v_int and depend on all flags and - * enumerations being passed on the stack in a 32-bit field. See FIXME comment in - * g_field_info_get_field. The same assumption of enums cast to 32-bit signed integers - * is found in g_value_set_enum/g_value_set_flags(). - */ - -gint64 -_gjs_enum_from_int (GIEnumInfo *enum_info, - int int_value) -{ +// This is hacky - g_function_info_invoke() and g_field_info_get/set_field() +// expect the enum value in gjs_arg_member(arg) and depend on all flags and +// enumerations being passed on the stack in a 32-bit field. See FIXME comment +// in g_field_info_get_field(). The same assumption of enums cast to 32-bit +// signed integers is found in g_value_set_enum()/g_value_set_flags(). +[[nodiscard]] int64_t _gjs_enum_from_int(GIEnumInfo* enum_info, int int_value) { if (_gjs_enum_uses_signed_type (enum_info)) - return (gint64)int_value; + return int64_t(int_value); else - return (gint64)(guint32)int_value; + return int64_t(uint32_t(int_value)); } /* Here for symmetry, but result is the same for the two cases */ -static int -_gjs_enum_to_int (GIEnumInfo *enum_info, - gint64 value) -{ - return (int)value; +[[nodiscard]] static int _gjs_enum_to_int(int64_t value) { + return static_cast(value); } /* Check if an argument of the given needs to be released if we created it * from a JS value to pass it into a function and aren't transfering ownership. */ -static bool -type_needs_release (GITypeInfo *type_info, - GITypeTag type_tag) -{ +[[nodiscard]] static bool type_needs_release(GITypeInfo* type_info, + GITypeTag type_tag) { if (type_tag == GI_TYPE_TAG_UTF8 || type_tag == GI_TYPE_TAG_FILENAME || type_tag == GI_TYPE_TAG_ARRAY || @@ -215,10 +219,8 @@ type_needs_release (GITypeInfo *type_info, /* Check if an argument of the given needs to be released if we obtained it * from out argument (or the return value), and we're transferring ownership */ -static bool -type_needs_out_release(GITypeInfo *type_info, - GITypeTag type_tag) -{ +[[nodiscard]] static bool type_needs_out_release(GITypeInfo* type_info, + GITypeTag type_tag) { if (type_tag == GI_TYPE_TAG_UTF8 || type_tag == GI_TYPE_TAG_FILENAME || type_tag == GI_TYPE_TAG_ARRAY || @@ -253,6 +255,120 @@ type_needs_out_release(GITypeInfo *type_info, return false; } +/* FIXME: This should be added to gobject-introspection */ +[[nodiscard]] static GITypeTag _g_type_info_get_storage_type(GITypeInfo* info) { + GITypeTag type_tag = g_type_info_get_tag(info); + + if (type_tag == GI_TYPE_TAG_INTERFACE) { + GjsAutoBaseInfo interface = g_type_info_get_interface(info); + GIInfoType info_type = g_base_info_get_type(interface); + if (info_type == GI_INFO_TYPE_ENUM || info_type == GI_INFO_TYPE_FLAGS) + return g_enum_info_get_storage_type(interface); + } + + return type_tag; +} + +/* FIXME: This should be added to gobject-introspection */ +static void _g_type_info_argument_from_hash_pointer(GITypeInfo* info, + void* hash_pointer, + GIArgument* arg) { + GITypeTag type_tag = _g_type_info_get_storage_type(info); + + switch (type_tag) { + case GI_TYPE_TAG_BOOLEAN: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_INT8: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_UINT8: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_INT16: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_UINT16: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_INT32: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_UINT32: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_UNICHAR: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_GTYPE: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_UTF8: + case GI_TYPE_TAG_FILENAME: + case GI_TYPE_TAG_INTERFACE: + case GI_TYPE_TAG_ARRAY: + case GI_TYPE_TAG_GLIST: + case GI_TYPE_TAG_GSLIST: + case GI_TYPE_TAG_GHASH: + case GI_TYPE_TAG_ERROR: + gjs_arg_set(arg, hash_pointer); + break; + case GI_TYPE_TAG_INT64: + case GI_TYPE_TAG_UINT64: + case GI_TYPE_TAG_FLOAT: + case GI_TYPE_TAG_DOUBLE: + default: + g_critical("Unsupported type for pointer-stuffing: %s", + g_type_tag_to_string(type_tag)); + gjs_arg_set(arg, hash_pointer); + } +} + +/* FIXME: This should be added to gobject-introspection */ +[[nodiscard]] static void* _g_type_info_hash_pointer_from_argument( + GITypeInfo* info, GIArgument* arg) { + GITypeTag type_tag = _g_type_info_get_storage_type(info); + + switch (type_tag) { + case GI_TYPE_TAG_BOOLEAN: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_INT8: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_UINT8: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_INT16: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_UINT16: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_INT32: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_UINT32: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_UNICHAR: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_GTYPE: + return gjs_arg_get_as_pointer(arg); + case GI_TYPE_TAG_UTF8: + case GI_TYPE_TAG_FILENAME: + case GI_TYPE_TAG_INTERFACE: + case GI_TYPE_TAG_ARRAY: + case GI_TYPE_TAG_GLIST: + case GI_TYPE_TAG_GSLIST: + case GI_TYPE_TAG_GHASH: + case GI_TYPE_TAG_ERROR: + return gjs_arg_get(arg); + case GI_TYPE_TAG_INT64: + case GI_TYPE_TAG_UINT64: + case GI_TYPE_TAG_FLOAT: + case GI_TYPE_TAG_DOUBLE: + default: + g_critical("Unsupported type for pointer-stuffing: %s", + g_type_tag_to_string(type_tag)); + return gjs_arg_get(arg); + } +} + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_g_list(JSContext *context, JS::Value array_value, @@ -312,12 +428,15 @@ gjs_array_to_g_list(JSContext *context, return false; } + void* hash_pointer = + _g_type_info_hash_pointer_from_argument(param_info, &elem_arg); + if (list_type == GI_TYPE_TAG_GLIST) { /* GList */ - list = g_list_prepend(list, elem_arg.v_pointer); + list = g_list_prepend(list, hash_pointer); } else { /* GSList */ - slist = g_slist_prepend(slist, elem_arg.v_pointer); + slist = g_slist_prepend(slist, hash_pointer); } } @@ -330,9 +449,8 @@ gjs_array_to_g_list(JSContext *context, return true; } -static GHashTable * -create_hash_table_for_key_type(GITypeInfo *key_param_info) -{ +[[nodiscard]] static GHashTable* create_hash_table_for_key_type( + GITypeInfo* key_param_info) { /* Don't use key/value destructor functions here, because we can't * construct correct ones in general if the value type is complex. * Rely on the type-aware g_argument_release functions. */ @@ -344,9 +462,69 @@ create_hash_table_for_key_type(GITypeInfo *key_param_info) return g_hash_table_new(NULL, NULL); } +template +GJS_JSAPI_RETURN_CONVENTION static inline std::enable_if_t< + std::is_integral_v && std::is_signed_v, bool> +js_value_convert(JSContext* cx, const JS::HandleValue& value, T* out) { + return JS::ToInt32(cx, value, out); +} + +template +GJS_JSAPI_RETURN_CONVENTION static inline std::enable_if_t< + std::is_integral_v && std::is_unsigned_v, bool> +js_value_convert(JSContext* cx, const JS::HandleValue& value, T* out) { + return JS::ToUint32(cx, value, out); +} + +template +GJS_JSAPI_RETURN_CONVENTION static bool hashtable_int_key( + JSContext* cx, const JS::HandleValue& value, bool* out_of_range, + void** pointer_out) { + Container i; + + static_assert(std::is_integral_v, "Need an integer"); + static_assert(std::numeric_limits::max() >= + std::numeric_limits::max(), + "Max possible Container value must be at least the max possible IntType value"); + static_assert(std::numeric_limits::min() <= + std::numeric_limits::min(), + "Min possible Container value must be at most the min possible IntType value"); + + if (!js_value_convert(cx, value, &i)) + return false; + + if (out_of_range && + (i > static_cast(std::numeric_limits::max()) || + i < static_cast(std::numeric_limits::min()))) + *out_of_range = true; + + *pointer_out = gjs_int_to_pointer(i); + + return true; +} + +template +GJS_JSAPI_RETURN_CONVENTION static inline std::enable_if_t< + std::is_signed_v, bool> +hashtable_int_key(JSContext* cx, const JS::HandleValue& value, + bool* out_of_range, void** pointer_out) { + return hashtable_int_key(cx, value, out_of_range, + pointer_out); +} + +template +GJS_JSAPI_RETURN_CONVENTION static inline std::enable_if_t< + std::is_unsigned_v, bool> +hashtable_int_key(JSContext* cx, const JS::HandleValue& value, + bool* out_of_range, void** pointer_out) { + return hashtable_int_key(cx, value, out_of_range, + pointer_out); +} + /* Converts a JS::Value to a GHashTable key, stuffing it into @pointer_out if * possible, otherwise giving the location of an allocated key in @pointer_out. */ +GJS_JSAPI_RETURN_CONVENTION static bool value_to_ghashtable_key(JSContext *cx, JS::HandleValue value, @@ -366,53 +544,49 @@ value_to_ghashtable_key(JSContext *cx, switch (type_tag) { case GI_TYPE_TAG_BOOLEAN: /* This doesn't seem particularly useful, but it's easy */ - *pointer_out = GUINT_TO_POINTER(JS::ToBoolean(value)); + *pointer_out = gjs_int_to_pointer(JS::ToBoolean(value)); break; case GI_TYPE_TAG_UNICHAR: if (value.isInt32()) { - *pointer_out = GINT_TO_POINTER(value.toInt32()); + *pointer_out = gjs_int_to_pointer(value.toInt32()); } else { uint32_t ch; if (!gjs_unichar_from_string(cx, value, &ch)) return false; - *pointer_out = GUINT_TO_POINTER(ch); + *pointer_out = gjs_int_to_pointer(ch); } break; -#define HANDLE_SIGNED_INT(bits) \ - case GI_TYPE_TAG_INT##bits: { \ - int32_t i; \ - if (!JS::ToInt32(cx, value, &i)) \ - return false; \ - if (i > G_MAXINT##bits || i < G_MININT##bits) \ - out_of_range = true; \ - *pointer_out = GINT_TO_POINTER(i); \ - break; \ - } + case GI_TYPE_TAG_INT8: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; - HANDLE_SIGNED_INT(8); - HANDLE_SIGNED_INT(16); - HANDLE_SIGNED_INT(32); + case GI_TYPE_TAG_INT16: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; -#undef HANDLE_SIGNED_INT + case GI_TYPE_TAG_INT32: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; -#define HANDLE_UNSIGNED_INT(bits) \ - case GI_TYPE_TAG_UINT##bits: { \ - uint32_t i; \ - if (!JS::ToUint32(cx, value, &i)) \ - return false; \ - if (i > G_MAXUINT##bits) \ - out_of_range = true; \ - *pointer_out = GUINT_TO_POINTER(i); \ - break; \ - } + case GI_TYPE_TAG_UINT8: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; - HANDLE_UNSIGNED_INT(8); - HANDLE_UNSIGNED_INT(16); - HANDLE_UNSIGNED_INT(32); + case GI_TYPE_TAG_UINT16: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; -#undef HANDLE_UNSIGNED_INT + case GI_TYPE_TAG_UINT32: + if (!hashtable_int_key(cx, value, &out_of_range, pointer_out)) + return false; + break; case GI_TYPE_TAG_FILENAME: { GjsAutoChar cstr; @@ -434,10 +608,10 @@ value_to_ghashtable_key(JSContext *cx, else str = value.toString(); - GjsAutoJSChar cstr = JS_EncodeStringToUTF8(cx, str); + JS::UniqueChars cstr(JS_EncodeStringToUTF8(cx, str)); if (!cstr) return false; - *pointer_out = cstr.copy(); + *pointer_out = g_strdup(cstr.get()); break; } @@ -481,6 +655,15 @@ value_to_ghashtable_key(JSContext *cx, return true; } +template +[[nodiscard]] static T* heap_value_new_from_arg(GIArgument* val_arg) { + T* heap_val = g_new(T, 1); + *heap_val = gjs_arg_get(val_arg); + + return heap_val; +} + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_object_to_g_hash(JSContext *context, JS::Value hash_value, @@ -489,7 +672,6 @@ gjs_object_to_g_hash(JSContext *context, GITransfer transfer, GHashTable **hash_p) { - GHashTable *result = NULL; size_t id_ix, id_len; g_assert(hash_value.isObjectOrNull()); @@ -514,7 +696,8 @@ gjs_object_to_g_hash(JSContext *context, if (!JS_Enumerate(context, props, &ids)) return false; - result = create_hash_table_for_key_type(key_param_info); + GjsAutoPointer result = + create_hash_table_for_key_type(key_param_info); JS::RootedValue key_js(context), val_js(context); JS::RootedId cur_id(context); @@ -523,56 +706,47 @@ gjs_object_to_g_hash(JSContext *context, gpointer key_ptr, val_ptr; GIArgument val_arg = { 0 }; - if (!JS_IdToValue(context, cur_id, &key_js)) - goto free_hash_and_fail; - - /* Type check key type. */ - if (!value_to_ghashtable_key(context, key_js, key_param_info, &key_ptr)) - goto free_hash_and_fail; - - if (!JS_GetPropertyById(context, props, cur_id, &val_js)) - goto free_hash_and_fail; - - /* Type check and convert value to a c type */ - if (!gjs_value_to_g_argument(context, val_js, val_param_info, NULL, - GJS_ARGUMENT_HASH_ELEMENT, - transfer, - true /* allow null */, - &val_arg)) - goto free_hash_and_fail; + if (!JS_IdToValue(context, cur_id, &key_js) || + // Type check key type. + !value_to_ghashtable_key(context, key_js, key_param_info, + &key_ptr) || + !JS_GetPropertyById(context, props, cur_id, &val_js) || + // Type check and convert value to a C type + !gjs_value_to_g_argument(context, val_js, val_param_info, nullptr, + GJS_ARGUMENT_HASH_ELEMENT, transfer, + true /* allow null */, &val_arg)) + return false; GITypeTag val_type = g_type_info_get_tag(val_param_info); /* Use heap-allocated values for types that don't fit in a pointer */ if (val_type == GI_TYPE_TAG_INT64) { - int64_t *heap_val = g_new(int64_t, 1); - *heap_val = val_arg.v_int64; - val_ptr = heap_val; + val_ptr = heap_value_new_from_arg(&val_arg); } else if (val_type == GI_TYPE_TAG_UINT64) { - uint64_t *heap_val = g_new(uint64_t, 1); - *heap_val = val_arg.v_uint64; - val_ptr = heap_val; + val_ptr = heap_value_new_from_arg(&val_arg); } else if (val_type == GI_TYPE_TAG_FLOAT) { - float *heap_val = g_new(float, 1); - *heap_val = val_arg.v_float; - val_ptr = heap_val; + val_ptr = heap_value_new_from_arg(&val_arg); } else if (val_type == GI_TYPE_TAG_DOUBLE) { - double *heap_val = g_new(double, 1); - *heap_val = val_arg.v_double; - val_ptr = heap_val; + val_ptr = heap_value_new_from_arg(&val_arg); } else { - /* Other types are simply stuffed inside v_pointer */ - val_ptr = val_arg.v_pointer; + // Other types are simply stuffed inside the pointer + val_ptr = _g_type_info_hash_pointer_from_argument(val_param_info, + &val_arg); } +#if __GNUC__ >= 8 // clang-format off +_Pragma("GCC diagnostic push") +_Pragma("GCC diagnostic ignored \"-Wmaybe-uninitialized\"") +#endif + // The compiler isn't smart enough to figure out that key_ptr will + // always be initialized if value_to_ghashtable_key() returns true. g_hash_table_insert(result, key_ptr, val_ptr); +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic pop") +#endif // clang-format on } - *hash_p = result; + *hash_p = result.release(); return true; - - free_hash_and_fail: - g_hash_table_destroy(result); - return false; } bool @@ -581,7 +755,7 @@ gjs_array_from_strv(JSContext *context, const char **strv) { guint i; - JS::AutoValueVector elems(context); + JS::RootedValueVector elems(context); /* We treat a NULL strv as an empty array, since this function should always * set an array value when returning true. @@ -590,14 +764,16 @@ gjs_array_from_strv(JSContext *context, * the case. */ for (i = 0; strv != NULL && strv[i] != NULL; i++) { - if (!elems.growBy(1)) - g_error("Unable to grow vector"); + if (!elems.growBy(1)) { + JS_ReportOutOfMemory(context); + return false; + } if (!gjs_string_from_utf8(context, strv[i], elems[i])) return false; } - JS::RootedObject obj(context, JS_NewArrayObject(context, elems)); + JS::RootedObject obj(context, JS::NewArrayObject(context, elems)); if (!obj) return false; @@ -629,12 +805,12 @@ gjs_array_to_strv(JSContext *context, return false; } - GjsAutoJSChar tmp_result; - if (!gjs_string_to_utf8(context, elem, &tmp_result)) { + JS::UniqueChars tmp_result = gjs_string_to_utf8(context, elem); + if (!tmp_result) { g_strfreev(result); return false; } - result[i] = tmp_result.copy(); + result[i] = g_strdup(tmp_result.get()); } *arr_p = result; @@ -642,6 +818,7 @@ gjs_array_to_strv(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_string_to_intarray(JSContext *context, JS::HandleString str, @@ -655,11 +832,11 @@ 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 = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars result(JS_EncodeStringToUTF8(context, str)); if (!result) return false; - *length = strlen(result); - *arr_p = result.copy(); + *length = strlen(result.get()); + *arr_p = g_strdup(result.get()); return true; } @@ -684,6 +861,7 @@ gjs_string_to_intarray(JSContext *context, return false; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_gboolean_array(JSContext *cx, JS::Value array_value, @@ -710,6 +888,7 @@ gjs_array_to_gboolean_array(JSContext *cx, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_intarray(JSContext *context, JS::Value array_value, @@ -771,17 +950,18 @@ gjs_array_to_intarray(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_gtypearray_to_array(JSContext *context, JS::Value array_value, unsigned int length, void **arr_p) { - GType *result; unsigned i; /* add one so we're always zero terminated */ - result = (GType *) g_malloc0((length+1) * sizeof(GType)); + GjsAutoPointer result = + static_cast(g_malloc0((length + 1) * sizeof(GType))); JS::RootedObject elem_obj(context), array(context, array_value.toObjectOrNull()); JS::RootedValue elem(context); @@ -789,33 +969,31 @@ gjs_gtypearray_to_array(JSContext *context, GType gtype; elem = JS::UndefinedValue(); - if (!JS_GetElement(context, array, i, &elem)) { - g_free(result); - gjs_throw(context, "Missing array element %u", i); + if (!JS_GetElement(context, array, i, &elem)) return false; - } - if (!elem.isObjectOrNull()) - goto err; + if (!elem.isObject()) { + gjs_throw(context, "Invalid element in GType array"); + return false; + } - elem_obj = elem.toObjectOrNull(); - gtype = gjs_gtype_get_actual_gtype(context, elem_obj); - if (gtype == G_TYPE_INVALID) - goto err; + elem_obj = &elem.toObject(); + if (!gjs_gtype_get_actual_gtype(context, elem_obj, >ype)) + return false; + if (gtype == G_TYPE_INVALID) { + gjs_throw(context, "Invalid element in GType array"); + return false; + } result[i] = gtype; } - *arr_p = result; + *arr_p = result.release(); return true; - - err: - g_free(result); - gjs_throw(context, "Invalid element in GType array"); - return false; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_floatarray(JSContext *context, JS::Value array_value, @@ -869,6 +1047,7 @@ gjs_array_to_floatarray(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_ptrarray(JSContext *context, JS::Value array_value, @@ -887,7 +1066,7 @@ gjs_array_to_ptrarray(JSContext *context, for (i = 0; i < length; i++) { GIArgument arg; - arg.v_pointer = NULL; + gjs_arg_unset(&arg); bool success; @@ -916,13 +1095,59 @@ gjs_array_to_ptrarray(JSContext *context, return false; } - array[i] = arg.v_pointer; + array[i] = gjs_arg_get(&arg); } *arr_p = array; return true; } +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_array_to_flat_struct_array(JSContext* cx, + JS::HandleValue array_value, + unsigned length, + GITypeInfo* param_info, + GIBaseInfo* interface_info, + GIInfoType info_type, void** arr_p) { + g_assert( + (info_type == GI_INFO_TYPE_STRUCT || info_type == GI_INFO_TYPE_UNION) && + "Only flat arrays of unboxed structs or unions are supported"); + size_t struct_size; + if (info_type == GI_INFO_TYPE_UNION) + struct_size = g_union_info_get_size(interface_info); + else + struct_size = g_struct_info_get_size(interface_info); + + GjsAutoPointer flat_array = + g_new0(uint8_t, struct_size * length); + + JS::RootedObject array(cx, &array_value.toObject()); + JS::RootedValue elem(cx); + for (unsigned i = 0; i < length; i++) { + elem = JS::UndefinedValue(); + + if (!JS_GetElement(cx, array, i, &elem)) { + gjs_throw(cx, "Missing array element %u", i); + return false; + } + + GIArgument arg; + if (!gjs_value_to_g_argument(cx, elem, param_info, + /* arg_name = */ nullptr, + GJS_ARGUMENT_ARRAY_ELEMENT, + GI_TRANSFER_NOTHING, + /* may_be_null = */ false, &arg)) + return false; + + memcpy(&flat_array[struct_size * i], gjs_arg_get(&arg), + struct_size); + } + + *arr_p = flat_array.release(); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_to_flat_gvalue_array(JSContext *context, JS::Value array_value, @@ -958,6 +1183,7 @@ gjs_array_to_flat_gvalue_array(JSContext *context, return result; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_flat_gvalue_array(JSContext *context, gpointer array, @@ -965,10 +1191,22 @@ gjs_array_from_flat_gvalue_array(JSContext *context, JS::MutableHandleValue value) { GValue *values = (GValue *)array; + + // a null array pointer takes precedence over whatever `length` says + if (!values) { + JSObject* jsarray = JS::NewArrayObject(context, 0); + if (!jsarray) + return false; + value.setObject(*jsarray); + return true; + } + unsigned int i; - JS::AutoValueVector elems(context); - if (!elems.resize(length)) - g_error("Unable to resize vector"); + JS::RootedValueVector elems(context); + if (!elems.resize(length)) { + JS_ReportOutOfMemory(context); + return false; + } bool result = true; @@ -981,17 +1219,14 @@ gjs_array_from_flat_gvalue_array(JSContext *context, if (result) { JSObject *jsarray; - jsarray = JS_NewArrayObject(context, elems); + jsarray = JS::NewArrayObject(context, elems); value.setObjectOrNull(jsarray); } return result; } -static bool -is_gvalue(GIBaseInfo *info, - GIInfoType info_type) -{ +[[nodiscard]] static bool is_gvalue(GIBaseInfo* info, GIInfoType info_type) { if (info_type == GI_INFO_TYPE_VALUE) return true; @@ -1006,10 +1241,8 @@ is_gvalue(GIBaseInfo *info, return false; } -static bool -is_gvalue_flat_array(GITypeInfo *param_info, - GITypeTag element_type) -{ +[[nodiscard]] static bool is_gvalue_flat_array(GITypeInfo* param_info, + GITypeTag element_type) { GIBaseInfo *interface_info; GIInfoType info_type; bool result; @@ -1028,31 +1261,18 @@ is_gvalue_flat_array(GITypeInfo *param_info, return result; } -static bool -gjs_array_to_array(JSContext *context, - JS::Value array_value, - gsize length, - GITransfer transfer, - GITypeInfo *param_info, - void **arr_p) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_array_to_array(JSContext* context, JS::HandleValue array_value, + size_t length, GITransfer transfer, + GITypeInfo* param_info, void** arr_p) { enum { UNSIGNED=false, SIGNED=true }; - GITypeTag element_type; - element_type = g_type_info_get_tag(param_info); + GITypeTag element_type = _g_type_info_get_storage_type(param_info); /* Special case for GValue "flat arrays" */ if (is_gvalue_flat_array(param_info, element_type)) return gjs_array_to_flat_gvalue_array(context, array_value, length, arr_p); - if (element_type == GI_TYPE_TAG_INTERFACE) { - GIBaseInfo *interface_info = g_type_info_get_interface(param_info); - GIInfoType info_type = g_base_info_get_type(interface_info); - if (info_type == GI_INFO_TYPE_ENUM || info_type == GI_INFO_TYPE_FLAGS) - element_type = g_enum_info_get_storage_type ((GIEnumInfo*) interface_info); - g_base_info_unref(interface_info); - } - switch (element_type) { case GI_TYPE_TAG_UTF8: return gjs_array_to_strv (context, array_value, length, arr_p); @@ -1097,6 +1317,20 @@ gjs_array_to_array(JSContext *context, /* Everything else is a pointer type */ case GI_TYPE_TAG_INTERFACE: + if (!g_type_info_is_pointer(param_info)) { + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(param_info); + GIInfoType info_type = g_base_info_get_type(interface_info); + if (info_type == GI_INFO_TYPE_STRUCT || + info_type == GI_INFO_TYPE_UNION) { + // Ignore transfer in the case of a flat struct array. Structs + // are copied by value. + return gjs_array_to_flat_struct_array( + context, array_value, length, param_info, interface_info, + info_type, arr_p); + } + } + [[fallthrough]]; case GI_TYPE_TAG_ARRAY: case GI_TYPE_TAG_GLIST: case GI_TYPE_TAG_GSLIST: @@ -1117,56 +1351,44 @@ gjs_array_to_array(JSContext *context, } } +GJS_JSAPI_RETURN_CONVENTION static GArray* gjs_g_array_new_for_type(JSContext *context, unsigned int length, GITypeInfo *param_info) { - GITypeTag element_type; guint element_size; - - element_type = g_type_info_get_tag(param_info); - - if (element_type == GI_TYPE_TAG_INTERFACE) { - GIInterfaceInfo *interface_info = g_type_info_get_interface(param_info); - GIInfoType interface_type = g_base_info_get_type(interface_info); - - if (interface_type == GI_INFO_TYPE_ENUM - || interface_type == GI_INFO_TYPE_FLAGS) - element_type = g_enum_info_get_storage_type((GIEnumInfo*) interface_info); - - g_base_info_unref((GIBaseInfo*) interface_info); - } + GITypeTag element_type = _g_type_info_get_storage_type(param_info); switch (element_type) { case GI_TYPE_TAG_BOOLEAN: element_size = sizeof(gboolean); break; case GI_TYPE_TAG_UNICHAR: - element_size = sizeof(gunichar); + element_size = sizeof(char32_t); break; case GI_TYPE_TAG_UINT8: case GI_TYPE_TAG_INT8: - element_size = sizeof(guint8); - break; + element_size = sizeof(uint8_t); + break; case GI_TYPE_TAG_UINT16: case GI_TYPE_TAG_INT16: - element_size = sizeof(guint16); - break; + element_size = sizeof(uint16_t); + break; case GI_TYPE_TAG_UINT32: case GI_TYPE_TAG_INT32: - element_size = sizeof(guint32); - break; + element_size = sizeof(uint32_t); + break; case GI_TYPE_TAG_UINT64: case GI_TYPE_TAG_INT64: - element_size = sizeof(guint64); - break; + element_size = sizeof(uint64_t); + break; case GI_TYPE_TAG_FLOAT: - element_size = sizeof(gfloat); - break; + element_size = sizeof(float); + break; case GI_TYPE_TAG_DOUBLE: - element_size = sizeof(gdouble); - break; + element_size = sizeof(double); + break; case GI_TYPE_TAG_GTYPE: element_size = sizeof(GType); break; @@ -1178,8 +1400,8 @@ gjs_g_array_new_for_type(JSContext *context, case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: - element_size = sizeof(gpointer); - break; + element_size = sizeof(void*); + break; case GI_TYPE_TAG_VOID: default: gjs_throw(context, @@ -1190,10 +1412,8 @@ gjs_g_array_new_for_type(JSContext *context, return g_array_sized_new(true, false, element_size, length); } -static gchar * -get_argument_display_name(const char *arg_name, - GjsArgumentType arg_type) -{ +char* gjs_argument_display_name(const char* arg_name, + GjsArgumentType arg_type) { switch (arg_type) { case GJS_ARGUMENT_ARGUMENT: return g_strdup_printf("Argument '%s'", arg_name); @@ -1212,9 +1432,8 @@ get_argument_display_name(const char *arg_name, } } -static const char * -type_tag_to_human_string(GITypeInfo *type_info) -{ +[[nodiscard]] static const char* type_tag_to_human_string( + GITypeInfo* type_info) { GITypeTag tag; tag = g_type_info_get_tag(type_info); @@ -1240,35 +1459,31 @@ throw_invalid_argument(JSContext *context, const char *arg_name, GjsArgumentType arg_type) { - gchar *display_name = get_argument_display_name(arg_name, arg_type); + GjsAutoChar display_name = gjs_argument_display_name(arg_name, arg_type); gjs_throw(context, "Expected type %s for %s but got type '%s'", - type_tag_to_human_string(arginfo), - display_name, JS::InformalValueTypeName(value)); - g_free(display_name); + type_tag_to_human_string(arginfo), display_name.get(), + JS::InformalValueTypeName(value)); } -static bool -gjs_array_to_explicit_array_internal(JSContext *context, - JS::HandleValue value, - GITypeInfo *type_info, - const char *arg_name, - GjsArgumentType arg_type, - GITransfer transfer, - bool may_be_null, - gpointer *contents, - gsize *length_p) -{ - bool ret = false; - GITypeInfo *param_info; +GJS_JSAPI_RETURN_CONVENTION +bool gjs_array_to_explicit_array(JSContext* context, JS::HandleValue value, + GITypeInfo* type_info, const char* arg_name, + GjsArgumentType arg_type, GITransfer transfer, + bool may_be_null, void** contents, + size_t* length_p) { bool found_length; - param_info = g_type_info_get_param_type(type_info, 0); + gjs_debug_marshal( + GJS_DEBUG_GFUNCTION, + "Converting argument '%s' JS value %s to C array, transfer %d", + arg_name, gjs_debug_value(value).c_str(), transfer); + + GjsAutoTypeInfo param_info = g_type_info_get_param_type(type_info, 0); if ((value.isNull() && !may_be_null) || (!value.isString() && !value.isObjectOrNull())) { throw_invalid_argument(context, value, param_info, arg_name, arg_type); - g_base_info_unref((GIBaseInfo*) param_info); return false; } @@ -1279,17 +1494,23 @@ gjs_array_to_explicit_array_internal(JSContext *context, /* Allow strings as int8/uint8/int16/uint16 arrays */ JS::RootedString str(context, value.toString()); if (!gjs_string_to_intarray(context, str, param_info, contents, length_p)) - goto out; + return false; } else { JS::RootedObject array_obj(context, &value.toObject()); - if (gjs_object_has_property(context, array_obj, GJS_STRING_LENGTH, - &found_length) && - found_length) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + GITypeTag element_type = g_type_info_get_tag(param_info); + if (JS_IsUint8Array(array_obj) && (element_type == GI_TYPE_TAG_INT8 || + element_type == GI_TYPE_TAG_UINT8)) { + GBytes* bytes = gjs_byte_array_get_bytes(array_obj); + *contents = g_bytes_unref_to_data(bytes, length_p); + } else if (JS_HasPropertyById(context, array_obj, atoms.length(), + &found_length) && + found_length) { guint32 length; - if (!gjs_object_require_converted_property(context, array_obj, NULL, - GJS_STRING_LENGTH, &length)) { - goto out; + if (!gjs_object_require_converted_property( + context, array_obj, nullptr, atoms.length(), &length)) { + return false; } else { if (!gjs_array_to_array(context, value, @@ -1297,26 +1518,20 @@ gjs_array_to_explicit_array_internal(JSContext *context, transfer, param_info, contents)) - goto out; + return false; *length_p = length; } } else { throw_invalid_argument(context, value, param_info, arg_name, arg_type); - goto out; + return false; } } - ret = true; - out: - g_base_info_unref((GIBaseInfo*) param_info); - - return ret; + return true; } -static bool -is_gdk_atom(GIBaseInfo *info) -{ +[[nodiscard]] 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); } @@ -1325,25 +1540,244 @@ 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"); + GjsAutoFunctionInfo atom_intern_fun = + g_irepository_find_by_name(nullptr, "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; + gjs_arg_set(&atom_intern_args[0], name); + gjs_arg_set(&atom_intern_args[1], false); g_function_info_invoke(atom_intern_fun, atom_intern_args, 2, nullptr, 0, ret, nullptr); +} + +static bool value_to_interface_gi_argument( + JSContext* cx, JS::HandleValue value, GIBaseInfo* interface_info, + GIInfoType interface_type, GITransfer transfer, bool expect_object, + GIArgument* arg, GjsArgumentType arg_type, bool* report_type_mismatch) { + g_assert(report_type_mismatch); + GType gtype = G_TYPE_NONE; + + if (interface_type == GI_INFO_TYPE_STRUCT || + interface_type == GI_INFO_TYPE_ENUM || + interface_type == GI_INFO_TYPE_FLAGS || + interface_type == GI_INFO_TYPE_OBJECT || + interface_type == GI_INFO_TYPE_INTERFACE || + interface_type == GI_INFO_TYPE_UNION || + interface_type == GI_INFO_TYPE_BOXED) { + // These are subtypes of GIRegisteredTypeInfo for which the cast is safe + gtype = g_registered_type_info_get_g_type(interface_info); + } else if (interface_type == GI_INFO_TYPE_VALUE) { + // Special case for GValues + gtype = G_TYPE_VALUE; + } + + if (gtype != G_TYPE_NONE) + gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "gtype of INTERFACE is %s", + g_type_name(gtype)); - g_base_info_unref(atom_intern_fun); + if (gtype == G_TYPE_VALUE) { + GValue gvalue = G_VALUE_INIT; + + if (!gjs_value_to_g_value(cx, value, &gvalue)) { + gjs_arg_unset(arg); + return false; + } + + gjs_arg_set(arg, g_boxed_copy(G_TYPE_VALUE, &gvalue)); + g_value_unset(&gvalue); + return true; + + } else if (is_gdk_atom(interface_info)) { + if (!value.isNull() && !value.isString()) { + *report_type_mismatch = true; + return false; + } else if (value.isNull()) { + intern_gdk_atom("NONE", arg); + return true; + } + + JS::RootedString str(cx, value.toString()); + JS::UniqueChars name(JS_EncodeStringToUTF8(cx, str)); + if (!name) + return false; + + intern_gdk_atom(name.get(), arg); + return true; + + } else if (expect_object != value.isObjectOrNull()) { + *report_type_mismatch = true; + return false; + + } else if (value.isNull()) { + gjs_arg_set(arg, nullptr); + return true; + + } else if (value.isObject()) { + JS::RootedObject obj(cx, &value.toObject()); + if (interface_type == GI_INFO_TYPE_STRUCT && + g_struct_info_is_gtype_struct(interface_info)) { + GType actual_gtype; + if (!gjs_gtype_get_actual_gtype(cx, obj, &actual_gtype)) + return false; + + if (actual_gtype == G_TYPE_NONE) { + *report_type_mismatch = true; + return false; + } + + // We use peek here to simplify reference counting (we just ignore + // transfer annotation, as GType classes are never really freed) + // We know that the GType class is referenced at least once when + // the JS constructor is initialized. + void* klass; + if (g_type_is_a(actual_gtype, G_TYPE_INTERFACE)) + klass = g_type_default_interface_peek(actual_gtype); + else + klass = g_type_class_peek(actual_gtype); + + gjs_arg_set(arg, klass); + return true; + + } else if ((interface_type == GI_INFO_TYPE_STRUCT || + interface_type == GI_INFO_TYPE_BOXED) && + !g_type_is_a(gtype, G_TYPE_CLOSURE)) { + // Handle Struct/Union first since we don't necessarily need a GType + // for them. We special case Closures later, so skip them here. + if (g_type_is_a(gtype, G_TYPE_BYTES) && JS_IsUint8Array(obj)) { + gjs_arg_set(arg, gjs_byte_array_get_bytes(obj)); + return true; + } + if (g_type_is_a(gtype, G_TYPE_ERROR)) { + return ErrorBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer); + } + return BoxedBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype, interface_info); + + } else if (interface_type == GI_INFO_TYPE_UNION) { + return UnionBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype, interface_info); + + } else if (gtype != G_TYPE_NONE) { + if (g_type_is_a(gtype, G_TYPE_OBJECT)) { + return ObjectBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype); + + } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { + if (!gjs_typecheck_param(cx, obj, gtype, true)) { + gjs_arg_unset(arg); + return false; + } + gjs_arg_set(arg, gjs_g_param_from_param(cx, obj)); + if (transfer != GI_TRANSFER_NOTHING) + g_param_spec_ref(gjs_arg_get(arg)); + return true; + + } else if (g_type_is_a(gtype, G_TYPE_BOXED)) { + if (g_type_is_a(gtype, G_TYPE_CLOSURE)) { + GClosure* closure = gjs_closure_new_marshaled( + cx, JS_GetObjectFunction(obj), "boxed"); + // GI doesn't know about floating GClosure references. We + // guess that if this is a return value going from JS::Value + // to GArgument, it's intended to be passed to a C API that + // will consume the floating reference. + if (arg_type != GJS_ARGUMENT_RETURN_VALUE) { + g_closure_ref(closure); + g_closure_sink(closure); + } + gjs_arg_set(arg, closure); + return true; + } + + // Should have been caught above as STRUCT/BOXED/UNION + gjs_throw( + cx, + "Boxed type %s registered for unexpected interface_type %d", + g_type_name(gtype), interface_type); + return false; + + } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { + return FundamentalBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype); + + } else if (G_TYPE_IS_INTERFACE(gtype)) { + // Could be a GObject interface that's missing a prerequisite, + // or could be a fundamental + if (ObjectBase::typecheck(cx, obj, nullptr, gtype, + GjsTypecheckNoThrow())) { + return ObjectBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype); + } + + // If this typecheck fails, then it's neither an object nor a + // fundamental + return FundamentalBase::transfer_to_gi_argument( + cx, obj, arg, GI_DIRECTION_IN, transfer, gtype); + } + + gjs_throw(cx, "Unhandled GType %s unpacking GIArgument from Object", + g_type_name(gtype)); + gjs_arg_unset(arg); + return false; + } + + gjs_debug(GJS_DEBUG_GFUNCTION, + "conversion of JSObject value %s to type %s failed", + gjs_debug_value(value).c_str(), + g_base_info_get_name(interface_info)); + + gjs_throw(cx, + "Unexpected unregistered type unpacking GIArgument from " + "Object"); + return false; + + } else if (value.isNumber()) { + if (interface_type == GI_INFO_TYPE_ENUM) { + int64_t value_int64; + + if (!JS::ToInt64(cx, value, &value_int64) || + !_gjs_enum_value_is_valid(cx, interface_info, value_int64)) + return false; + + gjs_arg_set( + arg, _gjs_enum_to_int(value_int64)); + return true; + + } else if (interface_type == GI_INFO_TYPE_FLAGS) { + int64_t value_int64; + + if (!JS::ToInt64(cx, value, &value_int64) || + !_gjs_flags_value_is_valid(cx, gtype, value_int64)) + return false; + + gjs_arg_set( + arg, _gjs_enum_to_int(value_int64)); + return true; + + } else if (gtype == G_TYPE_NONE) { + gjs_throw(cx, + "Unexpected unregistered type unpacking GIArgument from " + "Number"); + return false; + } + + gjs_throw(cx, "Unhandled GType %s unpacking GIArgument from Number", + g_type_name(gtype)); + return false; + } + + gjs_debug(GJS_DEBUG_GFUNCTION, + "JSObject type '%s' is neither null nor an object", + JS::InformalValueTypeName(value)); + *report_type_mismatch = true; + return false; } bool @@ -1356,27 +1790,24 @@ gjs_value_to_g_argument(JSContext *context, bool may_be_null, GArgument *arg) { - GITypeTag type_tag; - bool wrong; - bool out_of_range; - bool report_type_mismatch; - bool nullable_type; - - type_tag = g_type_info_get_tag( (GITypeInfo*) type_info); + GITypeTag type_tag = g_type_info_get_tag(type_info); - gjs_debug_marshal(GJS_DEBUG_GFUNCTION, - "Converting JS::Value to GArgument %s", - g_type_tag_to_string(type_tag)); + gjs_debug_marshal( + GJS_DEBUG_GFUNCTION, + "Converting argument '%s' JS value %s to GIArgument type %s", arg_name, + gjs_debug_value(value).c_str(), g_type_tag_to_string(type_tag)); - nullable_type = false; - wrong = false; /* return false */ - out_of_range = false; - report_type_mismatch = false; /* wrong=true, and still need to gjs_throw a type problem */ + bool nullable_type = false; + bool wrong = false; // return false + bool out_of_range = false; + bool report_type_mismatch = false; // wrong=true, and still need to + // gjs_throw a type problem switch (type_tag) { case GI_TYPE_TAG_VOID: nullable_type = true; - arg->v_pointer = NULL; /* just so it isn't uninitialized */ + // just so it isn't uninitialized + gjs_arg_unset(arg); break; case GI_TYPE_TAG_INT8: { @@ -1385,7 +1816,7 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (i > G_MAXINT8 || i < G_MININT8) out_of_range = true; - arg->v_int8 = (gint8)i; + gjs_arg_set(arg, i); break; } case GI_TYPE_TAG_UINT8: { @@ -1394,7 +1825,7 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (i > G_MAXUINT8) out_of_range = true; - arg->v_uint8 = (guint8)i; + gjs_arg_set(arg, i); break; } case GI_TYPE_TAG_INT16: { @@ -1403,7 +1834,7 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (i > G_MAXINT16 || i < G_MININT16) out_of_range = true; - arg->v_int16 = (gint16)i; + gjs_arg_set(arg, i); break; } @@ -1413,12 +1844,12 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (i > G_MAXUINT16) out_of_range = true; - arg->v_uint16 = (guint16)i; + gjs_arg_set(arg, i); break; } case GI_TYPE_TAG_INT32: - if (!JS::ToInt32(context, value, &arg->v_int)) + if (!JS::ToInt32(context, value, &gjs_arg_member(arg))) wrong = true; break; @@ -1428,7 +1859,7 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (i > G_MAXUINT32 || i < 0) out_of_range = true; - arg->v_uint32 = CLAMP(i, 0, G_MAXUINT32); + gjs_arg_set(arg, CLAMP(i, 0, G_MAXUINT32)); break; } @@ -1438,7 +1869,7 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (v > G_MAXINT64 || v < G_MININT64) out_of_range = true; - arg->v_int64 = v; + gjs_arg_set(arg, v); } break; @@ -1449,12 +1880,12 @@ gjs_value_to_g_argument(JSContext *context, if (v < 0) out_of_range = true; /* XXX we fail with values close to G_MAXUINT64 */ - arg->v_uint64 = MAX(v, 0); + gjs_arg_set(arg, MAX(v, 0)); } break; case GI_TYPE_TAG_BOOLEAN: - arg->v_boolean = JS::ToBoolean(value); + gjs_arg_set(arg, JS::ToBoolean(value)); break; case GI_TYPE_TAG_FLOAT: { @@ -1463,18 +1894,19 @@ gjs_value_to_g_argument(JSContext *context, wrong = true; if (v > G_MAXFLOAT || v < - G_MAXFLOAT) out_of_range = true; - arg->v_float = (gfloat)v; + gjs_arg_set(arg, v); } break; case GI_TYPE_TAG_DOUBLE: - if (!JS::ToNumber(context, value, &arg->v_double)) + if (!JS::ToNumber(context, value, &gjs_arg_member(arg))) wrong = true; break; case GI_TYPE_TAG_UNICHAR: if (value.isString()) { - if (!gjs_unichar_from_string(context, value, &arg->v_uint32)) + if (!gjs_unichar_from_string(context, value, + &gjs_arg_member(arg))) wrong = true; } else { wrong = true; @@ -1486,10 +1918,13 @@ gjs_value_to_g_argument(JSContext *context, if (value.isObjectOrNull()) { GType gtype; JS::RootedObject obj(context, value.toObjectOrNull()); - gtype = gjs_gtype_get_actual_gtype(context, obj); + if (!gjs_gtype_get_actual_gtype(context, obj, >ype)) { + wrong = true; + break; + } if (gtype == G_TYPE_INVALID) wrong = true; - arg->v_ssize = gtype; + gjs_arg_set(arg, gtype); } else { wrong = true; report_type_mismatch = true; @@ -1499,11 +1934,11 @@ gjs_value_to_g_argument(JSContext *context, case GI_TYPE_TAG_FILENAME: nullable_type = true; if (value.isNull()) { - arg->v_pointer = NULL; + gjs_arg_set(arg, nullptr); } else if (value.isString()) { GjsAutoChar filename_str; if (gjs_string_to_filename(context, value, &filename_str)) - arg->v_pointer = filename_str.release(); + gjs_arg_set(arg, filename_str.release()); else wrong = true; } else { @@ -1514,12 +1949,12 @@ gjs_value_to_g_argument(JSContext *context, case GI_TYPE_TAG_UTF8: nullable_type = true; if (value.isNull()) { - arg->v_pointer = NULL; + gjs_arg_set(arg, nullptr); } else if (value.isString()) { JS::RootedString str(context, value.toString()); - GjsAutoJSChar utf8_str = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars utf8_str(JS_EncodeStringToUTF8(context, str)); if (utf8_str) - arg->v_pointer = utf8_str.copy(); + gjs_arg_set(arg, g_strdup(utf8_str.get())); else wrong = true; } else { @@ -1531,17 +1966,12 @@ gjs_value_to_g_argument(JSContext *context, case GI_TYPE_TAG_ERROR: nullable_type = true; if (value.isNull()) { - arg->v_pointer = NULL; + gjs_arg_set(arg, nullptr); } else if (value.isObject()) { JS::RootedObject obj(context, &value.toObject()); - if (gjs_typecheck_gerror(context, obj, true)) { - arg->v_pointer = gjs_gerror_from_error(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) - arg->v_pointer = g_error_copy ((const GError *) arg->v_pointer); - } else { + if (!ErrorBase::transfer_to_gi_argument(context, obj, arg, + GI_DIRECTION_IN, transfer)) wrong = true; - } } else { wrong = true; report_type_mismatch = true; @@ -1550,16 +1980,13 @@ gjs_value_to_g_argument(JSContext *context, case GI_TYPE_TAG_INTERFACE: { - GIBaseInfo* interface_info; - GIInfoType interface_type; - GType gtype; bool expect_object; - interface_info = g_type_info_get_interface(type_info); - g_assert(interface_info != NULL); - - interface_type = g_base_info_get_type(interface_info); + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(type_info); + g_assert(interface_info); + GIInfoType interface_type = g_base_info_get_type(interface_info); if (interface_type == GI_INFO_TYPE_ENUM || interface_type == GI_INFO_TYPE_FLAGS) { nullable_type = false; @@ -1570,277 +1997,16 @@ gjs_value_to_g_argument(JSContext *context, } if (interface_type == GI_INFO_TYPE_STRUCT && - g_struct_info_is_foreign((GIStructInfo*)interface_info)) { - bool ret; - ret = gjs_struct_foreign_convert_to_g_argument( - context, value, interface_info, arg_name, - arg_type, transfer, may_be_null, arg); - g_base_info_unref(interface_info); - return ret; - } - - if (interface_type == GI_INFO_TYPE_STRUCT || - interface_type == GI_INFO_TYPE_ENUM || - interface_type == GI_INFO_TYPE_FLAGS || - interface_type == GI_INFO_TYPE_OBJECT || - interface_type == GI_INFO_TYPE_INTERFACE || - interface_type == GI_INFO_TYPE_UNION || - interface_type == GI_INFO_TYPE_BOXED) { - /* These are subtypes of GIRegisteredTypeInfo for which the - * cast is safe */ - gtype = g_registered_type_info_get_g_type - ((GIRegisteredTypeInfo*)interface_info); - } else if (interface_type == GI_INFO_TYPE_VALUE) { - /* Special case for GValues */ - gtype = G_TYPE_VALUE; - } else { - gtype = G_TYPE_NONE; + g_struct_info_is_foreign(interface_info)) { + return gjs_struct_foreign_convert_to_g_argument( + context, value, interface_info, arg_name, arg_type, + transfer, may_be_null, arg); } - if (gtype != G_TYPE_NONE) - gjs_debug_marshal(GJS_DEBUG_GFUNCTION, - "gtype of INTERFACE is %s", g_type_name(gtype)); - - if (gtype == G_TYPE_VALUE) { - GValue gvalue = { 0, }; - - if (gjs_value_to_g_value(context, value, &gvalue)) { - arg->v_pointer = g_boxed_copy (G_TYPE_VALUE, &gvalue); - g_value_unset (&gvalue); - } else { - 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()) { + if (!value_to_interface_gi_argument( + context, value, interface_info, interface_type, transfer, + expect_object, arg, arg_type, &report_type_mismatch)) wrong = true; - report_type_mismatch = true; - break; - } else if (value.isNull()) { - arg->v_pointer = NULL; - } else if (value.isObject()) { - JS::RootedObject obj(context, &value.toObject()); - if (interface_type == GI_INFO_TYPE_STRUCT && - g_struct_info_is_gtype_struct((GIStructInfo*)interface_info)) { - GType actual_gtype; - gpointer klass; - - actual_gtype = gjs_gtype_get_actual_gtype(context, obj); - - if (actual_gtype == G_TYPE_NONE) { - wrong = true; - report_type_mismatch = true; - break; - } - - /* We use peek here to simplify reference counting (we just ignore - transfer annotation, as GType classes are never really freed) - We know that the GType class is referenced at least once when - the JS constructor is initialized. - */ - - if (g_type_is_a(actual_gtype, G_TYPE_INTERFACE)) - klass = g_type_default_interface_peek(actual_gtype); - else - klass = g_type_class_peek(actual_gtype); - - arg->v_pointer = klass; - } else if ((interface_type == GI_INFO_TYPE_STRUCT || interface_type == GI_INFO_TYPE_BOXED) && - /* Handle Struct/Union first since we don't necessarily need a GType for them */ - /* We special case Closures later, so skip them here */ - !g_type_is_a(gtype, G_TYPE_CLOSURE)) { - - if (g_type_is_a(gtype, G_TYPE_BYTES) - && gjs_typecheck_bytearray(context, obj, false)) { - arg->v_pointer = gjs_byte_array_get_bytes(context, obj); - } else if (g_type_is_a(gtype, G_TYPE_ERROR)) { - if (!gjs_typecheck_gerror(context, obj, true)) { - arg->v_pointer = NULL; - wrong = true; - } else { - arg->v_pointer = gjs_gerror_from_error(context, obj); - } - } else { - if (!gjs_typecheck_boxed(context, obj, interface_info, - gtype, true)) { - arg->v_pointer = NULL; - wrong = true; - } else { - arg->v_pointer = gjs_c_struct_from_boxed(context, obj); - } - } - - if (!wrong && transfer != GI_TRANSFER_NOTHING) { - if (g_type_is_a(gtype, G_TYPE_BOXED)) - arg->v_pointer = g_boxed_copy (gtype, arg->v_pointer); - else if (g_type_is_a(gtype, G_TYPE_VARIANT)) - g_variant_ref ((GVariant *) arg->v_pointer); - else { - gjs_throw(context, - "Can't transfer ownership of a structure type not registered as boxed"); - arg->v_pointer = NULL; - wrong = true; - } - } - - } else if (interface_type == GI_INFO_TYPE_UNION) { - if (gjs_typecheck_union(context, obj, interface_info, gtype, true)) { - arg->v_pointer = gjs_c_union_from_union(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) { - if (g_type_is_a(gtype, G_TYPE_BOXED)) - arg->v_pointer = g_boxed_copy (gtype, arg->v_pointer); - else { - gjs_throw(context, - "Can't transfer ownership of a union type not registered as boxed"); - - arg->v_pointer = NULL; - wrong = true; - } - } - } else { - arg->v_pointer = NULL; - wrong = true; - } - - } else if (gtype != G_TYPE_NONE) { - if (g_type_is_a(gtype, G_TYPE_OBJECT)) { - if (gjs_typecheck_object(context, obj, gtype, true)) { - arg->v_pointer = gjs_g_object_from_object(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) - g_object_ref(G_OBJECT(arg->v_pointer)); - } else { - arg->v_pointer = NULL; - wrong = true; - } - } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { - if (gjs_typecheck_param(context, obj, gtype, true)) { - arg->v_pointer = gjs_g_param_from_param(context, obj); - if (transfer != GI_TRANSFER_NOTHING) - g_param_spec_ref(G_PARAM_SPEC(arg->v_pointer)); - } else { - arg->v_pointer = NULL; - wrong = true; - } - } else if (g_type_is_a(gtype, G_TYPE_BOXED)) { - if (g_type_is_a(gtype, G_TYPE_CLOSURE)) { - arg->v_pointer = gjs_closure_new_marshaled(context, - &value.toObject(), - "boxed"); - g_closure_ref((GClosure *) arg->v_pointer); - g_closure_sink((GClosure *) arg->v_pointer); - } else { - /* Should have been caught above as STRUCT/BOXED/UNION */ - gjs_throw(context, - "Boxed type %s registered for unexpected interface_type %d", - g_type_name(gtype), - interface_type); - } - } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { - if (gjs_typecheck_fundamental(context, obj, gtype, true)) { - arg->v_pointer = gjs_g_fundamental_from_object(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) - gjs_fundamental_ref(context, arg->v_pointer); - } else { - arg->v_pointer = NULL; - wrong = true; - } - } else if (G_TYPE_IS_INTERFACE(gtype)) { - /* Could be a GObject interface that's missing a prerequisite, or could - be a fundamental */ - if (gjs_typecheck_object(context, obj, gtype, false)) { - arg->v_pointer = gjs_g_object_from_object(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) - g_object_ref(arg->v_pointer); - } else if (gjs_typecheck_fundamental(context, obj, gtype, false)) { - arg->v_pointer = gjs_g_fundamental_from_object(context, obj); - - if (transfer != GI_TRANSFER_NOTHING) - gjs_fundamental_ref(context, arg->v_pointer); - } else { - /* Call again with throw=true to set the exception */ - gjs_typecheck_object(context, obj, gtype, true); - arg->v_pointer = NULL; - wrong = true; - } - } else { - gjs_throw(context, "Unhandled GType %s unpacking GArgument from Object", - g_type_name(gtype)); - arg->v_pointer = NULL; - wrong = true; - } - } else { - gjs_throw(context, "Unexpected unregistered type unpacking GArgument from Object"); - } - - if (arg->v_pointer == NULL) { - gjs_debug(GJS_DEBUG_GFUNCTION, - "conversion of JSObject %p type %s to type %s failed", - &value.toObject(), JS::InformalValueTypeName(value), - g_base_info_get_name ((GIBaseInfo *)interface_info)); - - /* gjs_throw should have been called already */ - wrong = true; - } - - } else if (value.isNumber()) { - if (interface_type == GI_INFO_TYPE_ENUM) { - int64_t value_int64; - - if (!JS::ToInt64(context, value, &value_int64)) - wrong = true; - else if (!_gjs_enum_value_is_valid(context, (GIEnumInfo *)interface_info, value_int64)) - wrong = true; - else - arg->v_int = _gjs_enum_to_int ((GIEnumInfo *)interface_info, value_int64); - - } else if (interface_type == GI_INFO_TYPE_FLAGS) { - int64_t value_int64; - - if (!JS::ToInt64(context, value, &value_int64)) - wrong = true; - else if (!_gjs_flags_value_is_valid(context, gtype, value_int64)) - wrong = true; - else - arg->v_int = _gjs_enum_to_int ((GIEnumInfo *)interface_info, value_int64); - - } else if (gtype == G_TYPE_NONE) { - gjs_throw(context, "Unexpected unregistered type unpacking GArgument from Number"); - wrong = true; - } else { - gjs_throw(context, "Unhandled GType %s unpacking GArgument from Number", - g_type_name(gtype)); - wrong = true; - } - - } else { - gjs_debug(GJS_DEBUG_GFUNCTION, - "JSObject type '%s' is neither null nor an object", - JS::InformalValueTypeName(value)); - wrong = true; - report_type_mismatch = true; - } - g_base_info_unref( (GIBaseInfo*) interface_info); } break; @@ -1852,16 +2018,16 @@ gjs_value_to_g_argument(JSContext *context, */ if (value.isObject()) { bool found_length; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject array_obj(context, &value.toObject()); - if (gjs_object_has_property(context, array_obj, - GJS_STRING_LENGTH, &found_length) && + if (JS_HasPropertyById(context, array_obj, atoms.length(), + &found_length) && found_length) { guint32 length; - if (!gjs_object_require_converted_property(context, array_obj, - NULL, GJS_STRING_LENGTH, - &length)) { + if (!gjs_object_require_converted_property( + context, array_obj, nullptr, atoms.length(), &length)) { wrong = true; } else { GList *list; @@ -1885,9 +2051,9 @@ gjs_value_to_g_argument(JSContext *context, } if (type_tag == GI_TYPE_TAG_GLIST) { - arg->v_pointer = list; + gjs_arg_set(arg, list); } else { - arg->v_pointer = slist; + gjs_arg_set(arg, slist); } g_base_info_unref((GIBaseInfo*) param_info); @@ -1905,7 +2071,7 @@ gjs_value_to_g_argument(JSContext *context, case GI_TYPE_TAG_GHASH: if (value.isNull()) { - arg->v_pointer = NULL; + gjs_arg_set(arg, nullptr); if (!may_be_null) { wrong = true; report_type_mismatch = true; @@ -1930,7 +2096,18 @@ gjs_value_to_g_argument(JSContext *context, &ghash)) { wrong = true; } else { - arg->v_pointer = ghash; +#if __GNUC__ >= 8 // clang-format off +_Pragma("GCC diagnostic push") +_Pragma("GCC diagnostic ignored \"-Wmaybe-uninitialized\"") +#endif + /* The compiler isn't smart enough to figure out that ghash + * will always be initialized if gjs_object_to_g_hash() + * returns true. + */ + gjs_arg_set(arg, ghash); +#if __GNUC__ >= 8 +_Pragma("GCC diagnostic pop") +#endif // clang-format on } g_base_info_unref((GIBaseInfo*) key_param_info); @@ -1944,59 +2121,57 @@ gjs_value_to_g_argument(JSContext *context, GIArrayType array_type = g_type_info_get_array_type(type_info); /* First, let's handle the case where we're passed an instance - * of our own byteArray class. + * of Uint8Array and it needs to be marshalled to GByteArray. */ - if (value.isObjectOrNull()) { - JS::RootedObject bytearray_obj(context, value.toObjectOrNull()); - if (gjs_typecheck_bytearray(context, bytearray_obj, false) - && array_type == GI_ARRAY_TYPE_BYTE_ARRAY) { - arg->v_pointer = gjs_byte_array_get_byte_array(context, bytearray_obj); + if (value.isObject()) { + JSObject* bytearray_obj = &value.toObject(); + if (JS_IsUint8Array(bytearray_obj) && + array_type == GI_ARRAY_TYPE_BYTE_ARRAY) { + gjs_arg_set(arg, gjs_byte_array_get_byte_array(bytearray_obj)); break; } else { /* Fall through, !handled */ } } - if (!gjs_array_to_explicit_array_internal(context, - value, - type_info, - arg_name, - arg_type, - transfer, - may_be_null, - &data, - &length)) { + if (!gjs_array_to_explicit_array(context, value, type_info, arg_name, + arg_type, transfer, may_be_null, &data, + &length)) { wrong = true; break; } GITypeInfo *param_info = g_type_info_get_param_type(type_info, 0); if (array_type == GI_ARRAY_TYPE_C) { - arg->v_pointer = data; + gjs_arg_set(arg, data); } else if (array_type == GI_ARRAY_TYPE_ARRAY) { GArray *array = gjs_g_array_new_for_type(context, length, param_info); if (!array) wrong = true; else { - g_array_append_vals(array, data, length); - arg->v_pointer = array; + if (data) + g_array_append_vals(array, data, length); + gjs_arg_set(arg, array); } g_free(data); } else if (array_type == GI_ARRAY_TYPE_BYTE_ARRAY) { GByteArray *byte_array = g_byte_array_sized_new(length); - g_byte_array_append(byte_array, (const guint8 *) data, length); - arg->v_pointer = byte_array; + if (data) + g_byte_array_append(byte_array, + static_cast(data), length); + gjs_arg_set(arg, byte_array); g_free(data); } else if (array_type == GI_ARRAY_TYPE_PTR_ARRAY) { GPtrArray *array = g_ptr_array_sized_new(length); g_ptr_array_set_size(array, length); - memcpy(array->pdata, data, sizeof(gpointer) * length); - arg->v_pointer = array; + if (data) + memcpy(array->pdata, data, sizeof(void*) * length); + gjs_arg_set(arg, array); g_free(data); } @@ -2017,21 +2192,16 @@ gjs_value_to_g_argument(JSContext *context, } return false; } else if (G_UNLIKELY(out_of_range)) { - gchar *display_name = get_argument_display_name (arg_name, arg_type); + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, arg_type); gjs_throw(context, "value is out of range for %s (type %s)", - display_name, - g_type_tag_to_string(type_tag)); - g_free (display_name); + display_name.get(), g_type_tag_to_string(type_tag)); return false; - } else if (nullable_type && - arg->v_pointer == NULL && - !may_be_null) { - gchar *display_name = get_argument_display_name (arg_name, arg_type); - gjs_throw(context, - "%s (type %s) may not be null", - display_name, + } else if (nullable_type && !gjs_arg_get(arg) && !may_be_null) { + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, arg_type); + gjs_throw(context, "%s (type %s) may not be null", display_name.get(), g_type_tag_to_string(type_tag)); - g_free (display_name); return false; } else { return true; @@ -2046,79 +2216,63 @@ gjs_value_to_g_argument(JSContext *context, * branch of GArgument. (Currently it appears that the return buffer * has a fixed size large enough for the union of all types.) */ -void -gjs_g_argument_init_default(JSContext *context, - GITypeInfo *type_info, - GArgument *arg) -{ +void gjs_gi_argument_init_default(GITypeInfo* type_info, GIArgument* arg) { GITypeTag type_tag; type_tag = g_type_info_get_tag( (GITypeInfo*) type_info); switch (type_tag) { - case GI_TYPE_TAG_VOID: - arg->v_pointer = NULL; /* just so it isn't uninitialized */ - break; - - case GI_TYPE_TAG_INT8: - arg->v_int8 = 0; - break; - - case GI_TYPE_TAG_UINT8: - arg->v_uint8 = 0; - break; - - case GI_TYPE_TAG_INT16: - arg->v_int16 = 0; - break; - - case GI_TYPE_TAG_UINT16: - arg->v_uint16 = 0; - break; - - case GI_TYPE_TAG_INT32: - arg->v_int = 0; - break; - - case GI_TYPE_TAG_UINT32: - case GI_TYPE_TAG_UNICHAR: - arg->v_uint32 = 0; - break; - - case GI_TYPE_TAG_INT64: - arg->v_int64 = 0; - break; - - case GI_TYPE_TAG_UINT64: - arg->v_uint64 = 0; - break; - - case GI_TYPE_TAG_BOOLEAN: - arg->v_boolean = false; - break; - - case GI_TYPE_TAG_FLOAT: - arg->v_float = 0.0f; - break; - - case GI_TYPE_TAG_DOUBLE: - arg->v_double = 0.0; - break; - - case GI_TYPE_TAG_GTYPE: - arg->v_ssize = 0; - break; - - case GI_TYPE_TAG_FILENAME: - case GI_TYPE_TAG_UTF8: - case GI_TYPE_TAG_GLIST: - case GI_TYPE_TAG_GSLIST: - case GI_TYPE_TAG_ERROR: - arg->v_pointer = NULL; - break; - - case GI_TYPE_TAG_INTERFACE: - { + case GI_TYPE_TAG_VOID: + // just so it isn't uninitialized + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_INT8: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_UINT8: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_INT16: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_UINT16: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_INT32: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_UINT32: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_UNICHAR: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_INT64: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_UINT64: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_BOOLEAN: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_FLOAT: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_DOUBLE: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_GTYPE: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_FILENAME: + case GI_TYPE_TAG_UTF8: + case GI_TYPE_TAG_GLIST: + case GI_TYPE_TAG_GSLIST: + case GI_TYPE_TAG_ERROR: + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_INTERFACE: { GIBaseInfo* interface_info; GIInfoType interface_type; @@ -2129,30 +2283,26 @@ gjs_g_argument_init_default(JSContext *context, if (interface_type == GI_INFO_TYPE_ENUM || interface_type == GI_INFO_TYPE_FLAGS) - arg->v_int = 0; + gjs_arg_unset(arg); else if (interface_type == GI_INFO_TYPE_VALUE) /* Better to use a non-NULL value holding NULL? */ - arg->v_pointer = NULL; + gjs_arg_unset(arg); else - arg->v_pointer = NULL; + gjs_arg_unset(arg); g_base_info_unref( (GIBaseInfo*) interface_info); - } - break; - - case GI_TYPE_TAG_GHASH: - /* Possibly better to return an empty hash table? */ - arg->v_pointer = NULL; - break; - - case GI_TYPE_TAG_ARRAY: - arg->v_pointer = NULL; - break; - - default: - g_warning("Unhandled type %s for default GArgument initialization", - g_type_tag_to_string(type_tag)); - break; + } break; + case GI_TYPE_TAG_GHASH: + // Possibly better to return an empty hash table? + gjs_arg_unset(arg); + break; + case GI_TYPE_TAG_ARRAY: + gjs_arg_unset(arg); + break; + default: + g_warning("Unhandled type %s for default GArgument initialization", + g_type_tag_to_string(type_tag)); + break; } } @@ -2176,28 +2326,7 @@ gjs_value_to_arg(JSContext *context, arg); } -bool -gjs_value_to_explicit_array (JSContext *context, - JS::HandleValue value, - GIArgInfo *arg_info, - GIArgument *arg, - size_t *length_p) -{ - GITypeInfo type_info; - - g_arg_info_load_type(arg_info, &type_info); - - return gjs_array_to_explicit_array_internal(context, - value, - &type_info, - g_base_info_get_name((GIBaseInfo*) arg_info), - GJS_ARGUMENT_ARGUMENT, - g_arg_info_get_ownership_transfer(arg_info), - g_arg_info_may_be_null(arg_info), - &arg->v_pointer, - length_p); -} - +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_g_list (JSContext *context, JS::MutableHandleValue value_p, @@ -2208,14 +2337,17 @@ gjs_array_from_g_list (JSContext *context, { unsigned int i; GArgument arg; - JS::AutoValueVector elems(context); + JS::RootedValueVector elems(context); i = 0; if (list_tag == GI_TYPE_TAG_GLIST) { for ( ; list != NULL; list = list->next) { - arg.v_pointer = list->data; - if (!elems.growBy(1)) - g_error("Unable to grow vector"); + _g_type_info_argument_from_hash_pointer(param_info, list->data, + &arg); + if (!elems.growBy(1)) { + JS_ReportOutOfMemory(context); + return false; + } if (!gjs_value_from_g_argument(context, elems[i], param_info, &arg, true)) @@ -2224,9 +2356,12 @@ gjs_array_from_g_list (JSContext *context, } } else { for ( ; slist != NULL; slist = slist->next) { - arg.v_pointer = slist->data; - if (!elems.growBy(1)) - g_error("Unable to grow vector"); + _g_type_info_argument_from_hash_pointer(param_info, slist->data, + &arg); + if (!elems.growBy(1)) { + JS_ReportOutOfMemory(context); + return false; + } if (!gjs_value_from_g_argument(context, elems[i], param_info, &arg, true)) @@ -2235,7 +2370,7 @@ gjs_array_from_g_list (JSContext *context, } } - JS::RootedObject obj(context, JS_NewArrayObject(context, elems)); + JS::RootedObject obj(context, JS::NewArrayObject(context, elems)); if (!obj) return false; @@ -2244,9 +2379,25 @@ gjs_array_from_g_list (JSContext *context, return true; } +template +GJS_JSAPI_RETURN_CONVENTION static bool fill_vector_from_carray( + JSContext* cx, JS::RootedValueVector& elems, // NOLINT(runtime/references) + GITypeInfo* param_info, GIArgument* arg, void* array, size_t length) { + for (size_t i = 0; i < length; i++) { + gjs_arg_set(arg, *(static_cast(array) + i)); + + if (!gjs_value_from_g_argument(cx, elems[i], param_info, arg, true)) + return false; + } + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_carray_internal (JSContext *context, JS::MutableHandleValue value_p, + GIArrayType array_type, GITypeInfo *param_info, guint length, gpointer array) @@ -2262,13 +2413,7 @@ gjs_array_from_carray_internal (JSContext *context, /* Special case array(guint8) */ if (element_type == GI_TYPE_TAG_UINT8) { - GByteArray gbytearray; - - gbytearray.data = (guint8 *) array; - gbytearray.len = length; - - JS::RootedObject obj(context, - gjs_byte_array_from_byte_array(context, &gbytearray)); + JSObject* obj = gjs_byte_array_from_data(context, length, array); if (!obj) return false; value_p.setObject(*obj); @@ -2279,16 +2424,19 @@ gjs_array_from_carray_internal (JSContext *context, if (element_type == GI_TYPE_TAG_UNICHAR) return gjs_string_from_ucs4(context, (gunichar *) array, length, value_p); - JS::AutoValueVector elems(context); - if (!elems.resize(length)) - g_error("Unable to resize vector"); + // a null array pointer takes precedence over whatever `length` says + if (!array) { + JSObject* jsarray = JS::NewArrayObject(context, 0); + if (!jsarray) + return false; + value_p.setObject(*jsarray); + return true; + } -#define ITERATE(type) \ - for (i = 0; i < length; i++) { \ - arg.v_##type = *(((g##type*)array) + i); \ - if (!gjs_value_from_g_argument(context, elems[i], param_info, \ - &arg, true)) \ - return false; \ + JS::RootedValueVector elems(context); + if (!elems.resize(length)) { + JS_ReportOutOfMemory(context); + return false; } switch (element_type) { @@ -2297,51 +2445,73 @@ gjs_array_from_carray_internal (JSContext *context, case GI_TYPE_TAG_UNICHAR: g_assert_not_reached(); case GI_TYPE_TAG_BOOLEAN: - ITERATE(boolean); + if (!fill_vector_from_carray( + context, elems, param_info, &arg, array, length)) + return false; break; case GI_TYPE_TAG_INT8: - ITERATE(int8); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_UINT16: - ITERATE(uint16); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_INT16: - ITERATE(int16); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_UINT32: - ITERATE(uint32); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_INT32: - ITERATE(int32); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_UINT64: - ITERATE(uint64); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_INT64: - ITERATE(int64); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_FLOAT: - ITERATE(float); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_DOUBLE: - ITERATE(double); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_INTERFACE: { GIBaseInfo *interface_info = g_type_info_get_interface (param_info); GIInfoType info_type = g_base_info_get_type (interface_info); - if ((info_type == GI_INFO_TYPE_STRUCT || + if (array_type != GI_ARRAY_TYPE_PTR_ARRAY && + (info_type == GI_INFO_TYPE_STRUCT || info_type == GI_INFO_TYPE_UNION) && - !g_type_info_is_pointer (param_info)) { + !g_type_info_is_pointer(param_info)) { size_t struct_size; if (info_type == GI_INFO_TYPE_UNION) - struct_size = g_union_info_get_size (interface_info); + struct_size = g_union_info_get_size(interface_info); else - struct_size = g_struct_info_get_size (interface_info); + struct_size = g_struct_info_get_size(interface_info); for (i = 0; i < length; i++) { - arg.v_pointer = static_cast(array) + (struct_size * i); + gjs_arg_set(&arg, + static_cast(array) + (struct_size * i)); if (!gjs_value_from_g_argument(context, elems[i], param_info, &arg, true)) @@ -2363,17 +2533,17 @@ gjs_array_from_carray_internal (JSContext *context, case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: - ITERATE(pointer); - break; + if (!fill_vector_from_carray(context, elems, param_info, + &arg, array, length)) + return false; + break; case GI_TYPE_TAG_VOID: default: gjs_throw(context, "Unknown Array element-type %d", element_type); return false; } -#undef ITERATE - - JS::RootedObject obj(context, JS_NewArrayObject(context, elems)); + JS::RootedObject obj(context, JS::NewArrayObject(context, elems)); if (!obj) return false; @@ -2382,6 +2552,7 @@ gjs_array_from_carray_internal (JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_fixed_size_array (JSContext *context, JS::MutableHandleValue value_p, @@ -2398,7 +2569,9 @@ gjs_array_from_fixed_size_array (JSContext *context, param_info = g_type_info_get_param_type(type_info, 0); - res = gjs_array_from_carray_internal(context, value_p, param_info, length, array); + res = gjs_array_from_carray_internal(context, value_p, + g_type_info_get_array_type(type_info), + param_info, length, array); g_base_info_unref((GIBaseInfo*)param_info); @@ -2413,17 +2586,19 @@ gjs_value_from_explicit_array(JSContext *context, int length) { GITypeInfo *param_info; - bool res; param_info = g_type_info_get_param_type(type_info, 0); - res = gjs_array_from_carray_internal(context, value_p, param_info, length, arg->v_pointer); + bool res = gjs_array_from_carray_internal( + context, value_p, g_type_info_get_array_type(type_info), param_info, + length, gjs_arg_get(arg)); g_base_info_unref((GIBaseInfo*)param_info); return res; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_boxed_array (JSContext *context, JS::MutableHandleValue value_p, @@ -2436,7 +2611,7 @@ gjs_array_from_boxed_array (JSContext *context, gpointer data = NULL; gsize length = 0; - if (arg->v_pointer == NULL) { + if (!gjs_arg_get(arg)) { value_p.setNull(); return true; } @@ -2445,12 +2620,12 @@ gjs_array_from_boxed_array (JSContext *context, case GI_ARRAY_TYPE_BYTE_ARRAY: /* GByteArray is just a typedef for GArray internally */ case GI_ARRAY_TYPE_ARRAY: - array = (GArray*)(arg->v_pointer); + array = gjs_arg_get(arg); data = array->data; length = array->len; break; case GI_ARRAY_TYPE_PTR_ARRAY: - ptr_array = (GPtrArray*)(arg->v_pointer); + ptr_array = gjs_arg_get(arg); data = ptr_array->pdata; length = ptr_array->len; break; @@ -2459,9 +2634,32 @@ gjs_array_from_boxed_array (JSContext *context, g_assert_not_reached(); } - return gjs_array_from_carray_internal(context, value_p, param_info, length, data); + return gjs_array_from_carray_internal(context, value_p, array_type, + param_info, length, data); +} + +template +GJS_JSAPI_RETURN_CONVENTION static bool fill_vector_from_zero_terminated_carray( + JSContext* cx, JS::RootedValueVector& elems, // NOLINT(runtime/references) + GITypeInfo* param_info, GIArgument* arg, void* c_array) { + T* array = static_cast(c_array); + + for (size_t i = 0; array[i]; i++) { + gjs_arg_set(arg, array[i]); + + if (!elems.growBy(1)) { + JS_ReportOutOfMemory(cx); + return false; + } + + if (!gjs_value_from_g_argument(cx, elems[i], param_info, arg, true)) + return false; + } + + return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_array_from_zero_terminated_c_array (JSContext *context, JS::MutableHandleValue value_p, @@ -2470,19 +2668,13 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, { GArgument arg; GITypeTag element_type; - guint i; element_type = g_type_info_get_tag(param_info); /* Special case array(guint8) */ if (element_type == GI_TYPE_TAG_UINT8) { - GByteArray gbytearray; - - gbytearray.data = (guint8 *) c_array; - gbytearray.len = strlen((const char *) c_array); - - JS::RootedObject obj(context, - gjs_byte_array_from_byte_array(context, &gbytearray)); + size_t len = strlen(static_cast(c_array)); + JSObject* obj = gjs_byte_array_from_data(context, len, c_array); if (!obj) return false; value_p.setObject(*obj); @@ -2493,20 +2685,7 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, if (element_type == GI_TYPE_TAG_UNICHAR) return gjs_string_from_ucs4(context, (gunichar *) c_array, -1, value_p); - JS::AutoValueVector elems(context); - -#define ITERATE(type) \ - do { \ - g##type *array = (g##type *) c_array; \ - for (i = 0; array[i]; i++) { \ - arg.v_##type = array[i]; \ - if (!elems.growBy(1)) \ - g_error("Unable to grow vector"); \ - if (!gjs_value_from_g_argument(context, elems[i], \ - param_info, &arg, true)) \ - return false; \ - } \ - } while(0); + JS::RootedValueVector elems(context); switch (element_type) { /* Special cases handled above. */ @@ -2514,32 +2693,50 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, case GI_TYPE_TAG_UNICHAR: g_assert_not_reached(); case GI_TYPE_TAG_INT8: - ITERATE(int8); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_UINT16: - ITERATE(uint16); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_INT16: - ITERATE(int16); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_UINT32: - ITERATE(uint32); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_INT32: - ITERATE(int32); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_UINT64: - ITERATE(uint64); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_INT64: - ITERATE(int64); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_FLOAT: - ITERATE(float); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_DOUBLE: - ITERATE(double); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; case GI_TYPE_TAG_GTYPE: case GI_TYPE_TAG_UTF8: case GI_TYPE_TAG_FILENAME: @@ -2549,8 +2746,10 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: - ITERATE(pointer); - break; + if (!fill_vector_from_zero_terminated_carray( + context, elems, param_info, &arg, c_array)) + return false; + break; /* Boolean zero-terminated array makes no sense, because FALSE is also * zero */ case GI_TYPE_TAG_BOOLEAN: @@ -2562,9 +2761,7 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, return false; } -#undef ITERATE - - JS::RootedObject obj(context, JS_NewArrayObject(context, elems)); + JS::RootedObject obj(context, JS::NewArrayObject(context, elems)); if (!obj) return false; @@ -2573,7 +2770,7 @@ gjs_array_from_zero_terminated_c_array (JSContext *context, return true; } - +GJS_JSAPI_RETURN_CONVENTION static bool gjs_object_from_g_hash (JSContext *context, JS::MutableHandleValue value_p, @@ -2600,8 +2797,11 @@ gjs_object_from_g_hash (JSContext *context, JS::RootedString keystr(context); g_hash_table_iter_init(&iter, hash); - while (g_hash_table_iter_next - (&iter, &keyarg.v_pointer, &valarg.v_pointer)) { + void* key_pointer; + void* val_pointer; + while (g_hash_table_iter_next(&iter, &key_pointer, &val_pointer)) { + _g_type_info_argument_from_hash_pointer(key_param_info, key_pointer, + &keyarg); if (!gjs_value_from_g_argument(context, &keyjs, key_param_info, &keyarg, true)) @@ -2611,25 +2811,25 @@ gjs_object_from_g_hash (JSContext *context, if (!keystr) return false; - GjsAutoJSChar keyutf8 = JS_EncodeStringToUTF8(context, keystr); + JS::UniqueChars keyutf8(JS_EncodeStringToUTF8(context, keystr)); if (!keyutf8) return false; + _g_type_info_argument_from_hash_pointer(val_param_info, val_pointer, + &valarg); if (!gjs_value_from_g_argument(context, &valjs, val_param_info, &valarg, true)) return false; - if (!JS_DefineProperty(context, obj, keyutf8, valjs, JSPROP_ENUMERATE)) + if (!JS_DefineProperty(context, obj, keyutf8.get(), valjs, + JSPROP_ENUMERATE)) return false; } return true; } -static const int64_t MAX_SAFE_INT64 = - int64_t(1) << std::numeric_limits::digits; - bool gjs_value_from_g_argument (JSContext *context, JS::MutableHandleValue value_p, @@ -2653,59 +2853,52 @@ gjs_value_from_g_argument (JSContext *context, break; case GI_TYPE_TAG_BOOLEAN: - value_p.setBoolean(!!arg->v_int); + value_p.setBoolean(gjs_arg_get(arg)); break; case GI_TYPE_TAG_INT32: - value_p.setInt32(arg->v_int); + value_p.setInt32(gjs_arg_get(arg)); break; case GI_TYPE_TAG_UINT32: - value_p.setNumber(arg->v_uint); + value_p.setNumber(gjs_arg_get(arg)); break; case GI_TYPE_TAG_INT64: - if (arg->v_int64 == G_MININT64 || - std::abs(arg->v_int64) > MAX_SAFE_INT64) - g_warning("Value %" G_GINT64_FORMAT " cannot be safely stored in " - "a JS Number and may be rounded", arg->v_int64); - value_p.setNumber(static_cast(arg->v_int64)); + value_p.setNumber(gjs_arg_get_maybe_rounded(arg)); break; case GI_TYPE_TAG_UINT64: - if (arg->v_uint64 > MAX_SAFE_INT64) - g_warning("Value %" G_GUINT64_FORMAT " cannot be safely stored in " - "a JS Number and may be rounded", arg->v_uint64); - value_p.setNumber(static_cast(arg->v_uint64)); + value_p.setNumber(gjs_arg_get_maybe_rounded(arg)); break; case GI_TYPE_TAG_UINT16: - value_p.setInt32(arg->v_uint16); + value_p.setInt32(gjs_arg_get(arg)); break; case GI_TYPE_TAG_INT16: - value_p.setInt32(arg->v_int16); + value_p.setInt32(gjs_arg_get(arg)); break; case GI_TYPE_TAG_UINT8: - value_p.setInt32(arg->v_uint8); + value_p.setInt32(gjs_arg_get(arg)); break; case GI_TYPE_TAG_INT8: - value_p.setInt32(arg->v_int8); + value_p.setInt32(gjs_arg_get(arg)); break; case GI_TYPE_TAG_FLOAT: - value_p.setNumber(arg->v_float); + value_p.setNumber(gjs_arg_get(arg)); break; case GI_TYPE_TAG_DOUBLE: - value_p.setNumber(arg->v_double); + value_p.setNumber(gjs_arg_get(arg)); break; case GI_TYPE_TAG_GTYPE: { - GType gtype = arg->v_ssize; + GType gtype = gjs_arg_get(arg); if (gtype == 0) return true; /* value_p is set to JS null */ @@ -2718,68 +2911,59 @@ gjs_value_from_g_argument (JSContext *context, } break; - case GI_TYPE_TAG_UNICHAR: - { - char utf8[7]; - gint bytes; + case GI_TYPE_TAG_UNICHAR: { + char32_t value = gjs_arg_get(arg); - /* Preserve the bidirectional mapping between 0 and "" */ - if (arg->v_uint32 == 0) { - 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, - arg->v_uint32); - return false; - } else { - bytes = g_unichar_to_utf8 (arg->v_uint32, utf8); - return gjs_string_from_utf8_n(context, utf8, bytes, value_p); - } + // Preserve the bidirectional mapping between 0 and "" + if (value == 0) { + value_p.set(JS_GetEmptyStringValue(context)); + return true; + } else if (!g_unichar_validate(value)) { + gjs_throw(context, "Invalid unicode codepoint %" G_GUINT32_FORMAT, + value); + return false; } + char utf8[7]; + int bytes = g_unichar_to_utf8(value, utf8); + return gjs_string_from_utf8_n(context, utf8, bytes, value_p); + } + case GI_TYPE_TAG_FILENAME: - if (arg->v_pointer) - return gjs_string_from_filename(context, (const char *) arg->v_pointer, -1, value_p); - else { - /* For NULL we'll return JS::NullValue(), which is already set - * in *value_p - */ - return true; - } - case GI_TYPE_TAG_UTF8: - 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 - */ + case GI_TYPE_TAG_UTF8: { + const char* str = gjs_arg_get(arg); + // For nullptr we'll return JS::NullValue(), which is already set + // in *value_p + if (!str) return true; - } - case GI_TYPE_TAG_ERROR: - { - if (arg->v_pointer) { - JSObject *obj = gjs_error_from_gerror(context, (GError *) arg->v_pointer, false); - if (obj) { - value_p.setObject(*obj); - return true; - } + if (type_tag == GI_TYPE_TAG_FILENAME) + return gjs_string_from_filename(context, str, -1, value_p); - return false; - } + return gjs_string_from_utf8(context, str, value_p); + } + + case GI_TYPE_TAG_ERROR: { + GError* ptr = gjs_arg_get(arg); + if (!ptr) return true; - } + + JSObject* obj = ErrorInstance::object_for_c_ptr(context, ptr); + if (!obj) + return false; + + value_p.setObject(*obj); + return true; + } case GI_TYPE_TAG_INTERFACE: { - JS::RootedValue value(context); - GIBaseInfo* interface_info; GIInfoType interface_type; GType gtype; - interface_info = g_type_info_get_interface(type_info); - g_assert(interface_info != NULL); + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(type_info); + g_assert(interface_info); interface_type = g_base_info_get_type(interface_info); @@ -2787,64 +2971,67 @@ gjs_value_from_g_argument (JSContext *context, gjs_throw(context, "Unable to resolve arg type '%s'", g_base_info_get_name(interface_info)); - goto out; + return false; } /* Enum/Flags are aren't pointer types, unlike the other interface subtypes */ if (interface_type == GI_INFO_TYPE_ENUM) { - gint64 value_int64 = _gjs_enum_from_int ((GIEnumInfo *)interface_info, arg->v_int); + int64_t value_int64 = _gjs_enum_from_int( + interface_info, + gjs_arg_get(arg)); - if (_gjs_enum_value_is_valid(context, (GIEnumInfo *)interface_info, value_int64)) { - value = JS::NumberValue(value_int64); - } + if (!_gjs_enum_value_is_valid(context, interface_info, + value_int64)) + return false; + + value_p.setNumber(static_cast(value_int64)); + return true; + } - goto out; - } else if (interface_type == GI_INFO_TYPE_FLAGS) { - gint64 value_int64 = _gjs_enum_from_int ((GIEnumInfo *)interface_info, arg->v_int); + if (interface_type == GI_INFO_TYPE_FLAGS) { + int64_t value_int64 = _gjs_enum_from_int( + interface_info, + gjs_arg_get(arg)); gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)interface_info); - if (_gjs_flags_value_is_valid(context, gtype, value_int64)) { - value = JS::NumberValue(value_int64); - } + if (!_gjs_flags_value_is_valid(context, gtype, value_int64)) + return false; - goto out; - } else if (interface_type == GI_INFO_TYPE_STRUCT && - g_struct_info_is_foreign((GIStructInfo*)interface_info)) { - bool ret; - ret = gjs_struct_foreign_convert_from_g_argument(context, value_p, interface_info, arg); - g_base_info_unref(interface_info); - return ret; + value_p.setNumber(static_cast(value_int64)); + return true; + } + + if (interface_type == GI_INFO_TYPE_STRUCT && + g_struct_info_is_foreign((GIStructInfo*)interface_info)) { + return gjs_struct_foreign_convert_from_g_argument( + context, value_p, interface_info, arg); } /* Everything else is a pointer type, NULL is the easy case */ - if (arg->v_pointer == NULL) { - value = JS::NullValue(); - goto out; + if (!gjs_arg_get(arg)) { + value_p.setNull(); + return true; } if (interface_type == GI_INFO_TYPE_STRUCT && g_struct_info_is_gtype_struct((GIStructInfo*)interface_info)) { - bool ret; - /* XXX: here we make the implicit assumption that GTypeClass is the same as GTypeInterface. This is true for the GType field, which is what we use, but not for the rest of the structure! */ - gtype = G_TYPE_FROM_CLASS(arg->v_pointer); - - if (g_type_is_a(gtype, G_TYPE_INTERFACE)) - ret = gjs_lookup_interface_constructor(context, gtype, value_p); - else - ret = gjs_lookup_object_constructor(context, gtype, value_p); + gtype = G_TYPE_FROM_CLASS(gjs_arg_get(arg)); - g_base_info_unref(interface_info); - return ret; + if (g_type_is_a(gtype, G_TYPE_INTERFACE)) { + return gjs_lookup_interface_constructor(context, gtype, + value_p); + } + return gjs_lookup_object_constructor(context, gtype, value_p); } gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)interface_info); if (G_TYPE_IS_INSTANTIATABLE(gtype) || G_TYPE_IS_INTERFACE(gtype)) - gtype = G_TYPE_FROM_INSTANCE(arg->v_pointer); + gtype = G_TYPE_FROM_INSTANCE(gjs_arg_get(arg)); gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "gtype of INTERFACE is %s", g_type_name(gtype)); @@ -2852,21 +3039,17 @@ gjs_value_from_g_argument (JSContext *context, /* Test GValue and GError before Struct, or it will be handled as the latter */ if (g_type_is_a(gtype, G_TYPE_VALUE)) { - if (!gjs_value_from_g_value(context, &value, (const GValue *) arg->v_pointer)) - value = JS::UndefinedValue(); /* Make sure error is flagged */ - - goto out; + return gjs_value_from_g_value(context, value_p, + gjs_arg_get(arg)); } - if (g_type_is_a(gtype, G_TYPE_ERROR)) { - JSObject *obj; - obj = gjs_error_from_gerror(context, (GError *) arg->v_pointer, false); - if (obj) - value = JS::ObjectValue(*obj); - else - value = JS::UndefinedValue(); - - goto out; + if (g_type_is_a(gtype, G_TYPE_ERROR)) { + JSObject* obj = ErrorInstance::object_for_c_ptr( + context, gjs_arg_get(arg)); + if (!obj) + return false; + value_p.setObject(*obj); + return true; } if (interface_type == GI_INFO_TYPE_STRUCT || interface_type == GI_INFO_TYPE_BOXED) { @@ -2881,106 +3064,106 @@ gjs_value_from_g_argument (JSContext *context, 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(); + GjsAutoChar name = gjs_arg_get(&atom_name_ret); + if (g_strcmp0("NONE", name) == 0) { + value_p.setNull(); 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; + return gjs_string_from_utf8(context, name, value_p); } JSObject *obj; - GjsBoxedCreationFlags flags; - if (copy_structs) - flags = GJS_BOXED_CREATION_NONE; - else if (g_type_is_a(gtype, G_TYPE_VARIANT)) - flags = GJS_BOXED_CREATION_NONE; + if (copy_structs || g_type_is_a(gtype, G_TYPE_VARIANT)) + obj = BoxedInstance::new_for_c_struct( + context, interface_info, gjs_arg_get(arg)); else - flags = GJS_BOXED_CREATION_NO_COPY; + obj = BoxedInstance::new_for_c_struct( + context, interface_info, gjs_arg_get(arg), + BoxedInstance::NoCopy()); - obj = gjs_boxed_from_c_struct(context, - (GIStructInfo *)interface_info, - arg->v_pointer, - flags); + if (!obj) + return false; - if (obj) - value = JS::ObjectValue(*obj); + value_p.setObject(*obj); + return true; + } - goto out; - } else if (interface_type == GI_INFO_TYPE_UNION) { - JSObject *obj; - obj = gjs_union_from_c_union(context, (GIUnionInfo *)interface_info, arg->v_pointer); - if (obj) - value = JS::ObjectValue(*obj); + if (interface_type == GI_INFO_TYPE_UNION) { + JSObject* obj = gjs_union_from_c_union( + context, static_cast(interface_info), + gjs_arg_get(arg)); + if (!obj) + return false; - goto out; + value_p.setObject(*obj); + return true; } if (g_type_is_a(gtype, G_TYPE_OBJECT)) { - JSObject *obj; - obj = gjs_object_from_g_object(context, G_OBJECT(arg->v_pointer)); - if (obj) - value = JS::ObjectValue(*obj); - } else if (g_type_is_a(gtype, G_TYPE_BOXED) || - g_type_is_a(gtype, G_TYPE_ENUM) || - g_type_is_a(gtype, G_TYPE_FLAGS)) { + // Null arg is already handled above + JSObject* obj = ObjectInstance::wrapper_from_gobject( + context, G_OBJECT(gjs_arg_get(arg))); + if (!obj) + return false; + value_p.setObject(*obj); + return true; + } + + if (g_type_is_a(gtype, G_TYPE_BOXED) || + g_type_is_a(gtype, G_TYPE_ENUM) || + g_type_is_a(gtype, G_TYPE_FLAGS)) { /* Should have been handled above */ gjs_throw(context, "Type %s registered for unexpected interface_type %d", g_type_name(gtype), interface_type); return false; - } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { - JSObject *obj; - obj = gjs_param_from_g_param(context, G_PARAM_SPEC(arg->v_pointer)); - if (obj) - value = JS::ObjectValue(*obj); - } else if (gtype == G_TYPE_NONE) { - gjs_throw(context, "Unexpected unregistered type packing GArgument into JS::Value"); - } else if (G_TYPE_IS_INSTANTIATABLE(gtype) || G_TYPE_IS_INTERFACE(gtype)) { - JSObject *obj; - obj = gjs_object_from_g_fundamental(context, (GIObjectInfo *)interface_info, arg->v_pointer); - if (obj) - value = JS::ObjectValue(*obj); - } else { - gjs_throw(context, "Unhandled GType %s packing GArgument into JS::Value", - g_type_name(gtype)); } - out: - g_base_info_unref( (GIBaseInfo*) interface_info); + if (g_type_is_a(gtype, G_TYPE_PARAM)) { + JSObject* obj = gjs_param_from_g_param( + context, G_PARAM_SPEC(gjs_arg_get(arg))); + if (!obj) + return false; + value_p.setObject(*obj); + return true; + } - if (value.isUndefined()) + if (gtype == G_TYPE_NONE) { + gjs_throw(context, "Unexpected unregistered type packing GArgument into JS::Value"); return false; + } + + if (G_TYPE_IS_INSTANTIATABLE(gtype) || G_TYPE_IS_INTERFACE(gtype)) { + JSObject* obj = FundamentalInstance::object_for_c_ptr( + context, gjs_arg_get(arg)); + if (!obj) + return false; + value_p.setObject(*obj); + return true; + } - value_p.set(value); + gjs_throw(context, + "Unhandled GType %s packing GArgument into JS::Value", + g_type_name(gtype)); + return false; } - break; case GI_TYPE_TAG_ARRAY: - if (arg->v_pointer == NULL) { + if (!gjs_arg_get(arg)) { /* OK, but no conversion to do */ } else if (g_type_info_get_array_type(type_info) == GI_ARRAY_TYPE_C) { - if (g_type_info_is_zero_terminated(type_info)) { GITypeInfo *param_info; - bool result; param_info = g_type_info_get_param_type(type_info, 0); g_assert(param_info != NULL); - result = gjs_array_from_zero_terminated_c_array(context, - value_p, - param_info, - arg->v_pointer); + bool result = gjs_array_from_zero_terminated_c_array( + context, value_p, param_info, gjs_arg_get(arg)); g_base_info_unref((GIBaseInfo*) param_info); @@ -2990,13 +3173,17 @@ gjs_value_from_g_argument (JSContext *context, g_assert(((void) "Use gjs_value_from_explicit_array() for " "arrays with length param", g_type_info_get_array_length(type_info) == -1)); - return gjs_array_from_fixed_size_array(context, value_p, type_info, arg->v_pointer); + return gjs_array_from_fixed_size_array( + context, value_p, type_info, gjs_arg_get(arg)); } - } else if (g_type_info_get_array_type(type_info) == GI_ARRAY_TYPE_BYTE_ARRAY) { - JSObject *array = gjs_byte_array_from_byte_array(context, - (GByteArray*)arg->v_pointer); + } else if (g_type_info_get_array_type(type_info) == + GI_ARRAY_TYPE_BYTE_ARRAY) { + auto* byte_array = gjs_arg_get(arg); + JSObject* array = + gjs_byte_array_from_byte_array(context, byte_array); if (!array) { - gjs_throw(context, "Couldn't convert GByteArray to a ByteArray"); + gjs_throw(context, + "Couldn't convert GByteArray to a Uint8Array"); return false; } value_p.setObject(*array); @@ -3025,19 +3212,16 @@ gjs_value_from_g_argument (JSContext *context, case GI_TYPE_TAG_GSLIST: { GITypeInfo *param_info; - bool result; param_info = g_type_info_get_param_type(type_info, 0); g_assert(param_info != NULL); - result = gjs_array_from_g_list(context, - value_p, - type_tag, - param_info, - type_tag == GI_TYPE_TAG_GLIST ? - (GList *) arg->v_pointer : NULL, - type_tag == GI_TYPE_TAG_GSLIST ? - (GSList *) arg->v_pointer : NULL); + bool result = gjs_array_from_g_list( + context, value_p, type_tag, param_info, + type_tag == GI_TYPE_TAG_GLIST ? gjs_arg_get(arg) + : nullptr, + type_tag == GI_TYPE_TAG_GSLIST ? gjs_arg_get(arg) + : nullptr); g_base_info_unref((GIBaseInfo*) param_info); @@ -3048,18 +3232,15 @@ gjs_value_from_g_argument (JSContext *context, case GI_TYPE_TAG_GHASH: { GITypeInfo *key_param_info, *val_param_info; - bool result; key_param_info = g_type_info_get_param_type(type_info, 0); g_assert(key_param_info != NULL); val_param_info = g_type_info_get_param_type(type_info, 1); g_assert(val_param_info != NULL); - result = gjs_object_from_g_hash(context, - value_p, - key_param_info, - val_param_info, - (GHashTable *) arg->v_pointer); + bool result = gjs_object_from_g_hash(context, value_p, + key_param_info, val_param_info, + gjs_arg_get(arg)); g_base_info_unref((GIBaseInfo*) key_param_info); g_base_info_unref((GIBaseInfo*) val_param_info); @@ -3077,6 +3258,7 @@ gjs_value_from_g_argument (JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_g_arg_release_internal(JSContext *context, GITransfer transfer, GITypeInfo *type_info, @@ -3094,8 +3276,8 @@ static gboolean gjs_ghr_helper(gpointer key, gpointer val, gpointer user_data) { GHR_closure *c = (GHR_closure *) user_data; GArgument key_arg, val_arg; - key_arg.v_pointer = key; - val_arg.v_pointer = val; + gjs_arg_set(&key_arg, key); + gjs_arg_set(&val_arg, val); if (!gjs_g_arg_release_internal(c->context, c->transfer, c->key_param_info, g_type_info_get_tag(c->key_param_info), @@ -3107,7 +3289,7 @@ gjs_ghr_helper(gpointer key, gpointer val, gpointer user_data) { val_type == GI_TYPE_TAG_UINT64 || val_type == GI_TYPE_TAG_FLOAT || val_type == GI_TYPE_TAG_DOUBLE) { - g_free(val_arg.v_pointer); + g_clear_pointer(&gjs_arg_member(&val_arg), g_free); } else if (!gjs_g_arg_release_internal(c->context, c->transfer, c->val_param_info, val_type, &val_arg)) { @@ -3122,6 +3304,7 @@ gjs_ghr_helper(gpointer key, gpointer val, gpointer user_data) { */ #define TRANSFER_IN_NOTHING (GI_TRANSFER_EVERYTHING + 1) +GJS_JSAPI_RETURN_CONVENTION static bool gjs_g_arg_release_internal(JSContext *context, GITransfer transfer, @@ -3154,22 +3337,22 @@ gjs_g_arg_release_internal(JSContext *context, case GI_TYPE_TAG_FILENAME: case GI_TYPE_TAG_UTF8: - g_free(arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_free); break; case GI_TYPE_TAG_ERROR: if (transfer != TRANSFER_IN_NOTHING) - g_error_free ((GError *) arg->v_pointer); + g_clear_error(&gjs_arg_member(arg)); break; case GI_TYPE_TAG_INTERFACE: { - GIBaseInfo* interface_info; GIInfoType interface_type; GType gtype; - interface_info = g_type_info_get_interface(type_info); - g_assert(interface_info != NULL); + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(type_info); + g_assert(interface_info); interface_type = g_base_info_get_type(interface_info); @@ -3179,16 +3362,16 @@ gjs_g_arg_release_internal(JSContext *context, transfer, interface_info, arg); if (interface_type == GI_INFO_TYPE_ENUM || interface_type == GI_INFO_TYPE_FLAGS) - goto out; + return true; /* Anything else is a pointer */ - if (arg->v_pointer == NULL) - goto out; + if (!gjs_arg_get(arg)) + return true; gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)interface_info); if (G_TYPE_IS_INSTANTIATABLE(gtype) || G_TYPE_IS_INTERFACE(gtype)) - gtype = G_TYPE_FROM_INSTANCE(arg->v_pointer); + gtype = G_TYPE_FROM_INSTANCE(gjs_arg_get(arg)); gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "gtype of INTERFACE is %s", g_type_name(gtype)); @@ -3201,42 +3384,51 @@ gjs_g_arg_release_internal(JSContext *context, if (g_type_is_a(gtype, G_TYPE_OBJECT)) { if (transfer != TRANSFER_IN_NOTHING) - g_object_unref(G_OBJECT(arg->v_pointer)); + g_clear_object(&gjs_arg_member(arg)); } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { if (transfer != TRANSFER_IN_NOTHING) - g_param_spec_unref(G_PARAM_SPEC(arg->v_pointer)); + g_clear_pointer(&gjs_arg_member(arg), + g_param_spec_unref); } else if (g_type_is_a(gtype, G_TYPE_CLOSURE)) { - g_closure_unref((GClosure *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_closure_unref); } else if (g_type_is_a(gtype, G_TYPE_VALUE)) { /* G_TYPE_VALUE is-a G_TYPE_BOXED, but we special case it */ if (g_type_info_is_pointer (type_info)) - g_boxed_free(gtype, arg->v_pointer); + g_boxed_free( + gtype, + g_steal_pointer(&gjs_arg_member(arg))); else - g_value_unset ((GValue *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_value_unset); } else if (g_type_is_a(gtype, G_TYPE_BOXED)) { if (transfer != TRANSFER_IN_NOTHING) - g_boxed_free(gtype, arg->v_pointer); + g_boxed_free( + gtype, + g_steal_pointer(&gjs_arg_member(arg))); } else if (g_type_is_a(gtype, G_TYPE_VARIANT)) { if (transfer != TRANSFER_IN_NOTHING) - g_variant_unref ((GVariant *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_variant_unref); } else if (gtype == G_TYPE_NONE) { if (transfer != TRANSFER_IN_NOTHING) { gjs_throw(context, "Don't know how to release GArgument: not an object or boxed type"); failed = true; } } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { - if (transfer != TRANSFER_IN_NOTHING) - gjs_fundamental_unref(context, arg->v_pointer); + if (transfer != TRANSFER_IN_NOTHING) { + auto* priv = + FundamentalPrototype::for_gtype(context, gtype); + priv->call_unref_function( + g_steal_pointer(&gjs_arg_member(arg))); + } } else { gjs_throw(context, "Unhandled GType %s releasing GArgument", g_type_name(gtype)); - failed = true; + return false; } - - out: - g_base_info_unref( (GIBaseInfo*) interface_info); } - break; + return true; case GI_TYPE_TAG_GLIST: if (transfer != GI_TRANSFER_CONTAINER) { @@ -3246,11 +3438,9 @@ gjs_g_arg_release_internal(JSContext *context, param_info = g_type_info_get_param_type(type_info, 0); g_assert(param_info != NULL); - for (list = (GList *) arg->v_pointer; - list != NULL; - list = list->next) { + for (list = gjs_arg_get(arg); list; list = list->next) { GArgument elem; - elem.v_pointer = list->data; + gjs_arg_set(&elem, list->data); if (!gjs_g_arg_release_internal(context, transfer, @@ -3264,14 +3454,14 @@ gjs_g_arg_release_internal(JSContext *context, g_base_info_unref((GIBaseInfo*) param_info); } - g_list_free((GList *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_list_free); break; case GI_TYPE_TAG_ARRAY: { GIArrayType array_type = g_type_info_get_array_type(type_info); - if (arg->v_pointer == NULL) { + if (!gjs_arg_get(arg)) { /* OK */ } else if (array_type == GI_ARRAY_TYPE_C) { GITypeInfo *param_info; @@ -3294,12 +3484,12 @@ gjs_g_arg_release_internal(JSContext *context, } for (i = 0; i < len; i++) { - GValue *v = ((GValue*)arg->v_pointer) + i; + GValue* v = gjs_arg_get(arg) + i; g_value_unset(v); } } - g_free(arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_free); g_base_info_unref(param_info); return true; } @@ -3308,9 +3498,11 @@ gjs_g_arg_release_internal(JSContext *context, case GI_TYPE_TAG_UTF8: case GI_TYPE_TAG_FILENAME: if (transfer == GI_TRANSFER_CONTAINER) - g_free(arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_free); else - g_strfreev ((gchar **) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_strfreev); break; case GI_TYPE_TAG_BOOLEAN: @@ -3326,10 +3518,21 @@ gjs_g_arg_release_internal(JSContext *context, case GI_TYPE_TAG_DOUBLE: case GI_TYPE_TAG_UNICHAR: case GI_TYPE_TAG_GTYPE: - g_free (arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_free); break; case GI_TYPE_TAG_INTERFACE: + if (!g_type_info_is_pointer(param_info)) { + GjsAutoBaseInfo interface_info = + g_type_info_get_interface(param_info); + GIInfoType info_type = g_base_info_get_type(interface_info); + if (info_type == GI_INFO_TYPE_STRUCT || + info_type == GI_INFO_TYPE_UNION) { + g_clear_pointer(&gjs_arg_member(arg), g_free); + break; + } + } + [[fallthrough]]; case GI_TYPE_TAG_GLIST: case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_ARRAY: @@ -3341,8 +3544,9 @@ gjs_g_arg_release_internal(JSContext *context, gpointer *array; GArgument elem; - for (array = (void **) arg->v_pointer; *array; array++) { - elem.v_pointer = *array; + for (array = gjs_arg_get(arg); *array; + array++) { + gjs_arg_set(&elem, *array); if (!gjs_g_arg_release_internal(context, GI_TRANSFER_EVERYTHING, param_info, @@ -3359,7 +3563,7 @@ gjs_g_arg_release_internal(JSContext *context, g_assert(len != -1); for (i = 0; i < len; i++) { - elem.v_pointer = ((gpointer*)arg->v_pointer)[i]; + gjs_arg_set(&elem, gjs_arg_get(arg)[i]); if (!gjs_g_arg_release_internal(context, GI_TRANSFER_EVERYTHING, param_info, @@ -3370,7 +3574,7 @@ gjs_g_arg_release_internal(JSContext *context, } } } - g_free (arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_free); break; case GI_TYPE_TAG_VOID: @@ -3403,7 +3607,7 @@ gjs_g_arg_release_internal(JSContext *context, case GI_TYPE_TAG_FLOAT: case GI_TYPE_TAG_DOUBLE: case GI_TYPE_TAG_GTYPE: - g_array_free((GArray*) arg->v_pointer, true); + g_clear_pointer(&gjs_arg_member(arg), g_array_unref); break; case GI_TYPE_TAG_UTF8: @@ -3415,20 +3619,20 @@ gjs_g_arg_release_internal(JSContext *context, case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: if (transfer == GI_TRANSFER_CONTAINER) { - g_array_free((GArray*) arg->v_pointer, true); + g_clear_pointer(&gjs_arg_member(arg), + g_array_unref); } else if (type_needs_out_release (param_info, element_type)) { - GArray *array = (GArray *) arg->v_pointer; + GArray* array = gjs_arg_get(arg); guint i; for (i = 0; i < array->len; i++) { GArgument arg_iter; - arg_iter.v_pointer = g_array_index (array, gpointer, i); - gjs_g_arg_release_internal(context, - transfer, - param_info, - element_type, - &arg_iter); + gjs_arg_set(&arg_iter, + g_array_index(array, gpointer, i)); + failed = !gjs_g_arg_release_internal( + context, transfer, param_info, element_type, + &arg_iter); } g_array_free (array, true); @@ -3446,13 +3650,14 @@ gjs_g_arg_release_internal(JSContext *context, g_base_info_unref((GIBaseInfo*) param_info); } else if (array_type == GI_ARRAY_TYPE_BYTE_ARRAY) { - g_byte_array_unref ((GByteArray*)arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_byte_array_unref); } else if (array_type == GI_ARRAY_TYPE_PTR_ARRAY) { GITypeInfo *param_info; GPtrArray *array; param_info = g_type_info_get_param_type(type_info, 0); - array = (GPtrArray *) arg->v_pointer; + array = gjs_arg_get(arg); if (transfer != GI_TRANSFER_CONTAINER) { guint i; @@ -3460,11 +3665,9 @@ gjs_g_arg_release_internal(JSContext *context, for (i = 0; i < array->len; i++) { GArgument arg_iter; - arg_iter.v_pointer = g_ptr_array_index (array, i); - gjs_g_argument_release(context, - transfer, - param_info, - &arg_iter); + gjs_arg_set(&arg_iter, g_ptr_array_index(array, i)); + failed = !gjs_g_argument_release(context, transfer, + param_info, &arg_iter); } } @@ -3485,11 +3688,10 @@ gjs_g_arg_release_internal(JSContext *context, param_info = g_type_info_get_param_type(type_info, 0); g_assert(param_info != NULL); - for (slist = (GSList *) arg->v_pointer; - slist != NULL; + for (slist = gjs_arg_get(arg); slist; slist = slist->next) { GArgument elem; - elem.v_pointer = slist->data; + gjs_arg_set(&elem, slist->data); if (!gjs_g_arg_release_internal(context, transfer, @@ -3503,13 +3705,13 @@ gjs_g_arg_release_internal(JSContext *context, g_base_info_unref((GIBaseInfo*) param_info); } - g_slist_free((GSList *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), g_slist_free); break; case GI_TYPE_TAG_GHASH: - if (arg->v_pointer) { + if (gjs_arg_get(arg)) { if (transfer == GI_TRANSFER_CONTAINER) - g_hash_table_steal_all ((GHashTable *) arg->v_pointer); + g_hash_table_steal_all(gjs_arg_get(arg)); else { GHR_closure c = { context, NULL, NULL, @@ -3522,8 +3724,8 @@ gjs_g_arg_release_internal(JSContext *context, c.val_param_info = g_type_info_get_param_type(type_info, 1); g_assert(c.val_param_info != NULL); - g_hash_table_foreach_steal ((GHashTable *) arg->v_pointer, - gjs_ghr_helper, &c); + g_hash_table_foreach_steal(gjs_arg_get(arg), + gjs_ghr_helper, &c); failed = c.failed; @@ -3531,7 +3733,8 @@ gjs_g_arg_release_internal(JSContext *context, g_base_info_unref ((GIBaseInfo *)c.val_param_info); } - g_hash_table_destroy ((GHashTable *) arg->v_pointer); + g_clear_pointer(&gjs_arg_member(arg), + g_hash_table_destroy); } break; @@ -3594,15 +3797,10 @@ gjs_g_argument_release_in_arg(JSContext *context, return true; } -bool -gjs_g_argument_release_in_array (JSContext *context, - GITransfer transfer, - GITypeInfo *type_info, - guint length, - GArgument *arg) -{ +bool gjs_g_argument_release_in_array(JSContext* context, GITransfer transfer, + GITypeInfo* type_info, unsigned length, + GIArgument* arg) { GITypeInfo *param_type; - gpointer *array; GArgument elem; guint i; bool ret = true; @@ -3614,7 +3812,7 @@ gjs_g_argument_release_in_array (JSContext *context, gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Releasing GArgument array in param"); - array = (gpointer *) arg->v_pointer; + void** array = gjs_arg_get(arg); param_type = g_type_info_get_param_type(type_info, 0); type_tag = g_type_info_get_tag(param_type); @@ -3628,7 +3826,7 @@ gjs_g_argument_release_in_array (JSContext *context, if (type_needs_release(param_type, type_tag)) { for (i = 0; i < length; i++) { - elem.v_pointer = array[i]; + gjs_arg_set(&elem, array[i]); if (!gjs_g_arg_release_internal(context, (GITransfer) TRANSFER_IN_NOTHING, param_type, type_tag, &elem)) { ret = false; @@ -3643,15 +3841,10 @@ gjs_g_argument_release_in_array (JSContext *context, return ret; } -bool -gjs_g_argument_release_out_array (JSContext *context, - GITransfer transfer, - GITypeInfo *type_info, - guint length, - GArgument *arg) -{ +bool gjs_g_argument_release_out_array(JSContext* context, GITransfer transfer, + GITypeInfo* type_info, unsigned length, + GIArgument* arg) { GITypeInfo *param_type; - gpointer *array; GArgument elem; guint i; bool ret = true; @@ -3663,7 +3856,7 @@ gjs_g_argument_release_out_array (JSContext *context, gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Releasing GArgument array out param"); - array = (gpointer *) arg->v_pointer; + void** array = gjs_arg_get(arg); param_type = g_type_info_get_param_type(type_info, 0); type_tag = g_type_info_get_tag(param_type); @@ -3671,7 +3864,8 @@ gjs_g_argument_release_out_array (JSContext *context, if (transfer != GI_TRANSFER_CONTAINER && type_needs_out_release(param_type, type_tag)) { for (i = 0; i < length; i++) { - elem.v_pointer = array[i]; + gjs_arg_set(&elem, array[i]); + JS::AutoSaveExceptionState saved_exc(context); if (!gjs_g_arg_release_internal(context, GI_TRANSFER_EVERYTHING, param_type, diff --git a/gi/arg.h b/gi/arg.h index 2225a18..ba481b0 100644 --- a/gi/arg.h +++ b/gi/arg.h @@ -21,19 +21,23 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_ARG_H__ -#define __GJS_ARG_H__ +#ifndef GI_ARG_H_ +#define GI_ARG_H_ -#include -#include +#include -#include "cjs/jsapi-util.h" +#include // for size_t +#include #include +#include -G_BEGIN_DECLS +#include -/* Different roles for a GArgument */ +#include "cjs/macros.h" + +// Different roles for a GIArgument; currently used only in exception and debug +// messages. typedef enum { GJS_ARGUMENT_ARGUMENT, GJS_ARGUMENT_RETURN_VALUE, @@ -43,21 +47,25 @@ typedef enum { GJS_ARGUMENT_ARRAY_ELEMENT } GjsArgumentType; +[[nodiscard]] char* gjs_argument_display_name(const char* arg_name, + GjsArgumentType arg_type); + +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_to_arg(JSContext *context, JS::HandleValue value, GIArgInfo *arg_info, GIArgument *arg); -bool gjs_value_to_explicit_array(JSContext *context, - JS::HandleValue value, - GIArgInfo *arg_info, - GIArgument *arg, - size_t *length_p); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_array_to_explicit_array(JSContext* cx, JS::HandleValue value, + GITypeInfo* type_info, const char* arg_name, + GjsArgumentType arg_type, GITransfer transfer, + bool may_be_null, void** contents, + size_t* length_p); -void gjs_g_argument_init_default (JSContext *context, - GITypeInfo *type_info, - GArgument *arg); +void gjs_gi_argument_init_default(GITypeInfo* type_info, GIArgument* arg); +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_to_g_argument (JSContext *context, JS::HandleValue value, GITypeInfo *type_info, @@ -67,53 +75,53 @@ bool gjs_value_to_g_argument (JSContext *context, bool may_be_null, GArgument *arg); +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_from_g_argument(JSContext *context, JS::MutableHandleValue value_p, GITypeInfo *type_info, GIArgument *arg, bool copy_structs); +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_from_explicit_array(JSContext *context, JS::MutableHandleValue value_p, GITypeInfo *type_info, GIArgument *arg, int length); +GJS_JSAPI_RETURN_CONVENTION bool gjs_g_argument_release (JSContext *context, GITransfer transfer, GITypeInfo *type_info, GArgument *arg); -bool gjs_g_argument_release_out_array (JSContext *context, - GITransfer transfer, - GITypeInfo *type_info, - guint length, - GArgument *arg); -bool gjs_g_argument_release_in_array (JSContext *context, - GITransfer transfer, - GITypeInfo *type_info, - guint length, - GArgument *arg); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_g_argument_release_out_array(JSContext* cx, GITransfer transfer, + GITypeInfo* type_info, unsigned length, + GIArgument* arg); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_g_argument_release_in_array(JSContext* cx, GITransfer transfer, + GITypeInfo* type_info, unsigned length, + GIArgument* arg); +GJS_JSAPI_RETURN_CONVENTION bool gjs_g_argument_release_in_arg (JSContext *context, GITransfer transfer, GITypeInfo *type_info, GArgument *arg); -bool _gjs_flags_value_is_valid (JSContext *context, - GType gtype, - gint64 value); +GJS_JSAPI_RETURN_CONVENTION +bool _gjs_flags_value_is_valid(JSContext* cx, GType gtype, int64_t value); -gint64 _gjs_enum_from_int (GIEnumInfo *enum_info, - int int_value); +[[nodiscard]] int64_t _gjs_enum_from_int(GIEnumInfo* enum_info, int int_value); +GJS_JSAPI_RETURN_CONVENTION bool gjs_array_from_strv(JSContext *context, JS::MutableHandleValue value_p, const char **strv); +GJS_JSAPI_RETURN_CONVENTION bool gjs_array_to_strv (JSContext *context, JS::Value array_value, unsigned int length, void **arr_p); -G_END_DECLS - -#endif /* __GJS_ARG_H__ */ +#endif // GI_ARG_H_ diff --git a/gi/boxed.cpp b/gi/boxed.cpp index 6d14d42..72bd7be 100644 --- a/gi/boxed.cpp +++ b/gi/boxed.cpp @@ -23,129 +23,57 @@ #include -#include +#include +#include // for memcpy, size_t, strcmp -#include "boxed.h" -#include "arg.h" -#include "object.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" -#include "repo.h" -#include "proxyutils.h" -#include "function.h" -#include "gtype.h" - -#include +#include // for string +#include // for remove_reference +#include // for move, forward #include - -/* Reserved slots of JSNative accessor wrappers */ -enum { - SLOT_PROP_NAME, -}; - -struct Boxed { - /* prototype info */ - GIBoxedInfo *info; - GType gtype; - gint zero_args_constructor; /* -1 if none */ - JS::Heap zero_args_constructor_name; - gint default_constructor; /* -1 if none */ - JS::Heap default_constructor_name; - - /* instance info */ - void *gboxed; /* NULL if we are the prototype and not an instance */ - GHashTable *field_map; - - guint can_allocate_directly : 1; - guint allocated_directly : 1; - guint not_owning_gboxed : 1; /* if set, the JS wrapper does not own - the reference to the C gboxed */ -}; - -static bool struct_is_simple(GIStructInfo *info); - -static bool boxed_set_field_from_value(JSContext *context, - Boxed *priv, - GIFieldInfo *field_info, - JS::HandleValue value); - -extern struct JSClass gjs_boxed_class; - -GJS_DEFINE_PRIV_FROM_JS(Boxed, gjs_boxed_class) - -static bool -gjs_define_static_methods(JSContext *context, - JS::HandleObject constructor, - GType gtype, - GIStructInfo *boxed_info) -{ - int i; - int n_methods; - - n_methods = g_struct_info_get_n_methods(boxed_info); - - for (i = 0; i < n_methods; i++) { - GIFunctionInfo *meth_info; - GIFunctionInfoFlags flags; - - meth_info = g_struct_info_get_method (boxed_info, i); - flags = g_function_info_get_flags (meth_info); - - /* Anything that isn't a method we put on the prototype of the - * constructor. This includes introspection - * methods, as well as the forthcoming "static methods" - * support. We may want to change this to use - * GI_FUNCTION_IS_CONSTRUCTOR and GI_FUNCTION_IS_STATIC or the - * like in the near future. - */ - if (!(flags & GI_FUNCTION_IS_METHOD)) { - gjs_define_function(context, constructor, gtype, - (GICallableInfo *)meth_info); - } - - g_base_info_unref((GIBaseInfo*) meth_info); - } - return true; +#include + +#include +#include +#include // for GCHashMap +#include // for MutableWrappedPtrOperations +#include +#include +#include +#include +#include // for IdVector, JS_AtomizeAndPinJSString +#include + +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/boxed.h" +#include "gi/function.h" +#include "gi/gerror.h" +#include "gi/repo.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-class.h" +#include "cjs/mem-private.h" +#include "util/log.h" + +BoxedInstance::BoxedInstance(JSContext* cx, JS::HandleObject obj) + : GIWrapperInstance(cx, obj), + m_allocated_directly(false), + m_owning_ptr(false) { + m_ptr = nullptr; + GJS_INC_COUNTER(boxed_instance); } -/* 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 = 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) { - /* 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; - } +[[nodiscard]] static bool struct_is_simple(GIStructInfo* info); - 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); +// See GIWrapperBase::resolve(). +bool BoxedPrototype::resolve_impl(JSContext* cx, JS::HandleObject obj, + JS::HandleId, const char* prop_name, + bool* resolved) { + // Look for methods and other class properties + GjsAutoFunctionInfo method_info = + g_struct_info_find_method(info(), prop_name); if (!method_info) { *resolved = false; return true; @@ -155,101 +83,150 @@ boxed_resolve(JSContext *context, #endif 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)); + gjs_debug(GJS_DEBUG_GBOXED, "Defining method %s in prototype for %s.%s", + method_info.name(), ns(), name()); /* obj is the Boxed prototype */ - if (!gjs_define_function(context, obj, priv->gtype, method_info)) { - g_base_info_unref( (GIBaseInfo*) method_info); + if (!gjs_define_function(cx, obj, gtype(), method_info)) return false; - } *resolved = true; } else { *resolved = false; } - g_base_info_unref(method_info); return true; } -/* Check to see if JS::Value passed in is another Boxed object of the same, - * and if so, retrieves the Boxed private structure for it. - */ -static bool -boxed_get_copy_source(JSContext *context, - Boxed *priv, - JS::Value value, - Boxed **source_priv_out) -{ - Boxed *source_priv; +// See GIWrapperBase::new_enumerate(). +bool BoxedPrototype::new_enumerate_impl(JSContext* cx, JS::HandleObject, + JS::MutableHandleIdVector properties, + bool only_enumerable [[maybe_unused]]) { + int n_methods = g_struct_info_get_n_methods(info()); + for (int i = 0; i < n_methods; i++) { + GjsAutoFunctionInfo meth_info = g_struct_info_get_method(info(), i); + GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info); + + if (flags & GI_FUNCTION_IS_METHOD) { + const char* name = meth_info.name(); + jsid id = gjs_intern_string_to_id(cx, name); + if (id == JSID_VOID) + return false; + if (!properties.append(id)) { + JS_ReportOutOfMemory(cx); + return false; + } + } + } + return true; +} + +/* + * BoxedBase::get_copy_source(): + * + * Check to see if JS::Value passed in is another Boxed instance object of the + * same type, and if so, retrieve the BoxedInstance private structure for it. + * This function does not throw any JS exceptions. + */ +BoxedBase* BoxedBase::get_copy_source(JSContext* context, + JS::Value value) const { if (!value.isObject()) - return false; + return nullptr; JS::RootedObject object(context, &value.toObject()); - if (!priv_from_js_with_typecheck(context, object, &source_priv)) - return false; + BoxedBase* source_priv = BoxedBase::for_js(context, object); + if (!source_priv || !g_base_info_equal(info(), source_priv->info())) + return nullptr; - if (!g_base_info_equal((GIBaseInfo*) priv->info, (GIBaseInfo*) source_priv->info)) - return false; - - *source_priv_out = source_priv; - - return true; + return source_priv; } -static void -boxed_new_direct(Boxed *priv) -{ - g_assert(priv->can_allocate_directly); +/* + * BoxedInstance::allocate_directly: + * + * Allocate a boxed object of the correct size, set all the bytes to 0, and set + * m_ptr to point to it. This is used when constructing a boxed object that can + * be allocated directly (i.e., does not need to be created by a constructor + * function.) + */ +void BoxedInstance::allocate_directly(void) { + g_assert(get_prototype()->can_allocate_directly()); - priv->gboxed = g_slice_alloc0(g_struct_info_get_size (priv->info)); - priv->allocated_directly = true; + own_ptr(g_slice_alloc0(g_struct_info_get_size(info()))); + m_allocated_directly = true; - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "JSObject created by directly allocating %s", - g_base_info_get_name ((GIBaseInfo *)priv->info)); + debug_lifecycle("Boxed pointer directly allocated"); } /* When initializing a boxed object from a hash of properties, we don't want * to do n O(n) lookups, so put put the fields into a hash table and store it on proto->priv * for fast lookup. */ -static GHashTable * -get_field_map(GIStructInfo *struct_info) -{ - GHashTable *result; +BoxedPrototype::FieldMap* BoxedPrototype::create_field_map( + JSContext* cx, GIStructInfo* struct_info) { int n_fields; int i; - result = g_hash_table_new_full(g_str_hash, g_str_equal, - NULL, (GDestroyNotify)g_base_info_unref); + auto* result = new BoxedPrototype::FieldMap(); n_fields = g_struct_info_get_n_fields(struct_info); + if (!result->reserve(n_fields)) { + JS_ReportOutOfMemory(cx); + return nullptr; + } for (i = 0; i < n_fields; i++) { - GIFieldInfo *field_info = g_struct_info_get_field(struct_info, i); - g_hash_table_insert(result, (char *)g_base_info_get_name((GIBaseInfo *)field_info), field_info); + GjsAutoFieldInfo field_info = g_struct_info_get_field(struct_info, i); + + // We get the string as a jsid later, which is interned. We intern the + // string here as well, so it will be the same string pointer + JS::RootedString name(cx, JS_NewStringCopyZ(cx, field_info.name())); + JSString* atom = JS_AtomizeAndPinJSString(cx, name); + + result->putNewInfallible(atom, std::move(field_info)); } return result; } +/* + * BoxedPrototype::ensure_field_map: + * + * BoxedPrototype keeps a cache of field names to introspection info. + * We only create the field cache the first time it is needed. An alternative + * would be to create it when the prototype is created, in BoxedPrototype::init. + */ +bool BoxedPrototype::ensure_field_map(JSContext* cx) { + if (!m_field_map) + m_field_map = create_field_map(cx, info()); + return !!m_field_map; +} + +/* + * BoxedPrototype::lookup_field: + * + * Look up the introspection info corresponding to the field name @prop_name, + * creating the field cache if necessary. + */ +GIFieldInfo* BoxedPrototype::lookup_field(JSContext* cx, JSString* prop_name) { + if (!ensure_field_map(cx)) + return nullptr; + + auto entry = m_field_map->lookup(prop_name); + if (!entry) { + gjs_throw(cx, "No field %s on boxed type %s", + gjs_debug_string(prop_name).c_str(), name()); + return nullptr; + } + + return entry->value().get(); +} + /* Initialize a newly created Boxed from an object that is a "hash" of * properties to set as fieds of the object. We don't require that every field * of the object be set. */ -static bool -boxed_init_from_props(JSContext *context, - JSObject *obj, - Boxed *priv, - JS::Value props_value) -{ +bool BoxedInstance::init_from_props(JSContext* context, JS::Value props_value) { size_t ix, length; if (!props_value.isObject()) { @@ -264,23 +241,17 @@ boxed_init_from_props(JSContext *context, return false; } - if (!priv->field_map) - priv->field_map = get_field_map(priv->info); - JS::RootedValue value(context); for (ix = 0, length = ids.length(); ix < length; ix++) { - GIFieldInfo *field_info; - GjsAutoJSChar name; - - if (!gjs_get_string_id(context, ids[ix], &name)) + if (!JSID_IS_STRING(ids[ix])) { + gjs_throw(context, "Fields hash contained a non-string field"); return false; + } - field_info = (GIFieldInfo *) g_hash_table_lookup(priv->field_map, name); - if (field_info == NULL) { - gjs_throw(context, "No field %s on boxed type %s", - name.get(), g_base_info_get_name((GIBaseInfo *)priv->info)); + GIFieldInfo* field_info = + get_prototype()->lookup_field(context, JSID_TO_STRING(ids[ix])); + if (!field_info) return false; - } /* ids[ix] is reachable because props is rooted, but require_property * doesn't know that */ @@ -289,24 +260,22 @@ boxed_init_from_props(JSContext *context, &value)) return false; - if (!boxed_set_field_from_value(context, priv, field_info, value)) + if (!field_setter_impl(context, field_info, value)) return false; } return true; } -static bool -boxed_invoke_constructor(JSContext *context, - JS::HandleObject obj, - JS::HandleId constructor_name, - JS::CallArgs& args) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool boxed_invoke_constructor(JSContext* context, JS::HandleObject obj, + JS::HandleId constructor_name, + const JS::CallArgs& args) { + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); JS::RootedObject js_constructor(context); - if (!gjs_object_require_property(context, obj, NULL, - GJS_STRING_CONSTRUCTOR, - &js_constructor)) + if (!gjs_object_require_property( + context, obj, nullptr, gjs->atoms().constructor(), &js_constructor)) return false; JS::RootedValue js_constructor_func(context); @@ -314,24 +283,78 @@ boxed_invoke_constructor(JSContext *context, constructor_name, &js_constructor_func)) return false; - return gjs_call_function_value(context, nullptr, js_constructor_func, - args, args.rval()); + return gjs->call_function(nullptr, js_constructor_func, args, args.rval()); } -static bool -boxed_new(JSContext *context, - JS::HandleObject obj, /* "this" for constructor */ - Boxed *priv, - JS::CallArgs& args) -{ - if (priv->gtype == G_TYPE_VARIANT) { +/* + * BoxedInstance::copy_boxed: + * + * Allocate a new boxed pointer using g_boxed_copy(), either from a raw boxed + * pointer or another BoxedInstance. + */ +void BoxedInstance::copy_boxed(void* boxed_ptr) { + own_ptr(g_boxed_copy(gtype(), boxed_ptr)); + debug_lifecycle("Boxed pointer created with g_boxed_copy()"); +} + +void BoxedInstance::copy_boxed(BoxedInstance* source) { + copy_boxed(source->ptr()); +} + +/* + * BoxedInstance::copy_memory: + * + * Allocate a new boxed pointer by copying the contents of another boxed pointer + * or another BoxedInstance. + */ +void BoxedInstance::copy_memory(void* boxed_ptr) { + allocate_directly(); + memcpy(m_ptr, boxed_ptr, g_struct_info_get_size(info())); +} + +void BoxedInstance::copy_memory(BoxedInstance* source) { + copy_memory(source->ptr()); +} + +// See GIWrapperBase::constructor(). +bool BoxedInstance::constructor_impl(JSContext* context, JS::HandleObject obj, + const JS::CallArgs& args) { + // Short-circuit copy-construction in the case where we can use copy_boxed() + // or copy_memory() + BoxedBase* source_priv; + if (args.length() == 1 && + (source_priv = get_copy_source(context, args[0]))) { + if (!source_priv->check_is_instance(context, "construct boxed object")) + return false; + + if (g_type_is_a(gtype(), G_TYPE_BOXED)) { + copy_boxed(source_priv->to_instance()); + return true; + } else if (get_prototype()->can_allocate_directly()) { + copy_memory(source_priv->to_instance()); + return true; + } + } + + if (gtype() == G_TYPE_VARIANT) { /* Short-circuit construction for GVariants by calling into the JS packing function */ - JS::HandleId constructor_name = - gjs_context_get_const_string(context, GJS_STRING_NEW_INTERNAL); - return boxed_invoke_constructor(context, obj, constructor_name, args); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (!boxed_invoke_constructor(context, obj, atoms.new_internal(), args)) + return false; + + // The return value of GLib.Variant.new_internal() gets its own + // BoxedInstance, and the one we're setting up in this constructor is + // discarded. + debug_lifecycle( + "Boxed construction delegated to GVariant constructor, " + "boxed object discarded"); + + return true; } + BoxedPrototype* proto = get_prototype(); + /* If the structure is registered as a boxed, we can create a new instance by * looking for a zero-args constructor and calling it. * Constructors don't really make sense for non-boxed types, since there is no @@ -341,8 +364,8 @@ boxed_new(JSContext *context, * For backward compatibility, we choose the zero args constructor if one * exists, otherwise we choose the internal slice allocator if possible; * finally, we fallback on the default constructor */ - if (priv->zero_args_constructor >= 0) { - GIFunctionInfo *func_info = g_struct_info_get_method (priv->info, priv->zero_args_constructor); + if (proto->has_zero_args_constructor()) { + GjsAutoFunctionInfo func_info = proto->zero_args_constructor_info(); GIArgument rval_arg; GError *error = NULL; @@ -350,32 +373,43 @@ boxed_new(JSContext *context, if (!g_function_info_invoke(func_info, NULL, 0, NULL, 0, &rval_arg, &error)) { gjs_throw(context, "Failed to invoke boxed constructor: %s", error->message); g_clear_error(&error); - g_base_info_unref((GIBaseInfo*) func_info); return false; } - g_base_info_unref((GIBaseInfo*) func_info); - - priv->gboxed = rval_arg.v_pointer; + own_ptr(g_steal_pointer(&gjs_arg_member(&rval_arg))); - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "JSObject created with boxed instance %p type %s", - priv->gboxed, g_type_name(priv->gtype)); + debug_lifecycle("Boxed pointer created from zero-args constructor"); - } else if (priv->can_allocate_directly) { - boxed_new_direct(priv); - } else if (priv->default_constructor >= 0) { + } else if (proto->can_allocate_directly()) { + allocate_directly(); + } else if (proto->has_default_constructor()) { /* 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); + * object was created with the right prototype. */ + if (!boxed_invoke_constructor(context, obj, + proto->default_constructor_name(), args)) + return false; + + // Define the expected Error properties + if (gtype() == G_TYPE_ERROR) { + JS::RootedObject gerror(context, &args.rval().toObject()); + if (!gjs_define_error_properties(context, gerror)) + return false; + } + + // The return value of the JS constructor gets its own BoxedInstance, + // and this one is discarded. + debug_lifecycle( + "Boxed construction delegated to JS constructor, " + "boxed object discarded"); + + return true; } 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)); + gjs_throw(context, + "Unable to construct struct type %s since it has no default " + "constructor and cannot be allocated directly", + name()); return false; } @@ -385,188 +419,102 @@ boxed_new(JSContext *context, return true; if (args.length() > 1) { - gjs_throw(context, "Constructor with multiple arguments not supported for %s", - g_base_info_get_name((GIBaseInfo *)priv->info)); + gjs_throw(context, + "Constructor with multiple arguments not supported for %s", + name()); return false; } - return boxed_init_from_props(context, obj, priv, args[0]); + return init_from_props(context, args[0]); } -GJS_NATIVE_CONSTRUCTOR_DECLARE(boxed) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(boxed) - Boxed *priv; - Boxed *proto_priv; - JS::RootedObject proto(context); - Boxed *source_priv; - bool retval; - - GJS_NATIVE_CONSTRUCTOR_PRELUDE(boxed); - - priv = g_slice_new0(Boxed); - new (priv) Boxed(); - - GJS_INC_COUNTER(boxed); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "boxed constructor, obj %p priv %p", - object.get(), priv); - - JS_GetPrototype(context, object, &proto); - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, "boxed instance __proto__ is %p", - proto.get()); - /* If we're the prototype, then post-construct we'll fill in priv->info. - * If we are not the prototype, though, then we'll get ->info from the - * prototype and then create a GObject if we don't have one already. - */ - proto_priv = priv_from_js(context, proto); - if (proto_priv == NULL) { - gjs_debug(GJS_DEBUG_GBOXED, - "Bad prototype set on boxed? Must match JSClass of object. JS error should have been reported."); - return false; - } - - *priv = *proto_priv; - g_base_info_ref( (GIBaseInfo*) priv->info); - - /* Short-circuit copy-construction in the case where we can use g_boxed_copy or memcpy */ - if (argc == 1 && - boxed_get_copy_source(context, priv, argv[0], &source_priv)) { - - if (g_type_is_a (priv->gtype, G_TYPE_BOXED)) { - priv->gboxed = g_boxed_copy(priv->gtype, source_priv->gboxed); - - GJS_NATIVE_CONSTRUCTOR_FINISH(boxed); - return true; - } else if (priv->can_allocate_directly) { - boxed_new_direct (priv); - memcpy(priv->gboxed, source_priv->gboxed, - g_struct_info_get_size (priv->info)); - - GJS_NATIVE_CONSTRUCTOR_FINISH(boxed); - return true; - } - } - - /* we may need to return a value different from object - (for example because we delegate to another constructor) - */ - - argv.rval().setUndefined(); - retval = boxed_new(context, object, priv, argv); - - if (argv.rval().isUndefined()) - GJS_NATIVE_CONSTRUCTOR_FINISH(boxed); - - return retval; -} - -static void -boxed_finalize(JSFreeOp *fop, - JSObject *obj) -{ - Boxed *priv; - - priv = (Boxed *) JS_GetPrivate(obj); - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) - return; /* wrong class? */ - - if (priv->gboxed && !priv->not_owning_gboxed) { - if (priv->allocated_directly) { - g_slice_free1(g_struct_info_get_size (priv->info), priv->gboxed); +BoxedInstance::~BoxedInstance() { + if (m_owning_ptr) { + if (m_allocated_directly) { + g_slice_free1(g_struct_info_get_size(info()), m_ptr); } else { - if (g_type_is_a (priv->gtype, G_TYPE_BOXED)) - g_boxed_free (priv->gtype, priv->gboxed); - else if (g_type_is_a (priv->gtype, G_TYPE_VARIANT)) - g_variant_unref ((GVariant *) priv->gboxed); + if (g_type_is_a(gtype(), G_TYPE_BOXED)) + g_boxed_free(gtype(), m_ptr); + else if (g_type_is_a(gtype(), G_TYPE_VARIANT)) + g_variant_unref(static_cast(m_ptr)); else g_assert_not_reached (); } - priv->gboxed = NULL; + m_ptr = nullptr; } - if (priv->info) { - g_base_info_unref( (GIBaseInfo*) priv->info); - priv->info = NULL; - } + GJS_DEC_COUNTER(boxed_instance); +} - if (priv->field_map) { - g_hash_table_destroy(priv->field_map); - } +BoxedPrototype::~BoxedPrototype(void) { + g_clear_pointer(&m_info, g_base_info_unref); + + if (m_field_map) + delete m_field_map; - GJS_DEC_COUNTER(boxed); - priv->~Boxed(); - g_slice_free(Boxed, priv); + GJS_DEC_COUNTER(boxed_prototype); } -static GIFieldInfo * -get_field_info(JSContext *cx, - Boxed *priv, - uint32_t id) -{ - GIFieldInfo *field_info = g_struct_info_get_field(priv->info, id); +/* + * BoxedBase::get_field_info: + * + * Does the same thing as g_struct_info_get_field(), but throws a JS exception + * if there is no such field. + */ +GIFieldInfo* BoxedBase::get_field_info(JSContext* cx, uint32_t id) const { + GIFieldInfo* field_info = g_struct_info_get_field(info(), id); if (field_info == NULL) { - gjs_throw(cx, "No field %d on boxed type %s", - id, g_base_info_get_name((GIBaseInfo *)priv->info)); + gjs_throw(cx, "No field %d on boxed type %s", id, name()); return NULL; } return field_info; } -static bool -get_nested_interface_object(JSContext *context, - JSObject *parent_obj, - Boxed *parent_priv, - GIFieldInfo *field_info, - GITypeInfo *type_info, - GIBaseInfo *interface_info, - JS::MutableHandleValue value) -{ - JSObject *obj; +/* + * BoxedInstance::get_nested_interface_object: + * @parent_obj: the BoxedInstance JS object that owns `this` + * @field_info: introspection info for the field of the parent boxed type that + * is another boxed type + * @interface_info: introspection info for the nested boxed type + * @value: return location for a new BoxedInstance JS object + * + * Some boxed types have a field that consists of another boxed type. We want to + * be able to expose these nested boxed types without copying them, because + * changing fields of the nested boxed struct should affect the enclosing boxed + * struct. + * + * This method creates a new BoxedInstance and JS object for a nested boxed + * struct. Since both the nested JS object and the parent boxed's JS object + * refer to the same memory, the parent JS object will be prevented from being + * garbage collected while the nested JS object is active. + */ +bool BoxedInstance::get_nested_interface_object( + JSContext* context, JSObject* parent_obj, GIFieldInfo* field_info, + GIBaseInfo* interface_info, JS::MutableHandleValue value) const { int offset; - Boxed *priv; - Boxed *proto_priv; if (!struct_is_simple ((GIStructInfo *)interface_info)) { - gjs_throw(context, "Reading field %s.%s is not supported", - g_base_info_get_name ((GIBaseInfo *)parent_priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); + gjs_throw(context, "Reading field %s.%s is not supported", name(), + g_base_info_get_name(field_info)); return false; } - JS::RootedObject proto(context, - gjs_lookup_generic_prototype(context, - (GIBoxedInfo*) interface_info)); - proto_priv = priv_from_js(context, proto); - offset = g_field_info_get_offset (field_info); - obj = JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto); - + JS::RootedObject obj(context, gjs_new_object_with_generic_prototype( + context, interface_info)); if (!obj) return false; - GJS_INC_COUNTER(boxed); - priv = g_slice_new0(Boxed); - new (priv) Boxed(); - JS_SetPrivate(obj, priv); - priv->info = (GIBoxedInfo*) interface_info; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->gtype = g_registered_type_info_get_g_type ((GIRegisteredTypeInfo*) interface_info); - priv->can_allocate_directly = proto_priv->can_allocate_directly; + BoxedInstance* priv = BoxedInstance::new_for_js_object(context, obj); /* A structure nested inside a parent object; doesn't have an independent allocation */ - priv->gboxed = ((char *)parent_priv->gboxed) + offset; - priv->not_owning_gboxed = true; + priv->share_ptr(raw_ptr() + offset); + priv->debug_lifecycle( + "Boxed pointer created, pointing inside memory owned by parent"); /* We never actually read the reserved slot, but we put the parent object * into it to hold onto the parent object. @@ -577,177 +525,129 @@ get_nested_interface_object(JSContext *context, return true; } -static JSObject * -define_native_accessor_wrapper(JSContext *cx, - JSNative call, - unsigned nargs, - const char *func_name, - uint32_t id) -{ - JSFunction *func = js::NewFunctionWithReserved(cx, call, nargs, 0, - func_name); - if (!func) - return NULL; - - JSObject *func_obj = JS_GetFunctionObject(func); - js::SetFunctionNativeReserved(func_obj, SLOT_PROP_NAME, - JS::PrivateUint32Value(id)); - return func_obj; -} +/* + * BoxedBase::field_getter: + * + * JSNative property getter that is called when accessing a field defined on a + * boxed type. Delegates to BoxedInstance::field_getter_impl() if the minimal + * conditions have been met. + */ +bool BoxedBase::field_getter(JSContext* context, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(context, argc, vp, args, obj, BoxedBase, priv); + if (!priv->check_is_instance(context, "get a field")) + return false; -static uint32_t -native_accessor_slot(JSObject *func_obj) -{ - return js::GetFunctionNativeReserved(func_obj, SLOT_PROP_NAME) + uint32_t field_ix = gjs_dynamic_property_private_slot(&args.callee()) .toPrivateUint32(); -} - -static bool -boxed_field_getter(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, obj, Boxed, priv); - GIFieldInfo *field_info; - GITypeInfo *type_info; - GArgument arg; - bool success = false; - - field_info = get_field_info(context, priv, - native_accessor_slot(&args.callee())); + GjsAutoFieldInfo field_info = priv->get_field_info(context, field_ix); if (!field_info) return false; - type_info = g_field_info_get_type (field_info); - - if (priv->gboxed == NULL) { /* direct access to proto field */ - gjs_throw(context, "Can't get field %s.%s from a prototype", - g_base_info_get_name ((GIBaseInfo *)priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); - goto out; - } - - if (!g_type_info_is_pointer (type_info) && - g_type_info_get_tag (type_info) == GI_TYPE_TAG_INTERFACE) { - - GIBaseInfo *interface_info = g_type_info_get_interface(type_info); - - if (g_base_info_get_type (interface_info) == GI_INFO_TYPE_STRUCT || - g_base_info_get_type (interface_info) == GI_INFO_TYPE_BOXED) { + return priv->to_instance()->field_getter_impl(context, obj, field_info, + args.rval()); +} - success = get_nested_interface_object (context, obj, priv, - field_info, type_info, interface_info, - args.rval()); +// See BoxedBase::field_getter(). +bool BoxedInstance::field_getter_impl(JSContext* cx, JSObject* obj, + GIFieldInfo* field_info, + JS::MutableHandleValue rval) const { + GjsAutoTypeInfo type_info = g_field_info_get_type(field_info); - g_base_info_unref ((GIBaseInfo *)interface_info); + if (!g_type_info_is_pointer(type_info) && + g_type_info_get_tag(type_info) == GI_TYPE_TAG_INTERFACE) { + GjsAutoBaseInfo interface_info = g_type_info_get_interface(type_info); - goto out; + if (interface_info.type() == GI_INFO_TYPE_STRUCT || + interface_info.type() == GI_INFO_TYPE_BOXED) { + return get_nested_interface_object(cx, obj, field_info, + interface_info, rval); } - - g_base_info_unref ((GIBaseInfo *)interface_info); } - if (!g_field_info_get_field (field_info, priv->gboxed, &arg)) { - gjs_throw(context, "Reading field %s.%s is not supported", - g_base_info_get_name ((GIBaseInfo *)priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); - goto out; + GIArgument arg; + if (!g_field_info_get_field(field_info, m_ptr, &arg)) { + gjs_throw(cx, "Reading field %s.%s is not supported", name(), + g_base_info_get_name(field_info)); + return false; } - if (!gjs_value_from_g_argument(context, args.rval(), type_info, - &arg, true)) - goto out; - - success = true; - -out: - g_base_info_unref ((GIBaseInfo *)field_info); - g_base_info_unref ((GIBaseInfo *)type_info); - - return success; + return gjs_value_from_g_argument(cx, rval, type_info, &arg, true); } -static bool -set_nested_interface_object (JSContext *context, - Boxed *parent_priv, - GIFieldInfo *field_info, - GITypeInfo *type_info, - GIBaseInfo *interface_info, - JS::HandleValue value) -{ +/* + * BoxedInstance::set_nested_interface_object: + * @field_info: introspection info for the field of the parent boxed type that + * is another boxed type + * @interface_info: introspection info for the nested boxed type + * @value: holds a BoxedInstance JS object of type @interface_info + * + * Some boxed types have a field that consists of another boxed type. This + * method is called from BoxedInstance::field_setter_impl() when any such field + * is being set. The contents of the BoxedInstance JS object in @value are + * copied into the correct place in this BoxedInstance's memory. + */ +bool BoxedInstance::set_nested_interface_object(JSContext* context, + GIFieldInfo* field_info, + GIBaseInfo* interface_info, + JS::HandleValue value) { int offset; - Boxed *proto_priv; - Boxed *source_priv; if (!struct_is_simple ((GIStructInfo *)interface_info)) { - gjs_throw(context, "Writing field %s.%s is not supported", - g_base_info_get_name ((GIBaseInfo *)parent_priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); + gjs_throw(context, "Writing field %s.%s is not supported", name(), + g_base_info_get_name(field_info)); return false; } - JS::RootedObject proto(context, - gjs_lookup_generic_prototype(context, - (GIBoxedInfo*) interface_info)); - proto_priv = priv_from_js(context, proto); + JS::RootedObject proto( + context, gjs_lookup_generic_prototype(context, interface_info)); + + if (!proto) + return false; /* If we can't directly copy from the source object we need * to construct a new temporary object. */ - if (!boxed_get_copy_source(context, proto_priv, value, &source_priv)) { - JS::AutoValueArray<1> args(context); + BoxedBase* source_priv = get_copy_source(context, value); + if (!source_priv) { + JS::RootedValueArray<1> args(context); args[0].set(value); JS::RootedObject tmp_object(context, gjs_construct_object_dynamic(context, proto, args)); if (!tmp_object) return false; - source_priv = priv_from_js(context, tmp_object); + source_priv = BoxedBase::for_js_typecheck(context, tmp_object); if (!source_priv) return false; } + if (!source_priv->check_is_instance(context, "copy")) + return false; + offset = g_field_info_get_offset (field_info); - memcpy(((char *)parent_priv->gboxed) + offset, - source_priv->gboxed, - g_struct_info_get_size (source_priv->info)); + memcpy(raw_ptr() + offset, source_priv->to_instance()->ptr(), + g_struct_info_get_size(source_priv->info())); return true; } -static bool -boxed_set_field_from_value(JSContext *context, - Boxed *priv, - GIFieldInfo *field_info, - JS::HandleValue value) -{ - GITypeInfo *type_info; +// See BoxedBase::field_setter(). +bool BoxedInstance::field_setter_impl(JSContext* context, + GIFieldInfo* field_info, + JS::HandleValue value) { GArgument arg; - bool success = false; - bool need_release = false; - - type_info = g_field_info_get_type (field_info); + GjsAutoTypeInfo type_info = g_field_info_get_type(field_info); if (!g_type_info_is_pointer (type_info) && g_type_info_get_tag (type_info) == GI_TYPE_TAG_INTERFACE) { + GjsAutoBaseInfo interface_info = g_type_info_get_interface(type_info); - GIBaseInfo *interface_info = g_type_info_get_interface(type_info); - - if (g_base_info_get_type (interface_info) == GI_INFO_TYPE_STRUCT || - g_base_info_get_type (interface_info) == GI_INFO_TYPE_BOXED) { - - success = set_nested_interface_object (context, priv, - field_info, type_info, - interface_info, value); - - g_base_info_unref ((GIBaseInfo *)interface_info); - - goto out; + if (interface_info.type() == GI_INFO_TYPE_STRUCT || + interface_info.type() == GI_INFO_TYPE_BOXED) { + return set_nested_interface_object(context, field_info, + interface_info, value); } - - g_base_info_unref ((GIBaseInfo *)interface_info); - } if (!gjs_value_to_g_argument(context, value, @@ -756,69 +656,57 @@ boxed_set_field_from_value(JSContext *context, GJS_ARGUMENT_FIELD, GI_TRANSFER_NOTHING, true, &arg)) - goto out; - - need_release = true; + return false; - if (!g_field_info_set_field (field_info, priv->gboxed, &arg)) { - gjs_throw(context, "Writing field %s.%s is not supported", - g_base_info_get_name ((GIBaseInfo *)priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); - goto out; + bool success = true; + if (!g_field_info_set_field(field_info, m_ptr, &arg)) { + gjs_throw(context, "Writing field %s.%s is not supported", name(), + g_base_info_get_name(field_info)); + success = false; } - success = true; - -out: - if (need_release) - gjs_g_argument_release (context, GI_TRANSFER_NOTHING, - type_info, - &arg); - - g_base_info_unref ((GIBaseInfo *)type_info); + JS::AutoSaveExceptionState saved_exc(context); + if (!gjs_g_argument_release(context, GI_TRANSFER_NOTHING, type_info, &arg)) + gjs_log_exception(context); + saved_exc.restore(); return success; } -static bool -boxed_field_setter(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(cx, argc, vp, args, obj, Boxed, priv); - GIFieldInfo *field_info; - bool success = false; - - field_info = get_field_info(cx, priv, - native_accessor_slot(&args.callee())); - if (!field_info) +/* + * BoxedBase::field_setter: + * + * JSNative property setter that is called when writing to a field defined on a + * boxed type. Delegates to BoxedInstance::field_setter_impl() if the minimal + * conditions have been met. + */ +bool BoxedBase::field_setter(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, BoxedBase, priv); + if (!priv->check_is_instance(cx, "set a field")) return false; - if (priv->gboxed == NULL) { /* direct access to proto field */ - gjs_throw(cx, "Can't set field %s.%s on prototype", - g_base_info_get_name ((GIBaseInfo *)priv->info), - g_base_info_get_name ((GIBaseInfo *)field_info)); - goto out; - } + uint32_t field_ix = gjs_dynamic_property_private_slot(&args.callee()) + .toPrivateUint32(); + GjsAutoFieldInfo field_info = priv->get_field_info(cx, field_ix); + if (!field_info) + return false; - if (!boxed_set_field_from_value(cx, priv, field_info, args[0])) - goto out; + if (!priv->to_instance()->field_setter_impl(cx, field_info, args[0])) + return false; args.rval().setUndefined(); /* No stored value */ - success = true; - -out: - g_base_info_unref ((GIBaseInfo *)field_info); - - return success; + return true; } -static bool -define_boxed_class_fields(JSContext *cx, - Boxed *priv, - JS::HandleObject proto) -{ - int n_fields = g_struct_info_get_n_fields (priv->info); +/* + * BoxedPrototype::define_boxed_class_fields: + * + * Defines properties on the JS prototype object, with JSNative getters and + * setters, for all the fields exposed by GObject introspection. + */ +bool BoxedPrototype::define_boxed_class_fields(JSContext* cx, + JS::HandleObject proto) { + int n_fields = g_struct_info_get_n_fields(info()); int i; /* We define all fields as read/write so that the user gets an @@ -837,81 +725,39 @@ define_boxed_class_fields(JSContext *cx, * memory overhead. */ 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); - GjsAutoChar getter_name = g_strconcat("boxed_field_get::", - field_name, NULL); - GjsAutoChar setter_name = g_strconcat("boxed_field_set::", - field_name, NULL); - g_base_info_unref ((GIBaseInfo *)field); - - /* In order to have one getter and setter for all the properties - * we define, we must provide the property index in a "reserved - * slot" for which we must unfortunately use the jsfriendapi. */ - JS::RootedObject getter(cx, - define_native_accessor_wrapper(cx, boxed_field_getter, 0, - getter_name, i)); - if (!getter) - return false; - - JS::RootedObject setter(cx, - define_native_accessor_wrapper(cx, boxed_field_setter, 1, - setter_name, i)); - if (!setter) - return false; - - if (!JS_DefineProperty(cx, proto, field_name, JS::UndefinedHandleValue, - JSPROP_PERMANENT | JSPROP_SHARED | JSPROP_GETTER | JSPROP_SETTER, - JS_DATA_TO_FUNC_PTR(JSNative, getter.get()), - JS_DATA_TO_FUNC_PTR(JSNative, setter.get()))) + GjsAutoFieldInfo field = g_struct_info_get_field(info(), i); + JS::RootedValue private_id(cx, JS::PrivateUint32Value(i)); + if (!gjs_define_property_dynamic(cx, proto, field.name(), "boxed_field", + &BoxedBase::field_getter, + &BoxedBase::field_setter, private_id, + GJS_MODULE_PROP_FLAGS)) return false; } return true; } -static bool -to_string_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, rec, obj, Boxed, priv); - return _gjs_proxy_to_string_func(context, obj, "boxed", - (GIBaseInfo*)priv->info, priv->gtype, - priv->gboxed, rec.rval()); -} - -static void -boxed_trace(JSTracer *tracer, - JSObject *obj) -{ - Boxed *priv = reinterpret_cast(JS_GetPrivate(obj)); - if (priv == NULL) - return; - - JS::TraceEdge(tracer, &priv->zero_args_constructor_name, - "Boxed::zero_args_constructor_name"); - JS::TraceEdge(tracer, &priv->default_constructor_name, +// Overrides GIWrapperPrototype::trace_impl(). +void BoxedPrototype::trace_impl(JSTracer* trc) { + JS::TraceEdge(trc, &m_default_constructor_name, "Boxed::default_constructor_name"); + if (m_field_map) + m_field_map->trace(trc); } -/* The bizarre thing about this vtable is that it applies to both - * instances of the object, and to the prototype that instances of the - * class have. - */ -static const struct JSClassOps gjs_boxed_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - boxed_resolve, - nullptr, /* mayResolve */ - boxed_finalize, - NULL, /* call */ - NULL, /* hasInstance */ - NULL, /* construct */ - boxed_trace +// clang-format off +const struct JSClassOps BoxedBase::class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + &BoxedBase::new_enumerate, + &BoxedBase::resolve, + nullptr, // mayResolve + &BoxedBase::finalize, + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + &BoxedBase::trace }; /* We allocate 1 reserved slot; this is typically unused, but if the @@ -919,25 +765,16 @@ static const struct JSClassOps gjs_boxed_class_ops = { * reserved slot is used to hold onto the parent Javascript object and * make sure it doesn't get freed. */ -struct JSClass gjs_boxed_class = { +const struct JSClass BoxedBase::klass = { "GObject_Boxed", JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE | JSCLASS_HAS_RESERVED_SLOTS(1), - &gjs_boxed_class_ops + &BoxedBase::class_ops }; +// clang-format on -JSPropertySpec gjs_boxed_proto_props[] = { - JS_PS_END -}; - -JSFunctionSpec gjs_boxed_proto_funcs[] = { - JS_FS("toString", to_string_func, 0, 0), - JS_FS_END -}; - -static bool -type_can_be_allocated_directly(GITypeInfo *type_info) -{ +[[nodiscard]] static bool type_can_be_allocated_directly( + GITypeInfo* type_info) { bool is_simple = true; if (g_type_info_is_pointer(type_info)) { @@ -949,6 +786,8 @@ type_can_be_allocated_directly(GITypeInfo *type_info) is_simple = type_can_be_allocated_directly(param_info); g_base_info_unref((GIBaseInfo*)param_info); + } else if (g_type_info_get_tag(type_info) == GI_TYPE_TAG_VOID) { + return true; } else { is_simple = false; } @@ -1027,9 +866,7 @@ type_can_be_allocated_directly(GITypeInfo *type_info) * type that we know how to assign to. If so, then we can allocate and free * instances without needing a constructor. */ -static bool -struct_is_simple(GIStructInfo *info) -{ +[[nodiscard]] static bool struct_is_simple(GIStructInfo* info) { int n_fields = g_struct_info_get_n_fields(info); bool is_simple = true; int i; @@ -1051,145 +888,110 @@ struct_is_simple(GIStructInfo *info) return is_simple; } -static void -boxed_fill_prototype_info(JSContext *context, - Boxed *priv) -{ +BoxedPrototype::BoxedPrototype(GIStructInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype), + m_zero_args_constructor(-1), + m_default_constructor(-1), + m_default_constructor_name(JSID_VOID), + m_field_map(nullptr), + m_can_allocate_directly(struct_is_simple(info)) { + GJS_INC_COUNTER(boxed_prototype); +} + +// Overrides GIWrapperPrototype::init(). +bool BoxedPrototype::init(JSContext* context) { int i, n_methods; int first_constructor = -1; jsid first_constructor_name = JSID_VOID; + jsid zero_args_constructor_name = JSID_VOID; - priv->gtype = g_registered_type_info_get_g_type( (GIRegisteredTypeInfo*) priv->info); - priv->zero_args_constructor = -1; - priv->zero_args_constructor_name = JSID_VOID; - priv->default_constructor = -1; - priv->default_constructor_name = JSID_VOID; - - if (priv->gtype != G_TYPE_NONE) { + if (m_gtype != G_TYPE_NONE) { /* If the structure is registered as a boxed, we can create a new instance by * looking for a zero-args constructor and calling it; constructors don't * really make sense for non-boxed types, since there is no memory management * for the return value. */ - n_methods = g_struct_info_get_n_methods(priv->info); + n_methods = g_struct_info_get_n_methods(m_info); for (i = 0; i < n_methods; ++i) { - GIFunctionInfo *func_info; GIFunctionInfoFlags flags; - func_info = g_struct_info_get_method(priv->info, i); + GjsAutoFunctionInfo func_info = g_struct_info_get_method(m_info, i); flags = g_function_info_get_flags(func_info); if ((flags & GI_FUNCTION_IS_CONSTRUCTOR) != 0) { if (first_constructor < 0) { - const char *name; - - name = g_base_info_get_name((GIBaseInfo*) func_info); first_constructor = i; - first_constructor_name = gjs_intern_string_to_id(context, name); + first_constructor_name = + gjs_intern_string_to_id(context, func_info.name()); + if (first_constructor_name == JSID_VOID) + return false; } - if (priv->zero_args_constructor < 0 && - g_callable_info_get_n_args((GICallableInfo*) func_info) == 0) { - const char *name; - - name = g_base_info_get_name((GIBaseInfo*) func_info); - priv->zero_args_constructor = i; - priv->zero_args_constructor_name = gjs_intern_string_to_id(context, name); + if (m_zero_args_constructor < 0 && + g_callable_info_get_n_args(func_info) == 0) { + m_zero_args_constructor = i; + zero_args_constructor_name = + gjs_intern_string_to_id(context, func_info.name()); + if (zero_args_constructor_name == JSID_VOID) + return false; } - if (priv->default_constructor < 0 && - strcmp(g_base_info_get_name ((GIBaseInfo*) func_info), "new") == 0) { - priv->default_constructor = i; - priv->default_constructor_name = gjs_context_get_const_string(context, GJS_STRING_NEW); + if (m_default_constructor < 0 && + strcmp(func_info.name(), "new") == 0) { + m_default_constructor = i; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + m_default_constructor_name = atoms.new_(); } } - - g_base_info_unref((GIBaseInfo*) func_info); } - if (priv->default_constructor < 0) { - priv->default_constructor = priv->zero_args_constructor; - priv->default_constructor_name = priv->zero_args_constructor_name; + if (m_default_constructor < 0) { + m_default_constructor = m_zero_args_constructor; + m_default_constructor_name = zero_args_constructor_name; } - if (priv->default_constructor < 0) { - priv->default_constructor = first_constructor; - priv->default_constructor_name = first_constructor_name; + if (m_default_constructor < 0) { + m_default_constructor = first_constructor; + m_default_constructor_name = first_constructor_name; } } -} - -void -gjs_define_boxed_class(JSContext *context, - JS::HandleObject in_object, - GIBoxedInfo *info) -{ - const char *constructor_name; - JS::RootedObject prototype(context), constructor(context); - Boxed *priv; - - /* See the comment in gjs_define_object_class() for an - * explanation of how this all works; Boxed is pretty much the - * same as Object. - */ - - constructor_name = g_base_info_get_name( (GIBaseInfo*) info); - - if (!gjs_init_class_dynamic(context, in_object, - nullptr, /* parent prototype */ - g_base_info_get_namespace( (GIBaseInfo*) info), - constructor_name, - &gjs_boxed_class, - gjs_boxed_constructor, 1, - /* props of prototype */ - &gjs_boxed_proto_props[0], - /* funcs of prototype */ - &gjs_boxed_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - NULL, - &prototype, - &constructor)) { - gjs_log_exception(context); - g_error("Can't init class %s", constructor_name); - } - - GJS_INC_COUNTER(boxed); - priv = g_slice_new0(Boxed); - new (priv) Boxed(); - priv->info = info; - boxed_fill_prototype_info(context, priv); - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->gtype = g_registered_type_info_get_g_type ((GIRegisteredTypeInfo*) priv->info); - JS_SetPrivate(prototype, priv); - - gjs_debug(GJS_DEBUG_GBOXED, "Defined class %s prototype is %p class %p in object %p", - constructor_name, prototype.get(), JS_GetClass(prototype), - in_object.get()); + return true; +} - priv->can_allocate_directly = struct_is_simple (priv->info); +/* + * BoxedPrototype::define_class: + * @in_object: Object where the constructor is stored, typically a repo object. + * @info: Introspection info for the boxed class. + * + * Define a boxed class constructor and prototype, including all the necessary + * methods and properties. + */ +bool BoxedPrototype::define_class(JSContext* context, + JS::HandleObject in_object, + GIStructInfo* info) { + JS::RootedObject prototype(context), unused_constructor(context); + GType gtype = g_registered_type_info_get_g_type(info); + BoxedPrototype* priv = BoxedPrototype::create_class( + context, in_object, info, gtype, &unused_constructor, &prototype); + if (!priv || !priv->define_boxed_class_fields(context, prototype)) + return false; - define_boxed_class_fields (context, priv, prototype); - gjs_define_static_methods (context, constructor, priv->gtype, priv->info); + if (gtype == G_TYPE_ERROR && + !JS_DefineFunction(context, prototype, "toString", + &ErrorBase::to_string, 0, GJS_MODULE_PROP_FLAGS)) + return false; - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, priv->gtype)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, - JSPROP_PERMANENT); + return true; } -JSObject* -gjs_boxed_from_c_struct(JSContext *context, - GIStructInfo *info, - void *gboxed, - GjsBoxedCreationFlags flags) -{ - JSObject *obj; - Boxed *priv; - Boxed *proto_priv; - +/* Helper function to make the public API more readable. The overloads are + * specified explicitly in the public API, but the implementation uses + * std::forward in order to avoid duplicating code. */ +template +JSObject* BoxedInstance::new_for_c_struct_impl(JSContext* cx, + GIStructInfo* info, void* gboxed, + Args&&... args) { if (gboxed == NULL) return NULL; @@ -1197,110 +999,86 @@ gjs_boxed_from_c_struct(JSContext *context, "Wrapping struct %s %p with JSObject", g_base_info_get_name((GIBaseInfo *)info), gboxed); - JS::RootedObject proto(context, gjs_lookup_generic_prototype(context, info)); - proto_priv = priv_from_js(context, proto); - - obj = JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto); - - GJS_INC_COUNTER(boxed); - priv = g_slice_new0(Boxed); - new (priv) Boxed(); + JS::RootedObject obj(cx, gjs_new_object_with_generic_prototype(cx, info)); + if (!obj) + return nullptr; - *priv = *proto_priv; - g_base_info_ref( (GIBaseInfo*) priv->info); + BoxedInstance* priv = BoxedInstance::new_for_js_object(cx, obj); + if (!priv) + return nullptr; - JS_SetPrivate(obj, priv); + if (!priv->init_from_c_struct(cx, gboxed, std::forward(args)...)) + return nullptr; - if ((flags & GJS_BOXED_CREATION_NO_COPY) != 0) { - /* we need to create a JS Boxed which references the - * original C struct, not a copy of it. Used for - * G_SIGNAL_TYPE_STATIC_SCOPE - */ - priv->gboxed = gboxed; - priv->not_owning_gboxed = true; - } else { - if (priv->gtype != G_TYPE_NONE && g_type_is_a (priv->gtype, G_TYPE_BOXED)) { - priv->gboxed = g_boxed_copy(priv->gtype, gboxed); - } else if (priv->gtype == G_TYPE_VARIANT) { - priv->gboxed = g_variant_ref_sink ((GVariant *) gboxed); - } else if (priv->can_allocate_directly) { - boxed_new_direct(priv); - memcpy(priv->gboxed, gboxed, g_struct_info_get_size (priv->info)); - } else { - gjs_throw(context, - "Can't create a Javascript object for %s; no way to copy", - g_base_info_get_name( (GIBaseInfo*) priv->info)); - } - } + if (priv->gtype() == G_TYPE_ERROR && !gjs_define_error_properties(cx, obj)) + return nullptr; return obj; } -void* -gjs_c_struct_from_boxed(JSContext *context, - JS::HandleObject obj) -{ - Boxed *priv; - - if (!obj) - return NULL; - - priv = priv_from_js(context, obj); - if (priv == NULL) - return NULL; - - return priv->gboxed; +/* + * BoxedInstance::new_for_c_struct: + * + * Creates a new BoxedInstance JS object from a C boxed struct pointer. + * + * There are two overloads of this method; the NoCopy overload will simply take + * the passed-in pointer but not own it, while the normal method will take a + * reference, or if the boxed type can be directly allocated, copy the memory. + */ +JSObject* BoxedInstance::new_for_c_struct(JSContext* cx, GIStructInfo* info, + void* gboxed) { + return new_for_c_struct_impl(cx, info, gboxed); } -bool -gjs_typecheck_boxed(JSContext *context, - JS::HandleObject object, - GIStructInfo *expected_info, - GType expected_type, - bool throw_error) -{ - Boxed *priv; - bool result; - - if (!do_base_typecheck(context, object, throw_error)) - return false; - - priv = priv_from_js(context, object); +JSObject* BoxedInstance::new_for_c_struct(JSContext* cx, GIStructInfo* info, + void* gboxed, NoCopy no_copy) { + return new_for_c_struct_impl(cx, info, gboxed, no_copy); +} - if (priv->gboxed == NULL) { - if (throw_error) { - 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)); - } +/* + * BoxedInstance::init_from_c_struct: + * + * Do the necessary initialization when creating a BoxedInstance JS object from + * a C boxed struct pointer. + * + * There are two overloads of this method; the NoCopy overload will simply take + * the passed-in pointer, while the normal method will take a reference, or if + * the boxed type can be directly allocated, copy the memory. + */ +bool BoxedInstance::init_from_c_struct(JSContext*, void* gboxed, NoCopy) { + // We need to create a JS Boxed which references the original C struct, not + // a copy of it. Used for G_SIGNAL_TYPE_STATIC_SCOPE. + share_ptr(gboxed); + debug_lifecycle("Boxed pointer acquired, memory not owned"); + return true; +} - return false; +bool BoxedInstance::init_from_c_struct(JSContext* cx, void* gboxed) { + if (gtype() != G_TYPE_NONE && g_type_is_a(gtype(), G_TYPE_BOXED)) { + copy_boxed(gboxed); + return true; + } else if (gtype() == G_TYPE_VARIANT) { + own_ptr(g_variant_ref_sink(static_cast(gboxed))); + debug_lifecycle("Boxed pointer created by sinking GVariant ref"); + return true; + } else if (get_prototype()->can_allocate_directly()) { + copy_memory(gboxed); + return true; } - if (expected_type != G_TYPE_NONE) - result = g_type_is_a (priv->gtype, expected_type); - else if (expected_info != NULL) - result = g_base_info_equal((GIBaseInfo*) priv->info, (GIBaseInfo*) expected_info); - else - result = true; - - if (!result && throw_error) { - if (expected_info != 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, 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)); - } - } + gjs_throw(cx, "Can't create a Javascript object for %s; no way to copy", + name()); + return false; +} - return result; +void* BoxedInstance::copy_ptr(JSContext* cx, GType gtype, void* ptr) { + if (g_type_is_a(gtype, G_TYPE_BOXED)) + return g_boxed_copy(gtype, ptr); + if (g_type_is_a(gtype, G_TYPE_VARIANT)) + return g_variant_ref(static_cast(ptr)); + + gjs_throw(cx, + "Can't transfer ownership of a structure type not registered as " + "boxed"); + return nullptr; } diff --git a/gi/boxed.h b/gi/boxed.h index 99f0cd9..874886e 100644 --- a/gi/boxed.h +++ b/gi/boxed.h @@ -21,42 +21,240 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_BOXED_H__ -#define __GJS_BOXED_H__ +#ifndef GI_BOXED_H_ +#define GI_BOXED_H_ -#include +#include + +#include + +#include +#include #include +#include // for GCHashMap +#include +#include +#include +#include // for DefaultHasher + +#include "gi/wrapperutils.h" #include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "util/log.h" -#include +class BoxedPrototype; +class BoxedInstance; +class JSTracer; +namespace JS { +class CallArgs; +} +namespace js { +class SystemAllocPolicy; +} + +/* To conserve memory, we have two different kinds of private data for GBoxed + * JS wrappers: BoxedInstance, and BoxedPrototype. Both inherit from BoxedBase + * for their common functionality. For more information, see the notes in + * wrapperutils.h. + */ + +class BoxedBase + : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit BoxedBase(BoxedPrototype* proto = nullptr) + : GIWrapperBase(proto) {} + ~BoxedBase(void) {} + + static const GjsDebugTopic debug_topic = GJS_DEBUG_GBOXED; + static constexpr const char* debug_tag = "GBoxed"; + + static const struct JSClassOps class_ops; + static const struct JSClass klass; + + // JS property accessors + + GJS_JSAPI_RETURN_CONVENTION + static bool field_getter(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool field_setter(JSContext* cx, unsigned argc, JS::Value* vp); + + // Helper methods that work on either instances or prototypes + + [[nodiscard]] const char* to_string_kind() const { return "boxed"; } + + GJS_JSAPI_RETURN_CONVENTION + GIFieldInfo* get_field_info(JSContext* cx, uint32_t id) const; + + public: + [[nodiscard]] BoxedBase* get_copy_source(JSContext* cx, + JS::Value value) const; +}; + +class BoxedPrototype : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; + + using FieldMap = + JS::GCHashMap, GjsAutoFieldInfo, + js::DefaultHasher, js::SystemAllocPolicy>; + + int m_zero_args_constructor; // -1 if none + int m_default_constructor; // -1 if none + JS::Heap m_default_constructor_name; + FieldMap* m_field_map; + bool m_can_allocate_directly : 1; + + explicit BoxedPrototype(GIStructInfo* info, GType gtype); + ~BoxedPrototype(void); + + GJS_JSAPI_RETURN_CONVENTION bool init(JSContext* cx); + + static constexpr InfoType::Tag info_type_tag = InfoType::Struct; + + // Accessors + + public: + [[nodiscard]] bool can_allocate_directly() const { + return m_can_allocate_directly; + } + [[nodiscard]] bool has_zero_args_constructor() const { + return m_zero_args_constructor >= 0; + } + [[nodiscard]] bool has_default_constructor() const { + return m_default_constructor >= 0; + } + [[nodiscard]] GIFunctionInfo* zero_args_constructor_info() const { + return g_struct_info_get_method(info(), m_zero_args_constructor); + } + // The ID is traced from the object, so it's OK to create a handle from it. + [[nodiscard]] JS::HandleId default_constructor_name() const { + return JS::HandleId::fromMarkedLocation( + m_default_constructor_name.address()); + } + + // JSClass operations + + private: + GJS_JSAPI_RETURN_CONVENTION + bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* prop_name, bool* resolved); + + GJS_JSAPI_RETURN_CONVENTION + bool new_enumerate_impl(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool only_enumerable); + void trace_impl(JSTracer* trc); + + // Helper methods + + GJS_JSAPI_RETURN_CONVENTION + static FieldMap* create_field_map(JSContext* cx, GIStructInfo* struct_info); + GJS_JSAPI_RETURN_CONVENTION + bool ensure_field_map(JSContext* cx); + GJS_JSAPI_RETURN_CONVENTION + bool define_boxed_class_fields(JSContext* cx, JS::HandleObject proto); + + public: + GJS_JSAPI_RETURN_CONVENTION + static bool define_class(JSContext* cx, JS::HandleObject in_object, + GIStructInfo* info); + GJS_JSAPI_RETURN_CONVENTION + GIFieldInfo* lookup_field(JSContext* cx, JSString* prop_name); +}; + +class BoxedInstance + : public GIWrapperInstance { + friend class GIWrapperInstance; + friend class GIWrapperBase; + friend class BoxedBase; // for field_getter, etc. + + bool m_allocated_directly : 1; + bool m_owning_ptr : 1; // if set, the JS wrapper owns the C memory referred + // to by m_ptr. + + explicit BoxedInstance(JSContext* cx, JS::HandleObject obj); + ~BoxedInstance(void); + + // Don't set GIWrapperBase::m_ptr directly. Instead, use one of these + // setters to express your intention to own the pointer or not. + void own_ptr(void* boxed_ptr) { + g_assert(!m_ptr); + m_ptr = boxed_ptr; + m_owning_ptr = true; + } + void share_ptr(void* unowned_boxed_ptr) { + g_assert(!m_ptr); + m_ptr = unowned_boxed_ptr; + m_owning_ptr = false; + } + + // Methods for different ways to allocate the GBoxed pointer + + void allocate_directly(void); + void copy_boxed(void* boxed_ptr); + void copy_boxed(BoxedInstance* source); + void copy_memory(void* boxed_ptr); + void copy_memory(BoxedInstance* source); + + // Helper methods + + GJS_JSAPI_RETURN_CONVENTION + bool init_from_props(JSContext* cx, JS::Value props_value); + + GJS_JSAPI_RETURN_CONVENTION + bool get_nested_interface_object(JSContext* cx, JSObject* parent_obj, + GIFieldInfo* field_info, + GIBaseInfo* interface_info, + JS::MutableHandleValue value) const; + GJS_JSAPI_RETURN_CONVENTION + bool set_nested_interface_object(JSContext* cx, GIFieldInfo* field_info, + GIBaseInfo* interface_info, + JS::HandleValue value); + + GJS_JSAPI_RETURN_CONVENTION + static void* copy_ptr(JSContext* cx, GType gtype, void* ptr); + + // JS property accessors + + GJS_JSAPI_RETURN_CONVENTION + bool field_getter_impl(JSContext* cx, JSObject* obj, GIFieldInfo* info, + JS::MutableHandleValue rval) const; + GJS_JSAPI_RETURN_CONVENTION + bool field_setter_impl(JSContext* cx, GIFieldInfo* info, + JS::HandleValue value); -G_BEGIN_DECLS + // JS constructor -typedef enum { - GJS_BOXED_CREATION_NONE = 0, - GJS_BOXED_CREATION_NO_COPY = (1 << 0) -} GjsBoxedCreationFlags; + GJS_JSAPI_RETURN_CONVENTION + bool constructor_impl(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args); -/* Hack for now... why doesn't gobject-introspection have this? */ -typedef GIStructInfo GIBoxedInfo; + // Public API for initializing BoxedInstance JS object from C struct -void gjs_define_boxed_class (JSContext *context, - JS::HandleObject in_object, - GIBoxedInfo *info); + public: + struct NoCopy {}; -void* gjs_c_struct_from_boxed (JSContext *context, - JS::HandleObject obj); -JSObject* gjs_boxed_from_c_struct (JSContext *context, - GIStructInfo *info, - void *gboxed, - GjsBoxedCreationFlags flags); -bool gjs_typecheck_boxed (JSContext *context, - JS::HandleObject obj, - GIStructInfo *expected_info, - GType expected_type, - bool throw_error); + private: + GJS_JSAPI_RETURN_CONVENTION + bool init_from_c_struct(JSContext* cx, void* gboxed); + GJS_JSAPI_RETURN_CONVENTION + bool init_from_c_struct(JSContext* cx, void* gboxed, NoCopy); + template + GJS_JSAPI_RETURN_CONVENTION static JSObject* new_for_c_struct_impl( + JSContext* cx, GIStructInfo* info, void* gboxed, Args&&... args); -G_END_DECLS + public: + GJS_JSAPI_RETURN_CONVENTION + static JSObject* new_for_c_struct(JSContext* cx, GIStructInfo* info, + void* gboxed); + GJS_JSAPI_RETURN_CONVENTION + static JSObject* new_for_c_struct(JSContext* cx, GIStructInfo* info, + void* gboxed, NoCopy); +}; -#endif /* __GJS_BOXED_H__ */ +#endif // GI_BOXED_H_ diff --git a/gi/closure.cpp b/gi/closure.cpp index cf2996a..c02a7ed 100644 --- a/gi/closure.cpp +++ b/gi/closure.cpp @@ -23,18 +23,25 @@ #include -#include -#include -#include +#include -#include "closure.h" +#include + +#include +#include +#include +#include // for JS_IsExceptionPending, Call, JS_Get... + +#include "gi/closure.h" +#include "cjs/context-private.h" #include "cjs/jsapi-util-root.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" struct Closure { JSContext *context; - GjsMaybeOwned obj; + GjsMaybeOwned func; }; struct GjsClosure { @@ -86,11 +93,11 @@ invalidate_js_pointers(GjsClosure *gc) c = &gc->priv; - if (c->obj == nullptr) + if (!c->func) return; - c->obj.reset(); - c->context = NULL; + c->func.reset(); + c->context = nullptr; /* Notify any closure reference holders they * may want to drop references. @@ -98,19 +105,17 @@ invalidate_js_pointers(GjsClosure *gc) g_closure_invalidate(&gc->base); } -static void -global_context_finalized(JS::HandleObject obj, - void *data) -{ +static void global_context_finalized(JS::HandleFunction func, void* data) { GjsClosure *gc = (GjsClosure*) data; Closure *c = &gc->priv; - gjs_debug_closure("Context global object destroy notifier on closure %p " - "which calls object %p", - c, c->obj.get()); + gjs_debug_closure( + "Context global object destroy notifier on closure %p which calls " + "object %p", + c, c->func.debug_addr()); - if (c->obj) { - g_assert(c->obj == obj.get()); + if (c->func) { + g_assert(c->func == func.get()); invalidate_js_pointers(gc); } @@ -127,19 +132,16 @@ global_context_finalized(JS::HandleObject obj, * * Unlike "dispose" invalidation only happens once. */ -static void -closure_invalidated(gpointer data, - GClosure *closure) -{ +static void closure_invalidated(void*, GClosure* closure) { Closure *c; c = &((GjsClosure*) closure)->priv; GJS_DEC_COUNTER(closure); - gjs_debug_closure("Invalidating closure %p which calls object %p", - closure, c->obj.get()); + gjs_debug_closure("Invalidating closure %p which calls function %p", + closure, c->func.debug_addr()); - if (c->obj == nullptr) { + if (!c->func) { gjs_debug_closure(" (closure %p already dead, nothing to do)", closure); return; @@ -156,30 +158,24 @@ closure_invalidated(gpointer data, "removing our destroy notifier on global object)", closure); - c->obj.reset(); + c->func.reset(); c->context = nullptr; } -static void -closure_set_invalid(gpointer data, - GClosure *closure) -{ +static void closure_set_invalid(void*, GClosure* closure) { Closure *self = &((GjsClosure*) closure)->priv; - gjs_debug_closure("Invalidating signal closure %p which calls object %p", - closure, self->obj.get()); + gjs_debug_closure("Invalidating signal closure %p which calls function %p", + closure, self->func.debug_addr()); - self->obj.prevent_collection(); - self->obj.reset(); + self->func.prevent_collection(); + self->func.reset(); self->context = nullptr; GJS_DEC_COUNTER(closure); } -static void -closure_finalize(gpointer data, - GClosure *closure) -{ +static void closure_finalize(void*, GClosure* closure) { Closure *self = &((GjsClosure*) closure)->priv; self->~Closure(); @@ -197,28 +193,27 @@ gjs_closure_invoke(GClosure *closure, c = &((GjsClosure*) closure)->priv; - if (c->obj == nullptr) { + if (!c->func) { /* We were destroyed; become a no-op */ - c->context = NULL; + c->context = nullptr; return false; } context = c->context; - JSAutoRequest ar(context); - JSAutoCompartment ac(context, c->obj); + JSAutoRealm ar(context, JS_GetFunctionObject(c->func)); - if (JS_IsExceptionPending(context)) { + if (gjs_log_exception(context)) { gjs_debug_closure("Exception was pending before invoking callback??? " "Not expected - closure %p", closure); - gjs_log_exception(context); } - JS::RootedValue v_closure(context, JS::ObjectValue(*c->obj)); - if (!gjs_call_function_value(context, this_obj, v_closure, args, retval)) { + JS::RootedFunction func(context, c->func); + if (!JS::Call(context, this_obj, func, args, retval)) { /* Exception thrown... */ - gjs_debug_closure("Closure invocation failed (exception should " - "have been thrown) closure %p callable %p", - closure, c->obj.get()); + gjs_debug_closure( + "Closure invocation failed (exception should have been thrown) " + "closure %p function %p", + closure, c->func.debug_addr()); /* 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 @@ -227,7 +222,7 @@ gjs_closure_invoke(GClosure *closure, if (return_exception) JS_GetPendingException(context, retval); else - gjs_log_exception(context); + gjs_log_exception_uncaught(context); } else { retval.setUndefined(); gjs_debug_closure("Closure invocation failed but no exception was set?" @@ -236,12 +231,13 @@ gjs_closure_invoke(GClosure *closure, return false; } - if (gjs_log_exception(context)) { + if (gjs_log_exception_uncaught(context)) { gjs_debug_closure("Closure invocation succeeded but an exception was set" " - closure %p", closure); } - JS_MaybeGC(context); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + gjs->schedule_gc_if_needed(); return true; } @@ -252,7 +248,7 @@ gjs_closure_is_valid(GClosure *closure) c = &((GjsClosure*) closure)->priv; - return c->context != NULL; + return !!c->context; } JSContext* @@ -265,14 +261,12 @@ gjs_closure_get_context(GClosure *closure) return c->context; } -JSObject* -gjs_closure_get_callable(GClosure *closure) -{ +JSFunction* gjs_closure_get_callable(GClosure* closure) { Closure *c; c = &((GjsClosure*) closure)->priv; - return c->obj; + return c->func; } void @@ -283,22 +277,19 @@ gjs_closure_trace(GClosure *closure, c = &((GjsClosure*) closure)->priv; - if (c->obj == nullptr) + if (!c->func) return; - c->obj.trace(tracer, "signal connection"); + c->func.trace(tracer, "signal connection"); } -GClosure* -gjs_closure_new(JSContext *context, - JSObject *callable, - const char *description, - bool root_function) -{ - GjsClosure *gc; +GClosure* gjs_closure_new(JSContext* context, JSFunction* callable, + const char* description GJS_USED_VERBOSE_GCLOSURE, + bool root_function) { Closure *c; - gc = (GjsClosure*) g_closure_new_simple(sizeof(GjsClosure), NULL); + auto* gc = reinterpret_cast( + g_closure_new_simple(sizeof(GjsClosure), nullptr)); c = new (&gc->priv) Closure(); /* The saved context is used for lifetime management, so that the closure will @@ -307,28 +298,27 @@ gjs_closure_new(JSContext *context, * the context that created it. */ c->context = context; - JS_BeginRequest(context); GJS_INC_COUNTER(closure); if (root_function) { /* Fully manage closure lifetime if so asked */ - c->obj.root(context, callable, global_context_finalized, gc); + c->func.root(context, callable, global_context_finalized, gc); - g_closure_add_invalidate_notifier(&gc->base, NULL, closure_invalidated); + g_closure_add_invalidate_notifier(&gc->base, nullptr, + closure_invalidated); } else { - c->obj = callable; + c->func = callable; /* Only mark the closure as invalid if memory is managed outside (i.e. by object.c for signals) */ - g_closure_add_invalidate_notifier(&gc->base, NULL, closure_set_invalid); + g_closure_add_invalidate_notifier(&gc->base, nullptr, + closure_set_invalid); } - g_closure_add_finalize_notifier(&gc->base, NULL, closure_finalize); - - gjs_debug_closure("Create closure %p which calls object %p '%s'", - gc, c->obj.get(), description); + g_closure_add_finalize_notifier(&gc->base, nullptr, closure_finalize); - JS_EndRequest(context); + gjs_debug_closure("Create closure %p which calls function %p '%s'", gc, + c->func.debug_addr(), description); return &gc->base; } diff --git a/gi/closure.h b/gi/closure.h index c2fbea6..38836b7 100644 --- a/gi/closure.h +++ b/gi/closure.h @@ -21,34 +21,35 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_CLOSURE_H__ -#define __GJS_CLOSURE_H__ +#ifndef GI_CLOSURE_H_ +#define GI_CLOSURE_H_ + +#include -#include #include -#include "cjs/jsapi-util.h" +#include -G_BEGIN_DECLS +class JSTracer; +namespace JS { +class HandleValueArray; +} -GClosure* gjs_closure_new (JSContext *context, - JSObject *callable, - const char *description, - bool root_function); +[[nodiscard]] GClosure* gjs_closure_new(JSContext* cx, JSFunction* callable, + const char* description, + bool root_function); -bool gjs_closure_invoke(GClosure *closure, - JS::HandleObject this_obj, - const JS::HandleValueArray& args, - JS::MutableHandleValue retval, - bool return_exception); +[[nodiscard]] bool gjs_closure_invoke(GClosure* closure, + JS::HandleObject this_obj, + const JS::HandleValueArray& args, + JS::MutableHandleValue retval, + bool return_exception); -JSContext* gjs_closure_get_context (GClosure *closure); -bool gjs_closure_is_valid (GClosure *closure); -JSObject* gjs_closure_get_callable (GClosure *closure); +[[nodiscard]] JSContext* gjs_closure_get_context(GClosure* closure); +[[nodiscard]] bool gjs_closure_is_valid(GClosure* closure); +[[nodiscard]] JSFunction* gjs_closure_get_callable(GClosure* closure); void gjs_closure_trace (GClosure *closure, JSTracer *tracer); -G_END_DECLS - -#endif /* __GJS_CLOSURE_H__ */ +#endif // GI_CLOSURE_H_ diff --git a/gi/enumeration.cpp b/gi/enumeration.cpp index 0eb56c1..badb03d 100644 --- a/gi/enumeration.cpp +++ b/gi/enumeration.cpp @@ -23,19 +23,20 @@ #include -#include - -#include "cjs/jsapi-wrapper.h" -#include "repo.h" -#include "gtype.h" -#include "function.h" - -#include - #include +#include +#include -#include "enumeration.h" +#include +#include +#include // for JS_DefineProperty, JS_NewPlainObject +#include "gi/enumeration.h" +#include "gi/wrapperutils.h" +#include "cjs/jsapi-util.h" +#include "util/log.h" + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_define_enum_value(JSContext *context, JS::HandleObject in_object, @@ -83,7 +84,6 @@ gjs_define_enum_values(JSContext *context, JS::HandleObject in_object, GIEnumInfo *info) { - GType gtype; int i, n_values; /* Fill in enum values first, so we don't define the enum itself until we're @@ -102,47 +102,6 @@ gjs_define_enum_values(JSContext *context, return false; } } - - gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)info); - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, gtype)); - JS_DefineProperty(context, in_object, "$gtype", gtype_obj, JSPROP_PERMANENT); - - return true; -} - -bool -gjs_define_enum_static_methods(JSContext *context, - JS::HandleObject constructor, - GIEnumInfo *enum_info) -{ - int i, n_methods; - - n_methods = g_enum_info_get_n_methods(enum_info); - - for (i = 0; i < n_methods; i++) { - GIFunctionInfo *meth_info; - GIFunctionInfoFlags flags; - - meth_info = g_enum_info_get_method(enum_info, i); - flags = g_function_info_get_flags(meth_info); - - g_warn_if_fail(!(flags & GI_FUNCTION_IS_METHOD)); - /* Anything that isn't a method we put on the prototype of the - * constructor. This includes introspection - * methods, as well as the forthcoming "static methods" - * support. We may want to change this to use - * GI_FUNCTION_IS_CONSTRUCTOR and GI_FUNCTION_IS_STATIC or the - * like in the near future. - */ - if (!(flags & GI_FUNCTION_IS_METHOD)) { - gjs_define_function(context, constructor, G_TYPE_NONE, - (GICallableInfo *)meth_info); - } - - g_base_info_unref((GIBaseInfo*) meth_info); - } - return true; } @@ -166,14 +125,18 @@ gjs_define_enumeration(JSContext *context, JS::RootedObject enum_obj(context, JS_NewPlainObject(context)); if (!enum_obj) { - g_error("Could not create enumeration %s.%s", - g_base_info_get_namespace( (GIBaseInfo*) info), - enum_name); + gjs_throw(context, "Could not create enumeration %s.%s", + g_base_info_get_namespace(info), enum_name); + return false; } - if (!gjs_define_enum_values(context, enum_obj, info)) + GType gtype = g_registered_type_info_get_g_type(info); + + if (!gjs_define_enum_values(context, enum_obj, info) || + !gjs_define_static_methods(context, enum_obj, gtype, + info) || + !gjs_wrapper_define_gtype_prop(context, enum_obj, gtype)) return false; - gjs_define_enum_static_methods (context, enum_obj, info); gjs_debug(GJS_DEBUG_GENUM, "Defining %s.%s as %p", diff --git a/gi/enumeration.h b/gi/enumeration.h index 64e7990..54d2159 100644 --- a/gi/enumeration.h +++ b/gi/enumeration.h @@ -21,30 +21,25 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_ENUMERATION_H__ -#define __GJS_ENUMERATION_H__ +#ifndef GI_ENUMERATION_H_ +#define GI_ENUMERATION_H_ -#include -#include - -#include "cjs/jsapi-util.h" +#include #include -G_BEGIN_DECLS +#include + +#include "cjs/macros.h" +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_enum_values(JSContext *context, JS::HandleObject in_object, GIEnumInfo *info); -bool gjs_define_enum_static_methods(JSContext *context, - JS::HandleObject constructor, - GIEnumInfo *enum_info); - +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_enumeration(JSContext *context, JS::HandleObject in_object, GIEnumInfo *info); -G_END_DECLS - -#endif /* __GJS_ENUMERATION_H__ */ +#endif // GI_ENUMERATION_H_ diff --git a/gi/foreign.cpp b/gi/foreign.cpp index 3d2f2c5..99d4201 100644 --- a/gi/foreign.cpp +++ b/gi/foreign.cpp @@ -23,12 +23,17 @@ #include -#include +#include // for strcmp + #include +#include + +#include -#include "arg.h" -#include "foreign.h" -#include "cjs/jsapi-wrapper.h" +#include "gi/arg.h" +#include "gi/foreign.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" static struct { char *gi_namespace; @@ -41,9 +46,7 @@ static struct { static GHashTable* foreign_structs_table = NULL; -static GHashTable* -get_foreign_structs(void) -{ +[[nodiscard]] static GHashTable* get_foreign_structs() { // FIXME: look into hasing on GITypeInfo instead. if (!foreign_structs_table) { foreign_structs_table = g_hash_table_new_full(g_str_hash, g_str_equal, @@ -54,10 +57,8 @@ get_foreign_structs(void) return foreign_structs_table; } -static bool -gjs_foreign_load_foreign_module(JSContext *context, - const gchar *gi_namespace) -{ +[[nodiscard]] static bool gjs_foreign_load_foreign_module( + JSContext* context, const char* gi_namespace) { int i; for (i = 0; foreign_modules[i].gi_namespace; ++i) { @@ -73,8 +74,8 @@ gjs_foreign_load_foreign_module(JSContext *context, // and only execute this statement if isn't script = g_strdup_printf("imports.%s;", gi_namespace); JS::RootedValue retval(context); - if (!gjs_eval_with_scope(context, nullptr, script, strlen(script), - "", &retval)) { + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + if (!gjs->eval_with_scope(nullptr, script, -1, "", &retval)) { g_critical("ERROR importing foreign module %s\n", gi_namespace); g_free(script); return false; @@ -87,26 +88,20 @@ gjs_foreign_load_foreign_module(JSContext *context, return false; } -bool -gjs_struct_foreign_register(const char *gi_namespace, - const char *type_name, - GjsForeignInfo *info) -{ +void gjs_struct_foreign_register(const char* gi_namespace, + const char* type_name, GjsForeignInfo* info) { char *canonical_name; - g_return_val_if_fail(info != NULL, false); - g_return_val_if_fail(info->to_func != NULL, false); - g_return_val_if_fail(info->from_func != NULL, false); + g_return_if_fail(info); + g_return_if_fail(info->to_func); + g_return_if_fail(info->from_func); canonical_name = g_strdup_printf("%s.%s", gi_namespace, type_name); g_hash_table_insert(get_foreign_structs(), canonical_name, info); - return true; } -static GjsForeignInfo * -gjs_struct_foreign_lookup(JSContext *context, - GIBaseInfo *interface_info) -{ +[[nodiscard]] static GjsForeignInfo* gjs_struct_foreign_lookup( + JSContext* context, GIBaseInfo* interface_info) { GjsForeignInfo *retval = NULL; GHashTable *hash_table; char *key; diff --git a/gi/foreign.h b/gi/foreign.h index 09e9935..d1dbb32 100644 --- a/gi/foreign.h +++ b/gi/foreign.h @@ -21,13 +21,19 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_OVERRIDE_H__ -#define __GJS_OVERRIDE_H__ +#ifndef GI_FOREIGN_H_ +#define GI_FOREIGN_H_ + +#include -#include #include -#include -#include "arg.h" + +#include +#include +#include + +#include "gi/arg.h" +#include "cjs/macros.h" typedef bool (*GjsArgOverrideToGArgumentFunc) (JSContext *context, JS::Value value, @@ -51,10 +57,10 @@ typedef struct { GjsArgOverrideReleaseGArgumentFunc release_func; } GjsForeignInfo; -bool gjs_struct_foreign_register (const char *gi_namespace, - const char *type_name, - GjsForeignInfo *info); +void gjs_struct_foreign_register(const char* gi_namespace, + const char* type_name, GjsForeignInfo* info); +GJS_JSAPI_RETURN_CONVENTION bool gjs_struct_foreign_convert_to_g_argument (JSContext *context, JS::Value value, GIBaseInfo *interface_info, @@ -63,14 +69,16 @@ bool gjs_struct_foreign_convert_to_g_argument (JSContext *context, GITransfer transfer, bool may_be_null, GArgument *arg); +GJS_JSAPI_RETURN_CONVENTION bool gjs_struct_foreign_convert_from_g_argument(JSContext *context, JS::MutableHandleValue value_p, GIBaseInfo *interface_info, GIArgument *arg); +GJS_JSAPI_RETURN_CONVENTION bool gjs_struct_foreign_release_g_argument (JSContext *context, GITransfer transfer, GIBaseInfo *interface_info, GArgument *arg); -#endif /* __GJS_OVERRIDE_H__ */ +#endif // GI_FOREIGN_H_ diff --git a/gi/function.cpp b/gi/function.cpp index d0584a1..f7d7166 100644 --- a/gi/function.cpp +++ b/gi/function.cpp @@ -23,27 +23,46 @@ #include -#include "function.h" -#include "arg.h" -#include "object.h" -#include "fundamental.h" -#include "boxed.h" -#include "union.h" -#include "gerror.h" -#include "closure.h" -#include "gtype.h" -#include "param.h" -#include "cjs/context-private.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" +#include +#include // for exit +#include // for strcmp, memset, size_t -#include +#include +#include +#include #include - -#include -#include +#include +#include +#include + +#include +#include +#include +#include +#include // for JSPROP_PERMANENT +#include +#include // for GetRealmFunctionPrototype +#include +#include +#include +#include +#include // for HandleValueArray, JS_GetElement + +#include "gi/arg-cache.h" +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/closure.h" +#include "gi/function.h" +#include "gi/gerror.h" +#include "gi/object.h" +#include "gi/utils-inl.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" /* We use guint8 for arguments; functions can't * have more than this. @@ -51,11 +70,11 @@ #define GJS_ARG_INDEX_INVALID G_MAXUINT8 typedef struct { - GIFunctionInfo *info; + GICallableInfo* info; - GjsParamType *param_types; + GjsArgumentCache* arguments; - guint8 expected_js_argc; + uint8_t js_in_argc; guint8 js_out_argc; GIFunctionInvoker invoker; } Function; @@ -83,45 +102,70 @@ 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_clear_pointer(&trampoline->js_function, g_closure_unref); + if (trampoline->info && trampoline->closure) + g_callable_info_free_closure(trampoline->info, trampoline->closure); + g_clear_pointer(&trampoline->info, g_base_info_unref); g_free (trampoline->param_types); g_slice_free(GjsCallbackTrampoline, trampoline); } } +template +static inline std::enable_if_t && std::is_signed_v> +set_ffi_arg(void* result, GIArgument* value) { + *static_cast(result) = gjs_arg_get(value); +} + +template +static inline std::enable_if_t || + std::is_unsigned_v> +set_ffi_arg(void* result, GIArgument* value) { + *static_cast(result) = gjs_arg_get(value); +} + +template +static inline std::enable_if_t> set_ffi_arg( + void* result, GIArgument* value) { + *static_cast(result) = + gjs_pointer_to_int(gjs_arg_get(value)); +} + static void set_return_ffi_arg_from_giargument (GITypeInfo *ret_type, void *result, GIArgument *return_value) { + // Be consistent with gjs_value_to_g_argument() switch (g_type_info_get_tag(ret_type)) { case GI_TYPE_TAG_VOID: g_assert_not_reached(); case GI_TYPE_TAG_INT8: - *(ffi_sarg *) result = return_value->v_int8; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT8: - *(ffi_arg *) result = return_value->v_uint8; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT16: - *(ffi_sarg *) result = return_value->v_int16; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT16: - *(ffi_arg *) result = return_value->v_uint16; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT32: - *(ffi_sarg *) result = return_value->v_int32; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_UINT32: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_BOOLEAN: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_UNICHAR: - *(ffi_arg *) result = return_value->v_uint32; - + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INT64: - *(ffi_sarg *) result = return_value->v_int64; + set_ffi_arg(result, return_value); break; case GI_TYPE_TAG_INTERFACE: { @@ -133,27 +177,37 @@ set_return_ffi_arg_from_giargument (GITypeInfo *ret_type, if (interface_type == GI_INFO_TYPE_ENUM || interface_type == GI_INFO_TYPE_FLAGS) - *(ffi_sarg *) result = return_value->v_long; + set_ffi_arg(result, return_value); else - *(ffi_arg *) result = (ffi_arg) return_value->v_pointer; + set_ffi_arg(result, return_value); g_base_info_unref(interface_info); } break; case GI_TYPE_TAG_UINT64: - /* Other primitive and pointer types need to squeeze into 64-bit ffi_arg too */ + // Other primitive types need to squeeze into 64-bit ffi_arg too + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_FLOAT: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_DOUBLE: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_GTYPE: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_UTF8: case GI_TYPE_TAG_FILENAME: + set_ffi_arg(result, return_value); + break; case GI_TYPE_TAG_ARRAY: case GI_TYPE_TAG_GLIST: case GI_TYPE_TAG_GSLIST: case GI_TYPE_TAG_GHASH: case GI_TYPE_TAG_ERROR: default: - *(ffi_arg *) result = (ffi_arg) return_value->v_uint64; + set_ffi_arg(result, return_value); break; } } @@ -171,7 +225,6 @@ warn_about_illegal_js_callback(const GjsCallbackTrampoline *trampoline, g_critical("The offending callback was %s()%s.", name, trampoline->is_vfunc ? ", a vfunc" : ""); } - gjs_dumpstack(); return; } @@ -182,18 +235,13 @@ warn_about_illegal_js_callback(const GjsCallbackTrampoline *trampoline, * In other words, everything we need to call the JS function and * getting the return value back. */ -static void -gjs_callback_closure(ffi_cif *cif, - void *result, - void **ffi_args, - void *data) -{ +static void gjs_callback_closure(ffi_cif* cif [[maybe_unused]], void* result, + void** ffi_args, void* data) { JSContext *context; GjsCallbackTrampoline *trampoline; int i, n_args, n_jsargs, n_outargs, c_args_offset = 0; GITypeInfo ret_type; bool success = false; - bool ret_type_is_void; auto args = reinterpret_cast(ffi_args); trampoline = (GjsCallbackTrampoline *) data; @@ -204,30 +252,31 @@ gjs_callback_closure(ffi_cif *cif, warn_about_illegal_js_callback(trampoline, "during shutdown", "destroying a Clutter actor or GTK widget with ::destroy signal " "connected, or using the destroy(), dispose(), or remove() vfuncs"); + gjs_dumpstack(); gjs_callback_trampoline_unref(trampoline); return; } context = gjs_closure_get_context(trampoline->js_function); - if (G_UNLIKELY(_gjs_context_is_sweeping(context))) { + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + if (G_UNLIKELY(gjs->sweeping())) { warn_about_illegal_js_callback(trampoline, "during garbage collection", "destroying a Clutter actor or GTK widget with ::destroy signal " "connected, or using the destroy(), dispose(), or remove() vfuncs"); + gjs_dumpstack(); gjs_callback_trampoline_unref(trampoline); return; } - auto gjs_cx = static_cast(JS_GetContextPrivate(context)); - if (G_UNLIKELY (!_gjs_context_get_is_owner_thread(gjs_cx))) { + if (G_UNLIKELY(!gjs->is_owner_thread())) { warn_about_illegal_js_callback(trampoline, "on a different thread", "an API not intended to be used in JS"); gjs_callback_trampoline_unref(trampoline); return; } - JS_BeginRequest(context); - JSAutoCompartment ac(context, - gjs_closure_get_callable(trampoline->js_function)); + JSAutoRealm ar(context, JS_GetFunctionObject(gjs_closure_get_callable( + trampoline->js_function))); bool can_throw_gerror = g_callable_info_can_throw_gerror(trampoline->info); n_args = g_callable_info_get_n_args(trampoline->info); @@ -236,8 +285,14 @@ gjs_callback_closure(ffi_cif *cif, 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); + GObject* gobj = G_OBJECT(gjs_arg_get(args[0])); + if (gobj) { + this_object = ObjectInstance::wrapper_from_gobject(context, gobj); + if (!this_object) { + gjs_log_exception(context); + return; + } + } /* "this" is not included in the GI signature, but is in the C (and * FFI) signature */ @@ -245,13 +300,16 @@ gjs_callback_closure(ffi_cif *cif, } n_outargs = 0; - JS::AutoValueVector jsargs(context); + JS::RootedValueVector jsargs(context); if (!jsargs.reserve(n_args)) g_error("Unable to reserve space for vector"); JS::RootedValue rval(context); + g_callable_info_load_return_type(trampoline->info, &ret_type); + bool ret_type_is_void = g_type_info_get_tag (&ret_type) == GI_TYPE_TAG_VOID; + for (i = 0, n_jsargs = 0; i < n_args; i++) { GIArgInfo arg_info; GITypeInfo type_info; @@ -300,19 +358,24 @@ gjs_callback_closure(ffi_cif *cif, goto out; break; } - case PARAM_NORMAL: + case PARAM_NORMAL: { if (!jsargs.growBy(1)) g_error("Unable to grow vector"); + GIArgument* arg = args[i + c_args_offset]; + if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_INOUT) + arg = *reinterpret_cast(arg); + if (!gjs_value_from_g_argument(context, jsargs[n_jsargs++], - &type_info, - args[i + c_args_offset], - false)) + &type_info, arg, false)) goto out; break; + } case PARAM_CALLBACK: /* Callbacks that accept another callback as a parameter are not * supported, see gjs_callback_trampoline_new() */ + case PARAM_UNKNOWN: + // PARAM_UNKNOWN is currently not ever set on a callback's args. default: g_assert_not_reached(); } @@ -322,9 +385,6 @@ gjs_callback_closure(ffi_cif *cif, 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; - if (n_outargs == 0 && ret_type_is_void) { /* void return value, no out args, nothing to do */ } else if (n_outargs == 0) { @@ -352,25 +412,32 @@ gjs_callback_closure(ffi_cif *cif, * be a single return value. */ for (i = 0; i < n_args; i++) { GIArgInfo arg_info; - GITypeInfo type_info; g_callable_info_load_arg(trampoline->info, i, &arg_info); if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_IN) continue; - g_arg_info_load_type(&arg_info, &type_info); - if (!gjs_value_to_g_argument(context, - rval, - &type_info, - "callback", - GJS_ARGUMENT_ARGUMENT, - GI_TRANSFER_NOTHING, - true, - *(GIArgument **)args[i + c_args_offset])) + if (!gjs_value_to_arg(context, rval, &arg_info, + *reinterpret_cast(args[i + c_args_offset]))) goto out; break; } } else { + bool is_array = rval.isObject(); + if (!JS::IsArrayObject(context, rval, &is_array)) + goto out; + + if (!is_array) { + JSFunction* fn = gjs_closure_get_callable(trampoline->js_function); + gjs_throw(context, + "Function %s (%s.%s) returned unexpected value, " + "expecting an Array", + gjs_debug_string(JS_GetFunctionDisplayId(fn)).c_str(), + g_base_info_get_namespace(trampoline->info), + g_base_info_get_name(trampoline->info)); + goto out; + } + JS::RootedValue elem(context); JS::RootedObject out_array(context, rval.toObjectOrNull()); gsize elem_idx = 0; @@ -379,18 +446,14 @@ gjs_callback_closure(ffi_cif *cif, if (!ret_type_is_void) { GIArgument argument; + GITransfer transfer = g_callable_info_get_caller_owns(trampoline->info); if (!JS_GetElement(context, out_array, elem_idx, &elem)) goto out; - if (!gjs_value_to_g_argument(context, - elem, - &ret_type, - "callback", - GJS_ARGUMENT_ARGUMENT, - GI_TRANSFER_NOTHING, - true, - &argument)) + if (!gjs_value_to_g_argument(context, elem, &ret_type, "callback", + GJS_ARGUMENT_RETURN_VALUE, transfer, + true, &argument)) goto out; set_return_ffi_arg_from_giargument(&ret_type, @@ -402,23 +465,15 @@ gjs_callback_closure(ffi_cif *cif, for (i = 0; i < n_args; i++) { GIArgInfo arg_info; - GITypeInfo type_info; g_callable_info_load_arg(trampoline->info, i, &arg_info); if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_IN) continue; - g_arg_info_load_type(&arg_info, &type_info); if (!JS_GetElement(context, out_array, elem_idx, &elem)) goto out; - if (!gjs_value_to_g_argument(context, - elem, - &type_info, - "callback", - GJS_ARGUMENT_ARGUMENT, - GI_TRANSFER_NOTHING, - true, - *(GIArgument **)args[i + c_args_offset])) + if (!gjs_value_to_arg(context, elem, &arg_info, + *(GIArgument **)args[i + c_args_offset])) goto out; elem_idx++; @@ -434,19 +489,25 @@ out: * main loop, or maybe not, but there's no way to tell, so we have * to exit here instead of propagating the exception back to the * original calling JS code. */ - auto gcx = static_cast(JS_GetContextPrivate(context)); uint8_t code; - if (_gjs_context_should_exit(gcx, &code)) + if (gjs->should_exit(&code)) exit(code); /* Some other uncatchable exception, e.g. out of memory */ - g_error("Function %s terminated with uncatchable exception", + JSFunction* fn = gjs_closure_get_callable(trampoline->js_function); + g_error("Function %s (%s.%s) terminated with uncatchable exception", + gjs_debug_string(JS_GetFunctionDisplayId(fn)).c_str(), + g_base_info_get_namespace(trampoline->info), g_base_info_get_name(trampoline->info)); } /* Fill in the result with some hopefully neutral value */ - g_callable_info_load_return_type(trampoline->info, &ret_type); - gjs_g_argument_init_default (context, &ret_type, (GArgument *) result); + if (!ret_type_is_void) { + GIArgument argument = {}; + g_callable_info_load_return_type(trampoline->info, &ret_type); + gjs_gi_argument_init_default(&ret_type, &argument); + set_return_ffi_arg_from_giargument(&ret_type, result, &argument); + } /* If the callback has a GError** argument and invoking the closure * returned an error, try to make a GError from it */ @@ -458,14 +519,14 @@ out: /* 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); + auto* gerror = gjs_arg_get(error_argument); g_propagate_error(gerror, local_error); JS_ClearPendingException(context); /* don't log */ } } else if (!rval.isUndefined()) { JS_SetPendingException(context, rval); } - gjs_log_exception(context); + gjs_log_exception_uncaught(context); } if (trampoline->scope == GI_SCOPE_TYPE_ASYNC) { @@ -473,39 +534,17 @@ out: } gjs_callback_trampoline_unref(trampoline); - gjs_schedule_gc_if_needed(context); - - JS_EndRequest(context); + gjs->schedule_gc_if_needed(); } -/* The global entry point for any invocations of GDestroyNotify; - * look up the callback through the user_data and then free it. - */ -static void -gjs_destroy_notify_callback(gpointer data) -{ - GjsCallbackTrampoline *trampoline = (GjsCallbackTrampoline *) data; - - g_assert(trampoline); - gjs_callback_trampoline_unref(trampoline); -} - -GjsCallbackTrampoline* -gjs_callback_trampoline_new(JSContext *context, - JS::HandleValue function, - GICallableInfo *callable_info, - GIScopeType scope, - JS::HandleObject scope_object, - bool is_vfunc) -{ +GjsCallbackTrampoline* gjs_callback_trampoline_new( + JSContext* context, JS::HandleFunction function, + GICallableInfo* callable_info, GIScopeType scope, bool has_scope_object, + bool is_vfunc) { GjsCallbackTrampoline *trampoline; int n_args, i; - if (function.isNull()) { - return NULL; - } - - g_assert(JS_TypeOfValue(context, function) == JSTYPE_FUNCTION); + g_assert(function); trampoline = g_slice_new(GjsCallbackTrampoline); new (trampoline) GjsCallbackTrampoline(); @@ -513,19 +552,6 @@ gjs_callback_trampoline_new(JSContext *context, trampoline->info = callable_info; g_base_info_ref((GIBaseInfo*)trampoline->info); - /* 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); trampoline->param_types = g_new0(GjsParamType, n_args); @@ -557,8 +583,13 @@ gjs_callback_trampoline_new(JSContext *context, interface_info = g_type_info_get_interface(&type_info); interface_type = g_base_info_get_type(interface_info); if (interface_type == GI_INFO_TYPE_CALLBACK) { - gjs_throw(context, "Callback accepts another callback as a parameter. This is not supported"); + gjs_throw(context, + "%s %s accepts another callback as a parameter. This " + "is not supported", + is_vfunc ? "VFunc" : "Callback", + g_base_info_get_name(callable_info)); g_base_info_unref(interface_info); + gjs_callback_trampoline_unref(trampoline); return NULL; } g_base_info_unref(interface_info); @@ -571,7 +602,12 @@ gjs_callback_trampoline_new(JSContext *context, g_callable_info_load_arg(trampoline->info, array_length_pos, &length_arg_info); if (g_arg_info_get_direction(&length_arg_info) != direction) { - gjs_throw(context, "Callback has an array with different-direction length arg, not supported"); + gjs_throw(context, + "%s %s has an array with different-direction " + "length argument. This is not supported", + is_vfunc ? "VFunc" : "Callback", + g_base_info_get_name(callable_info)); + gjs_callback_trampoline_unref(trampoline); return NULL; } @@ -585,172 +621,30 @@ gjs_callback_trampoline_new(JSContext *context, trampoline->closure = g_callable_info_prepare_closure(callable_info, &trampoline->cif, gjs_callback_closure, trampoline); + // The rule is: + // - notify callbacks in GObject methods are traced from the scope object + // - async and call callbacks, and other notify callbacks, are rooted + // - vfuncs are traced from the GObject prototype + bool should_root = scope != GI_SCOPE_TYPE_NOTIFIED || !has_scope_object; + trampoline->js_function = gjs_closure_new( + context, function, g_base_info_get_name(callable_info), should_root); + trampoline->scope = scope; trampoline->is_vfunc = is_vfunc; return trampoline; } -/* an helper function to retrieve array lengths from a GArgument - (letting the compiler generate good instructions in case of - big endian machines) */ -static unsigned long -get_length_from_arg (GArgument *arg, GITypeTag tag) -{ - if (tag == GI_TYPE_TAG_INT8) - return arg->v_int8; - if (tag == GI_TYPE_TAG_UINT8) - return arg->v_uint8; - if (tag == GI_TYPE_TAG_INT16) - return arg->v_int16; - if (tag == GI_TYPE_TAG_UINT16) - return arg->v_uint16; - if (tag == GI_TYPE_TAG_INT32) - return arg->v_int32; - if (tag == GI_TYPE_TAG_UINT32) - return arg->v_uint32; - if (tag == GI_TYPE_TAG_INT64) - return arg->v_int64; - if (tag == GI_TYPE_TAG_UINT64) - return arg->v_uint64; - g_assert_not_reached(); -} - -static bool -gjs_fill_method_instance(JSContext *context, - JS::HandleObject obj, - Function *function, - 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)) { - if (!gjs_typecheck_gerror(context, obj, true)) - return false; - - out_arg->v_pointer = gjs_gerror_from_error(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) - out_arg->v_pointer = g_error_copy ((GError*) out_arg->v_pointer); - } else if (type == GI_INFO_TYPE_STRUCT && - g_struct_info_is_gtype_struct((GIStructInfo*) container)) { - /* And so do GType structures */ - GType actual_gtype; - gpointer klass; - - actual_gtype = gjs_gtype_get_actual_gtype(context, obj); - - if (actual_gtype == G_TYPE_NONE) { - gjs_throw(context, "Invalid GType class passed for instance parameter"); - return false; - } - - /* We use peek here to simplify reference counting (we just ignore - transfer annotation, as GType classes are never really freed) - We know that the GType class is referenced at least once when - the JS constructor is initialized. - */ - - if (g_type_is_a(actual_gtype, G_TYPE_INTERFACE)) - klass = g_type_default_interface_peek(actual_gtype); - else - klass = g_type_class_peek(actual_gtype); - - out_arg->v_pointer = klass; - } else { - if (!gjs_typecheck_boxed(context, obj, container, gtype, true)) - return false; - - out_arg->v_pointer = gjs_c_struct_from_boxed(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) { - if (gtype != G_TYPE_NONE) - out_arg->v_pointer = g_boxed_copy (gtype, out_arg->v_pointer); - else { - gjs_throw (context, "Cannot transfer ownership of instance argument for non boxed structure"); - return false; - } - } - } - - } else if (type == GI_INFO_TYPE_UNION) { - if (!gjs_typecheck_union(context, obj, container, gtype, true)) - return false; - - out_arg->v_pointer = gjs_c_union_from_union(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) - out_arg->v_pointer = g_boxed_copy (gtype, out_arg->v_pointer); - - } else if (type == GI_INFO_TYPE_OBJECT || type == GI_INFO_TYPE_INTERFACE) { - if (g_type_is_a(gtype, G_TYPE_OBJECT)) { - 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)) { - if (!gjs_typecheck_param(context, obj, G_TYPE_PARAM, true)) - return false; - out_arg->v_pointer = gjs_g_param_from_param(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) - g_param_spec_ref ((GParamSpec*) out_arg->v_pointer); - } else if (G_TYPE_IS_INTERFACE(gtype)) { - if (gjs_typecheck_is_object(context, obj, false)) { - 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 (!gjs_typecheck_fundamental(context, obj, gtype, true)) - return false; - out_arg->v_pointer = gjs_g_fundamental_from_object(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) - gjs_fundamental_ref (context, out_arg->v_pointer); - } - } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { - if (!gjs_typecheck_fundamental(context, obj, gtype, true)) - return false; - out_arg->v_pointer = gjs_g_fundamental_from_object(context, obj); - if (transfer == GI_TRANSFER_EVERYTHING) - gjs_fundamental_ref (context, out_arg->v_pointer); - } else { - 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)); - return false; - } - - } else { - g_assert_not_reached(); - } - - return true; -} - /* Intended for error messages. Return value must be freed */ -static char * -format_function_name(Function *function, - bool is_method) -{ - auto baseinfo = static_cast(function->info); - if (is_method) - return g_strdup_printf("method %s.%s.%s", - g_base_info_get_namespace(baseinfo), - g_base_info_get_name(g_base_info_get_container(baseinfo)), - g_base_info_get_name(baseinfo)); +[[nodiscard]] static char* format_function_name(Function* function) { + if (g_callable_info_is_method(function->info)) + return g_strdup_printf( + "method %s.%s.%s", g_base_info_get_namespace(function->info), + g_base_info_get_name(g_base_info_get_container(function->info)), + g_base_info_get_name(function->info)); return g_strdup_printf("function %s.%s", - g_base_info_get_namespace(baseinfo), - g_base_info_get_name(baseinfo)); + g_base_info_get_namespace(function->info), + g_base_info_get_name(function->info)); } static void @@ -766,54 +660,74 @@ complete_async_calls(void) } } -/* - * 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 - * you can decide to keep the return values in #GArgument format by - * providing a @r_value argument. - */ -static bool -gjs_invoke_c_function(JSContext *context, - Function *function, - JS::HandleObject obj, /* "this" object */ - const JS::HandleValueArray& args, - mozilla::Maybe js_rval, - GIArgument *r_value) -{ - /* These first four are arrays which hold argument pointers. - * @in_arg_cvalues: C values which are passed on input (in or inout) - * @out_arg_cvalues: C values which are returned as arguments (out or inout) - * @inout_original_arg_cvalues: For the special case of (inout) args, we need to - * keep track of the original values we passed into the function, in case we - * need to free it. - * @ffi_arg_pointers: For passing data to FFI, we need to create another layer - * of indirection; this array is a pointer to an element in in_arg_cvalues - * or out_arg_cvalues. - * @return_value: The actual return value of the C function, i.e. not an (out) param - */ - GArgument *in_arg_cvalues; - GArgument *out_arg_cvalues; - GArgument *inout_original_arg_cvalues; - gpointer *ffi_arg_pointers; +static void* get_return_ffi_pointer_from_giargument( + GjsArgumentCache* return_arg, GIFFIReturnValue* return_value) { + // This should be the inverse of gi_type_info_extract_ffi_return_value(). + if (return_arg->skip_out) + return nullptr; + + // FIXME: Note that v_long and v_ulong don't have type-safe template + // overloads yet, and I don't understand why they won't compile + switch (g_type_info_get_tag(&return_arg->type_info)) { + case GI_TYPE_TAG_INT8: + case GI_TYPE_TAG_INT16: + case GI_TYPE_TAG_INT32: + return &return_value->v_long; + case GI_TYPE_TAG_UINT8: + case GI_TYPE_TAG_UINT16: + case GI_TYPE_TAG_UINT32: + case GI_TYPE_TAG_BOOLEAN: + case GI_TYPE_TAG_UNICHAR: + return &return_value->v_ulong; + case GI_TYPE_TAG_INT64: + return &gjs_arg_member(return_value); + case GI_TYPE_TAG_UINT64: + return &gjs_arg_member(return_value); + case GI_TYPE_TAG_FLOAT: + return &gjs_arg_member(return_value); + case GI_TYPE_TAG_DOUBLE: + return &gjs_arg_member(return_value); + case GI_TYPE_TAG_INTERFACE: { + GjsAutoBaseInfo info = + g_type_info_get_interface(&return_arg->type_info); + + switch (g_base_info_get_type(info)) { + case GI_INFO_TYPE_ENUM: + case GI_INFO_TYPE_FLAGS: + return &return_value->v_long; + default: + return &gjs_arg_member(return_value); + } + break; + } + default: + return &gjs_arg_member(return_value); + } +} + +// This function can be called in two different ways. You can either use it to +// create JavaScript objects by calling it without @r_value, or you can decide +// to keep the return values in #GArgument format by providing a @r_value +// argument. +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_invoke_c_function(JSContext* context, Function* function, + const JS::CallArgs& args, + JS::HandleObject this_obj = nullptr, + GIArgument* r_value = nullptr) { + g_assert((args.isConstructing() || !this_obj) && + "If not a constructor, then pass the 'this' object via CallArgs"); + + void* return_value_p; // will point inside the return GIArgument union GIFFIReturnValue return_value; - gpointer return_value_p; /* Will point inside the union return_value */ - GArgument return_gargument; - guint8 processed_c_args = 0; - guint8 gi_argc, gi_arg_pos; - guint8 c_argc, c_arg_pos; - guint8 js_arg_pos; + int gi_argc, gi_arg_pos; bool can_throw_gerror; bool did_throw_gerror = false; GError *local_error = NULL; bool failed, postinvoke_release_failed; bool is_method; - bool is_object_method = false; - GITypeInfo return_info; - GITypeTag return_tag; - JS::AutoValueVector return_values(context); - guint8 next_rval = 0; /* index into return_values */ + JS::RootedValueVector return_values(context); /* Because we can't free a closure while we're in it, we defer * freeing until the next time a C function is invoked. What @@ -824,246 +738,146 @@ gjs_invoke_c_function(JSContext *context, is_method = g_callable_info_is_method(function->info); can_throw_gerror = g_callable_info_can_throw_gerror(function->info); - c_argc = function->invoker.cif.nargs; + unsigned ffi_argc = function->invoker.cif.nargs; gi_argc = g_callable_info_get_n_args( (GICallableInfo*) function->info); - - /* @c_argc is the number of arguments that the underlying C - * function takes. @gi_argc is the number of arguments the - * GICallableInfo describes (which does not include "this" or - * GError**). @function->expected_js_argc is the number of - * arguments we expect the JS function to take (which does not - * include PARAM_SKIPPED args). - * - * @args.length() is the number of arguments that were actually passed. - */ - if (args.length() > function->expected_js_argc) { - GjsAutoChar name = format_function_name(function, is_method); - JS_ReportWarningUTF8(context, "Too many arguments to %s: expected %d, " - "got %" G_GSIZE_FORMAT, name.get(), - function->expected_js_argc, args.length()); - } else if (args.length() < function->expected_js_argc) { - GjsAutoChar name = format_function_name(function, is_method); - gjs_throw(context, "Too few arguments to %s: " - "expected %d, got %" G_GSIZE_FORMAT, - name.get(), function->expected_js_argc, args.length()); + if (gi_argc > GjsArgumentCache::MAX_ARGS) { + GjsAutoChar name = format_function_name(function); + gjs_throw(context, "Function %s has too many arguments", name.get()); return false; } - g_callable_info_load_return_type( (GICallableInfo*) function->info, &return_info); - return_tag = g_type_info_get_tag(&return_info); - - in_arg_cvalues = g_newa(GArgument, c_argc); - ffi_arg_pointers = g_newa(gpointer, c_argc); - out_arg_cvalues = g_newa(GArgument, c_argc); - inout_original_arg_cvalues = g_newa(GArgument, c_argc); + // ffi_argc is the number of arguments that the underlying C function takes. + // gi_argc is the number of arguments the GICallableInfo describes (which + // does not include "this" or GError**). function->js_in_argc is the number + // of arguments we expect the JS function to take (which does not include + // PARAM_SKIPPED args). + // args.length() is the number of arguments that were actually passed. + if (args.length() > function->js_in_argc) { + GjsAutoChar name = format_function_name(function); + + if (!JS::WarnUTF8(context, + "Too many arguments to %s: expected %u, got %u", + name.get(), function->js_in_argc, args.length())) + return false; + } else if (args.length() < function->js_in_argc) { + GjsAutoChar name = format_function_name(function); - failed = false; - c_arg_pos = 0; /* index into in_arg_cvalues, etc */ - js_arg_pos = 0; /* index into argv */ + args.reportMoreArgsNeeded(context, name, function->js_in_argc, + args.length()); + return false; + } + // These arrays hold argument pointers. + // - state.in_cvalues: C values which are passed on input (in or inout) + // - state.out_cvalues: C values which are returned as arguments (out or + // inout) + // - state.inout_original_cvalues: For the special case of (inout) args, we + // need to keep track of the original values we passed into the function, + // in case we need to free it. + // - ffi_arg_pointers: For passing data to FFI, we need to create another + // layer of indirection; this array is a pointer to an element in + // state.in_cvalues or state.out_cvalues. + // - return_value: The actual return value of the C function, i.e. not an + // (out) param + // + // The 3 GIArgument arrays are indexed by the GI argument index, with the + // following exceptions: + // - [-1] is the return value (which can be nothing/garbage if the function + // function returns void) + // - [-2] is the instance parameter, if present + // ffi_arg_pointers, on the other hand, represents the actual C arguments, + // in the way ffi expects them. + // + // Use gi_arg_pos to index inside the GIArgument array. Use ffi_arg_pos to + // index inside ffi_arg_pointers. + GjsFunctionCallState state(context); if (is_method) { - 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; + state.in_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; + state.out_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; + state.inout_original_cvalues = g_newa(GIArgument, gi_argc + 2) + 2; + } else { + state.in_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; + state.out_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; + state.inout_original_cvalues = g_newa(GIArgument, gi_argc + 1) + 1; } - processed_c_args = c_arg_pos; - for (gi_arg_pos = 0; gi_arg_pos < gi_argc; gi_arg_pos++, c_arg_pos++) { - GIDirection direction; - GIArgInfo arg_info; - bool arg_removed = false; - - /* gjs_debug(GJS_DEBUG_GFUNCTION, "gi_arg_pos: %d c_arg_pos: %d js_arg_pos: %d", gi_arg_pos, c_arg_pos, js_arg_pos); */ - - g_callable_info_load_arg( (GICallableInfo*) function->info, gi_arg_pos, &arg_info); - direction = g_arg_info_get_direction(&arg_info); - - g_assert_cmpuint(c_arg_pos, <, c_argc); - ffi_arg_pointers[c_arg_pos] = &in_arg_cvalues[c_arg_pos]; - - if (direction == GI_DIRECTION_OUT) { - if (g_arg_info_is_caller_allocates(&arg_info)) { - GITypeTag type_tag; - GITypeInfo ainfo; + void** ffi_arg_pointers = g_newa(void*, ffi_argc); - g_arg_info_load_type(&arg_info, &ainfo); - type_tag = g_type_info_get_tag(&ainfo); - - if (type_tag == GI_TYPE_TAG_INTERFACE) { - GIBaseInfo* interface_info; - GIInfoType interface_type; - gsize size; - - interface_info = g_type_info_get_interface(&ainfo); - g_assert(interface_info != NULL); - - interface_type = g_base_info_get_type(interface_info); - - if (interface_type == GI_INFO_TYPE_STRUCT) { - size = g_struct_info_get_size((GIStructInfo*)interface_info); - } else if (interface_type == GI_INFO_TYPE_UNION) { - size = g_union_info_get_size((GIUnionInfo*)interface_info); - } else { - failed = true; - } - - g_base_info_unref((GIBaseInfo*)interface_info); - - if (!failed) { - in_arg_cvalues[c_arg_pos].v_pointer = g_slice_alloc0(size); - out_arg_cvalues[c_arg_pos].v_pointer = in_arg_cvalues[c_arg_pos].v_pointer; - } - } else { - failed = true; - } - if (failed) - gjs_throw(context, "Unsupported type %s for (out caller-allocates)", g_type_tag_to_string(type_tag)); - } else { - out_arg_cvalues[c_arg_pos].v_pointer = NULL; - in_arg_cvalues[c_arg_pos].v_pointer = &out_arg_cvalues[c_arg_pos]; - } - } else { - GArgument *in_value; - GITypeInfo ainfo; - GjsParamType param_type; - - g_arg_info_load_type(&arg_info, &ainfo); - - in_value = &in_arg_cvalues[c_arg_pos]; - - param_type = function->param_types[gi_arg_pos]; - - switch (param_type) { - case PARAM_CALLBACK: { - GICallableInfo *callable_info; - GIScopeType scope = g_arg_info_get_scope(&arg_info); - GjsCallbackTrampoline *trampoline; - ffi_closure *closure; - JS::HandleValue current_arg = args[js_arg_pos]; - - if (current_arg.isNull() && g_arg_info_may_be_null(&arg_info)) { - closure = NULL; - trampoline = NULL; - } else { - if (!(JS_TypeOfValue(context, current_arg) == JSTYPE_FUNCTION)) { - gjs_throw(context, "Error invoking %s.%s: Expected function for callback argument %s, got %s", - g_base_info_get_namespace( (GIBaseInfo*) function->info), - g_base_info_get_name( (GIBaseInfo*) function->info), - g_base_info_get_name( (GIBaseInfo*) &arg_info), - JS::InformalValueTypeName(current_arg)); - failed = true; - break; - } - - callable_info = (GICallableInfo*) g_type_info_get_interface(&ainfo); - trampoline = gjs_callback_trampoline_new(context, - current_arg, - callable_info, - scope, - is_object_method ? obj : nullptr, - false); - closure = trampoline->closure; - g_base_info_unref(callable_info); - } - - gint destroy_pos = g_arg_info_get_destroy(&arg_info); - gint closure_pos = g_arg_info_get_closure(&arg_info); - if (destroy_pos >= 0) { - gint c_pos = is_method ? destroy_pos + 1 : destroy_pos; - g_assert (function->param_types[destroy_pos] == PARAM_SKIPPED); - in_arg_cvalues[c_pos].v_pointer = trampoline ? (gpointer) gjs_destroy_notify_callback : NULL; - } - if (closure_pos >= 0) { - gint c_pos = is_method ? closure_pos + 1 : closure_pos; - g_assert (function->param_types[closure_pos] == PARAM_SKIPPED); - in_arg_cvalues[c_pos].v_pointer = trampoline; - } - - if (trampoline && scope != GI_SCOPE_TYPE_CALL) { - /* Add an extra reference that will be cleared when collecting - async calls, or when GDestroyNotify is called */ - gjs_callback_trampoline_ref(trampoline); - } - in_value->v_pointer = closure; - break; - } - case PARAM_SKIPPED: - arg_removed = true; - break; - case PARAM_ARRAY: { - GIArgInfo array_length_arg; - - gint array_length_pos = g_type_info_get_array_length(&ainfo); - gsize length; - - if (!gjs_value_to_explicit_array(context, args[js_arg_pos], - &arg_info, in_value, &length)) { - failed = true; - break; - } + failed = false; + unsigned ffi_arg_pos = 0; // index into ffi_arg_pointers + unsigned js_arg_pos = 0; // index into args - g_callable_info_load_arg(function->info, array_length_pos, &array_length_arg); + JS::RootedObject obj(context, this_obj); + if (!args.isConstructing() && !args.computeThis(context, &obj)) + return false; - array_length_pos += is_method ? 1 : 0; - JS::RootedValue v_length(context, JS::Int32Value(length)); - if (!gjs_value_to_arg(context, v_length, &array_length_arg, - in_arg_cvalues + array_length_pos)) { - failed = true; - break; - } - /* Also handle the INOUT for the length here */ - if (direction == GI_DIRECTION_INOUT) { - if (in_value->v_pointer == NULL) { - /* Special case where we were given JS null to - * also pass null for length, and not a - * pointer to an integer that derefs to 0. - */ - in_arg_cvalues[array_length_pos].v_pointer = NULL; - out_arg_cvalues[array_length_pos].v_pointer = NULL; - inout_original_arg_cvalues[array_length_pos].v_pointer = NULL; - } else { - out_arg_cvalues[array_length_pos] = inout_original_arg_cvalues[array_length_pos] = *(in_arg_cvalues + array_length_pos); - in_arg_cvalues[array_length_pos].v_pointer = &out_arg_cvalues[array_length_pos]; - } - } - break; - } - case PARAM_NORMAL: { - /* Ok, now just convert argument normally */ - g_assert_cmpuint(js_arg_pos, <, args.length()); - if (!gjs_value_to_arg(context, args[js_arg_pos], &arg_info, - in_value)) - failed = true; + if (is_method) { + GjsArgumentCache* cache = &function->arguments[-2]; + GIArgument* in_value = &state.in_cvalues[-2]; + JS::RootedValue in_js_value(context, JS::ObjectValue(*obj)); - break; - } + if (!cache->marshallers->in(context, cache, &state, in_value, + in_js_value)) + return false; - default: - ; - } + ffi_arg_pointers[ffi_arg_pos] = in_value; + ++ffi_arg_pos; + + // Callback lifetimes will be attached to the instance object if it is + // a GObject or GInterface + if (cache->contents.info) { + GType gtype = + g_registered_type_info_get_g_type(cache->contents.info); + if (g_type_is_a(gtype, G_TYPE_OBJECT) || + g_type_is_a(gtype, G_TYPE_INTERFACE)) + state.instance_object = obj; + } + } - if (direction == GI_DIRECTION_INOUT && !arg_removed && !failed) { - out_arg_cvalues[c_arg_pos] = inout_original_arg_cvalues[c_arg_pos] = in_arg_cvalues[c_arg_pos]; - in_arg_cvalues[c_arg_pos].v_pointer = &out_arg_cvalues[c_arg_pos]; - } + unsigned processed_c_args = ffi_arg_pos; + for (gi_arg_pos = 0; gi_arg_pos < gi_argc; gi_arg_pos++, ffi_arg_pos++) { + GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; + GIArgument* in_value = &state.in_cvalues[gi_arg_pos]; + + gjs_debug_marshal(GJS_DEBUG_GFUNCTION, + "Marshalling argument '%s' in, %d/%d GI args, %u/%u " + "C args, %u/%u JS args", + cache->arg_name, gi_arg_pos, gi_argc, ffi_arg_pos, + ffi_argc, js_arg_pos, args.length()); + + ffi_arg_pointers[ffi_arg_pos] = in_value; + + if (!cache->marshallers->in) { + gjs_throw(context, + "Error invoking %s.%s: impossible to determine what " + "to pass to the '%s' argument. It may be that the " + "function is unsupported, or there may be a bug in " + "its annotations.", + g_base_info_get_namespace(function->info), + g_base_info_get_name(function->info), cache->arg_name); + failed = true; + break; + } - if (failed) { - /* Exit from the loop */ - break; - } + JS::RootedValue js_in_arg(context); + if (js_arg_pos < args.length()) + js_in_arg = args[js_arg_pos]; - if (!failed && !arg_removed) - ++js_arg_pos; + if (!cache->marshallers->in(context, cache, &state, in_value, + js_in_arg)) { + failed = true; + break; } - if (failed) - break; + if (!cache->skip_in) + js_arg_pos++; processed_c_args++; } + // This pointer needs to exist on the stack across the ffi_call() call + GError** errorp = &local_error; + /* Did argument conversion fail? In that case, skip invocation and jump to release * processing. */ if (failed) { @@ -1072,28 +886,21 @@ gjs_invoke_c_function(JSContext *context, } if (can_throw_gerror) { - g_assert_cmpuint(c_arg_pos, <, c_argc); - in_arg_cvalues[c_arg_pos].v_pointer = &local_error; - ffi_arg_pointers[c_arg_pos] = &(in_arg_cvalues[c_arg_pos]); - c_arg_pos++; + g_assert(ffi_arg_pos < ffi_argc && "GError** argument number mismatch"); + ffi_arg_pointers[ffi_arg_pos] = &errorp; + ffi_arg_pos++; /* don't update processed_c_args as we deal with local_error * separately */ } - g_assert_cmpuint(c_arg_pos, ==, c_argc); + g_assert_cmpuint(ffi_arg_pos, ==, ffi_argc); g_assert_cmpuint(gi_arg_pos, ==, gi_argc); - /* See comment for GjsFFIReturnValue above */ - if (return_tag == GI_TYPE_TAG_FLOAT) - return_value_p = &return_value.v_float; - else if (return_tag == GI_TYPE_TAG_DOUBLE) - return_value_p = &return_value.v_double; - else if (return_tag == GI_TYPE_TAG_INT64 || return_tag == GI_TYPE_TAG_UINT64) - return_value_p = &return_value.v_uint64; - else - return_value_p = &return_value.v_long; - ffi_call(&(function->invoker.cif), FFI_FN(function->invoker.native_address), return_value_p, ffi_arg_pointers); + return_value_p = get_return_ffi_pointer_from_giargument( + &function->arguments[-1], &return_value); + ffi_call(&(function->invoker.cif), FFI_FN(function->invoker.native_address), + return_value_p, ffi_arg_pointers); /* Return value and out arguments are valid only if invocation doesn't * return error. In arguments need to be released always. @@ -1104,286 +911,119 @@ gjs_invoke_c_function(JSContext *context, did_throw_gerror = false; } - if (js_rval) - js_rval.ref().setUndefined(); - - /* Only process return values if the function didn't throw */ - if (function->js_out_argc > 0 && !did_throw_gerror) { - for (size_t i = 0; i < function->js_out_argc; i++) - if (!return_values.append(JS::UndefinedValue())) - g_error("Unable to append to vector"); - - if (return_tag != GI_TYPE_TAG_VOID) { - GITransfer transfer = g_callable_info_get_caller_owns((GICallableInfo*) function->info); - bool arg_failed = false; - gint array_length_pos; + if (!r_value) + args.rval().setUndefined(); - g_assert_cmpuint(next_rval, <, function->js_out_argc); - - gi_type_info_extract_ffi_return_value(&return_info, &return_value, &return_gargument); + if (!function->arguments[-1].skip_out) { + gi_type_info_extract_ffi_return_value( + &function->arguments[-1].type_info, &return_value, + &state.out_cvalues[-1]); + } - array_length_pos = g_type_info_get_array_length(&return_info); - if (array_length_pos >= 0) { - GIArgInfo array_length_arg; - GITypeInfo arg_type_info; - JS::RootedValue length(context); + // Process out arguments and return values. This loop is skipped if we fail + // the type conversion above, or if did_throw_gerror is true. + js_arg_pos = 0; + for (gi_arg_pos = -1; gi_arg_pos < gi_argc; gi_arg_pos++) { + GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; + GIArgument* out_value = &state.out_cvalues[gi_arg_pos]; + + gjs_debug_marshal(GJS_DEBUG_GFUNCTION, + "Marshalling argument '%s' out, %d/%d GI args", + cache->arg_name, gi_arg_pos, gi_argc); + + JS::RootedValue js_out_arg(context); + if (!r_value) { + if (!cache->marshallers->out(context, cache, &state, out_value, + &js_out_arg)) { + failed = true; + break; + } + } - g_callable_info_load_arg(function->info, array_length_pos, &array_length_arg); - g_arg_info_load_type(&array_length_arg, &arg_type_info); - array_length_pos += is_method ? 1 : 0; - arg_failed = !gjs_value_from_g_argument(context, &length, - &arg_type_info, - &out_arg_cvalues[array_length_pos], - true); - if (!arg_failed && js_rval) { - arg_failed = !gjs_value_from_explicit_array(context, - return_values[next_rval], - &return_info, - &return_gargument, - length.toInt32()); - } - if (!arg_failed && - !r_value && - !gjs_g_argument_release_out_array(context, - transfer, - &return_info, - length.toInt32(), - &return_gargument)) - failed = true; - } else { - if (js_rval) - arg_failed = !gjs_value_from_g_argument(context, - return_values[next_rval], - &return_info, &return_gargument, - true); - /* Free GArgument, the JS::Value should have ref'd or copied it */ - if (!arg_failed && - !r_value && - !gjs_g_argument_release(context, - transfer, - &return_info, - &return_gargument)) + if (!cache->skip_out) { + if (!r_value) { + if (!return_values.append(js_out_arg)) { + JS_ReportOutOfMemory(context); failed = true; + break; + } } - if (arg_failed) - failed = true; - - ++next_rval; + js_arg_pos++; } } + g_assert(failed || did_throw_gerror || js_arg_pos == function->js_out_argc); + release: - /* We walk over all args, release in args (if allocated) and convert - * all out args to JS - */ - c_arg_pos = is_method ? 1 : 0; + // If we failed before calling the function, or if the function threw an + // exception, then any GI_TRANSFER_EVERYTHING or GI_TRANSFER_CONTAINER + // in-parameters were not transferred. Treat them as GI_TRANSFER_NOTHING so + // that they are freed. + if (!failed && !did_throw_gerror) + state.call_completed = true; + + // In this loop we use ffi_arg_pos just to ensure we don't release stuff + // we haven't allocated yet, if we failed in type conversion above. + // If we start from -1 (the return value), we need to process 1 more than + // processed_c_args. + // If we start from -2 (the instance parameter), we need to process 2 more + ffi_arg_pos = is_method ? 1 : 0; + unsigned ffi_arg_max = processed_c_args + (is_method ? 2 : 1); postinvoke_release_failed = false; - for (gi_arg_pos = 0; gi_arg_pos < gi_argc && c_arg_pos < processed_c_args; gi_arg_pos++, c_arg_pos++) { - GIDirection direction; - GIArgInfo arg_info; - GITypeInfo arg_type_info; - GjsParamType param_type; - - g_callable_info_load_arg( (GICallableInfo*) function->info, gi_arg_pos, &arg_info); - direction = g_arg_info_get_direction(&arg_info); - - g_arg_info_load_type(&arg_info, &arg_type_info); - param_type = function->param_types[gi_arg_pos]; - - if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT) { - GArgument *arg; - GITransfer transfer; - - if (direction == GI_DIRECTION_IN) { - arg = &in_arg_cvalues[c_arg_pos]; - transfer = g_arg_info_get_ownership_transfer(&arg_info); - } else { - arg = &inout_original_arg_cvalues[c_arg_pos]; - /* For inout, transfer refers to what we get back from the function; for - * the temporary C value we allocated, clearly we're responsible for - * freeing it. - */ - transfer = GI_TRANSFER_NOTHING; - } - if (param_type == PARAM_CALLBACK) { - ffi_closure *closure = (ffi_closure *) arg->v_pointer; - if (closure) { - GjsCallbackTrampoline *trampoline = (GjsCallbackTrampoline *) closure->user_data; - /* CallbackTrampolines are refcounted because for notified/async closures - it is possible to destroy it while in call, and therefore we cannot check - its scope at this point */ - gjs_callback_trampoline_unref(trampoline); - arg->v_pointer = NULL; - } - } else if (param_type == PARAM_ARRAY) { - gsize length; - GIArgInfo array_length_arg; - GITypeInfo array_length_type; - gint array_length_pos = g_type_info_get_array_length(&arg_type_info); - - g_assert(array_length_pos >= 0); - - g_callable_info_load_arg(function->info, array_length_pos, &array_length_arg); - g_arg_info_load_type(&array_length_arg, &array_length_type); - - array_length_pos += is_method ? 1 : 0; - - length = get_length_from_arg(in_arg_cvalues + array_length_pos, - g_type_info_get_tag(&array_length_type)); - - if (!gjs_g_argument_release_in_array(context, - transfer, - &arg_type_info, - length, - arg)) { - postinvoke_release_failed = true; - } - } else if (param_type == PARAM_NORMAL) { - if (!gjs_g_argument_release_in_arg(context, - transfer, - &arg_type_info, - arg)) { - postinvoke_release_failed = true; - } - } - } - - /* Don't free out arguments if function threw an exception or we failed - * earlier - note "postinvoke_release_failed" is separate from "failed". We - * sync them up after this loop. - */ - if (did_throw_gerror || failed) + for (gi_arg_pos = is_method ? -2 : -1; + gi_arg_pos < gi_argc && ffi_arg_pos < ffi_arg_max; + gi_arg_pos++, ffi_arg_pos++) { + GjsArgumentCache* cache = &function->arguments[gi_arg_pos]; + GIArgument* in_value = &state.in_cvalues[gi_arg_pos]; + GIArgument* out_value = &state.out_cvalues[gi_arg_pos]; + + gjs_debug_marshal( + GJS_DEBUG_GFUNCTION, + "Releasing argument '%s', %d/%d GI args, %u/%u C args", + cache->arg_name, gi_arg_pos, gi_argc, ffi_arg_pos, + processed_c_args); + + // Only process in or inout arguments if we failed, the rest is garbage + if (failed && cache->skip_in) continue; - if ((direction == GI_DIRECTION_OUT || direction == GI_DIRECTION_INOUT) && param_type != PARAM_SKIPPED) { - GArgument *arg; - bool arg_failed = false; - gint array_length_pos; - JS::RootedValue array_length(context, JS::Int32Value(0)); - GITransfer transfer; - - g_assert(next_rval < function->js_out_argc); - - arg = &out_arg_cvalues[c_arg_pos]; - - array_length_pos = g_type_info_get_array_length(&arg_type_info); - - if (js_rval) { - if (array_length_pos >= 0) { - GIArgInfo array_length_arg; - GITypeInfo array_length_type_info; - - g_callable_info_load_arg(function->info, array_length_pos, &array_length_arg); - g_arg_info_load_type(&array_length_arg, &array_length_type_info); - array_length_pos += is_method ? 1 : 0; - arg_failed = !gjs_value_from_g_argument(context, &array_length, - &array_length_type_info, - &out_arg_cvalues[array_length_pos], - true); - if (!arg_failed) { - arg_failed = !gjs_value_from_explicit_array(context, - return_values[next_rval], - &arg_type_info, - arg, - array_length.toInt32()); - } - } else { - arg_failed = !gjs_value_from_g_argument(context, - return_values[next_rval], - &arg_type_info, - arg, - true); - } - } - - if (arg_failed) - postinvoke_release_failed = true; - - /* Free GArgument, the JS::Value should have ref'd or copied it */ - transfer = g_arg_info_get_ownership_transfer(&arg_info); - if (!arg_failed) { - if (array_length_pos >= 0) { - gjs_g_argument_release_out_array(context, - transfer, - &arg_type_info, - array_length.toInt32(), - arg); - } else { - gjs_g_argument_release(context, - transfer, - &arg_type_info, - arg); - } - } - - /* For caller-allocates, what happens here is we allocate - * a structure above, then gjs_value_from_g_argument calls - * g_boxed_copy on it, and takes ownership of that. So - * here we release the memory allocated above. It would be - * better to special case this and directly hand JS the boxed - * object and tell gjs_boxed it owns the memory, but for now - * this works OK. We could also alloca() the structure instead - * of slice allocating. - */ - if (g_arg_info_is_caller_allocates(&arg_info)) { - GITypeTag type_tag; - GIBaseInfo* interface_info; - GIInfoType interface_type; - gsize size; - - type_tag = g_type_info_get_tag(&arg_type_info); - g_assert(type_tag == GI_TYPE_TAG_INTERFACE); - interface_info = g_type_info_get_interface(&arg_type_info); - interface_type = g_base_info_get_type(interface_info); - if (interface_type == GI_INFO_TYPE_STRUCT) { - size = g_struct_info_get_size((GIStructInfo*)interface_info); - } else if (interface_type == GI_INFO_TYPE_UNION) { - size = g_union_info_get_size((GIUnionInfo*)interface_info); - } else { - g_assert_not_reached(); - } - - g_slice_free1(size, out_arg_cvalues[c_arg_pos].v_pointer); - g_base_info_unref((GIBaseInfo*)interface_info); - } + // Save the return GIArgument if it was requested + if (r_value && gi_arg_pos == -1) { + *r_value = *out_value; + continue; + } - ++next_rval; + if (!cache->marshallers->release(context, cache, &state, in_value, + out_value)) { + postinvoke_release_failed = true; + // continue with the release even if we fail, to avoid leaks } } if (postinvoke_release_failed) failed = true; - g_assert(failed || did_throw_gerror || next_rval == (guint8)function->js_out_argc); - g_assert_cmpuint(c_arg_pos, ==, processed_c_args); - - if (function->js_out_argc > 0 && (!failed && !did_throw_gerror)) { - /* if we have 1 return value or out arg, return that item - * on its own, otherwise return a JavaScript array with - * [return value, out arg 1, out arg 2, ...] - */ - if (js_rval) { - if (function->js_out_argc == 1) { - js_rval.ref().set(return_values[0]); + g_assert(ffi_arg_pos == processed_c_args + (is_method ? 2 : 1)); + + if (!r_value && function->js_out_argc > 0 && + (!failed && !did_throw_gerror)) { + // If we have one return value or out arg, return that item on its + // own, otherwise return a JavaScript array with [return value, + // out arg 1, out arg 2, ...] + if (function->js_out_argc == 1) { + args.rval().set(return_values[0]); + } else { + JSObject* array = JS::NewArrayObject(context, return_values); + if (!array) { + failed = true; } else { - JSObject *array; - array = JS_NewArrayObject(context, return_values); - if (array == NULL) { - failed = true; - } else { - js_rval.ref().setObject(*array); - } + args.rval().setObject(*array); } } - - if (r_value) { - *r_value = return_gargument; - } } if (!failed && did_throw_gerror) { - gjs_throw_g_error(context, local_error); - return false; + return gjs_throw_gerror(context, local_error); } else if (failed) { return false; } else { @@ -1391,33 +1031,25 @@ release: } } +GJS_JSAPI_RETURN_CONVENTION static bool function_call(JSContext *context, unsigned js_argc, JS::Value *vp) { - GJS_GET_THIS(context, js_argc, vp, js_argv, object); + JS::CallArgs js_argv = JS::CallArgsFromVp(js_argc, vp); JS::RootedObject callee(context, &js_argv.callee()); - bool success; Function *priv; - JS::RootedValue retval(context); priv = priv_from_js(context, callee); - gjs_debug_marshal(GJS_DEBUG_GFUNCTION, - "Call callee %p priv %p this obj %p", callee.get(), - priv, object.get()); + gjs_debug_marshal(GJS_DEBUG_GFUNCTION, "Call callee %p priv %p", + callee.get(), priv); if (priv == NULL) return true; /* we are the prototype, or have the wrong class */ - success = gjs_invoke_c_function(context, priv, object, js_argv, - mozilla::Some(&retval), - NULL); - if (success) - js_argv.rval().set(retval); - - return success; + return gjs_invoke_c_function(context, priv, js_argv); } GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(function) @@ -1428,18 +1060,29 @@ GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(function) static void uninit_cached_function_data (Function *function) { - if (function->info) - g_base_info_unref( (GIBaseInfo*) function->info); - if (function->param_types) - g_free(function->param_types); + g_assert(function->info && "Don't know how to free cache without GI info"); + + if (function->arguments) { + // Careful! function->arguments is offset by one or two elements inside + // the allocated space, so we have to free index -1 or -2. + int start_index = g_callable_info_is_method(function->info) ? -2 : -1; + int gi_argc = g_callable_info_get_n_args(function->info); + for (int ix = start_index; ix < gi_argc; ix++) { + if (function->arguments[ix].marshallers->free) + function->arguments[ix].marshallers->free( + &function->arguments[ix]); + } + + g_free(&function->arguments[start_index]); + function->arguments = nullptr; + } + + g_base_info_unref(function->info); g_function_invoker_destroy(&function->invoker); } -static void -function_finalize(JSFreeOp *fop, - JSObject *obj) -{ +static void function_finalize(JSFreeOp*, JSObject* obj) { Function *priv; priv = (Function *) JS_GetPrivate(obj); @@ -1454,100 +1097,66 @@ function_finalize(JSFreeOp *fop, g_slice_free(Function, priv); } +GJS_JSAPI_RETURN_CONVENTION static bool get_num_arguments (JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, rec, to, Function, priv); - int n_args, n_jsargs, i; - - if (priv == NULL) - return false; - - n_args = g_callable_info_get_n_args(priv->info); - n_jsargs = 0; - for (i = 0; i < n_args; i++) { - GIArgInfo arg_info; - - if (priv->param_types[i] == PARAM_SKIPPED) - continue; - - g_callable_info_load_arg(priv->info, i, &arg_info); - - if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT) - continue; - - n_jsargs++; - } - - rec.rval().setInt32(n_jsargs); + rec.rval().setInt32(priv->js_in_argc); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool function_to_string (JSContext *context, guint argc, JS::Value *vp) { GJS_GET_PRIV(context, argc, vp, rec, to, Function, priv); - gchar *string; - bool free; - bool ret = false; int i, n_args, n_jsargs; GString *arg_names_str; gchar *arg_names; if (priv == NULL) { - string = (gchar *) "function () {\n}"; - free = false; - goto out; + JSString* retval = JS_NewStringCopyZ(context, "function () {\n}"); + if (!retval) + return false; + rec.rval().setString(retval); + return true; } - free = true; - n_args = g_callable_info_get_n_args(priv->info); n_jsargs = 0; arg_names_str = g_string_new(""); for (i = 0; i < n_args; i++) { - GIArgInfo arg_info; - - if (priv->param_types[i] == PARAM_SKIPPED) - continue; - - g_callable_info_load_arg(priv->info, i, &arg_info); - - if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT) + if (priv->arguments[i].skip_in) continue; if (n_jsargs > 0) g_string_append(arg_names_str, ", "); n_jsargs++; - g_string_append(arg_names_str, g_base_info_get_name(&arg_info)); + g_string_append(arg_names_str, priv->arguments[i].arg_name); } arg_names = g_string_free(arg_names_str, false); + GjsAutoChar descr; if (g_base_info_get_type(priv->info) == GI_INFO_TYPE_FUNCTION) { - string = g_strdup_printf("function %s(%s) {\n\t/* proxy for native symbol %s(); */\n}", - g_base_info_get_name ((GIBaseInfo *) priv->info), - arg_names, - g_function_info_get_symbol ((GIFunctionInfo *) priv->info)); + descr = g_strdup_printf( + "function %s(%s) {\n\t/* wrapper for native symbol %s(); */\n}", + g_base_info_get_name(priv->info), arg_names, + g_function_info_get_symbol(priv->info)); } else { - string = g_strdup_printf("function %s(%s) {\n\t/* proxy for native symbol */\n}", - g_base_info_get_name ((GIBaseInfo *) priv->info), - arg_names); + descr = g_strdup_printf( + "function %s(%s) {\n\t/* wrapper for native symbol */\n}", + g_base_info_get_name(priv->info), arg_names); } g_free(arg_names); - out: - if (gjs_string_from_utf8(context, string, rec.rval())) - ret = true; - - if (free) - g_free(string); - return ret; + return gjs_string_from_utf8(context, descr, rec.rval()); } /* The bizarre thing about this vtable is that it applies to both @@ -1555,16 +1164,14 @@ function_to_string (JSContext *context, * class have. */ static const struct JSClassOps gjs_function_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - NULL, /* resolve */ - nullptr, /* mayResolve */ + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve function_finalize, - function_call -}; + function_call}; struct JSClass gjs_function_class = { "GIRepositoryFunction", /* means "new GIRepositoryFunction()" works */ @@ -1574,8 +1181,8 @@ struct JSClass gjs_function_class = { static JSPropertySpec gjs_function_proto_props[] = { JS_PSG("length", get_num_arguments, JSPROP_PERMANENT), - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "GIRepositoryFunction", JSPROP_READONLY), + JS_PS_END}; /* The original Function.prototype.toString complains when given a GIRepository function as an argument */ @@ -1586,6 +1193,7 @@ static JSFunctionSpec gjs_function_proto_funcs[] = { static JSFunctionSpec *gjs_function_static_funcs = nullptr; +GJS_JSAPI_RETURN_CONVENTION static bool init_cached_function_data (JSContext *context, Function *function, @@ -1593,9 +1201,7 @@ init_cached_function_data (JSContext *context, GICallableInfo *info) { guint8 i, n_args; - int array_length_pos; GError *error = NULL; - GITypeInfo return_type; GIInfoType info_type; info_type = g_base_info_get_type((GIBaseInfo *)info); @@ -1604,8 +1210,7 @@ init_cached_function_data (JSContext *context, if (!g_function_info_prep_invoker((GIFunctionInfo *)info, &(function->invoker), &error)) { - gjs_throw_g_error(context, error); - return false; + return gjs_throw_gerror(context, error); } } else if (info_type == GI_INFO_TYPE_VFUNC) { gpointer addr; @@ -1613,120 +1218,77 @@ init_cached_function_data (JSContext *context, addr = g_vfunc_info_get_address((GIVFuncInfo *)info, gtype, &error); if (error != NULL) { if (error->code != G_INVOKE_ERROR_SYMBOL_NOT_FOUND) - gjs_throw_g_error(context, error); + return gjs_throw_gerror(context, error); - g_clear_error(&error); + gjs_throw(context, "Virtual function not implemented: %s", + error->message); return false; } if (!g_function_invoker_new_for_address(addr, info, &(function->invoker), &error)) { - gjs_throw_g_error(context, error); - return false; + return gjs_throw_gerror(context, error); } } - g_callable_info_load_return_type((GICallableInfo*)info, &return_type); - if (g_type_info_get_tag(&return_type) != GI_TYPE_TAG_VOID) - function->js_out_argc += 1; - + bool is_method = g_callable_info_is_method(info); n_args = g_callable_info_get_n_args((GICallableInfo*) info); - function->param_types = g_new0(GjsParamType, n_args); - array_length_pos = g_type_info_get_array_length(&return_type); - if (array_length_pos >= 0 && array_length_pos < n_args) - function->param_types[array_length_pos] = PARAM_SKIPPED; + // arguments is one or two inside an array of n_args + 2, so + // arguments[-1] is the return value (which can be skipped if void) + // arguments[-2] is the instance parameter + size_t offset = is_method ? 2 : 1; + GjsArgumentCache* arguments = + g_new0(GjsArgumentCache, n_args + offset) + offset; + + if (is_method && + !gjs_arg_cache_build_instance(context, &arguments[-2], info)) + return false; + + bool inc_counter; + if (!gjs_arg_cache_build_return(context, &arguments[-1], arguments, info, + &inc_counter)) + return false; + + int out_argc = inc_counter ? 1 : 0; + int in_argc = 0; for (i = 0; i < n_args; i++) { GIDirection direction; GIArgInfo arg_info; - GITypeInfo type_info; - int destroy = -1; - int closure = -1; - GITypeTag type_tag; - if (function->param_types[i] == PARAM_SKIPPED) + if (arguments[i].skip_in || arguments[i].skip_out) continue; g_callable_info_load_arg((GICallableInfo*) info, i, &arg_info); - g_arg_info_load_type(&arg_info, &type_info); - direction = g_arg_info_get_direction(&arg_info); - type_tag = g_type_info_get_tag(&type_info); - if (type_tag == GI_TYPE_TAG_INTERFACE) { - GIBaseInfo* interface_info; - GIInfoType interface_type; - - interface_info = g_type_info_get_interface(&type_info); - interface_type = g_base_info_get_type(interface_info); - if (interface_type == GI_INFO_TYPE_CALLBACK) { - if (strcmp(g_base_info_get_name(interface_info), "DestroyNotify") == 0 && - strcmp(g_base_info_get_namespace(interface_info), "GLib") == 0) { - /* Skip GDestroyNotify if they appear before the respective callback */ - function->param_types[i] = PARAM_SKIPPED; - } else { - function->param_types[i] = PARAM_CALLBACK; - function->expected_js_argc += 1; - - destroy = g_arg_info_get_destroy(&arg_info); - closure = g_arg_info_get_closure(&arg_info); - - if (destroy >= 0 && destroy < n_args) - function->param_types[destroy] = PARAM_SKIPPED; - - if (closure >= 0 && closure < n_args) - function->param_types[closure] = PARAM_SKIPPED; - - if (destroy >= 0 && closure < 0) { - gjs_throw(context, "Function %s.%s has a GDestroyNotify but no user_data, not supported", - g_base_info_get_namespace( (GIBaseInfo*) info), - g_base_info_get_name( (GIBaseInfo*) info)); - g_base_info_unref(interface_info); - return false; - } - } - } - g_base_info_unref(interface_info); - } else if (type_tag == GI_TYPE_TAG_ARRAY) { - if (g_type_info_get_array_type(&type_info) == GI_ARRAY_TYPE_C) { - array_length_pos = g_type_info_get_array_length(&type_info); - - if (array_length_pos >= 0 && array_length_pos < n_args) { - GIArgInfo length_arg_info; - - g_callable_info_load_arg((GICallableInfo*) info, array_length_pos, &length_arg_info); - if (g_arg_info_get_direction(&length_arg_info) != direction) { - gjs_throw(context, "Function %s.%s has an array with different-direction length arg, not supported", - g_base_info_get_namespace( (GIBaseInfo*) info), - g_base_info_get_name( (GIBaseInfo*) info)); - return false; - } - - function->param_types[array_length_pos] = PARAM_SKIPPED; - function->param_types[i] = PARAM_ARRAY; + if (!gjs_arg_cache_build_arg(context, &arguments[i], arguments, i, + direction, &arg_info, info, &inc_counter)) + return false; - if (array_length_pos < i) { - /* we already collected array_length_pos, remove it */ - if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT) - function->expected_js_argc -= 1; - if (direction == GI_DIRECTION_OUT || direction == GI_DIRECTION_INOUT) - function->js_out_argc -= 1; - } - } + if (inc_counter) { + switch (direction) { + case GI_DIRECTION_INOUT: + out_argc++; + [[fallthrough]]; + case GI_DIRECTION_IN: + in_argc++; + break; + case GI_DIRECTION_OUT: + out_argc++; + break; + default: + g_assert_not_reached(); } } - - if (function->param_types[i] == PARAM_NORMAL || - function->param_types[i] == PARAM_ARRAY) { - if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT) - function->expected_js_argc += 1; - if (direction == GI_DIRECTION_OUT || direction == GI_DIRECTION_INOUT) - function->js_out_argc += 1; - } } + function->arguments = arguments; + + function->js_in_argc = in_argc; + function->js_out_argc = out_argc; function->info = info; g_base_info_ref((GIBaseInfo*) function->info); @@ -1734,15 +1296,14 @@ init_cached_function_data (JSContext *context, return true; } -static inline JSObject * -gjs_builtin_function_get_proto(JSContext *cx) -{ - JS::RootedObject global(cx, gjs_get_import_global(cx)); - return JS_GetFunctionPrototype(cx, global); +[[nodiscard]] static inline JSObject* gjs_builtin_function_get_proto( + JSContext* cx) { + return JS::GetRealmFunctionPrototype(cx); } GJS_DEFINE_PROTO_FUNCS_WITH_PARENT(function, builtin_function) +GJS_JSAPI_RETURN_CONVENTION static JSObject* function_new(JSContext *context, GType gtype, @@ -1778,6 +1339,7 @@ function_new(JSContext *context, return function; } +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_define_function(JSContext *context, JS::HandleObject in_object, @@ -1790,8 +1352,6 @@ gjs_define_function(JSContext *context, info_type = g_base_info_get_type((GIBaseInfo *)info); - JSAutoRequest ar(context); - JS::RootedObject function(context, function_new(context, gtype, info)); if (!function) return NULL; @@ -1818,38 +1378,17 @@ gjs_define_function(JSContext *context, return function; } +bool gjs_invoke_constructor_from_c(JSContext* context, GIFunctionInfo* info, + JS::HandleObject obj, + const JS::CallArgs& args, + GIArgument* rvalue) { + Function function; -bool -gjs_invoke_c_function_uncached(JSContext *context, - GIFunctionInfo *info, - JS::HandleObject obj, - const JS::HandleValueArray& args, - JS::MutableHandleValue rval) -{ - Function function; - bool result; - - memset (&function, 0, sizeof (Function)); - if (!init_cached_function_data (context, &function, 0, info)) - return false; - - result = gjs_invoke_c_function(context, &function, obj, args, - mozilla::Some(rval), NULL); - uninit_cached_function_data (&function); - return result; -} - -bool -gjs_invoke_constructor_from_c(JSContext *context, - JS::HandleObject constructor, - JS::HandleObject obj, - const JS::HandleValueArray& args, - GIArgument *rvalue) -{ - Function *priv; - - priv = priv_from_js(context, constructor); + memset(&function, 0, sizeof(Function)); + if (!init_cached_function_data(context, &function, 0, info)) + return false; - mozilla::Maybe m_jsrval; - return gjs_invoke_c_function(context, priv, obj, args, m_jsrval, rvalue); + bool result = gjs_invoke_c_function(context, &function, args, obj, rvalue); + uninit_cached_function_data(&function); + return result; } diff --git a/gi/function.h b/gi/function.h index b1138b8..5a65593 100644 --- a/gi/function.h +++ b/gi/function.h @@ -21,29 +21,34 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_FUNCTION_H__ -#define __GJS_FUNCTION_H__ +#ifndef GI_FUNCTION_H_ +#define GI_FUNCTION_H_ -#include -#include - -#include "cjs/jsapi-util.h" -#include "cjs/jsapi-util-root.h" +#include +#include #include -#include +#include + +#include +#include -G_BEGIN_DECLS +#include "cjs/macros.h" + +namespace JS { +class CallArgs; +} typedef enum { PARAM_NORMAL, PARAM_SKIPPED, PARAM_ARRAY, - PARAM_CALLBACK + PARAM_CALLBACK, + PARAM_UNKNOWN, } GjsParamType; struct GjsCallbackTrampoline { - gint ref_count; + int ref_count; GICallableInfo *info; GClosure *js_function; @@ -55,33 +60,36 @@ struct GjsCallbackTrampoline { GjsParamType *param_types; }; -GjsCallbackTrampoline* gjs_callback_trampoline_new(JSContext *context, - JS::HandleValue function, - GICallableInfo *callable_info, - GIScopeType scope, - JS::HandleObject scope_object, - bool is_vfunc); +GJS_JSAPI_RETURN_CONVENTION +GjsCallbackTrampoline* gjs_callback_trampoline_new( + JSContext* cx, JS::HandleFunction function, GICallableInfo* callable_info, + GIScopeType scope, bool has_scope_object, bool is_vfunc); void gjs_callback_trampoline_unref(GjsCallbackTrampoline *trampoline); void gjs_callback_trampoline_ref(GjsCallbackTrampoline *trampoline); +// Stack allocation only! +struct GjsFunctionCallState { + GIArgument* in_cvalues; + GIArgument* out_cvalues; + GIArgument* inout_original_cvalues; + JS::RootedObject instance_object; + bool call_completed; + + explicit GjsFunctionCallState(JSContext* cx) + : instance_object(cx), call_completed(false) {} +}; + +GJS_JSAPI_RETURN_CONVENTION JSObject *gjs_define_function(JSContext *context, JS::HandleObject in_object, GType gtype, GICallableInfo *info); -bool gjs_invoke_c_function_uncached(JSContext *context, - GIFunctionInfo *info, - JS::HandleObject obj, - const JS::HandleValueArray& args, - JS::MutableHandleValue rval); - -bool gjs_invoke_constructor_from_c(JSContext *context, - JS::HandleObject constructor, - JS::HandleObject obj, - const JS::HandleValueArray& args, - GIArgument *rvalue); - -G_END_DECLS +GJS_JSAPI_RETURN_CONVENTION +bool gjs_invoke_constructor_from_c(JSContext* cx, GIFunctionInfo* info, + JS::HandleObject this_obj, + const JS::CallArgs& args, + GIArgument* rvalue); -#endif /* __GJS_FUNCTION_H__ */ +#endif // GI_FUNCTION_H_ diff --git a/gi/fundamental.cpp b/gi/fundamental.cpp index 0410a63..9e3f712 100644 --- a/gi/fundamental.cpp +++ b/gi/fundamental.cpp @@ -24,190 +24,66 @@ #include -#include "fundamental.h" - -#include "arg.h" -#include "object.h" -#include "boxed.h" -#include "function.h" -#include "gtype.h" -#include "proxyutils.h" -#include "repo.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" - -#include -#include #include +#include + +#include // for SystemAllocPolicy +#include +#include // for WeakCache +#include +#include +#include // for InformalValueTypeName, JS_GetClass +#include + +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/function.h" +#include "gi/fundamental.h" +#include "gi/repo.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" + +namespace JS { +class CallArgs; +} -/* - * Structure allocated for prototypes. - */ -struct Fundamental { - /* instance info */ - void *gfundamental; - Fundamental *prototype; /* NULL if prototype */ - - /* prototype info */ - GIObjectInfo *info; - GType gtype; - GIObjectInfoRefFunction ref_function; - GIObjectInfoUnrefFunction unref_function; - GIObjectInfoGetValueFunction get_value_function; - GIObjectInfoSetValueFunction set_value_function; - - JS::Heap constructor_name; - GICallableInfo *constructor_info; -}; +FundamentalInstance::FundamentalInstance(JSContext* cx, JS::HandleObject obj) + : GIWrapperInstance(cx, obj) { + GJS_INC_COUNTER(fundamental_instance); +} /* - * Structure allocated for instances. + * FundamentalInstance::associate_js_instance: + * + * Associates @gfundamental with @object so that @object can be retrieved in the + * future if you have a pointer to @gfundamental. (Assuming @object has not been + * garbage collected in the meantime.) */ -typedef struct { - void *gfundamental; - Fundamental *prototype; -} FundamentalInstance; - -extern struct JSClass gjs_fundamental_instance_class; - -GJS_DEFINE_PRIV_FROM_JS(FundamentalInstance, gjs_fundamental_instance_class) - -static GQuark -gjs_fundamental_table_quark (void) -{ - static GQuark val = 0; - if (!val) - val = g_quark_from_static_string("gjs::fundamental-table"); - - return val; -} +bool FundamentalInstance::associate_js_instance(JSContext* cx, JSObject* object, + void* gfundamental) { + m_ptr = gfundamental; -static GHashTable * -_ensure_mapping_table(GjsContext *context) -{ - GHashTable *table = - (GHashTable *) g_object_get_qdata ((GObject *) context, - gjs_fundamental_table_quark()); - - if (G_UNLIKELY(table == NULL)) { - table = g_hash_table_new(NULL, NULL); - g_object_set_qdata_full((GObject *) context, - gjs_fundamental_table_quark(), - table, - (GDestroyNotify) g_hash_table_unref); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + if (!gjs->fundamental_table().putNew(gfundamental, object)) { + JS_ReportOutOfMemory(cx); + return false; } - return table; -} - -static void -_fundamental_add_object(void *native_object, JSObject *js_object) -{ - GHashTable *table = _ensure_mapping_table(gjs_context_get_current()); - - g_hash_table_insert(table, native_object, js_object); -} - -static void -_fundamental_remove_object(void *native_object) -{ - GHashTable *table = _ensure_mapping_table(gjs_context_get_current()); - - g_hash_table_remove(table, native_object); -} - -static JSObject * -_fundamental_lookup_object(void *native_object) -{ - GHashTable *table = _ensure_mapping_table(gjs_context_get_current()); - - return (JSObject *) g_hash_table_lookup(table, native_object); -} - -/**/ - -static inline bool -fundamental_is_prototype(Fundamental *priv) -{ - return (priv->prototype == nullptr); -} - -static inline bool -fundamental_is_prototype(FundamentalInstance *priv) -{ - return (priv->prototype == nullptr); -} - -static inline Fundamental * -proto_priv_from_js(JSContext *context, - JS::HandleObject obj) -{ - JS::RootedObject proto(context); - JS_GetPrototype(context, obj, &proto); - return (Fundamental*) priv_from_js(context, proto); -} - -static FundamentalInstance * -init_fundamental_instance(JSContext *context, - JS::HandleObject object) -{ - Fundamental *proto_priv; - FundamentalInstance *priv; - - JS_BeginRequest(context); - - priv = g_slice_new0(FundamentalInstance); - - GJS_INC_COUNTER(fundamental); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - gjs_debug_lifecycle(GJS_DEBUG_GFUNDAMENTAL, - "fundamental instance constructor, obj %p priv %p", - object.get(), priv); - - proto_priv = proto_priv_from_js(context, object); - g_assert(proto_priv != NULL); - - priv->prototype = proto_priv; + debug_lifecycle(object, "associated JSObject with fundamental"); - JS_EndRequest(context); - - return priv; -} - -static void -associate_js_instance_to_fundamental(JSContext *context, - JS::HandleObject object, - void *gfundamental, - bool owned_ref) -{ - FundamentalInstance *priv; - - priv = priv_from_js(context, object); - priv->gfundamental = gfundamental; - - g_assert(_fundamental_lookup_object(gfundamental) == NULL); - _fundamental_add_object(gfundamental, object); - - gjs_debug_lifecycle(GJS_DEBUG_GFUNDAMENTAL, - "associated JSObject %p with fundamental %p", - object.get(), gfundamental); - - if (!owned_ref) - priv->prototype->ref_function(gfundamental); + ref(); + return true; } /**/ /* Find the first constructor */ -static GIFunctionInfo * -find_fundamental_constructor(JSContext *context, - GIObjectInfo *info, - JS::MutableHandleId constructor_name) -{ +[[nodiscard]] static GIFunctionInfo* find_fundamental_constructor( + GIObjectInfo* info) { int i, n_methods; n_methods = g_object_info_get_n_methods(info); @@ -219,70 +95,44 @@ find_fundamental_constructor(JSContext *context, func_info = g_object_info_get_method(info, i); flags = g_function_info_get_flags(func_info); - if ((flags & GI_FUNCTION_IS_CONSTRUCTOR) != 0) { - const char *name; - - name = g_base_info_get_name((GIBaseInfo *) func_info); - constructor_name.set(gjs_intern_string_to_id(context, name)); - + if ((flags & GI_FUNCTION_IS_CONSTRUCTOR) != 0) return func_info; - } g_base_info_unref((GIBaseInfo *) func_info); } - return NULL; + return nullptr; } /**/ -static bool -fundamental_instance_resolve_interface(JSContext *context, - JS::HandleObject obj, - bool *resolved, - Fundamental *proto_priv, - const char *name) -{ - GIFunctionInfo *method_info; +bool FundamentalPrototype::resolve_interface(JSContext* cx, + JS::HandleObject obj, + bool* resolved, const char* name) { bool ret; GType *interfaces; guint n_interfaces; guint i; ret = true; - interfaces = g_type_interfaces(proto_priv->gtype, &n_interfaces); + interfaces = g_type_interfaces(gtype(), &n_interfaces); for (i = 0; i < n_interfaces; i++) { - GIBaseInfo *base_info; - GIInterfaceInfo *iface_info; - - base_info = g_irepository_find_by_gtype(g_irepository_get_default(), - interfaces[i]); + GjsAutoInterfaceInfo iface_info = + g_irepository_find_by_gtype(nullptr, interfaces[i]); - if (base_info == NULL) + if (!iface_info) continue; - /* An interface GType ought to have interface introspection info */ - g_assert(g_base_info_get_type(base_info) == GI_INFO_TYPE_INTERFACE); - - iface_info = (GIInterfaceInfo *) base_info; - - method_info = g_interface_info_find_method(iface_info, name); - - g_base_info_unref(base_info); + GjsAutoFunctionInfo method_info = + g_interface_info_find_method(iface_info, name); - - if (method_info != NULL) { - if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) { - if (gjs_define_function(context, obj, - proto_priv->gtype, - (GICallableInfo *) method_info)) { - *resolved = true; - } else { - ret = false; - } + if (method_info && + g_function_info_get_flags(method_info) & GI_FUNCTION_IS_METHOD) { + if (gjs_define_function(cx, obj, gtype(), method_info)) { + *resolved = true; + } else { + ret = false; } - - g_base_info_unref((GIBaseInfo *) method_info); } } @@ -290,290 +140,136 @@ fundamental_instance_resolve_interface(JSContext *context, return ret; } -/* - * The *resolved out parameter, on success, should be false to indicate that id - * was not resolved; and true if id was resolved. - */ -static bool -fundamental_instance_resolve(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - bool *resolved) -{ - FundamentalInstance *priv; - - priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GFUNDAMENTAL, - "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 (!fundamental_is_prototype(priv)) { - /* 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; - } - - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; /* not resolved, but no error */ - } - +// See GIWrapperBase::resolve(). +bool FundamentalPrototype::resolve_impl(JSContext* cx, JS::HandleObject obj, + JS::HandleId, const char* prop_name, + bool* resolved) { /* We are the prototype, so look for methods and other class properties */ - Fundamental *proto_priv = (Fundamental *) priv; - GIFunctionInfo *method_info; - - method_info = g_object_info_find_method((GIStructInfo*) proto_priv->info, - name); - - if (method_info != NULL) { - const char *method_name; + GjsAutoFunctionInfo method_info = + g_object_info_find_method(info(), prop_name); + if (method_info) { #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); - /* we do not define deprecated methods in the prototype */ - if (g_base_info_is_deprecated((GIBaseInfo *) method_info)) { + if (g_base_info_is_deprecated(method_info)) { gjs_debug(GJS_DEBUG_GFUNDAMENTAL, - "Ignoring definition of deprecated method %s in prototype %s.%s", - method_name, - g_base_info_get_namespace((GIBaseInfo *) proto_priv->info), - g_base_info_get_name((GIBaseInfo *) proto_priv->info)); - g_base_info_unref((GIBaseInfo *) method_info); + "Ignoring definition of deprecated method %s in " + "prototype %s.%s", + method_info.name(), ns(), name()); *resolved = false; return true; } gjs_debug(GJS_DEBUG_GFUNDAMENTAL, "Defining method %s in prototype for %s.%s", - method_name, - g_base_info_get_namespace((GIBaseInfo *) proto_priv->info), - g_base_info_get_name((GIBaseInfo *) proto_priv->info)); + method_info.name(), ns(), name()); - if (gjs_define_function(context, obj, proto_priv->gtype, - method_info) == NULL) { - g_base_info_unref((GIBaseInfo *) method_info); + if (!gjs_define_function(cx, obj, gtype(), method_info)) return false; - } *resolved = true; } - - g_base_info_unref((GIBaseInfo *) method_info); } else { *resolved = false; } - bool status = - fundamental_instance_resolve_interface(context, obj, resolved, - proto_priv, name); - return status; + return resolve_interface(cx, obj, resolved, prop_name); } -static bool -fundamental_invoke_constructor(FundamentalInstance *priv, - JSContext *context, - JS::HandleObject obj, - const JS::HandleValueArray& args, - GIArgument *rvalue) -{ - JS::RootedObject js_constructor(context); - - if (!gjs_object_require_property(context, obj, NULL, - GJS_STRING_CONSTRUCTOR, - &js_constructor) || - priv->prototype->constructor_name.get() == JSID_VOID) { - gjs_throw (context, - "Couldn't find a constructor for type %s.%s", - g_base_info_get_namespace((GIBaseInfo*) priv->prototype->info), - g_base_info_get_name((GIBaseInfo*) priv->prototype->info)); - return false; - } - - JS::RootedObject constructor(context); - JS::RootedId constructor_name(context, priv->prototype->constructor_name); - if (!gjs_object_require_property(context, js_constructor, NULL, - constructor_name, &constructor)) { - gjs_throw (context, - "Couldn't find a constructor for type %s.%s", - g_base_info_get_namespace((GIBaseInfo*) priv->prototype->info), - g_base_info_get_name((GIBaseInfo*) priv->prototype->info)); +/* + * FundamentalInstance::invoke_constructor: + * + * Finds the type's static constructor method (the static method given by + * FundamentalPrototype::constructor_info()) and invokes it with the given + * arguments. + */ +bool FundamentalInstance::invoke_constructor(JSContext* context, + JS::HandleObject obj, + const JS::CallArgs& args, + GIArgument* rvalue) { + GIFunctionInfo* constructor_info = get_prototype()->constructor_info(); + if (!constructor_info) { + gjs_throw(context, "Couldn't find a constructor for type %s.%s", ns(), + name()); return false; } - return gjs_invoke_constructor_from_c(context, constructor, obj, args, rvalue); + return gjs_invoke_constructor_from_c(context, constructor_info, obj, args, + rvalue); } -/* If we set JSCLASS_CONSTRUCT_PROTOTYPE flag, then this is called on - * the prototype in addition to on each instance. When called on the - * prototype, "obj" is the prototype, and "retval" is the prototype - * also, but can be replaced with another object to use instead as the - * prototype. If we don't set JSCLASS_CONSTRUCT_PROTOTYPE we can - * identify the prototype as an object of our class with NULL private - * data. - */ -GJS_NATIVE_CONSTRUCTOR_DECLARE(fundamental_instance) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(fundamental_instance) - FundamentalInstance *priv; +// See GIWrapperBase::constructor(). +bool FundamentalInstance::constructor_impl(JSContext* cx, + JS::HandleObject object, + const JS::CallArgs& argv) { GArgument ret_value; GITypeInfo return_info; - GJS_NATIVE_CONSTRUCTOR_PRELUDE(fundamental_instance); - - priv = init_fundamental_instance(context, object); - - gjs_debug_lifecycle(GJS_DEBUG_GFUNDAMENTAL, - "fundamental constructor, obj %p priv %p", - object.get(), priv); - - if (!fundamental_invoke_constructor(priv, context, object, argv, &ret_value)) + if (!invoke_constructor(cx, object, argv, &ret_value) || + !associate_js_instance(cx, object, gjs_arg_get(&ret_value))) return false; - associate_js_instance_to_fundamental(context, object, ret_value.v_pointer, false); + GICallableInfo* constructor_info = get_prototype()->constructor_info(); + g_callable_info_load_return_type(constructor_info, &return_info); - g_callable_info_load_return_type((GICallableInfo*) priv->prototype->constructor_info, &return_info); - - if (!gjs_g_argument_release (context, - g_callable_info_get_caller_owns((GICallableInfo*) priv->prototype->constructor_info), - &return_info, - &ret_value)) - return false; - - GJS_NATIVE_CONSTRUCTOR_FINISH(fundamental_instance); - - return true; + return gjs_g_argument_release( + cx, g_callable_info_get_caller_owns(constructor_info), &return_info, + &ret_value); } -static void -fundamental_finalize(JSFreeOp *fop, - JSObject *obj) -{ - FundamentalInstance *priv; - - priv = (FundamentalInstance *) JS_GetPrivate(obj); - - gjs_debug_lifecycle(GJS_DEBUG_GFUNDAMENTAL, - "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) - return; /* wrong class? */ - - if (!fundamental_is_prototype(priv)) { - if (priv->gfundamental) { - _fundamental_remove_object(priv->gfundamental); - priv->prototype->unref_function(priv->gfundamental); - priv->gfundamental = NULL; - } - - g_slice_free(FundamentalInstance, priv); - GJS_DEC_COUNTER(fundamental); - } else { - Fundamental *proto_priv = (Fundamental *) priv; - - /* Only unref infos when freeing the prototype */ - if (proto_priv->constructor_info) - g_base_info_unref (proto_priv->constructor_info); - proto_priv->constructor_info = NULL; - if (proto_priv->info) - g_base_info_unref((GIBaseInfo *) proto_priv->info); - proto_priv->info = NULL; - - proto_priv->~Fundamental(); - g_slice_free(Fundamental, proto_priv); +FundamentalInstance::~FundamentalInstance(void) { + if (m_ptr) { + unref(); + m_ptr = nullptr; } + GJS_DEC_COUNTER(fundamental_instance); } -static bool -to_string_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, rec, obj, FundamentalInstance, priv); - - if (fundamental_is_prototype(priv)) { - Fundamental *proto_priv = (Fundamental *) priv; - - if (!_gjs_proxy_to_string_func(context, obj, "fundamental", - (GIBaseInfo *) proto_priv->info, - proto_priv->gtype, - proto_priv->gfundamental, - rec.rval())) - return false; - } else { - if (!_gjs_proxy_to_string_func(context, obj, "fundamental", - (GIBaseInfo *) priv->prototype->info, - priv->prototype->gtype, - priv->gfundamental, - rec.rval())) - return false; - } - - return true; +FundamentalPrototype::FundamentalPrototype(GIObjectInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype), + m_ref_function(g_object_info_get_ref_function_pointer(info)), + m_unref_function(g_object_info_get_unref_function_pointer(info)), + m_get_value_function(g_object_info_get_get_value_function_pointer(info)), + m_set_value_function(g_object_info_get_set_value_function_pointer(info)), + m_constructor_info(find_fundamental_constructor(info)) { + g_assert(m_ref_function); + g_assert(m_unref_function); + g_assert(m_set_value_function); + g_assert(m_get_value_function); + GJS_INC_COUNTER(fundamental_prototype); } -static void -fundamental_trace(JSTracer *tracer, - JSObject *obj) -{ - auto priv = static_cast(JS_GetPrivate(obj)); - if (priv == nullptr || !fundamental_is_prototype(priv)) - return; /* Only prototypes need tracing */ - - JS::TraceEdge(tracer, &priv->constructor_name, - "Fundamental::constructor_name"); +FundamentalPrototype::~FundamentalPrototype(void) { + g_clear_pointer(&m_constructor_info, g_base_info_unref); + GJS_DEC_COUNTER(fundamental_prototype); } -/* The bizarre thing about this vtable is that it applies to both - * instances of the object, and to the prototype that instances of the - * class have. - * - * Also, there's a constructor field in here, but as far as I can - * tell, it would only be used if no constructor were provided to - * JS_InitClass. The constructor from JS_InitClass is not applied to - * the prototype unless JSCLASS_CONSTRUCT_PROTOTYPE is in flags. - */ -static const struct JSClassOps gjs_fundamental_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - fundamental_instance_resolve, - nullptr, /* mayResolve */ - fundamental_finalize, - NULL, /* call */ - NULL, /* hasInstance */ - NULL, /* construct */ - fundamental_trace +// clang-format off +const struct JSClassOps FundamentalBase::class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + &FundamentalBase::resolve, + nullptr, // mayResolve + &FundamentalBase::finalize, + nullptr, // call + nullptr, // hasInstance + nullptr, // construct + &FundamentalBase::trace }; -struct JSClass gjs_fundamental_instance_class = { +const struct JSClass FundamentalBase::klass = { "GFundamental_Object", JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, - &gjs_fundamental_class_ops -}; - -static JSPropertySpec gjs_fundamental_instance_proto_props[] = { - JS_PS_END -}; - -static JSFunctionSpec gjs_fundamental_instance_proto_funcs[] = { - JS_FS("toString", to_string_func, 0, 0), - JS_FS_END + &FundamentalBase::class_ops }; +// clang-format on +GJS_JSAPI_RETURN_CONVENTION static JSObject * gjs_lookup_fundamental_prototype(JSContext *context, GIObjectInfo *info, @@ -591,323 +287,195 @@ gjs_lookup_fundamental_prototype(JSContext *context, } if (G_UNLIKELY (!in_object)) - return NULL; + return nullptr; JS::RootedValue value(context); if (!JS_GetProperty(context, in_object, constructor_name, &value)) - return NULL; + return nullptr; JS::RootedObject constructor(context); if (value.isUndefined()) { /* In case we're looking for a private type, and we don't find it, we need to define it first. */ - JS::RootedObject ignored(context); - gjs_define_fundamental_class(context, in_object, info, &constructor, - &ignored); + if (!FundamentalPrototype::define_class(context, in_object, info, + &constructor)) + return nullptr; } else { - if (G_UNLIKELY (!value.isObject())) - return NULL; + if (G_UNLIKELY(!value.isObject())) { + gjs_throw(context, + "Fundamental constructor was not an object, it was a %s", + JS::InformalValueTypeName(value)); + return nullptr; + } constructor = &value.toObject(); } g_assert(constructor); - if (!gjs_object_get_property(context, constructor, - GJS_STRING_PROTOTYPE, &value)) - return NULL; - - if (G_UNLIKELY (!value.isObjectOrNull())) - return NULL; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + JS::RootedObject prototype(context); + if (!gjs_object_require_property(context, constructor, "constructor object", + atoms.prototype(), &prototype)) + return nullptr; - return value.toObjectOrNull(); + return prototype; } +GJS_JSAPI_RETURN_CONVENTION static JSObject* gjs_lookup_fundamental_prototype_from_gtype(JSContext *context, GType gtype) { - GIObjectInfo *info; - JSObject *proto; + GjsAutoObjectInfo info; /* A given gtype might not have any definition in the introspection * data. If that's the case, try to look for a definition of any of the * parent type. */ - while ((info = (GIObjectInfo *) - g_irepository_find_by_gtype(g_irepository_get_default(), - gtype)) == NULL && - gtype != G_TYPE_INVALID) + while (gtype != G_TYPE_INVALID && + !(info = g_irepository_find_by_gtype(nullptr, gtype))) gtype = g_type_parent(gtype); - proto = gjs_lookup_fundamental_prototype(context, info, gtype); - if (info) - g_base_info_unref((GIBaseInfo*)info); + return gjs_lookup_fundamental_prototype(context, info, gtype); +} - return proto; +// Overrides GIWrapperPrototype::get_parent_proto(). +bool FundamentalPrototype::get_parent_proto( + JSContext* cx, JS::MutableHandleObject proto) const { + GType parent_gtype = g_type_parent(gtype()); + if (parent_gtype != G_TYPE_INVALID) { + proto.set( + gjs_lookup_fundamental_prototype_from_gtype(cx, parent_gtype)); + if (!proto) + return false; + } + return true; } -bool -gjs_define_fundamental_class(JSContext *context, - JS::HandleObject in_object, - GIObjectInfo *info, - JS::MutableHandleObject constructor, - JS::MutableHandleObject prototype) -{ - const char *constructor_name; - JS::RootedId js_constructor_name(context); - JS::RootedObject parent_proto(context); - Fundamental *priv; - GType parent_gtype; - GType gtype; - GIFunctionInfo *constructor_info; - /* See the comment in gjs_define_object_class() for an explanation - * of how this all works; Fundamental is pretty much the same as - * Object. - */ +// Overrides GIWrapperPrototype::constructor_nargs(). +unsigned FundamentalPrototype::constructor_nargs(void) const { + if (m_constructor_info) + return g_callable_info_get_n_args(m_constructor_info); + return 0; +} - constructor_name = g_base_info_get_name((GIBaseInfo *) info); - constructor_info = find_fundamental_constructor(context, info, - &js_constructor_name); +/* + * FundamentalPrototype::define_class: + * @in_object: Object where the constructor is stored, typically a repo object. + * @info: Introspection info for the fundamental class. + * @constructor: Return location for the constructor object. + * + * Define a fundamental class constructor and prototype, including all the + * necessary methods and properties. Provides the constructor object as an out + * parameter, for convenience elsewhere. + */ +bool FundamentalPrototype::define_class(JSContext* cx, + JS::HandleObject in_object, + GIObjectInfo* info, + JS::MutableHandleObject constructor) { + GType gtype; gtype = g_registered_type_info_get_g_type (info); - parent_gtype = g_type_parent(gtype); - if (parent_gtype != G_TYPE_INVALID) - parent_proto = gjs_lookup_fundamental_prototype_from_gtype(context, - parent_gtype); - - if (!gjs_init_class_dynamic(context, in_object, - /* parent prototype JSObject* for - * prototype; NULL for - * Object.prototype - */ - parent_proto, - g_base_info_get_namespace((GIBaseInfo *) info), - constructor_name, - &gjs_fundamental_instance_class, - gjs_fundamental_instance_constructor, - /* number of constructor args (less can be passed) */ - constructor_info != NULL ? g_callable_info_get_n_args((GICallableInfo *) constructor_info) : 0, - /* props of prototype */ - parent_proto ? NULL : &gjs_fundamental_instance_proto_props[0], - /* funcs of prototype */ - parent_proto ? NULL : &gjs_fundamental_instance_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - NULL, - prototype, - constructor)) { - gjs_log_exception(context); - g_error("Can't init class %s", constructor_name); - } - /* Put the info in the prototype */ - priv = g_slice_new0(Fundamental); - new (priv) Fundamental(); - g_assert(priv != NULL); - g_assert(priv->info == NULL); - priv->info = g_base_info_ref((GIBaseInfo *) info); - priv->gtype = gtype; - priv->constructor_name = js_constructor_name; - priv->constructor_info = constructor_info; - priv->ref_function = g_object_info_get_ref_function_pointer(info); - g_assert(priv->ref_function != NULL); - priv->unref_function = g_object_info_get_unref_function_pointer(info); - g_assert(priv->unref_function != NULL); - priv->set_value_function = g_object_info_get_set_value_function_pointer(info); - g_assert(priv->set_value_function != NULL); - priv->get_value_function = g_object_info_get_get_value_function_pointer(info); - g_assert(priv->get_value_function != NULL); - JS_SetPrivate(prototype, priv); - - gjs_debug(GJS_DEBUG_GFUNDAMENTAL, - "Defined class %s prototype is %p class %p in object %p constructor %s.%s.%s", - constructor_name, prototype.get(), JS_GetClass(prototype), - in_object.get(), - constructor_info != NULL ? g_base_info_get_namespace(constructor_info) : "unknown", - constructor_info != NULL ? g_base_info_get_name(g_base_info_get_container(constructor_info)) : "unknown", - constructor_info != NULL ? g_base_info_get_name(constructor_info) : "unknown"); - - if (g_object_info_get_n_fields(priv->info) > 0) { + JS::RootedObject prototype(cx); + FundamentalPrototype* priv = FundamentalPrototype::create_class( + cx, in_object, info, gtype, constructor, &prototype); + if (!priv) + return false; + + if (g_object_info_get_n_fields(info) > 0) { gjs_debug(GJS_DEBUG_GFUNDAMENTAL, "Fundamental type '%s.%s' apparently has accessible fields. " "Gjs has no support for this yet, ignoring these.", - g_base_info_get_namespace((GIBaseInfo *)priv->info), - g_base_info_get_name ((GIBaseInfo *)priv->info)); + priv->ns(), priv->name()); } - gjs_object_define_static_methods(context, constructor, gtype, info); - - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, gtype)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, JSPROP_PERMANENT); - return true; } -JSObject* -gjs_object_from_g_fundamental(JSContext *context, - GIObjectInfo *info, - void *gfundamental) -{ - if (gfundamental == NULL) - return NULL; +/* + * FundamentalInstance::object_for_c_ptr: + * + * Given a pointer to a C fundamental object, returns a JS object. This JS + * object may have been cached, or it may be newly created. + */ +JSObject* FundamentalInstance::object_for_c_ptr(JSContext* context, + void* gfundamental) { + if (!gfundamental) { + gjs_throw(context, "Cannot get JSObject for null fundamental pointer"); + return nullptr; + } - JS::RootedObject object(context, _fundamental_lookup_object(gfundamental)); - if (object) - return object; + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + auto p = gjs->fundamental_table().lookup(gfundamental); + if (p) + return p->value(); gjs_debug_marshal(GJS_DEBUG_GFUNDAMENTAL, - "Wrapping fundamental %s.%s %p with JSObject", - g_base_info_get_namespace((GIBaseInfo *) info), - g_base_info_get_name((GIBaseInfo *) info), - gfundamental); + "Wrapping fundamental %p with JSObject", gfundamental); JS::RootedObject proto(context, gjs_lookup_fundamental_prototype_from_gtype(context, G_TYPE_FROM_INSTANCE(gfundamental))); if (!proto) - return NULL; + return nullptr; - object = JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto); + JS::RootedObject object(context, JS_NewObjectWithGivenProto( + context, JS_GetClass(proto), proto)); if (!object) - goto out; + return nullptr; - init_fundamental_instance(context, object); + auto* priv = FundamentalInstance::new_for_js_object(context, object); - associate_js_instance_to_fundamental(context, object, gfundamental, false); + if (!priv->associate_js_instance(context, object, gfundamental)) + return nullptr; - out: return object; } -JSObject * -gjs_fundamental_from_g_value(JSContext *context, - const GValue *value, - GType gtype) -{ - Fundamental *proto_priv; - void *fobj; - - JS::RootedObject proto(context, - gjs_lookup_fundamental_prototype_from_gtype(context, gtype)); +/* + * FundamentalPrototype::for_gtype: + * + * Returns the FundamentalPrototype instance associated with the given GType. + * Use this if you don't have the prototype object. + */ +FundamentalPrototype* FundamentalPrototype::for_gtype(JSContext* cx, + GType gtype) { + JS::RootedObject proto( + cx, gjs_lookup_fundamental_prototype_from_gtype(cx, gtype)); if (!proto) - return NULL; - - proto_priv = (Fundamental *) priv_from_js(context, proto); - - fobj = proto_priv->get_value_function(value); - if (!fobj) { - gjs_throw(context, - "Failed to convert GValue to a fundamental instance"); - return NULL; - } + return nullptr; - return gjs_object_from_g_fundamental(context, proto_priv->info, fobj); + return FundamentalPrototype::for_js(cx, proto); } -void* -gjs_g_fundamental_from_object(JSContext *context, - JS::HandleObject obj) -{ - FundamentalInstance *priv; - - if (!obj) - return NULL; - - priv = priv_from_js(context, obj); - - if (priv == NULL) { - gjs_throw(context, - "No introspection information for %p", obj.get()); - return NULL; - } - - if (priv->gfundamental == NULL) { - gjs_throw(context, - "Object is %s.%s.prototype, not an object instance - cannot convert to a fundamental instance", - g_base_info_get_namespace((GIBaseInfo *) priv->prototype->info), - g_base_info_get_name((GIBaseInfo *) priv->prototype->info)); - return NULL; +JSObject* FundamentalInstance::object_for_gvalue(JSContext* cx, + const GValue* value, + GType gtype) { + auto* proto_priv = FundamentalPrototype::for_gtype(cx, gtype); + void* fobj = proto_priv->call_get_value_function(value); + if (!fobj) { + gjs_throw(cx, "Failed to convert GValue to a fundamental instance"); + return nullptr; } - return priv->gfundamental; + return FundamentalInstance::object_for_c_ptr(cx, fobj); } -bool -gjs_typecheck_fundamental(JSContext *context, - JS::HandleObject object, - GType expected_gtype, - bool throw_error) -{ - FundamentalInstance *priv; - bool result; - - if (!do_base_typecheck(context, object, throw_error)) - return false; - - priv = priv_from_js(context, object); - g_assert(priv != NULL); - - if (fundamental_is_prototype(priv)) { - if (throw_error) { - Fundamental *proto_priv = (Fundamental *) priv; - gjs_throw(context, - "Object is %s.%s.prototype, not an fundamental instance - cannot convert to void*", - proto_priv->info ? g_base_info_get_namespace((GIBaseInfo *) proto_priv->info) : "", - proto_priv->info ? g_base_info_get_name((GIBaseInfo *) proto_priv->info) : g_type_name(proto_priv->gtype)); - } - +bool FundamentalBase::to_gvalue(JSContext* cx, JS::HandleObject obj, + GValue* gvalue) { + auto* priv = FundamentalBase::for_js_typecheck(cx, obj); + if (!priv || !priv->check_is_instance(cx, "convert to GValue")) return false; - } - - if (expected_gtype != G_TYPE_NONE) - result = g_type_is_a(priv->prototype->gtype, expected_gtype); - else - result = true; - - if (!result && throw_error) { - if (priv->prototype->info) { - 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, JSProto_TypeError, nullptr, - "Object is of type %s - cannot convert to %s", - g_type_name(priv->prototype->gtype), - g_type_name(expected_gtype)); - } - } - return result; -} - -void * -gjs_fundamental_ref(JSContext *context, - void *gfundamental) -{ - Fundamental *proto_priv; - JS::RootedObject proto(context, - gjs_lookup_fundamental_prototype_from_gtype(context, G_TYPE_FROM_INSTANCE(gfundamental))); - - proto_priv = (Fundamental *) priv_from_js(context, proto); - - return proto_priv->ref_function(gfundamental); + priv->to_instance()->set_value(gvalue); + return true; } -void -gjs_fundamental_unref(JSContext *context, - void *gfundamental) -{ - Fundamental *proto_priv; - JS::RootedObject proto(context, - gjs_lookup_fundamental_prototype_from_gtype(context, G_TYPE_FROM_INSTANCE(gfundamental))); - - proto_priv = (Fundamental *) priv_from_js(context, proto); - - proto_priv->unref_function(gfundamental); +void* FundamentalInstance::copy_ptr(JSContext* cx, GType gtype, + void* gfundamental) { + auto* priv = FundamentalPrototype::for_gtype(cx, gtype); + return priv->call_ref_function(gfundamental); } diff --git a/gi/fundamental.h b/gi/fundamental.h index c1b3ff8..7296c50 100644 --- a/gi/fundamental.h +++ b/gi/fundamental.h @@ -22,43 +22,164 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_FUNDAMENTAL_H__ -#define __GJS_FUNDAMENTAL_H__ +#ifndef GI_FUNDAMENTAL_H_ +#define GI_FUNDAMENTAL_H_ + +#include -#include -#include #include -#include "cjs/jsapi-util.h" +#include + +#include + +#include "gi/wrapperutils.h" +#include "cjs/macros.h" +#include "util/log.h" + +class FundamentalPrototype; +class FundamentalInstance; +namespace JS { class CallArgs; } + +/* To conserve memory, we have two different kinds of private data for JS + * wrappers for fundamental types: FundamentalInstance, and + * FundamentalPrototype. Both inherit from FundamentalBase for their common + * functionality. For more information, see the notes in wrapperutils.h. + */ + +class FundamentalBase + : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit FundamentalBase(FundamentalPrototype* proto = nullptr) + : GIWrapperBase(proto) {} + ~FundamentalBase(void) {} + + static const GjsDebugTopic debug_topic = GJS_DEBUG_GFUNDAMENTAL; + static constexpr const char* debug_tag = "fundamental"; + + static const struct JSClassOps class_ops; + static const struct JSClass klass; + + // Helper methods + + [[nodiscard]] const char* to_string_kind() const { return "fundamental"; } + + // Public API + + public: + GJS_JSAPI_RETURN_CONVENTION + static bool to_gvalue(JSContext* cx, JS::HandleObject obj, GValue* gvalue); +}; + +class FundamentalPrototype + : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; + + GIObjectInfoRefFunction m_ref_function; + GIObjectInfoUnrefFunction m_unref_function; + GIObjectInfoGetValueFunction m_get_value_function; + GIObjectInfoSetValueFunction m_set_value_function; + GICallableInfo* m_constructor_info; + + explicit FundamentalPrototype(GIObjectInfo* info, GType gtype); + ~FundamentalPrototype(void); + + static constexpr InfoType::Tag info_type_tag = InfoType::Object; + + public: + GJS_JSAPI_RETURN_CONVENTION + static FundamentalPrototype* for_gtype(JSContext* cx, GType gtype); + + // Accessors + + [[nodiscard]] GICallableInfo* constructor_info() const { + return m_constructor_info; + } + + void* call_ref_function(void* ptr) const { return m_ref_function(ptr); } + void call_unref_function(void* ptr) const { m_unref_function(ptr); } + [[nodiscard]] void* call_get_value_function(const GValue* value) const { + return m_get_value_function(value); + } + void call_set_value_function(GValue* value, void* object) const { + m_set_value_function(value, object); + } + + // Helper methods + + private: + GJS_JSAPI_RETURN_CONVENTION + bool get_parent_proto(JSContext* cx, JS::MutableHandleObject proto) const; + + [[nodiscard]] unsigned constructor_nargs() const; + + GJS_JSAPI_RETURN_CONVENTION + bool resolve_interface(JSContext* cx, JS::HandleObject obj, bool* resolved, + const char* name); + + // JSClass operations + + GJS_JSAPI_RETURN_CONVENTION + bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* prop_name, bool* resolved); + + // Public API + public: + GJS_JSAPI_RETURN_CONVENTION + static bool define_class(JSContext* cx, JS::HandleObject in_object, + GIObjectInfo* info, + JS::MutableHandleObject constructor); +}; + +class FundamentalInstance + : public GIWrapperInstance { + friend class FundamentalBase; // for set_value() + friend class GIWrapperInstance; + friend class GIWrapperBase; + + explicit FundamentalInstance(JSContext* cx, JS::HandleObject obj); + ~FundamentalInstance(void); -G_BEGIN_DECLS + // Helper methods -bool gjs_define_fundamental_class(JSContext *context, - JS::HandleObject in_object, - GIObjectInfo *info, - JS::MutableHandleObject constructor, - JS::MutableHandleObject prototype); + GJS_JSAPI_RETURN_CONVENTION + bool invoke_constructor(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args, GIArgument* rvalue); -JSObject* gjs_object_from_g_fundamental (JSContext *context, - GIObjectInfo *info, - void *fobj); + void ref(void) { get_prototype()->call_ref_function(m_ptr); } + void unref(void) { get_prototype()->call_unref_function(m_ptr); } + void set_value(GValue* gvalue) const { + get_prototype()->call_set_value_function(gvalue, m_ptr); + } -void *gjs_g_fundamental_from_object(JSContext *context, - JS::HandleObject obj); + GJS_JSAPI_RETURN_CONVENTION + bool associate_js_instance(JSContext* cx, JSObject* object, + void* gfundamental); -JSObject *gjs_fundamental_from_g_value (JSContext *context, - const GValue *value, - GType gtype); + // JS constructor -bool gjs_typecheck_fundamental(JSContext *context, - JS::HandleObject object, - GType expected_gtype, - bool throw_error); + GJS_JSAPI_RETURN_CONVENTION + bool constructor_impl(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args); -void* gjs_fundamental_ref (JSContext *context, - void *fobj); -void gjs_fundamental_unref (JSContext *context, - void *fobj); + public: + GJS_JSAPI_RETURN_CONVENTION + static JSObject* object_for_c_ptr(JSContext* cx, void* gfundamental); + GJS_JSAPI_RETURN_CONVENTION + static JSObject* object_for_gvalue(JSContext* cx, const GValue* gvalue, + GType gtype); -G_END_DECLS + static void* copy_ptr(JSContext* cx, GType gtype, void* gfundamental); +}; -#endif /* __GJS_FUNDAMENTAL_H__ */ +#endif // GI_FUNDAMENTAL_H_ diff --git a/gi/gerror.cpp b/gi/gerror.cpp index db94367..f60c218 100644 --- a/gi/gerror.cpp +++ b/gi/gerror.cpp @@ -23,216 +23,164 @@ #include -#include - -#include "boxed.h" -#include "enumeration.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" -#include "repo.h" -#include "gerror.h" -#include "util/error.h" - -#include +#include #include +#include + +#include +#include +#include // for JSPROP_ENUMERATE +#include +#include +#include +#include // for UniqueChars +#include +#include +#include // for JS_DefinePropertyById, JS_GetProp... +#include // for JSProtoKey, JSProto_Error, JSProt... + +#include "gi/arg-inl.h" +#include "gi/boxed.h" +#include "gi/enumeration.h" +#include "gi/gerror.h" +#include "gi/repo.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/error-types.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" + +ErrorPrototype::ErrorPrototype(GIEnumInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype), + m_domain(g_quark_from_string(g_enum_info_get_error_domain(info))) { + GJS_INC_COUNTER(gerror_prototype); +} -typedef struct { - GIEnumInfo *info; - GQuark domain; - GError *gerror; /* NULL if we are the prototype and not an instance */ -} Error; - -extern struct JSClass gjs_error_class; +ErrorPrototype::~ErrorPrototype(void) { GJS_DEC_COUNTER(gerror_prototype); } -static void define_error_properties(JSContext *, JS::HandleObject); +ErrorInstance::ErrorInstance(JSContext* cx, JS::HandleObject obj) + : GIWrapperInstance(cx, obj) { + GJS_INC_COUNTER(gerror_instance); +} -GJS_DEFINE_PRIV_FROM_JS(Error, gjs_error_class) +ErrorInstance::~ErrorInstance(void) { + g_clear_error(&m_ptr); + GJS_DEC_COUNTER(gerror_instance); +} -GJS_NATIVE_CONSTRUCTOR_DECLARE(error) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(error) - Error *priv; - Error *proto_priv; - int32_t code; +/* + * ErrorBase::domain: + * + * Fetches ErrorPrototype::domain() for instances as well as prototypes. + */ +GQuark ErrorBase::domain(void) const { return get_prototype()->domain(); } - /* Check early to avoid allocating memory for nothing */ - if (argc != 1 || !argv[0].isObject()) { +// See GIWrapperBase::constructor(). +bool ErrorInstance::constructor_impl(JSContext* context, + JS::HandleObject object, + const JS::CallArgs& argv) { + if (argv.length() != 1 || !argv[0].isObject()) { gjs_throw(context, "Invalid parameters passed to GError constructor, expected one object"); return false; } - GJS_NATIVE_CONSTRUCTOR_PRELUDE(error); - - priv = g_slice_new0(Error); - - GJS_INC_COUNTER(gerror); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - gjs_debug_lifecycle(GJS_DEBUG_GERROR, - "GError constructor, obj %p priv %p", - object.get(), priv); - - JS::RootedObject proto(context); - JS_GetPrototype(context, object, &proto); - gjs_debug_lifecycle(GJS_DEBUG_GERROR, "GError instance __proto__ is %p", - proto.get()); - - /* If we're the prototype, then post-construct we'll fill in priv->info. - * If we are not the prototype, though, then we'll get ->info from the - * prototype and then create a GObject if we don't have one already. - */ - proto_priv = priv_from_js(context, proto); - if (proto_priv == NULL) { - gjs_debug(GJS_DEBUG_GERROR, - "Bad prototype set on GError? Must match JSClass of object. JS error should have been reported."); - return false; - } - - priv->info = proto_priv->info; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->domain = proto_priv->domain; - JS::RootedObject params_obj(context, &argv[0].toObject()); - GjsAutoJSChar message; - if (!gjs_object_require_property(context, params_obj, - "GError constructor", - GJS_STRING_MESSAGE, &message)) + JS::UniqueChars message; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (!gjs_object_require_property(context, params_obj, "GError constructor", + atoms.message(), &message)) return false; - if (!gjs_object_require_property(context, params_obj, - "GError constructor", - GJS_STRING_CODE, &code)) + int32_t code; + if (!gjs_object_require_property(context, params_obj, "GError constructor", + atoms.code(), &code)) return false; - priv->gerror = g_error_new_literal(priv->domain, code, message); + m_ptr = g_error_new_literal(domain(), code, message.get()); /* We assume this error will be thrown in the same line as the constructor */ - define_error_properties(context, object); - - GJS_NATIVE_CONSTRUCTOR_FINISH(boxed); - - return true; + return gjs_define_error_properties(context, object); } -static void -error_finalize(JSFreeOp *fop, - JSObject *obj) -{ - Error *priv; - - priv = (Error*) JS_GetPrivate(obj); - gjs_debug_lifecycle(GJS_DEBUG_GERROR, - "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) - return; /* wrong class? */ - - g_clear_error (&priv->gerror); - - if (priv->info) { - g_base_info_unref( (GIBaseInfo*) priv->info); - priv->info = NULL; - } - - GJS_DEC_COUNTER(gerror); - g_slice_free(Error, priv); +/* + * ErrorBase::get_domain: + * + * JSNative property getter for `domain`. This property works on prototypes as + * well as instances. + */ +bool ErrorBase::get_domain(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ErrorBase, priv); + args.rval().setInt32(priv->domain()); + return true; } -static bool -error_get_domain(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, obj, Error, priv); - - if (priv == NULL) +// JSNative property getter for `message`. +bool ErrorBase::get_message(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ErrorBase, priv); + if (!priv->check_is_instance(cx, "get a field")) return false; - args.rval().setInt32(priv->domain); - return true; + return gjs_string_from_utf8(cx, priv->to_instance()->message(), + args.rval()); } -static bool -error_get_message(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, obj, Error, priv); - - if (priv == NULL) +// JSNative property getter for `code`. +bool ErrorBase::get_code(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ErrorBase, priv); + if (!priv->check_is_instance(cx, "get a field")) return false; - if (priv->gerror == NULL) { - /* Object is prototype, not instance */ - gjs_throw(context, "Can't get a field from a GError prototype"); - return false; - } - - return gjs_string_from_utf8(context, priv->gerror->message, args.rval()); + args.rval().setInt32(priv->to_instance()->code()); + return true; } -static bool -error_get_code(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, args, obj, Error, priv); +// JSNative implementation of `toString()`. +bool ErrorBase::to_string(JSContext* context, unsigned argc, JS::Value* vp) { + GJS_GET_THIS(context, argc, vp, rec, self); - if (priv == NULL) - return false; + GjsAutoChar descr; - if (priv->gerror == NULL) { - /* Object is prototype, not instance */ - gjs_throw(context, "Can't get a field from a GError prototype"); - return false; + // An error created via `new GLib.Error` will have a Boxed* private pointer, + // not an Error*, so we can't call regular to_string() on it. + if (BoxedBase::typecheck(context, self, nullptr, G_TYPE_ERROR, + GjsTypecheckNoThrow())) { + auto* gerror = BoxedBase::to_c_ptr(context, self); + if (!gerror) + return false; + descr = + g_strdup_printf("GLib.Error %s: %s", + g_quark_to_string(gerror->domain), gerror->message); + + return gjs_string_from_utf8(context, descr, rec.rval()); } - args.rval().setInt32(priv->gerror->code); - return true; -} - -static bool -error_to_string(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, rec, self, Error, priv); - - if (priv == NULL) + ErrorBase* priv = ErrorBase::for_js_typecheck(context, self, rec); + if (!priv) return 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 (priv->is_prototype()) { + descr = g_strdup_printf("%s.%s", priv->ns(), priv->name()); } 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); + descr = g_strdup_printf("%s.%s: %s", priv->ns(), priv->name(), + priv->to_instance()->message()); } return gjs_string_from_utf8(context, descr, rec.rval()); } -static bool -error_constructor_value_of(JSContext *context, - unsigned argc, - JS::Value *vp) -{ +// JSNative implementation of `valueOf()`. +bool ErrorBase::value_of(JSContext* context, unsigned argc, JS::Value* vp) { GJS_GET_THIS(context, argc, vp, rec, self); - Error *priv; JS::RootedObject prototype(context); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); if (!gjs_object_require_property(context, self, "constructor", - GJS_STRING_PROTOTYPE, &prototype)) { + atoms.prototype(), &prototype)) { /* This error message will be more informative */ JS_ClearPendingException(context); gjs_throw(context, "GLib.Error.valueOf() called on something that is not" @@ -240,137 +188,99 @@ error_constructor_value_of(JSContext *context, return false; } - priv = priv_from_js(context, prototype); - - if (priv == NULL) + ErrorBase* priv = ErrorBase::for_js_typecheck(context, prototype, rec); + if (!priv) return false; - rec.rval().setInt32(priv->domain); + rec.rval().setInt32(priv->domain()); return true; } - -/* The bizarre thing about this vtable is that it applies to both - * instances of the object, and to the prototype that instances of the - * class have. - */ -static const struct JSClassOps gjs_error_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - NULL, /* resolve */ - nullptr, /* mayResolve */ - error_finalize +// clang-format off +const struct JSClassOps ErrorBase::class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + &ErrorBase::finalize, }; -struct JSClass gjs_error_class = { +const struct JSClass ErrorBase::klass = { "GLib_Error", JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE, - &gjs_error_class_ops + &ErrorBase::class_ops }; /* We need to shadow all fields of GError, to prevent calling the getter from GBoxed (which would trash memory accessing the instance private data) */ -JSPropertySpec gjs_error_proto_props[] = { - JS_PSG("domain", error_get_domain, GJS_MODULE_PROP_FLAGS), - JS_PSG("code", error_get_code, GJS_MODULE_PROP_FLAGS), - JS_PSG("message", error_get_message, GJS_MODULE_PROP_FLAGS), +JSPropertySpec ErrorBase::proto_properties[] = { + JS_PSG("domain", &ErrorBase::get_domain, GJS_MODULE_PROP_FLAGS), + JS_PSG("code", &ErrorBase::get_code, GJS_MODULE_PROP_FLAGS), + JS_PSG("message", &ErrorBase::get_message, GJS_MODULE_PROP_FLAGS), JS_PS_END }; -JSFunctionSpec gjs_error_proto_funcs[] = { - JS_FS("toString", error_to_string, 0, GJS_MODULE_PROP_FLAGS), - JS_FS_END -}; - -static JSFunctionSpec gjs_error_constructor_funcs[] = { - JS_FS("valueOf", error_constructor_value_of, 0, GJS_MODULE_PROP_FLAGS), +JSFunctionSpec ErrorBase::static_methods[] = { + JS_FN("valueOf", &ErrorBase::value_of, 0, GJS_MODULE_PROP_FLAGS), JS_FS_END }; +// clang-format on + +// Overrides GIWrapperPrototype::get_parent_proto(). +bool ErrorPrototype::get_parent_proto(JSContext* cx, + JS::MutableHandleObject proto) const { + g_irepository_require(nullptr, "GLib", "2.0", GIRepositoryLoadFlags(0), + nullptr); + GjsAutoStructInfo glib_error_info = + g_irepository_find_by_name(nullptr, "GLib", "Error"); + proto.set(gjs_lookup_generic_prototype(cx, glib_error_info)); + return !!proto; +} -void -gjs_define_error_class(JSContext *context, - JS::HandleObject in_object, - GIEnumInfo *info) -{ - const char *constructor_name; - GIBoxedInfo *glib_error_info; +bool ErrorPrototype::define_class(JSContext* context, + JS::HandleObject in_object, + GIEnumInfo* info) { JS::RootedObject prototype(context), constructor(context); - Error *priv; - - /* See the comment in gjs_define_boxed_class() for an - * explanation of how this all works; Error is pretty much the - * same as Boxed (except that we inherit from GLib.Error). - */ - - constructor_name = g_base_info_get_name( (GIBaseInfo*) info); - - g_irepository_require(NULL, "GLib", "2.0", (GIRepositoryLoadFlags) 0, NULL); - glib_error_info = (GIBoxedInfo*) g_irepository_find_by_name(NULL, "GLib", "Error"); - JS::RootedObject parent_proto(context, - gjs_lookup_generic_prototype(context, glib_error_info)); - g_base_info_unref((GIBaseInfo*)glib_error_info); - - if (!gjs_init_class_dynamic(context, in_object, - parent_proto, - g_base_info_get_namespace( (GIBaseInfo*) info), - constructor_name, - &gjs_error_class, - gjs_error_constructor, 1, - /* props of prototype */ - &gjs_error_proto_props[0], - /* funcs of prototype */ - &gjs_error_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - &gjs_error_constructor_funcs[0], - &prototype, - &constructor)) { - gjs_log_exception(context); - g_error("Can't init class %s", constructor_name); - } - - GJS_INC_COUNTER(gerror); - priv = g_slice_new0(Error); - priv->info = info; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->domain = g_quark_from_string (g_enum_info_get_error_domain(priv->info)); - - JS_SetPrivate(prototype, priv); - - gjs_debug(GJS_DEBUG_GBOXED, "Defined class %s prototype is %p class %p in object %p", - constructor_name, prototype.get(), JS_GetClass(prototype), - in_object.get()); + if (!ErrorPrototype::create_class(context, in_object, info, G_TYPE_ERROR, + &constructor, &prototype)) + return false; - gjs_define_enum_values(context, constructor, priv->info); - gjs_define_enum_static_methods(context, constructor, priv->info); + // Define a toString() on the prototype, as it does not exist on the + // prototype of GLib.Error; and create_class() will not define it since we + // supply a parent in get_parent_proto(). + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + return JS_DefineFunctionById(context, prototype, atoms.to_string(), + &ErrorBase::to_string, 0, + GJS_MODULE_PROP_FLAGS) && + gjs_define_enum_values(context, constructor, info); } -static GIEnumInfo * -find_error_domain_info(GQuark domain) -{ +[[nodiscard]] static GIEnumInfo* find_error_domain_info(GQuark domain) { GIEnumInfo *info; /* first an attempt without loading extra libraries */ - info = g_irepository_find_by_error_domain(NULL, domain); + info = g_irepository_find_by_error_domain(nullptr, domain); if (info) return info; /* load standard stuff */ - g_irepository_require(NULL, "GLib", "2.0", (GIRepositoryLoadFlags) 0, NULL); - g_irepository_require(NULL, "GObject", "2.0", (GIRepositoryLoadFlags) 0, NULL); - g_irepository_require(NULL, "Gio", "2.0", (GIRepositoryLoadFlags) 0, NULL); - info = g_irepository_find_by_error_domain(NULL, domain); + g_irepository_require(nullptr, "GLib", "2.0", GIRepositoryLoadFlags(0), + nullptr); + g_irepository_require(nullptr, "GObject", "2.0", GIRepositoryLoadFlags(0), + nullptr); + g_irepository_require(nullptr, "Gio", "2.0", GIRepositoryLoadFlags(0), + nullptr); + info = g_irepository_find_by_error_domain(nullptr, domain); if (info) return info; /* last attempt: load GIRepository (for invoke errors, rarely needed) */ - g_irepository_require(NULL, "GIRepository", "1.0", (GIRepositoryLoadFlags) 0, NULL); - info = g_irepository_find_by_error_domain(NULL, domain); + g_irepository_require(nullptr, "GIRepository", "1.0", + GIRepositoryLoadFlags(0), nullptr); + info = g_irepository_find_by_error_domain(nullptr, domain); return info; } @@ -378,46 +288,37 @@ find_error_domain_info(GQuark domain) /* define properties that JS Error() expose, such as fileName, lineNumber and stack */ -static void -define_error_properties(JSContext *cx, - JS::HandleObject obj) -{ +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_error_properties(JSContext* cx, JS::HandleObject obj) { JS::RootedObject frame(cx); JS::RootedString stack(cx); JS::RootedString source(cx); uint32_t line, column; - JS::AutoSaveExceptionState exc(cx); if (!JS::CaptureCurrentStack(cx, &frame) || - !JS::BuildStackString(cx, frame, &stack)) { - exc.restore(); - return; + !JS::BuildStackString(cx, nullptr, frame, &stack)) + return false; + + auto ok = JS::SavedFrameResult::Ok; + if (JS::GetSavedFrameSource(cx, nullptr, frame, &source) != ok || + JS::GetSavedFrameLine(cx, nullptr, frame, &line) != ok || + JS::GetSavedFrameColumn(cx, nullptr, frame, &column) != ok) { + gjs_throw(cx, "Error getting saved frame information"); + return false; } - JS::SavedFrameResult result; - result = JS::GetSavedFrameSource(cx, frame, &source); - g_assert(result == JS::SavedFrameResult::Ok); - - result = JS::GetSavedFrameLine(cx, frame, &line); - g_assert(result == JS::SavedFrameResult::Ok); - - result = JS::GetSavedFrameColumn(cx, frame, &column); - g_assert(result == JS::SavedFrameResult::Ok); - - if (!gjs_object_define_property(cx, obj, GJS_STRING_STACK, stack, - JSPROP_ENUMERATE) || - !gjs_object_define_property(cx, obj, GJS_STRING_FILENAME, source, - JSPROP_ENUMERATE) || - !gjs_object_define_property(cx, obj, GJS_STRING_LINE_NUMBER, line, - JSPROP_ENUMERATE) || - !gjs_object_define_property(cx, obj, GJS_STRING_COLUMN_NUMBER, column, - JSPROP_ENUMERATE)) - exc.restore(); + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + return JS_DefinePropertyById(cx, obj, atoms.stack(), stack, + JSPROP_ENUMERATE) && + JS_DefinePropertyById(cx, obj, atoms.file_name(), source, + JSPROP_ENUMERATE) && + JS_DefinePropertyById(cx, obj, atoms.line_number(), line, + JSPROP_ENUMERATE) && + JS_DefinePropertyById(cx, obj, atoms.column_number(), column, + JSPROP_ENUMERATE); } -static JSProtoKey -proto_key_from_error_enum(int val) -{ +[[nodiscard]] static JSProtoKey proto_key_from_error_enum(int val) { switch (val) { case GJS_JS_ERROR_EVAL_ERROR: return JSProto_EvalError; @@ -427,8 +328,6 @@ proto_key_from_error_enum(int val) 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: @@ -441,11 +340,12 @@ proto_key_from_error_enum(int val) } } +GJS_JSAPI_RETURN_CONVENTION static JSObject * gjs_error_from_js_gerror(JSContext *cx, GError *gerror) { - JS::AutoValueArray<1> error_args(cx); + JS::RootedValueArray<1> error_args(cx); if (!gjs_string_from_utf8(cx, gerror->message, error_args[0])) return nullptr; @@ -457,17 +357,11 @@ gjs_error_from_js_gerror(JSContext *cx, return JS_New(cx, error_constructor, error_args); } -JSObject* -gjs_error_from_gerror(JSContext *context, - GError *gerror, - bool add_stack) -{ - Error *priv; - Error *proto_priv; +JSObject* ErrorInstance::object_for_c_ptr(JSContext* context, GError* gerror) { GIEnumInfo *info; - if (gerror == NULL) - return NULL; + if (!gerror) + return nullptr; if (gerror->domain == GJS_JS_ERROR) return gjs_error_from_js_gerror(context, gerror); @@ -480,8 +374,8 @@ gjs_error_from_gerror(JSContext *context, GIBaseInfo *glib_boxed; JSObject *retval; - glib_boxed = g_irepository_find_by_name(NULL, "GLib", "Error"); - retval = gjs_boxed_from_c_struct(context, glib_boxed, gerror, (GjsBoxedCreationFlags) 0); + glib_boxed = g_irepository_find_by_name(nullptr, "GLib", "Error"); + retval = BoxedInstance::new_for_c_struct(context, glib_boxed, gerror); g_base_info_unref(glib_boxed); return retval; @@ -491,106 +385,136 @@ gjs_error_from_gerror(JSContext *context, "Wrapping struct %s with JSObject", g_base_info_get_name((GIBaseInfo *)info)); - JS::RootedObject proto(context, gjs_lookup_generic_prototype(context, info)); - proto_priv = priv_from_js(context, proto); - JS::RootedObject obj(context, - JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto)); - - GJS_INC_COUNTER(gerror); - priv = g_slice_new0(Error); - JS_SetPrivate(obj, priv); - priv->info = info; - priv->domain = proto_priv->domain; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->gerror = g_error_copy(gerror); + gjs_new_object_with_generic_prototype(context, info)); + if (!obj) + return nullptr; - if (add_stack) - define_error_properties(context, obj); + ErrorInstance* priv = ErrorInstance::new_for_js_object(context, obj); + priv->copy_gerror(gerror); return obj; } -GError* -gjs_gerror_from_error(JSContext *context, - JS::HandleObject obj) -{ - Error *priv; - - if (!obj) - return NULL; - +GError* ErrorBase::to_c_ptr(JSContext* cx, JS::HandleObject obj) { /* If this is a plain GBoxed (i.e. a GError without metadata), delegate marshalling. */ - if (gjs_typecheck_boxed (context, obj, NULL, G_TYPE_ERROR, false)) - return (GError*) gjs_c_struct_from_boxed (context, obj); + if (BoxedBase::typecheck(cx, obj, nullptr, G_TYPE_ERROR, + GjsTypecheckNoThrow())) + return BoxedBase::to_c_ptr(cx, obj); + + return GIWrapperBase::to_c_ptr(cx, obj); +} - priv = priv_from_js(context, obj); +bool ErrorBase::transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj, + GIArgument* arg, + GIDirection transfer_direction, + GITransfer transfer_ownership) { + g_assert(transfer_direction != GI_DIRECTION_INOUT && + "transfer_to_gi_argument() must choose between in or out"); - if (priv == NULL) - return NULL; + if (!ErrorBase::typecheck(cx, obj)) { + gjs_arg_unset(arg); + return false; + } + + gjs_arg_set(arg, ErrorBase::to_c_ptr(cx, obj)); + if (!gjs_arg_get(arg)) + return false; - if (priv->gerror == NULL) { - gjs_throw(context, - "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)); - return NULL; + if ((transfer_direction == GI_DIRECTION_IN && + transfer_ownership != GI_TRANSFER_NOTHING) || + (transfer_direction == GI_DIRECTION_OUT && + transfer_ownership == GI_TRANSFER_EVERYTHING)) { + gjs_arg_set(arg, ErrorInstance::copy_ptr(cx, G_TYPE_ERROR, + gjs_arg_get(arg))); + if (!gjs_arg_get(arg)) + return false; } - return priv->gerror; + return true; } -bool -gjs_typecheck_gerror (JSContext *context, - JS::HandleObject obj, - bool throw_error) -{ - if (gjs_typecheck_boxed (context, obj, NULL, G_TYPE_ERROR, false)) +// Overrides GIWrapperBase::typecheck() +bool ErrorBase::typecheck(JSContext* cx, JS::HandleObject obj) { + if (BoxedBase::typecheck(cx, obj, nullptr, G_TYPE_ERROR, + GjsTypecheckNoThrow())) return true; + return GIWrapperBase::typecheck(cx, obj, nullptr, G_TYPE_ERROR); +} - return do_base_typecheck(context, obj, throw_error); +bool ErrorBase::typecheck(JSContext* cx, JS::HandleObject obj, + GjsTypecheckNoThrow no_throw) { + if (BoxedBase::typecheck(cx, obj, nullptr, G_TYPE_ERROR, no_throw)) + return true; + return GIWrapperBase::typecheck(cx, obj, nullptr, G_TYPE_ERROR, no_throw); } GError * gjs_gerror_make_from_error(JSContext *cx, JS::HandleObject obj) { - using AutoEnumClass = std::unique_ptr; - - if (gjs_typecheck_gerror(cx, obj, false)) { + if (ErrorBase::typecheck(cx, obj, GjsTypecheckNoThrow())) { /* This is already a GError, just copy it */ - GError *inner = gjs_gerror_from_error(cx, obj); + GError* inner = ErrorBase::to_c_ptr(cx, obj); + if (!inner) + return nullptr; return g_error_copy(inner); } /* Try to make something useful from the error name and message (in case this is a JS error) */ + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); JS::RootedValue v_name(cx); - if (!gjs_object_get_property(cx, obj, GJS_STRING_NAME, &v_name)) + if (!JS_GetPropertyById(cx, obj, atoms.name(), &v_name)) return nullptr; - GjsAutoJSChar name; - if (!gjs_string_to_utf8(cx, v_name, &name)) + JS::UniqueChars name = gjs_string_to_utf8(cx, v_name); + if (!name) return nullptr; JS::RootedValue v_message(cx); - if (!gjs_object_get_property(cx, obj, GJS_STRING_MESSAGE, &v_message)) + if (!JS_GetPropertyById(cx, obj, atoms.message(), &v_message)) return nullptr; - GjsAutoJSChar message; - if (!gjs_string_to_utf8(cx, v_message, &message)) + JS::UniqueChars message = gjs_string_to_utf8(cx, v_message); + if (!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); + GjsAutoTypeClass klass(GJS_TYPE_JS_ERROR); + const GEnumValue *value = g_enum_get_value_by_name(klass, name.get()); int code; if (value) code = value->value; else code = GJS_JS_ERROR_ERROR; - return g_error_new_literal(GJS_JS_ERROR, code, message); + return g_error_new_literal(GJS_JS_ERROR, code, message.get()); +} + +/* + * gjs_throw_gerror: + * + * Converts a GError into a JavaScript exception, and frees the GError. + * Differently from gjs_throw(), it will overwrite an existing exception, as it + * is used to report errors from C functions. + * + * Returns: false, for convenience in returning from the calling function. + */ +bool gjs_throw_gerror(JSContext* cx, GError* error) { + // return false even if the GError is null, as presumably something failed + // in the calling code, and the caller expects to throw. + g_return_val_if_fail(error, false); + + JS::RootedObject err_obj(cx, ErrorInstance::object_for_c_ptr(cx, error)); + if (!err_obj || !gjs_define_error_properties(cx, err_obj)) + return false; + + g_error_free(error); + + JS::RootedValue err(cx, JS::ObjectValue(*err_obj)); + JS_SetPendingException(cx, err); + + return false; } diff --git a/gi/gerror.h b/gi/gerror.h index 15f56cf..166622f 100644 --- a/gi/gerror.h +++ b/gi/gerror.h @@ -21,32 +21,166 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_ERROR_H__ -#define __GJS_ERROR_H__ +#ifndef GI_GERROR_H_ +#define GI_GERROR_H_ + +#include -#include -#include #include +#include +#include + +#include +#include + +#include "gi/wrapperutils.h" +#include "cjs/macros.h" +#include "util/log.h" + +class ErrorPrototype; +class ErrorInstance; +namespace JS { +class CallArgs; +} + +/* To conserve memory, we have two different kinds of private data for GError + * JS wrappers: ErrorInstance, and ErrorPrototype. Both inherit from ErrorBase + * for their common functionality. For more information, see the notes in + * wrapperutils.h. + * + * ErrorPrototype, unlike the other GIWrapperPrototype subclasses, represents a + * single error domain instead of a single GType. All Errors have a GType of + * G_TYPE_ERROR. + * + * Note that in some situations GError structs can show up as BoxedInstance + * instead of ErrorInstance. We have some special cases in this code to deal + * with that. + */ + +class ErrorBase + : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit ErrorBase(ErrorPrototype* proto = nullptr) + : GIWrapperBase(proto) {} + ~ErrorBase(void) {} + + static const GjsDebugTopic debug_topic = GJS_DEBUG_GERROR; + static constexpr const char* debug_tag = "gerror"; + + static const struct JSClassOps class_ops; + static const struct JSClass klass; + static JSPropertySpec proto_properties[]; + static JSFunctionSpec static_methods[]; + + // Accessors + + public: + [[nodiscard]] GQuark domain(void) const; + + // Property getters + + protected: + GJS_JSAPI_RETURN_CONVENTION + static bool get_domain(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool get_message(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool get_code(JSContext* cx, unsigned argc, JS::Value* vp); + + // JS methods + + GJS_JSAPI_RETURN_CONVENTION + static bool value_of(JSContext* cx, unsigned argc, JS::Value* vp); -#include "cjs/jsapi-util.h" + public: + GJS_JSAPI_RETURN_CONVENTION + static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp); -G_BEGIN_DECLS + // Helper methods -void gjs_define_error_class (JSContext *context, - JS::HandleObject in_object, - GIEnumInfo *info); -GError* gjs_gerror_from_error (JSContext *context, - JS::HandleObject obj); -JSObject* gjs_error_from_gerror (JSContext *context, - GError *gerror, - bool add_stack); -bool gjs_typecheck_gerror (JSContext *context, - JS::HandleObject obj, - bool throw_error); + GJS_JSAPI_RETURN_CONVENTION + static GError* to_c_ptr(JSContext* cx, JS::HandleObject obj); + GJS_JSAPI_RETURN_CONVENTION + static bool transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj, + GIArgument* arg, + GIDirection transfer_direction, + GITransfer transfer_ownership); + + GJS_JSAPI_RETURN_CONVENTION + static bool typecheck(JSContext* cx, JS::HandleObject obj); + [[nodiscard]] static bool typecheck(JSContext* cx, JS::HandleObject obj, + GjsTypecheckNoThrow); +}; + +class ErrorPrototype : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; + + GQuark m_domain; + + static constexpr InfoType::Tag info_type_tag = InfoType::Enum; + + explicit ErrorPrototype(GIEnumInfo* info, GType gtype); + ~ErrorPrototype(void); + + GJS_JSAPI_RETURN_CONVENTION + bool get_parent_proto(JSContext* cx, JS::MutableHandleObject proto) const; + + public: + [[nodiscard]] GQuark domain(void) const { return m_domain; } + + GJS_JSAPI_RETURN_CONVENTION + static bool define_class(JSContext* cx, JS::HandleObject in_object, + GIEnumInfo* info); +}; + +class ErrorInstance : public GIWrapperInstance { + friend class GIWrapperInstance; + friend class GIWrapperBase; + + explicit ErrorInstance(JSContext* cx, JS::HandleObject obj); + ~ErrorInstance(void); + + public: + void copy_gerror(GError* other) { m_ptr = g_error_copy(other); } + GJS_JSAPI_RETURN_CONVENTION + static GError* copy_ptr(JSContext*, GType, void* ptr) { + return g_error_copy(static_cast(ptr)); + } + + // Accessors + + [[nodiscard]] const char* message(void) const { return m_ptr->message; } + [[nodiscard]] int code(void) const { return m_ptr->code; } + + // JS constructor + + private: + GJS_JSAPI_RETURN_CONVENTION + bool constructor_impl(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args); + + // Public API + + public: + GJS_JSAPI_RETURN_CONVENTION + static JSObject* object_for_c_ptr(JSContext* cx, GError* gerror); +}; + +GJS_JSAPI_RETURN_CONVENTION GError *gjs_gerror_make_from_error(JSContext *cx, JS::HandleObject obj); -G_END_DECLS +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_error_properties(JSContext* cx, JS::HandleObject obj); + +bool gjs_throw_gerror(JSContext* cx, GError* error); -#endif /* __GJS_ERROR_H__ */ +#endif // GI_GERROR_H_ diff --git a/gi/gjs_gi_probes.d b/gi/gjs_gi_probes.d index 20e8ad3..79fd125 100644 --- a/gi/gjs_gi_probes.d +++ b/gi/gjs_gi_probes.d @@ -1,4 +1,4 @@ provider gjs { - probe object__proxy__new(void*, void*, char *, char *); - probe object__proxy__finalize(void*, void*, char *, char *); + probe object__wrapper__new(void*, void*, char *, char *); + probe object__wrapper__finalize(void*, void*, char *, char *); }; diff --git a/gi/gjs_gi_trace.h b/gi/gjs_gi_trace.h index 30f6f74..9e68654 100644 --- a/gi/gjs_gi_trace.h +++ b/gi/gjs_gi_trace.h @@ -23,13 +23,10 @@ * Author: Colin Walters */ +#ifndef GI_GJS_GI_TRACE_H_ +#define GI_GJS_GI_TRACE_H_ -#ifndef __GJS_TRACE_H__ -#define __GJS_TRACE_H__ - -#ifndef GETTEXT_PACKAGE -#error "config.h must be included prior to gjs_trace.h" -#endif +#include #ifdef HAVE_DTRACE @@ -44,4 +41,4 @@ #endif -#endif /* __GJS_TRACE_H__ */ +#endif // GI_GJS_GI_TRACE_H_ diff --git a/gi/gobject.cpp b/gi/gobject.cpp new file mode 100644 index 0000000..64054c8 --- /dev/null +++ b/gi/gobject.cpp @@ -0,0 +1,270 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2008 litl, LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include + +#include +#include // for move, pair + +#include +#include + +#include +#include +#include +#include // for JS_New, JSAutoRealm, JS_GetProperty + +#include "gi/gobject.h" +#include "gi/object.h" +#include "gi/value.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" + +static std::unordered_map class_init_properties; + +[[nodiscard]] static JSContext* current_context() { + GjsContext* gjs = gjs_context_get_current(); + return static_cast(gjs_context_get_native_context(gjs)); +} + +void push_class_init_properties(GType gtype, AutoParamArray* params) { + class_init_properties[gtype] = std::move(*params); +} + +bool pop_class_init_properties(GType gtype, AutoParamArray* params_out) { + auto found = class_init_properties.find(gtype); + if (found == class_init_properties.end()) + return false; + + *params_out = std::move(found->second); + class_init_properties.erase(found); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object, + const GValue* value, GParamSpec* pspec) { + JS::RootedValue jsvalue(cx); + if (!gjs_value_from_g_value(cx, &jsvalue, value)) + return false; + + GjsAutoChar underscore_name = gjs_hyphen_to_underscore(pspec->name); + return JS_SetProperty(cx, object, underscore_name, jsvalue); +} + +static void gjs_object_base_init(void* klass) { + auto* priv = ObjectPrototype::for_gtype(G_OBJECT_CLASS_TYPE(klass)); + if (priv) + priv->ref_vfuncs(); +} + +static void gjs_object_base_finalize(void* klass) { + auto* priv = ObjectPrototype::for_gtype(G_OBJECT_CLASS_TYPE(klass)); + if (priv) + priv->unref_vfuncs(); +} + +static GObject* gjs_object_constructor( + GType type, unsigned n_construct_properties, + GObjectConstructParam* construct_properties) { + JSContext* cx = current_context(); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + + if (!gjs->object_init_list().empty()) { + GType parent_type = g_type_parent(type); + + /* The object is being constructed from JS: + * Simply chain up to the first non-gjs constructor + */ + while (G_OBJECT_CLASS(g_type_class_peek(parent_type))->constructor == + gjs_object_constructor) + parent_type = g_type_parent(parent_type); + + return G_OBJECT_CLASS(g_type_class_peek(parent_type)) + ->constructor(type, n_construct_properties, construct_properties); + } + + /* The object is being constructed from native code (e.g. GtkBuilder): + * Construct the JS object from the constructor, then use the GObject + * that was associated in gjs_object_custom_init() + */ + JSAutoRealm ar(cx, gjs_get_import_global(cx)); + + JS::RootedObject constructor( + cx, gjs_lookup_object_constructor_from_info(cx, nullptr, type)); + if (!constructor) + return nullptr; + + JSObject* object; + if (n_construct_properties) { + JS::RootedObject props_hash(cx, JS_NewPlainObject(cx)); + + for (unsigned i = 0; i < n_construct_properties; i++) + if (!jsobj_set_gproperty(cx, props_hash, + construct_properties[i].value, + construct_properties[i].pspec)) + return nullptr; + + JS::RootedValueArray<1> args(cx); + args[0].set(JS::ObjectValue(*props_hash)); + object = JS_New(cx, constructor, args); + } else { + object = JS_New(cx, constructor, JS::HandleValueArray::empty()); + } + + if (!object) + return nullptr; + + auto* priv = ObjectBase::for_js_nocheck(object); + /* Should have been set in init_impl() and pushed into object_init_list, + * then popped from object_init_list in gjs_object_custom_init() */ + g_assert(priv); + /* We only hold a toggle ref at this point, add back a ref that the + * native code can own. + */ + return G_OBJECT(g_object_ref(priv->to_instance()->ptr())); +} + +static void gjs_object_set_gproperty(GObject* object, + unsigned property_id [[maybe_unused]], + const GValue* value, GParamSpec* pspec) { + auto* priv = ObjectInstance::for_gobject(object); + JSContext *cx = current_context(); + + JS::RootedObject js_obj(cx, priv->wrapper()); + JSAutoRealm ar(cx, js_obj); + + if (!jsobj_set_gproperty(cx, js_obj, value, pspec)) + gjs_log_exception_uncaught(cx); +} + +static void gjs_object_get_gproperty(GObject* object, + unsigned property_id [[maybe_unused]], + GValue* value, GParamSpec* pspec) { + auto* priv = ObjectInstance::for_gobject(object); + JSContext *cx = current_context(); + + JS::RootedObject js_obj(cx, priv->wrapper()); + JS::RootedValue jsvalue(cx); + JSAutoRealm ar(cx, js_obj); + + GjsAutoChar underscore_name = gjs_hyphen_to_underscore(pspec->name); + if (!JS_GetProperty(cx, js_obj, underscore_name, &jsvalue)) { + gjs_log_exception_uncaught(cx); + return; + } + if (!gjs_value_to_g_value(cx, jsvalue, value)) + gjs_log_exception(cx); +} + +static void gjs_object_class_init(void* class_pointer, void*) { + GObjectClass* klass = G_OBJECT_CLASS(class_pointer); + 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; + + AutoParamArray properties; + if (!pop_class_init_properties(gtype, &properties)) + return; + + unsigned i = 0; + for (GjsAutoParam& pspec : properties) { + g_param_spec_set_qdata(pspec, ObjectBase::custom_property_quark(), + GINT_TO_POINTER(1)); + g_object_class_install_property(klass, ++i, pspec); + } +} + +static void gjs_object_custom_init(GTypeInstance* instance, + void* g_class [[maybe_unused]]) { + JSContext *cx = current_context(); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + + if (gjs->object_init_list().empty()) + return; + + JS::RootedObject object(cx, gjs->object_init_list().back()); + auto* priv_base = ObjectBase::for_js_nocheck(object); + g_assert(priv_base); // Should have been set in init_impl() + ObjectInstance* priv = priv_base->to_instance(); + + if (priv_base->gtype() != G_TYPE_FROM_INSTANCE(instance)) { + /* This is not the most derived instance_init function, + do nothing. + */ + return; + } + + gjs->object_init_list().popBack(); + + if (!priv->init_custom_class_from_gobject(cx, object, G_OBJECT(instance))) + gjs_log_exception_uncaught(cx); +} + +static void gjs_interface_init(void* g_iface, void*) { + GType gtype = G_TYPE_FROM_INTERFACE(g_iface); + + AutoParamArray properties; + if (!pop_class_init_properties(gtype, &properties)) + return; + + for (GjsAutoParam& pspec : properties) { + g_param_spec_set_qdata(pspec, ObjectBase::custom_property_quark(), + GINT_TO_POINTER(1)); + g_object_interface_install_property(g_iface, pspec); + } +} + +constexpr GTypeInfo gjs_gobject_class_info = { + 0, // class_size + + gjs_object_base_init, + gjs_object_base_finalize, + + gjs_object_class_init, + GClassFinalizeFunc(nullptr), + nullptr, // class_data + + 0, // instance_size + 0, // n_preallocs + gjs_object_custom_init, +}; + +constexpr GTypeInfo gjs_gobject_interface_info = { + sizeof(GTypeInterface), // class_size + + GBaseInitFunc(nullptr), + GBaseFinalizeFunc(nullptr), + + gjs_interface_init, + GClassFinalizeFunc(nullptr), + nullptr, // class_data + + 0, // instance_size + 0, // n_preallocs + nullptr, // instance_init +}; diff --git a/gi/proxyutils.h b/gi/gobject.h similarity index 66% rename from gi/proxyutils.h rename to gi/gobject.h index 4bffb8e..e4afd59 100644 --- a/gi/proxyutils.h +++ b/gi/gobject.h @@ -1,6 +1,6 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* - * Copyright (c) 2008 litl, LLC + * Copyright (c) 2018 Philip Chimento * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -20,22 +20,21 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ +#ifndef GI_GOBJECT_H_ +#define GI_GOBJECT_H_ -#ifndef __GJS_PROXYUTILS_H__ -#define __GJS_PROXYUTILS_H__ +#include + +#include #include "cjs/jsapi-util.h" -G_BEGIN_DECLS +using AutoParamArray = std::vector; -bool _gjs_proxy_to_string_func(JSContext *context, - JSObject *this_obj, - const char *objtype, - GIBaseInfo *info, - GType gtype, - gpointer native_address, - JS::MutableHandleValue ret); +extern const GTypeInfo gjs_gobject_class_info; +extern const GTypeInfo gjs_gobject_interface_info; -G_END_DECLS +void push_class_init_properties(GType gtype, AutoParamArray* params); +bool pop_class_init_properties(GType gtype, AutoParamArray* params_out); -#endif /* __GJS_OBJECT_H__ */ +#endif // GI_GOBJECT_H_ diff --git a/gi/gtype.cpp b/gi/gtype.cpp index b049815..4e1deb3 100644 --- a/gi/gtype.cpp +++ b/gi/gtype.cpp @@ -24,18 +24,30 @@ #include -#include - -#include "gtype.h" +#include // for (implicit) move + +#include +#include + +#include // for SystemAllocPolicy +#include +#include +#include // for WeakCache +#include // for JSPROP_PERMANENT +#include +#include +#include +#include // for JS_GetPropertyById, JS_AtomizeString +#include + +#include "gi/gtype.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" #include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include -#include - -static bool weak_pointer_callback = false; -static std::set weak_pointer_list; +#include "cjs/jsapi-util.h" -static JSObject *gjs_gtype_get_proto(JSContext *) G_GNUC_UNUSED; +[[nodiscard]] [[maybe_unused]] static JSObject* gjs_gtype_get_proto(JSContext*); +GJS_JSAPI_RETURN_CONVENTION static bool gjs_gtype_define_proto(JSContext *, JS::HandleObject, JS::MutableHandleObject); @@ -45,60 +57,12 @@ GJS_DEFINE_PROTO_ABSTRACT("GIRepositoryGType", gtype, /* priv_from_js adds a "*", so this returns "void *" */ GJS_DEFINE_PRIV_FROM_JS(void, gjs_gtype_class); -static GQuark -gjs_get_gtype_wrapper_quark(void) -{ - static gsize once_init = 0; - static GQuark value = 0; - if (g_once_init_enter(&once_init)) { - value = g_quark_from_string("gjs-gtype-wrapper"); - g_once_init_leave(&once_init, 1); - } - return value; -} - -static void -update_gtype_weak_pointers(JSContext *cx, - JSCompartment *compartment, - void *data) -{ - for (auto iter = weak_pointer_list.begin(); iter != weak_pointer_list.end(); ) { - auto heap_wrapper = static_cast *>(g_type_get_qdata(*iter, gjs_get_gtype_wrapper_quark())); - JS_UpdateWeakPointerAfterGC(heap_wrapper); - - /* No read barriers are needed if the only thing we are doing with the - * pointer is comparing it to nullptr. */ - if (heap_wrapper->unbarrieredGet() == nullptr) - iter = weak_pointer_list.erase(iter); - else - iter++; - } -} - -static void -ensure_weak_pointer_callback(JSContext *cx) -{ - if (!weak_pointer_callback) { - JS_AddWeakPointerCompartmentCallback(cx, update_gtype_weak_pointers, - nullptr); - weak_pointer_callback = true; - } -} - -static void -gjs_gtype_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GType gtype = GPOINTER_TO_SIZE(JS_GetPrivate(obj)); - - /* proto doesn't have a private set */ - if (G_UNLIKELY(gtype == 0)) - return; - - weak_pointer_list.erase(gtype); - g_type_set_qdata(gtype, gjs_get_gtype_wrapper_quark(), NULL); +static void gjs_gtype_finalize(JSFreeOp*, JSObject*) { + // No private data is allocated, it's stuffed directly in the private field + // of JSObject, so nothing to free } +GJS_JSAPI_RETURN_CONVENTION static bool to_string_func(JSContext *cx, unsigned argc, @@ -108,7 +72,8 @@ to_string_func(JSContext *cx, GType gtype = GPOINTER_TO_SIZE(priv); if (gtype == 0) { - JS::RootedString str(cx, JS_NewStringCopyZ(cx, "[object GType prototype]")); + JS::RootedString str(cx, + JS_AtomizeString(cx, "[object GType prototype]")); if (!str) return false; rec.rval().setString(str); @@ -120,6 +85,7 @@ to_string_func(JSContext *cx, return gjs_string_from_utf8(cx, strval, rec.rval()); } +GJS_JSAPI_RETURN_CONVENTION static bool get_name_func (JSContext *context, unsigned argc, @@ -140,14 +106,14 @@ get_name_func (JSContext *context, /* Properties */ JSPropertySpec gjs_gtype_proto_props[] = { JS_PSG("name", get_name_func, JSPROP_PERMANENT), + JS_STRING_SYM_PS(toStringTag, "GIRepositoryGType", JSPROP_READONLY), JS_PS_END, }; /* Functions */ JSFunctionSpec gjs_gtype_proto_funcs[] = { - JS_FS("toString", to_string_func, 0, 0), - JS_FS_END -}; + JS_FN("toString", to_string_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_gtype_static_funcs[] = { JS_FS_END }; @@ -158,65 +124,69 @@ gjs_gtype_create_gtype_wrapper (JSContext *context, g_assert(((void) "Attempted to create wrapper object for invalid GType", gtype != 0)); - JSAutoRequest ar(context); - - auto heap_wrapper = - static_cast *>(g_type_get_qdata(gtype, gjs_get_gtype_wrapper_quark())); - if (heap_wrapper != nullptr) - return *heap_wrapper; + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + // We cannot use gtype_table().lookupForAdd() here, because in between the + // lookup and the add, GCs may take place and mutate the hash table. A GC + // may only remove an element, not add one, so it's still safe to do this + // without locking. + auto p = gjs->gtype_table().lookup(gtype); + if (p.found()) + return p->value(); JS::RootedObject proto(context); if (!gjs_gtype_define_proto(context, nullptr, &proto)) return nullptr; - heap_wrapper = new JS::Heap(); - *heap_wrapper = JS_NewObjectWithGivenProto(context, &gjs_gtype_class, proto); - if (*heap_wrapper == nullptr) + JS::RootedObject gtype_wrapper( + context, JS_NewObjectWithGivenProto(context, &gjs_gtype_class, proto)); + if (!gtype_wrapper) return nullptr; - JS_SetPrivate(*heap_wrapper, GSIZE_TO_POINTER(gtype)); - ensure_weak_pointer_callback(context); - g_type_set_qdata(gtype, gjs_get_gtype_wrapper_quark(), heap_wrapper); - weak_pointer_list.insert(gtype); + JS_SetPrivate(gtype_wrapper, GSIZE_TO_POINTER(gtype)); - return *heap_wrapper; -} + gjs->gtype_table().put(gtype, gtype_wrapper); -static GType -_gjs_gtype_get_actual_gtype(JSContext *context, - JS::HandleObject object, - int recurse) -{ - JSAutoRequest ar(context); + return gtype_wrapper; +} - if (JS_InstanceOf(context, object, &gjs_gtype_class, NULL)) - return GPOINTER_TO_SIZE(priv_from_js(context, object)); +GJS_JSAPI_RETURN_CONVENTION +static bool _gjs_gtype_get_actual_gtype(JSContext* context, + const GjsAtoms& atoms, + JS::HandleObject object, + GType* gtype_out, int recurse) { + if (JS_InstanceOf(context, object, &gjs_gtype_class, nullptr)) { + *gtype_out = GPOINTER_TO_SIZE(priv_from_js(context, object)); + return true; + } JS::RootedValue gtype_val(context); /* OK, we don't have a GType wrapper object -- grab the "$gtype" * property on that and hope it's a GType wrapper object */ - if (!JS_GetProperty(context, object, "$gtype", >ype_val) || - !gtype_val.isObject()) { - + if (!JS_GetPropertyById(context, object, atoms.gtype(), >ype_val)) + return false; + if (!gtype_val.isObject()) { /* OK, so we're not a class. But maybe we're an instance. Check for "constructor" and recurse on that. */ - if (!JS_GetProperty(context, object, "constructor", >ype_val)) - return G_TYPE_INVALID; + if (!JS_GetPropertyById(context, object, atoms.constructor(), + >ype_val)) + return false; } if (recurse > 0 && gtype_val.isObject()) { JS::RootedObject gtype_obj(context, >ype_val.toObject()); - return _gjs_gtype_get_actual_gtype(context, gtype_obj, recurse - 1); + return _gjs_gtype_get_actual_gtype(context, atoms, gtype_obj, + gtype_out, recurse - 1); } - return G_TYPE_INVALID; + *gtype_out = G_TYPE_INVALID; + return true; } -GType -gjs_gtype_get_actual_gtype(JSContext *context, - JS::HandleObject object) -{ +bool gjs_gtype_get_actual_gtype(JSContext* context, JS::HandleObject object, + GType* gtype_out) { + g_assert(gtype_out && "Missing return location"); + /* 2 means: recurse at most three times (including this call). The levels are calculated considering that, in the @@ -225,7 +195,8 @@ gjs_gtype_get_actual_gtype(JSContext *context, GType value. */ - return _gjs_gtype_get_actual_gtype(context, object, 2); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + return _gjs_gtype_get_actual_gtype(context, atoms, object, gtype_out, 2); } bool @@ -235,25 +206,3 @@ gjs_typecheck_gtype (JSContext *context, { return do_base_typecheck(context, obj, throw_error); } - -const char * -gjs_get_names_from_gtype_and_gi_info(GType gtype, - GIBaseInfo *info, - const char **constructor_name) -{ - const char *ns; - /* ns is only used to set the JSClass->name field (exposed by - * Object.prototype.toString). - * We can safely set "unknown" if there is no info, as in that case - * the name is globally unique (it's a GType name). */ - if (info) { - ns = g_base_info_get_namespace((GIBaseInfo*) info); - if (constructor_name) - *constructor_name = g_base_info_get_name((GIBaseInfo*) info); - } else { - ns = "unknown"; - if (constructor_name) - *constructor_name = g_type_name(gtype); - } - return ns; -} diff --git a/gi/gtype.h b/gi/gtype.h index c6d707a..cf4243d 100644 --- a/gi/gtype.h +++ b/gi/gtype.h @@ -22,30 +22,26 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_GTYPE_H__ -#define __GJS_GTYPE_H__ +#ifndef GI_GTYPE_H_ +#define GI_GTYPE_H_ -#include -#include -#include -#include "cjs/jsapi-util.h" +#include -G_BEGIN_DECLS +#include -JSObject * gjs_gtype_create_gtype_wrapper (JSContext *context, - GType gtype); +#include -GType gjs_gtype_get_actual_gtype(JSContext *context, - JS::HandleObject object); +#include "cjs/macros.h" -bool gjs_typecheck_gtype (JSContext *context, - JS::HandleObject obj, - bool throw_error); +GJS_JSAPI_RETURN_CONVENTION +JSObject * gjs_gtype_create_gtype_wrapper (JSContext *context, + GType gtype); -const char *gjs_get_names_from_gtype_and_gi_info(GType gtype, - GIBaseInfo *info, - const char **constructor_name); +GJS_JSAPI_RETURN_CONVENTION +bool gjs_gtype_get_actual_gtype(JSContext* context, JS::HandleObject object, + GType* gtype_out); -G_END_DECLS +[[nodiscard]] bool gjs_typecheck_gtype(JSContext* cx, JS::HandleObject obj, + bool throw_error); -#endif /* __GJS_INTERFACE_H__ */ +#endif // GI_GTYPE_H_ diff --git a/gi/interface.cpp b/gi/interface.cpp index 4633724..102095b 100644 --- a/gi/interface.cpp +++ b/gi/interface.cpp @@ -24,133 +24,55 @@ #include -#include "function.h" -#include "gtype.h" -#include "interface.h" -#include "object.h" -#include "repo.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" - -#include - #include -typedef struct { - GIInterfaceInfo *info; - GType gtype; - /* the GTypeInterface vtable wrapped by this JS Object (only used for - prototypes) */ - GTypeInterface *vtable; -} Interface; - -extern struct JSClass gjs_interface_class; - -GJS_DEFINE_PRIV_FROM_JS(Interface, gjs_interface_class) - -GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(interface) - -static void -interface_finalize(JSFreeOp *fop, - JSObject *obj) -{ - Interface *priv; - - priv = (Interface*) JS_GetPrivate(obj); - - if (priv == NULL) - return; - - if (priv->info != NULL) - g_base_info_unref((GIBaseInfo*)priv->info); - - g_clear_pointer(&priv->vtable, (GDestroyNotify)g_type_default_interface_unref); - - GJS_DEC_COUNTER(interface); - g_slice_free(Interface, priv); +#include +#include + +#include "gi/function.h" +#include "gi/interface.h" +#include "gi/object.h" +#include "gi/repo.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/mem-private.h" + +InterfacePrototype::InterfacePrototype(GIInterfaceInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype), + m_vtable( + static_cast(g_type_default_interface_ref(gtype))) { + GJS_INC_COUNTER(interface); } -static bool -gjs_define_static_methods(JSContext *context, - JS::HandleObject constructor, - GType gtype, - GIInterfaceInfo *info) -{ - int i; - int n_methods; - - n_methods = g_interface_info_get_n_methods(info); - - for (i = 0; i < n_methods; i++) { - GIFunctionInfo *meth_info; - GIFunctionInfoFlags flags; - - meth_info = g_interface_info_get_method (info, i); - flags = g_function_info_get_flags (meth_info); - - /* Anything that isn't a method we put on the prototype of the - * constructor. This includes introspection - * methods, as well as the forthcoming "static methods" - * support. We may want to change this to use - * GI_FUNCTION_IS_CONSTRUCTOR and GI_FUNCTION_IS_STATIC or the - * like in the near future. - */ - if (!(flags & GI_FUNCTION_IS_METHOD)) { - gjs_define_function(context, constructor, gtype, - (GICallableInfo *)meth_info); - } - - g_base_info_unref((GIBaseInfo*) meth_info); - } - return true; +InterfacePrototype::~InterfacePrototype(void) { + g_clear_pointer(&m_vtable, g_type_default_interface_unref); + GJS_DEC_COUNTER(interface); } -static bool -interface_resolve(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - bool *resolved) -{ - Interface *priv; - GIFunctionInfo *method_info; - - priv = priv_from_js(context, obj); - - if (priv == nullptr) - return false; - +// See GIWrapperBase::resolve(). +bool InterfacePrototype::resolve_impl(JSContext* context, JS::HandleObject obj, + JS::HandleId, const char* name, + bool* resolved) { /* If we have no GIRepository information then this interface was defined * from within GJS. In that case, it has no properties that need to be * resolved from within C code, as interfaces cannot inherit. */ - if (priv->info == NULL) { + if (!info()) { *resolved = false; return true; } - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; - } + GjsAutoFunctionInfo method_info = + g_interface_info_find_method(m_info, name); - method_info = g_interface_info_find_method((GIInterfaceInfo*) priv->info, name); - - if (method_info != NULL) { + if (method_info) { if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) { - if (gjs_define_function(context, obj, - priv->gtype, - (GICallableInfo*)method_info) == NULL) { - g_base_info_unref((GIBaseInfo*)method_info); + if (!gjs_define_function(context, obj, m_gtype, method_info)) return false; - } *resolved = true; } else { *resolved = false; } - - g_base_info_unref((GIBaseInfo*)method_info); } else { *resolved = false; } @@ -158,114 +80,65 @@ interface_resolve(JSContext *context, return true; } -static bool -interface_has_instance_func(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - /* This method is not called directly, so no need for error messages */ +/* + * InterfaceBase::has_instance: + * + * JSNative implementation of `[Symbol.hasInstance]()`. This method is never + * called directly, but instead is called indirectly by the JS engine as part of + * an `instanceof` expression. + */ +bool InterfaceBase::has_instance(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_THIS(cx, argc, vp, args, interface_constructor); - JS::RootedValue interface(cx, args.computeThis(cx)); - g_assert(interface.isObject()); - JS::RootedObject interface_constructor(cx, &interface.toObject()); JS::RootedObject interface_proto(cx); - gjs_object_require_property(cx, interface_constructor, - "interface constructor", - GJS_STRING_PROTOTYPE, &interface_proto); + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (!gjs_object_require_property(cx, interface_constructor, + "interface constructor", atoms.prototype(), + &interface_proto)) + return false; - Interface *priv; - if (!priv_from_js_with_typecheck(cx, interface_proto, &priv)) + InterfaceBase* priv = InterfaceBase::for_js_typecheck(cx, interface_proto); + if (!priv) return false; + return priv->to_prototype()->has_instance_impl(cx, args); +} + +// See InterfaceBase::has_instance(). +bool InterfacePrototype::has_instance_impl(JSContext* cx, + const JS::CallArgs& args) { + // This method is never called directly, so no need for error messages. g_assert(args.length() == 1); g_assert(args[0].isObject()); JS::RootedObject instance(cx, &args[0].toObject()); - bool isinstance = gjs_typecheck_object(cx, instance, priv->gtype, false); + bool isinstance = ObjectBase::typecheck(cx, instance, nullptr, m_gtype, + GjsTypecheckNoThrow()); args.rval().setBoolean(isinstance); return true; } -static const struct JSClassOps gjs_interface_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - interface_resolve, - nullptr, /* mayResolve */ - interface_finalize +// clang-format off +const struct JSClassOps InterfaceBase::class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + &InterfaceBase::resolve, + nullptr, // mayResolve + &InterfaceBase::finalize, }; -struct JSClass gjs_interface_class = { +const struct JSClass InterfaceBase::klass = { "GObject_Interface", JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE, - &gjs_interface_class_ops + &InterfaceBase::class_ops }; -JSPropertySpec gjs_interface_proto_props[] = { - JS_PS_END -}; - -JSFunctionSpec gjs_interface_proto_funcs[] = { +JSFunctionSpec InterfaceBase::static_methods[] = { + JS_SYM_FN(hasInstance, &InterfaceBase::has_instance, 1, 0), JS_FS_END }; - -JSFunctionSpec gjs_interface_static_funcs[] = { - JS_SYM_FN(hasInstance, interface_has_instance_func, 1, 0), - JS_FS_END -}; - -bool -gjs_define_interface_class(JSContext *context, - JS::HandleObject in_object, - GIInterfaceInfo *info, - GType gtype, - JS::MutableHandleObject constructor) -{ - Interface *priv; - const char *constructor_name; - const char *ns; - JS::RootedObject prototype(context); - - ns = gjs_get_names_from_gtype_and_gi_info(gtype, (GIBaseInfo *) info, - &constructor_name); - - if (!gjs_init_class_dynamic(context, in_object, nullptr, ns, - constructor_name, - &gjs_interface_class, - gjs_interface_constructor, 0, - /* props of prototype */ - &gjs_interface_proto_props[0], - /* funcs of prototype */ - &gjs_interface_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - gjs_interface_static_funcs, - &prototype, - constructor)) { - g_error("Can't init class %s", constructor_name); - } - - GJS_INC_COUNTER(interface); - priv = g_slice_new0(Interface); - priv->info = info == NULL ? NULL : g_base_info_ref((GIBaseInfo *) info); - priv->gtype = gtype; - priv->vtable = (GTypeInterface *) g_type_default_interface_ref(gtype); - JS_SetPrivate(prototype, priv); - - /* If we have no GIRepository information, then this interface was defined - * from within GJS and therefore has no C static methods to be defined. */ - if (priv->info) - gjs_define_static_methods(context, constructor, priv->gtype, priv->info); - - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, priv->gtype)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, JSPROP_PERMANENT); - - return true; -} +// clang-format on bool gjs_lookup_interface_constructor(JSContext *context, @@ -275,9 +148,9 @@ gjs_lookup_interface_constructor(JSContext *context, JSObject *constructor; GIBaseInfo *interface_info; - interface_info = g_irepository_find_by_gtype(NULL, gtype); + interface_info = g_irepository_find_by_gtype(nullptr, gtype); - if (interface_info == NULL) { + if (!interface_info) { gjs_throw(context, "Cannot expose non introspectable interface %s", g_type_name(gtype)); return false; @@ -287,7 +160,7 @@ gjs_lookup_interface_constructor(JSContext *context, GI_INFO_TYPE_INTERFACE); constructor = gjs_lookup_generic_constructor(context, interface_info); - if (G_UNLIKELY (constructor == NULL)) + if (G_UNLIKELY(!constructor)) return false; g_base_info_unref(interface_info); diff --git a/gi/interface.h b/gi/interface.h index b69e1f9..b2b0752 100644 --- a/gi/interface.h +++ b/gi/interface.h @@ -22,26 +22,120 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_INTERFACE_H__ -#define __GJS_INTERFACE_H__ +#ifndef GI_INTERFACE_H_ +#define GI_INTERFACE_H_ + +#include -#include -#include #include +#include +#include + +#include +#include +#include +#include + +#include "gi/wrapperutils.h" #include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "util/log.h" + +class InterfacePrototype; +class InterfaceInstance; + +/* For more information on this Base/Prototype/Interface scheme, see the notes + * in wrapperutils.h. + * + * What's unusual about this subclass is that InterfaceInstance should never + * actually be instantiated. Interfaces can't be constructed, and + * GIWrapperBase::constructor() is overridden to just throw an exception and not + * create any JS wrapper object. + * + * We use the template classes from wrapperutils.h anyway, because there is + * still a lot of common code. + */ + +class InterfaceBase : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit InterfaceBase(InterfacePrototype* proto = nullptr) + : GIWrapperBase(proto) {} + ~InterfaceBase(void) {} + + static const GjsDebugTopic debug_topic = GJS_DEBUG_GINTERFACE; + static constexpr const char* debug_tag = "GInterface"; + + static const struct JSClassOps class_ops; + static const struct JSClass klass; + static JSFunctionSpec static_methods[]; -G_BEGIN_DECLS + [[nodiscard]] const char* to_string_kind(void) const { return "interface"; } -bool gjs_define_interface_class(JSContext *context, - JS::HandleObject in_object, - GIInterfaceInfo *info, - GType gtype, - JS::MutableHandleObject constructor); + // JSNative methods + // Overrides GIWrapperBase::constructor(). + GJS_JSAPI_RETURN_CONVENTION + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + gjs_throw_abstract_constructor_error(cx, args); + return false; + } + + GJS_JSAPI_RETURN_CONVENTION + static bool has_instance(JSContext* cx, unsigned argc, JS::Value* vp); +}; + +class InterfacePrototype + : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; + friend class InterfaceBase; // for has_instance_impl + + // the GTypeInterface vtable wrapped by this JS object + GTypeInterface* m_vtable; + + static constexpr InfoType::Tag info_type_tag = InfoType::Interface; + + explicit InterfacePrototype(GIInterfaceInfo* info, GType gtype); + ~InterfacePrototype(void); + + // JSClass operations + + GJS_JSAPI_RETURN_CONVENTION + bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* name, bool* resolved); + + // JS methods + + GJS_JSAPI_RETURN_CONVENTION + bool has_instance_impl(JSContext* cx, const JS::CallArgs& args); +}; + +class InterfaceInstance + : public GIWrapperInstance { + friend class GIWrapperInstance; + friend class GIWrapperBase; + + [[noreturn]] InterfaceInstance(JSContext* cx, JS::HandleObject obj) + : GIWrapperInstance(cx, obj) { + g_assert_not_reached(); + } + [[noreturn]] ~InterfaceInstance(void) { g_assert_not_reached(); } +}; + +GJS_JSAPI_RETURN_CONVENTION bool gjs_lookup_interface_constructor(JSContext *context, GType gtype, JS::MutableHandleValue value_p); -G_END_DECLS - -#endif /* __GJS_INTERFACE_H__ */ +#endif // GI_INTERFACE_H_ diff --git a/gi/ns.cpp b/gi/ns.cpp index 2e5b3ce..b1818f7 100644 --- a/gi/ns.cpp +++ b/gi/ns.cpp @@ -23,17 +23,27 @@ #include -#include "ns.h" -#include "repo.h" -#include "param.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" - -#include #include - -#include +#include + +#include +#include +#include // for JSID_IS_STRING +#include +#include +#include +#include // for UniqueChars +#include // for JS_GetPrivate, JS_NewObjectWithGivenProto + +#include "gi/ns.h" +#include "gi/repo.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "cjs/mem-private.h" +#include "util/log.h" typedef struct { char *gi_namespace; @@ -45,6 +55,7 @@ GJS_DEFINE_PRIV_FROM_JS(Ns, gjs_ns_class) /* The *resolved out parameter, on success, should be false to indicate that id * was not resolved; and true if id was resolved. */ +GJS_JSAPI_RETURN_CONVENTION static bool ns_resolve(JSContext *context, JS::HandleObject obj, @@ -52,8 +63,6 @@ ns_resolve(JSContext *context, bool *resolved) { Ns *priv; - GIRepository *repo; - GIBaseInfo *info; bool defined; if (!JSID_IS_STRING(id)) { @@ -62,9 +71,8 @@ ns_resolve(JSContext *context, } /* let Object.prototype resolve these */ - JSFlatString *str = JSID_TO_FLAT_STRING(id); - if (JS_FlatStringEqualsAscii(str, "valueOf") || - JS_FlatStringEqualsAscii(str, "toString")) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (id == atoms.to_string() || id == atoms.value_of()) { *resolved = false; return true; } @@ -74,48 +82,72 @@ ns_resolve(JSContext *context, "Resolve prop '%s' hook, obj %s, priv %p", gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); - if (priv == NULL) { + if (!priv) { *resolved = false; /* we are the prototype, or have the wrong class */ return true; } - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { + JS::UniqueChars name; + if (!gjs_get_string_id(context, id, &name)) + return false; + if (!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); - if (info == NULL) { + GjsAutoBaseInfo info = + g_irepository_find_by_name(nullptr, priv->gi_namespace, name.get()); + if (!info) { *resolved = false; /* No property defined, but no error either */ return true; } gjs_debug(GJS_DEBUG_GNAMESPACE, "Found info type %s for '%s' in namespace '%s'", - gjs_info_type_name(g_base_info_get_type(info)), - g_base_info_get_name(info), - g_base_info_get_namespace(info)); - - JSAutoRequest ar(context); + gjs_info_type_name(info.type()), info.name(), info.ns()); if (!gjs_define_info(context, obj, info, &defined)) { - gjs_debug(GJS_DEBUG_GNAMESPACE, - "Failed to define info '%s'", - g_base_info_get_name(info)); - - g_base_info_unref(info); + gjs_debug(GJS_DEBUG_GNAMESPACE, "Failed to define info '%s'", + info.name()); return false; } /* we defined the property in this object? */ - g_base_info_unref(info); *resolved = defined; return true; } +GJS_JSAPI_RETURN_CONVENTION +static bool ns_new_enumerate(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool only_enumerable [[maybe_unused]]) { + Ns* priv = priv_from_js(cx, obj); + + if (!priv) { + return true; + } + + int n = g_irepository_get_n_infos(nullptr, priv->gi_namespace); + if (!properties.reserve(properties.length() + n)) { + JS_ReportOutOfMemory(cx); + return false; + } + + for (int k = 0; k < n; k++) { + GjsAutoBaseInfo info = + g_irepository_get_info(nullptr, priv->gi_namespace, k); + const char* name = info.name(); + + jsid id = gjs_intern_string_to_id(cx, name); + if (id == JSID_VOID) + return false; + properties.infallibleAppend(id); + } + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION static bool get_name (JSContext *context, unsigned argc, @@ -123,7 +155,7 @@ get_name (JSContext *context, { GJS_GET_PRIV(context, argc, vp, args, obj, Ns, priv); - if (priv == NULL) + if (!priv) return false; return gjs_string_from_utf8(context, priv->gi_namespace, args.rval()); @@ -131,16 +163,13 @@ get_name (JSContext *context, GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(ns) -static void -ns_finalize(JSFreeOp *fop, - JSObject *obj) -{ +static void ns_finalize(JSFreeOp*, JSObject* obj) { Ns *priv; priv = (Ns *)JS_GetPrivate(obj); gjs_debug_lifecycle(GJS_DEBUG_GNAMESPACE, "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) + if (!priv) return; /* we are the prototype, not a real instance */ if (priv->gi_namespace) @@ -154,16 +183,15 @@ ns_finalize(JSFreeOp *fop, * instances of the object, and to the prototype that instances of the * class have. */ +// clang-format off static const struct JSClassOps gjs_ns_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + ns_new_enumerate, ns_resolve, - nullptr, /* mayResolve */ - ns_finalize -}; + nullptr, // mayResolve + ns_finalize}; struct JSClass gjs_ns_class = { "GIRepositoryNamespace", @@ -172,15 +200,18 @@ struct JSClass gjs_ns_class = { }; static JSPropertySpec gjs_ns_proto_props[] = { + JS_STRING_SYM_PS(toStringTag, "GIRepositoryNamespace", JSPROP_READONLY), JS_PSG("__name__", get_name, GJS_MODULE_PROP_FLAGS), JS_PS_END }; +// clang-format on static JSFunctionSpec *gjs_ns_proto_funcs = nullptr; static JSFunctionSpec *gjs_ns_static_funcs = nullptr; GJS_DEFINE_PROTO_FUNCS(ns) +GJS_JSAPI_RETURN_CONVENTION static JSObject* ns_new(JSContext *context, const char *ns_name) @@ -194,13 +225,13 @@ ns_new(JSContext *context, JS::RootedObject ns(context, JS_NewObjectWithGivenProto(context, &gjs_ns_class, proto)); if (!ns) - g_error("No memory to create ns object"); + return nullptr; priv = g_slice_new0(Ns); GJS_INC_COUNTER(ns); - g_assert(priv_from_js(context, ns) == NULL); + g_assert(!priv_from_js(context, ns)); JS_SetPrivate(ns, priv); gjs_debug_lifecycle(GJS_DEBUG_GNAMESPACE, "ns constructor, obj %p priv %p", diff --git a/gi/ns.h b/gi/ns.h index 2a74842..aa2ac59 100644 --- a/gi/ns.h +++ b/gi/ns.h @@ -21,18 +21,16 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_NS_H__ -#define __GJS_NS_H__ +#ifndef GI_NS_H_ +#define GI_NS_H_ -#include -#include -#include "cjs/jsapi-util.h" +#include "cjs/macros.h" -G_BEGIN_DECLS +class JSObject; +struct JSContext; +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_create_ns(JSContext *context, const char *ns_name); -G_END_DECLS - -#endif /* __GJS_NS_H__ */ +#endif // GI_NS_H_ diff --git a/gi/object.cpp b/gi/object.cpp index aa96791..271182a 100644 --- a/gi/object.cpp +++ b/gi/object.cpp @@ -1,6 +1,7 @@ /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* * Copyright (c) 2008 litl, LLC + * Copyright (c) 2018 Philip Chimento * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -23,322 +24,350 @@ #include -#include -#include -#include -#include -#include -#include +#include +#include // for memset, strcmp + +#include // for find +#include // for mem_fn +#include +#include // for tie +#include // for remove_reference<>::type +#include // for move #include -#include "object.h" -#include "gtype.h" -#include "interface.h" -#include "cjs/jsapi-util-args.h" -#include "arg.h" -#include "repo.h" -#include "gtype.h" -#include "function.h" -#include "proxyutils.h" -#include "param.h" -#include "toggle.h" -#include "value.h" -#include "closure.h" -#include "gjs_gi_trace.h" +#include +#include +#include +#include + +#include +#include +#include +#include // for JS_AddWeakPointerCompartmentCallback +#include // for MutableWrappedPtrOperations +#include // for AddAssociatedMemory, RemoveAssoci... +#include // for JSPROP_PERMANENT, JSPROP_READONLY +#include +#include // for UniqueChars +#include +#include +#include // for JS_ReportOutOfMemory, IsCallable +#include // for JS_GetObjectFunction, IsFunctionO... +#include +#include + +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/closure.h" +#include "gi/function.h" +#include "gi/gjs_gi_trace.h" +#include "gi/object.h" +#include "gi/repo.h" +#include "gi/toggle.h" +#include "gi/value.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/deprecation.h" #include "cjs/jsapi-class.h" +#include "cjs/jsapi-util-args.h" #include "cjs/jsapi-util-root.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/context-private.h" -#include "cjs/mem.h" - -#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); +#include "cjs/mem-private.h" +#include "util/log.h" + +class JSTracer; + +/* This is a trick to print out the sizes of the structs at compile time, in + * an error message. */ +// template struct Measure; +// Measure instance_size; +// Measure prototype_size; + +#if defined(__x86_64__) && defined(__clang__) +/* This isn't meant to be comprehensive, but should trip on at least one CI job + * if sizeof(ObjectInstance) is increased. */ +static_assert(sizeof(ObjectInstance) <= 88, + "Think very hard before increasing the size of ObjectInstance. " + "There can be tens of thousands of them alive in a typical " + "gnome-shell run."); +#endif // x86-64 clang + +bool ObjectInstance::s_weak_pointer_callback = false; +ObjectInstance *ObjectInstance::wrapped_gobject_list = nullptr; + +// clang-format off +G_DEFINE_QUARK(gjs::custom-type, ObjectBase::custom_type) +G_DEFINE_QUARK(gjs::custom-property, ObjectBase::custom_property) +// clang-format on + +[[nodiscard]] static GQuark gjs_object_priv_quark() { + static GQuark val = 0; + if (G_UNLIKELY (!val)) + val = g_quark_from_static_string ("gjs::private"); - if (elem->m_prev) { - GjsListLink *prev = object_instance_get_link(elem->m_prev); - prev->m_next = this_instance; - this->m_prev = elem->m_prev; - } + return val; +} - elem->m_prev = this_instance; - this->m_next = head; - } +bool ObjectBase::is_custom_js_class() { + return !!g_type_get_qdata(gtype(), ObjectBase::custom_type_quark()); +} - 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; +// Plain g_type_query fails and leaves @query uninitialized for dynamic types. +// See https://gitlab.gnome.org/GNOME/glib/issues/623 +void ObjectBase::type_query_dynamic_safe(GTypeQuery* query) { + GType type = gtype(); + while (g_type_get_qdata(type, ObjectBase::custom_type_quark())) + type = g_type_parent(type); - m_prev = m_next = NULL; - } + g_type_query(type, query); +} - size_t size() { - GjsListLink *elem = this; - size_t count = 0; +void +GjsListLink::prepend(ObjectInstance *this_instance, + ObjectInstance *head) +{ + GjsListLink *elem = head->get_link(); - do { - count++; - if (!elem->m_next) - break; - elem = object_instance_get_link(elem->m_next); - } while (elem); + g_assert(this_instance->get_link() == this); - return count; + if (elem->m_prev) { + GjsListLink *prev = elem->m_prev->get_link(); + prev->m_next = this_instance; + this->m_prev = elem->m_prev; } -}; -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 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; - - 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; - -using ParamRef = std::unique_ptr; -using ParamRefArray = std::vector; -static std::unordered_map class_init_properties; - -static bool weak_pointer_callback = false; -ObjectInstance *wrapped_gobject_list; - -extern struct JSClass gjs_object_instance_class; -GJS_DEFINE_PRIV_FROM_JS(ObjectInstance, gjs_object_instance_class) - -static void disassociate_js_gobject(ObjectInstance *priv); -static void ensure_uses_toggle_ref(JSContext *cx, ObjectInstance *priv); - -typedef enum { - SOME_ERROR_OCCURRED = false, - NO_SUCH_G_PROPERTY, - VALUE_WAS_SET -} ValueFromPropertyResult; + elem->m_prev = this_instance; + this->m_next = head; +} -static GQuark -gjs_is_custom_type_quark (void) +void +GjsListLink::unlink(void) { - static GQuark val = 0; - if (!val) - val = g_quark_from_static_string ("gjs::custom-type"); + if (m_prev) + m_prev->get_link()->m_next = m_next; + if (m_next) + m_next->get_link()->m_prev = m_prev; - return val; + m_prev = m_next = nullptr; } -static GQuark -gjs_is_custom_property_quark (void) +size_t +GjsListLink::size(void) const { - static GQuark val = 0; - if (!val) - val = g_quark_from_static_string ("gjs::custom-property"); + const GjsListLink *elem = this; + size_t count = 0; - return val; -} + do { + count++; + if (!elem->m_next) + break; + elem = elem->m_next->get_link(); + } while (elem); -static GQuark -gjs_object_priv_quark (void) -{ - static GQuark val = 0; - if (G_UNLIKELY (!val)) - val = g_quark_from_static_string ("gjs::private"); + return count; +} - return val; +void ObjectInstance::link(void) { + if (wrapped_gobject_list) + m_instance_link.prepend(this, wrapped_gobject_list); + wrapped_gobject_list = this; } -/* Plain g_type_query fails and leaves @query uninitialized for - dynamic types. - See https://bugzilla.gnome.org/show_bug.cgi?id=687184 and - https://bugzilla.gnome.org/show_bug.cgi?id=687211 -*/ -static void -g_type_query_dynamic_safe (GType type, - GTypeQuery *query) -{ - while (g_type_get_qdata(type, gjs_is_custom_type_quark())) - type = g_type_parent(type); +void ObjectInstance::unlink(void) { + if (wrapped_gobject_list == this) + wrapped_gobject_list = m_instance_link.next(); + m_instance_link.unlink(); +} - g_type_query(type, query); +const void* ObjectBase::jsobj_addr(void) const { + if (is_prototype()) + return nullptr; + return to_instance()->m_wrapper.debug_addr(); } -static void -throw_priv_is_null_error(JSContext *context) -{ - gjs_throw(context, +// Overrides GIWrapperBase::typecheck(). We only override the overload that +// throws, so that we can throw our own more informative error. +bool ObjectBase::typecheck(JSContext* cx, JS::HandleObject obj, + GIObjectInfo* expected_info, GType expected_gtype) { + if (GIWrapperBase::typecheck(cx, obj, expected_info, expected_gtype)) + return true; + + gjs_throw(cx, "This JS object wrapper isn't wrapping a GObject." " If this is a custom subclass, are you sure you chained" " up to the parent _init properly?"); + return false; +} + +bool ObjectInstance::check_gobject_disposed(const char* for_what) const { + if (!m_gobj_disposed) + return true; + + g_critical( + "Object %s.%s (%p), has been already deallocated — impossible to %s " + "it. This might be caused by the object having been destroyed from C " + "code using something such as destroy(), dispose(), or remove() " + "vfuncs.", + ns(), name(), m_ptr, for_what); + gjs_dumpstack(); + return false; } -static ObjectInstance * -get_object_qdata(GObject *gobj) +ObjectInstance * +ObjectInstance::for_gobject(GObject *gobj) { auto priv = static_cast(g_object_get_qdata(gobj, gjs_object_priv_quark())); - 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))); - priv->js_object_finalized = false; - g_assert(!priv->keep_alive); /* should associate again with a new wrapper */ - } + if (priv) + priv->check_js_object_finalized(); return priv; } -static void -set_object_qdata(GObject *gobj, - ObjectInstance *priv) +void +ObjectInstance::check_js_object_finalized(void) { - g_object_set_qdata(gobj, gjs_object_priv_quark(), priv); + if (!m_uses_toggle_ref) + return; + if (G_UNLIKELY(m_wrapper_finalized)) { + g_critical( + "Object %p (a %s) resurfaced after the JS wrapper was finalized. " + "This is some library doing dubious memory management inside " + "dispose()", + m_ptr, type_name()); + m_wrapper_finalized = false; + g_assert(!m_wrapper); /* should associate again with a new wrapper */ + } +} + +ObjectPrototype* ObjectPrototype::for_gtype(GType gtype) { + return static_cast( + g_type_get_qdata(gtype, gjs_object_priv_quark())); } -static ValueFromPropertyResult -init_g_param_from_property(JSContext *context, - const char *js_prop_name, - JS::HandleValue value, - GType gtype, - GParameter *parameter, - bool constructing) +void ObjectPrototype::set_type_qdata(void) { + g_type_set_qdata(m_gtype, gjs_object_priv_quark(), this); +} + +void +ObjectInstance::set_object_qdata(void) { - char *gname; - GParamSpec *param_spec; - void *klass; + g_object_set_qdata(m_ptr, gjs_object_priv_quark(), this); +} - gname = gjs_hyphen_from_camel(js_prop_name); - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Hyphen name %s on %s", gname, g_type_name(gtype)); +void +ObjectInstance::unset_object_qdata(void) +{ + g_object_set_qdata(m_ptr, gjs_object_priv_quark(), nullptr); +} - klass = g_type_class_ref(gtype); - param_spec = g_object_class_find_property(G_OBJECT_CLASS(klass), - gname); - g_type_class_unref(klass); - g_free(gname); +GParamSpec* ObjectPrototype::find_param_spec_from_id(JSContext* cx, + JS::HandleString key) { + /* First check for the ID in the cache */ + auto entry = m_property_cache.lookupForAdd(key); + if (entry) + return entry->value(); - if (param_spec == NULL) { - /* not a GObject prop, so nothing else to do */ - return NO_SUCH_G_PROPERTY; - } + JS::UniqueChars js_prop_name(JS_EncodeStringToUTF8(cx, key)); + if (!js_prop_name) + return nullptr; - /* Do not set JS overridden properties through GObject, to avoid - * infinite recursion (but set them when constructing) */ - if (!constructing && - g_param_spec_get_qdata(param_spec, gjs_is_custom_property_quark())) - return NO_SUCH_G_PROPERTY; + GjsAutoChar gname = gjs_hyphen_from_camel(js_prop_name.get()); + GjsAutoTypeClass gobj_class(m_gtype); + GParamSpec* pspec = g_object_class_find_property(gobj_class, gname); + GjsAutoParam param_spec(pspec, GjsAutoTakeOwnership()); + if (!param_spec) { + gjs_wrapper_throw_nonexistent_field(cx, m_gtype, js_prop_name.get()); + return nullptr; + } - if ((param_spec->flags & G_PARAM_WRITABLE) == 0) { - /* prevent setting the prop even in JS */ - gjs_throw(context, "Property %s (GObject %s) is not writable", - js_prop_name, param_spec->name); - return SOME_ERROR_OCCURRED; + if (!m_property_cache.add(entry, key, std::move(param_spec))) { + JS_ReportOutOfMemory(cx); + return nullptr; } + return pspec; /* owned by property cache */ +} - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Syncing %s to GObject prop %s", - js_prop_name, param_spec->name); +/* A hook on adding a property to an object. This is called during a set + * property operation after all the resolve hooks on the prototype chain have + * failed to resolve. We use this to mark an object as needing toggle refs when + * custom state is set on it, because we need to keep the JS GObject wrapper + * alive in order not to lose custom "expando" properties. + */ +bool ObjectBase::add_property(JSContext* cx, JS::HandleObject obj, + JS::HandleId id, JS::HandleValue value) { + auto* priv = ObjectBase::for_js(cx, obj); - g_value_init(¶meter->value, G_PARAM_SPEC_VALUE_TYPE(param_spec)); - if (!gjs_value_to_g_value(context, value, ¶meter->value)) { - g_value_unset(¶meter->value); - return SOME_ERROR_OCCURRED; + /* priv is null during init: property is not being added from JS */ + if (!priv) { + debug_jsprop_static("Add property hook", id, obj); + return true; } + if (priv->is_prototype()) + return true; + + return priv->to_instance()->add_property_impl(cx, obj, id, value); +} + +bool ObjectInstance::add_property_impl(JSContext* cx, JS::HandleObject obj, + JS::HandleId id, JS::HandleValue) { + debug_jsprop("Add property hook", id, obj); - parameter->name = param_spec->name; + if (is_custom_js_class() || m_gobj_disposed) + return true; - return VALUE_WAS_SET; + ensure_uses_toggle_ref(cx); + return true; } -static inline ObjectInstance * -proto_priv_from_js(JSContext *context, - JS::HandleObject obj) -{ - JS::RootedObject proto(context); - JS_GetPrototype(context, obj, &proto); - return priv_from_js(context, proto); +bool ObjectBase::prop_getter(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + + JS::RootedString name(cx, + gjs_dynamic_property_private_slot(&args.callee()).toString()); + + priv->debug_jsprop("Property getter", name, obj); + + if (priv->is_prototype()) + return true; + /* Ignore silently; note that this is different from what we do for + * boxed types, for historical reasons */ + + return priv->to_instance()->prop_getter_impl(cx, name, args.rval()); } -static bool -get_prop_from_g_param(JSContext *context, - JS::HandleObject obj, - ObjectInstance *priv, - const char *name, - JS::MutableHandleValue value_p) -{ - char *gname; - GParamSpec *param; +bool ObjectInstance::prop_getter_impl(JSContext* cx, JS::HandleString name, + JS::MutableHandleValue rval) { + if (!check_gobject_disposed("get any property from")) + return true; + GValue gvalue = { 0, }; - gname = gjs_hyphen_from_camel(name); - param = g_object_class_find_property(G_OBJECT_GET_CLASS(priv->gobj), - gname); - g_free(gname); + ObjectPrototype* proto_priv = get_prototype(); + GParamSpec *param = proto_priv->find_param_spec_from_id(cx, name); - if (param == NULL) { - /* leave value_p as it was */ - return true; - } + /* This is guaranteed because we resolved the property before */ + g_assert(param); /* Do not fetch JS overridden properties from GObject, to avoid * infinite recursion. */ - if (g_param_spec_get_qdata(param, gjs_is_custom_property_quark())) + if (g_param_spec_get_qdata(param, ObjectBase::custom_property_quark())) return true; - if ((param->flags & G_PARAM_READABLE) == 0) + if ((param->flags & G_PARAM_READABLE) == 0) { + rval.setUndefined(); return true; + } - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Overriding %s with GObject prop %s", - name, param->name); + gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Accessing GObject property %s", + param->name); g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(param)); - g_object_get_property(priv->gobj, param->name, - &gvalue); - if (!gjs_value_from_g_value(context, value_p, &gvalue)) { + g_object_get_property(m_ptr, param->name, &gvalue); + if (!gjs_value_from_g_value(cx, rval, &gvalue)) { g_value_unset(&gvalue); return false; } @@ -347,57 +376,55 @@ get_prop_from_g_param(JSContext *context, return true; } -static GIFieldInfo * -lookup_field_info(GIObjectInfo *info, - const char *name) -{ +[[nodiscard]] static GjsAutoFieldInfo lookup_field_info(GIObjectInfo* info, + const char* name) { int n_fields = g_object_info_get_n_fields(info); int ix; - GIFieldInfo *retval = NULL; + GjsAutoFieldInfo retval; for (ix = 0; ix < n_fields; ix++) { retval = g_object_info_get_field(info, ix); - const char *field_name = g_base_info_get_name((GIBaseInfo *) retval); - if (strcmp(name, field_name) == 0) + if (strcmp(name, retval.name()) == 0) break; - g_clear_pointer(&retval, g_base_info_unref); + retval.reset(); } - if (!retval) + if (!retval || !(g_field_info_get_flags(retval) & GI_FIELD_IS_READABLE)) return nullptr; - if (!(g_field_info_get_flags(retval) & GI_FIELD_IS_READABLE)) { - g_base_info_unref(retval); - return nullptr; - } - return retval; } -static bool -get_prop_from_field(JSContext *cx, - JS::HandleObject obj, - ObjectInstance *priv, - const char *name, - JS::MutableHandleValue value_p) -{ - if (priv->info == NULL) - return true; /* Not resolved, but no error; leave value_p untouched */ +bool ObjectBase::field_getter(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + + JS::RootedString name(cx, + gjs_dynamic_property_private_slot(&args.callee()).toString()); + + priv->debug_jsprop("Field getter", name, obj); + + if (priv->is_prototype()) + return true; + /* Ignore silently; note that this is different from what we do for + * boxed types, for historical reasons */ - GIFieldInfo *field = lookup_field_info(priv->info, name); + return priv->to_instance()->field_getter_impl(cx, name, args.rval()); +} - if (field == NULL) +bool ObjectInstance::field_getter_impl(JSContext* cx, JS::HandleString name, + JS::MutableHandleValue rval) { + if (!check_gobject_disposed("get any property from")) return true; - bool retval = true; - GITypeInfo *type = NULL; + ObjectPrototype* proto_priv = get_prototype(); + GIFieldInfo* field = proto_priv->lookup_cached_field_info(cx, name); GITypeTag tag; GIArgument arg = { 0 }; gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Overriding %s with GObject field", - name); + gjs_debug_string(name).c_str()); - type = g_field_info_get_type(field); + GjsAutoTypeInfo type = g_field_info_get_type(field); tag = g_type_info_get_tag(type); if (tag == GI_TYPE_TAG_ARRAY || tag == GI_TYPE_TAG_INTERFACE || @@ -406,216 +433,129 @@ get_prop_from_field(JSContext *cx, tag == GI_TYPE_TAG_GHASH || tag == GI_TYPE_TAG_ERROR) { gjs_throw(cx, "Can't get field %s; GObject introspection supports only " - "fields with simple types, not %s", name, - g_type_tag_to_string(tag)); - retval = false; - goto out; + "fields with simple types, not %s", + gjs_debug_string(name).c_str(), g_type_tag_to_string(tag)); + return false; } - retval = g_field_info_get_field(field, priv->gobj, &arg); - if (!retval) { - gjs_throw(cx, "Error getting field %s from object", name); - goto out; + if (!g_field_info_get_field(field, m_ptr, &arg)) { + gjs_throw(cx, "Error getting field %s from object", + gjs_debug_string(name).c_str()); + return false; } - retval = gjs_value_from_g_argument(cx, value_p, type, &arg, true); + return gjs_value_from_g_argument(cx, rval, type, &arg, true); /* copy_structs is irrelevant because g_field_info_get_field() doesn't * handle boxed types */ - -out: - if (type != NULL) - g_base_info_unref((GIBaseInfo *) type); - g_base_info_unref((GIBaseInfo *) field); - return retval; } -/* a hook on getting a property; set value_p to override property's value. - * Return value is false on OOM/exception. - */ -static bool -object_instance_get_prop(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p) -{ - ObjectInstance *priv = priv_from_js(context, obj); - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Get prop '%s' hook, obj %s, priv %p", - gjs_debug_id(id).c_str(), gjs_debug_object(obj).c_str(), priv); +/* Dynamic setter for GObject properties. Returns false on OOM/exception. + * args.rval() becomes the "stored value" for the property. */ +bool ObjectBase::prop_setter(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); - if (priv == nullptr) - /* If we reach this point, either object_instance_new_resolve - * did not throw (so name == "_init"), or the property actually - * exists and it's not something we should be concerned with */ - return true; + JS::RootedString name(cx, + gjs_dynamic_property_private_slot(&args.callee()).toString()); - if (priv->gobj == NULL) /* prototype, not an instance. */ - return true; + priv->debug_jsprop("Property setter", name, obj); - 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(); + if (priv->is_prototype()) return true; - } + /* Ignore silently; note that this is different from what we do for + * boxed types, for historical reasons */ + + /* Clear the JS stored value, to avoid keeping additional references */ + args.rval().setUndefined(); - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) - return true; /* not resolved, but no error */ + return priv->to_instance()->prop_setter_impl(cx, name, args[0]); +} + +bool ObjectInstance::prop_setter_impl(JSContext* cx, JS::HandleString name, + JS::HandleValue value) { + if (!check_gobject_disposed("set any property on")) + return true; - if (!get_prop_from_g_param(context, obj, priv, name, value_p)) + ObjectPrototype* proto_priv = get_prototype(); + GParamSpec *param_spec = proto_priv->find_param_spec_from_id(cx, name); + if (!param_spec) return false; - if (!value_p.isUndefined()) + /* Do not set JS overridden properties through GObject, to avoid + * infinite recursion (unless constructing) */ + if (g_param_spec_get_qdata(param_spec, ObjectBase::custom_property_quark())) return true; - /* Fall back to fields */ - return get_prop_from_field(context, obj, priv, name, value_p); -} + if (!(param_spec->flags & G_PARAM_WRITABLE)) + /* prevent setting the prop even in JS */ + return gjs_wrapper_throw_readonly_field(cx, gtype(), param_spec->name); -static bool -set_g_param_from_prop(JSContext *context, - ObjectInstance *priv, - const char *name, - bool& was_set, - JS::HandleValue value_p, - JS::ObjectOpResult& result) -{ - GParameter param = { NULL, { 0, }}; - was_set = false; - - switch (init_g_param_from_property(context, name, - value_p, - G_TYPE_FROM_INSTANCE(priv->gobj), - ¶m, - false /* constructing */)) { - case SOME_ERROR_OCCURRED: + if (param_spec->flags & G_PARAM_DEPRECATED) + _gjs_warn_deprecated_once_per_callsite(cx, DeprecatedGObjectProperty); + + gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Setting GObject prop %s", + param_spec->name); + + GValue gvalue = G_VALUE_INIT; + g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(param_spec)); + if (!gjs_value_to_g_value(cx, value, &gvalue)) { + g_value_unset(&gvalue); 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: - break; - } - - g_object_set_property(priv->gobj, param.name, - ¶m.value); - - g_value_unset(¶m.value); - was_set = true; - return result.succeed(); -} - -static bool -check_set_field_from_prop(JSContext *cx, - ObjectInstance *priv, - const char *name, - JS::MutableHandleValue value_p, - JS::ObjectOpResult& result) -{ - if (priv->info == NULL) - return result.succeed(); + } - GIFieldInfo *field = lookup_field_info(priv->info, name); - if (field == NULL) - return result.succeed(); + g_object_set_property(m_ptr, param_spec->name, &gvalue); + g_value_unset(&gvalue); - bool retval = true; + return true; +} - /* As far as I know, GI never exposes GObject instance struct fields as - * writable, so no need to implement this for the time being */ - if (g_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE) { - g_message("Field %s of a GObject is writable, but setting it is not " - "implemented", name); - result.succeed(); - goto out; - } +bool ObjectBase::field_setter(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); - result.failReadOnly(); /* still return true; error only in strict mode */ + JS::RootedString name(cx, + gjs_dynamic_property_private_slot(&args.callee()).toString()); - /* We have to update value_p because JS caches it as the property's "stored + priv->debug_jsprop("Field setter", name, obj); + + if (priv->is_prototype()) + return true; + /* Ignore silently; note that this is different from what we do for + * boxed types, for historical reasons */ + + /* We have to update args.rval(), because JS caches it as the property's "stored * value" (https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/Stored_value) * and so subsequent gets would get the stored value instead of accessing * the field */ - value_p.setUndefined(); -out: - g_base_info_unref((GIBaseInfo *) field); - return retval; + args.rval().setUndefined(); + + return priv->to_instance()->field_setter_not_impl(cx, name); } -/* a hook on setting a property; set value_p to override property value to - * be set. Return value is false on OOM/exception. - */ -static bool -object_instance_set_prop(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - JS::MutableHandleValue value_p, - JS::ObjectOpResult& result) -{ - ObjectInstance *priv; - bool ret = true; - bool g_param_was_set = false; - - priv = priv_from_js(context, obj); - - 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 */ - return result.succeed(); - - 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; - - /* note that the prop will also have been set in JS, which I think - * is OK, since we hook get and set so will always override that - * value. We could also use JS_DefineProperty though and specify a - * getter/setter maybe, don't know if that is better. - */ - return check_set_field_from_prop(context, priv, name, value_p, result); +bool ObjectInstance::field_setter_not_impl(JSContext* cx, + JS::HandleString name) { + if (!check_gobject_disposed("set GObject field on")) + return true; + + ObjectPrototype* proto_priv = get_prototype(); + GIFieldInfo* field = proto_priv->lookup_cached_field_info(cx, name); + + /* As far as I know, GI never exposes GObject instance struct fields as + * writable, so no need to implement this for the time being */ + if (g_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE) { + g_message("Field %s of a GObject is writable, but setting it is not " + "implemented", gjs_debug_string(name).c_str()); + return true; + } + + return gjs_wrapper_throw_readonly_field(cx, gtype(), + g_base_info_get_name(field)); } -static bool -is_vfunc_unchanged(GIVFuncInfo *info, - GType gtype) -{ - GType ptype = g_type_parent(gtype); +bool ObjectPrototype::is_vfunc_unchanged(GIVFuncInfo* info) { + GType ptype = g_type_parent(m_gtype); GError *error = NULL; gpointer addr1, addr2; - addr1 = g_vfunc_info_get_address(info, gtype, &error); + addr1 = g_vfunc_info_get_address(info, m_gtype, &error); if (error) { g_clear_error(&error); return false; @@ -630,223 +570,236 @@ is_vfunc_unchanged(GIVFuncInfo *info, return addr1 == addr2; } -static GIVFuncInfo * -find_vfunc_on_parents(GIObjectInfo *info, - const char *name, - bool *out_defined_by_parent) -{ - GIVFuncInfo *vfunc = NULL; - GIObjectInfo *parent; +[[nodiscard]] static GjsAutoVFuncInfo find_vfunc_on_parents( + GIObjectInfo* info, const char* name, bool* out_defined_by_parent) { bool defined_by_parent = false; /* ref the first info so that we don't destroy * it when unrefing parents later */ - g_base_info_ref(info); - parent = info; + GjsAutoObjectInfo parent = g_base_info_ref(info); /* Since it isn't possible to override a vfunc on * an interface without reimplementing it, we don't need * to search the parent types when looking for a vfunc. */ - vfunc = g_object_info_find_vfunc_using_interfaces(parent, name, NULL); + GjsAutoVFuncInfo vfunc = + g_object_info_find_vfunc_using_interfaces(parent, name, nullptr); while (!vfunc && parent) { - GIObjectInfo *tmp = parent; - parent = g_object_info_get_parent(tmp); - g_base_info_unref(tmp); + parent = g_object_info_get_parent(parent); if (parent) vfunc = g_object_info_find_vfunc(parent, name); defined_by_parent = true; } - if (parent) - g_base_info_unref(parent); - if (out_defined_by_parent) *out_defined_by_parent = defined_by_parent; return vfunc; } -static bool -object_instance_resolve_no_info(JSContext *context, - JS::HandleObject obj, - bool *resolved, - ObjectInstance *priv, - const char *name) -{ - GIFunctionInfo *method_info; +/* Taken from GLib */ +static void canonicalize_key(const GjsAutoChar& key) { + for (char* p = key.get(); *p != 0; p++) { + char c = *p; + + if (c != '-' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') && + (c < 'a' || c > 'z')) + *p = '-'; + } +} + +/* @name must already be canonicalized */ +[[nodiscard]] static bool is_ginterface_property_name(GIInterfaceInfo* info, + const char* name) { + int n_props = g_interface_info_get_n_properties(info); + GjsAutoPropertyInfo prop_info; + + for (int ix = 0; ix < n_props; ix++) { + prop_info = g_interface_info_get_property(info, ix); + if (strcmp(name, prop_info.name()) == 0) + break; + prop_info.reset(); + } + + return !!prop_info; +} + +bool ObjectPrototype::lazy_define_gobject_property(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + bool* resolved, + const char* name) { + bool found = false; + if (!JS_AlreadyHasOwnPropertyById(cx, obj, id, &found)) + return false; + if (found) { + /* Already defined, so *resolved = false because we didn't just + * define it */ + *resolved = false; + return true; + } + + debug_jsprop("Defining lazy GObject property", id, obj); + + JS::RootedValue private_id(cx, JS::StringValue(JSID_TO_STRING(id))); + if (!gjs_define_property_dynamic( + cx, obj, name, "gobject_prop", &ObjectBase::prop_getter, + &ObjectBase::prop_setter, private_id, + // Make property configurable so that interface properties can be + // overridden by GObject.ParamSpec.override in the class that + // implements them + GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT)) + return false; + + *resolved = true; + return true; +} + +bool ObjectPrototype::resolve_no_info(JSContext* cx, JS::HandleObject obj, + JS::HandleId id, bool* resolved, + const char* name, + ResolveWhat resolve_props) { guint n_interfaces; guint i; - GType *interfaces = g_type_interfaces(priv->gtype, &n_interfaces); - for (i = 0; i < n_interfaces; i++) { - GIBaseInfo *base_info; - GIInterfaceInfo *iface_info; + GjsAutoChar canonical_name; + if (resolve_props == ConsiderMethodsAndProperties) { + // Optimization: GObject property names must start with a letter + if (g_ascii_isalpha(name[0])) { + canonical_name = gjs_hyphen_from_camel(name); + canonicalize_key(canonical_name); + } + } - base_info = g_irepository_find_by_gtype(g_irepository_get_default(), - interfaces[i]); + GjsAutoFree interfaces = g_type_interfaces(m_gtype, &n_interfaces); - if (base_info == NULL) - continue; + /* Fallback to GType system for non custom GObjects with no GI information + */ + if (canonical_name && G_TYPE_IS_CLASSED(m_gtype) && !is_custom_js_class()) { + GjsAutoTypeClass oclass(m_gtype); - /* An interface GType ought to have interface introspection info */ - g_assert (g_base_info_get_type(base_info) == GI_INFO_TYPE_INTERFACE); + if (g_object_class_find_property(oclass, canonical_name)) + return lazy_define_gobject_property(cx, obj, id, resolved, name); - iface_info = (GIInterfaceInfo*) base_info; + for (i = 0; i < n_interfaces; i++) { + if (!G_TYPE_IS_CLASSED(interfaces[i])) + continue; - method_info = g_interface_info_find_method(iface_info, name); + GjsAutoTypeClass iclass(interfaces[i]); - g_base_info_unref(base_info); + if (g_object_class_find_property(iclass, canonical_name)) + return lazy_define_gobject_property(cx, obj, id, resolved, name); + } + } + for (i = 0; i < n_interfaces; i++) { + GjsAutoInterfaceInfo iface_info = + g_irepository_find_by_gtype(nullptr, interfaces[i]); + if (!iface_info) + continue; - if (method_info != NULL) { + GjsAutoFunctionInfo method_info = + g_interface_info_find_method(iface_info, name); + if (method_info) { if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) { - if (!gjs_define_function(context, obj, priv->gtype, - (GICallableInfo *)method_info)) { - g_base_info_unref((GIBaseInfo*) method_info); - g_free(interfaces); + if (!gjs_define_function(cx, obj, m_gtype, method_info)) return false; - } - g_base_info_unref((GIBaseInfo*) method_info); *resolved = true; - g_free(interfaces); return true; } + } + + if (!canonical_name) + continue; - g_base_info_unref( (GIBaseInfo*) method_info); + /* If the name refers to a GObject property, lazily define the property + * in JS as we do below in the real resolve hook. We ignore fields here + * because I don't think interfaces can have fields */ + if (is_ginterface_property_name(iface_info, canonical_name)) { + GjsAutoTypeClass oclass(m_gtype); + // unowned + GParamSpec* pspec = g_object_class_find_property( + oclass, canonical_name); // unowned + if (pspec && pspec->owner_type == m_gtype) { + return lazy_define_gobject_property(cx, obj, id, resolved, + name); + } } } *resolved = false; - g_free(interfaces); return true; } -/* Taken from GLib */ -static void -canonicalize_key(char *key) -{ - for (char *p = key; *p != 0; p++) { - char c = *p; - - if (c != '-' && - (c < '0' || c > '9') && - (c < 'A' || c > 'Z') && - (c < 'a' || c > 'z')) - *p = '-'; - } -} +[[nodiscard]] static bool is_gobject_property_name(GIObjectInfo* info, + const char* name) { + // Optimization: GObject property names must start with a letter + if (!g_ascii_isalpha(name[0])) + return false; -static bool -is_gobject_property_name(GIObjectInfo *info, - const char *name) -{ int n_props = g_object_info_get_n_properties(info); + int n_ifaces = g_object_info_get_n_interfaces(info); int ix; - GIPropertyInfo *prop_info = nullptr; - char *canonical_name = gjs_hyphen_from_camel(name); + GjsAutoChar canonical_name = gjs_hyphen_from_camel(name); canonicalize_key(canonical_name); for (ix = 0; ix < n_props; ix++) { - prop_info = g_object_info_get_property(info, ix); - const char *prop_name = g_base_info_get_name(prop_info); - if (strcmp(canonical_name, prop_name) == 0) - break; - g_clear_pointer(&prop_info, g_base_info_unref); + GjsAutoPropertyInfo prop_info = g_object_info_get_property(info, ix); + if (strcmp(canonical_name, prop_info.name()) == 0) + return true; } - g_free(canonical_name); - - if (!prop_info) - return false; - - if (!(g_property_info_get_flags(prop_info) & G_PARAM_READABLE)) { - g_base_info_unref(prop_info); - return false; + for (ix = 0; ix < n_ifaces; ix++) { + GjsAutoInterfaceInfo iface_info = g_object_info_get_interface(info, ix); + if (is_ginterface_property_name(iface_info, canonical_name)) + return true; } - - g_base_info_unref(prop_info); - return true; + return false; } -static bool -is_gobject_field_name(GIObjectInfo *info, - const char *name) -{ - GIFieldInfo *field_info = lookup_field_info(info, name); - if (!field_info) - return false; - g_base_info_unref(field_info); - return true; +// Override of GIWrapperBase::id_is_never_lazy() +bool ObjectBase::id_is_never_lazy(jsid name, const GjsAtoms& atoms) { + // Keep this list in sync with ObjectBase::proto_properties and + // ObjectBase::proto_methods. However, explicitly do not include + // connect() in it, because there are a few cases where the lazy property + // should override the predefined one, such as Gio.Cancellable.connect(). + return name == atoms.init() || name == atoms.connect_after() || + name == atoms.emit(); } -/* 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, - JS::HandleId id, - bool *resolved) -{ - GIFunctionInfo *method_info; - ObjectInstance *priv = priv_from_js(context, obj); - - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "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) : "", - priv ? priv->gobj : NULL, - (priv && priv->gobj) ? g_type_name_from_instance((GTypeInstance*) priv->gobj) : "(type unknown)"); - - if (priv == NULL) { - /* We won't have a private until the initializer is called, so - * just defer to prototype chains in this case. - * - * This isn't too bad: either you get undefined if the field - * doesn't exist on any of the prototype chains, or whatever code - * will run afterwards will fail because of the "priv == NULL" - * check there. - */ +bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj, + JS::HandleId id, const char* name, + bool* resolved) { + if (m_unresolvable_cache.has(id)) { *resolved = false; return true; } - if (priv->gobj != NULL) { - *resolved = false; - return true; + if (!uncached_resolve(context, obj, id, name, resolved)) + return false; + + if (!*resolved && !m_unresolvable_cache.putNew(id)) { + JS_ReportOutOfMemory(context); + return false; } - 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 */ - } + return true; +} - /* 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. */ - if (priv->info == NULL) { - bool status = object_instance_resolve_no_info(context, obj, resolved, priv, name); - return status; - } +bool ObjectPrototype::uncached_resolve(JSContext* context, JS::HandleObject obj, + JS::HandleId id, const char* name, + bool* resolved) { + // If we have no GIRepository information (we're a JS GObject subclass or an + // internal non-introspected class such as GLocalFile), we need to look at + // exposing interfaces. Look up our interfaces through GType data, and then + // hope that *those* are introspectable. + if (!info()) + return resolve_no_info(context, obj, id, resolved, name, + ConsiderMethodsAndProperties); - if (g_str_has_prefix (name, "vfunc_")) { + if (g_str_has_prefix(name, "vfunc_")) { /* The only time we find a vfunc info is when we're the base * class that defined the vfunc. If we let regular prototype * chaining resolve this, we'd have the implementation for the base's @@ -862,23 +815,21 @@ object_instance_resolve(JSContext *context, */ const char *name_without_vfunc_ = &(name[6]); /* lifetime tied to name */ - GIVFuncInfo *vfunc; bool defined_by_parent; - - vfunc = find_vfunc_on_parents(priv->info, name_without_vfunc_, &defined_by_parent); - if (vfunc != NULL) { - + GjsAutoVFuncInfo vfunc = find_vfunc_on_parents( + m_info, name_without_vfunc_, &defined_by_parent); + if (vfunc) { /* In the event that the vfunc is unchanged, let regular * prototypal inheritance take over. */ - if (defined_by_parent && is_vfunc_unchanged(vfunc, priv->gtype)) { - g_base_info_unref((GIBaseInfo *)vfunc); + if (defined_by_parent && is_vfunc_unchanged(vfunc)) { *resolved = false; return true; } - gjs_define_function(context, obj, priv->gtype, vfunc); + if (!gjs_define_function(context, obj, m_gtype, vfunc)) + return false; + *resolved = true; - g_base_info_unref((GIBaseInfo *)vfunc); return true; } @@ -886,15 +837,38 @@ object_instance_resolve(JSContext *context, * method resolution. */ } - /* If the name refers to a GObject property or field, don't resolve. - * Instead, let the getProperty hook handle fetching the property from - * GObject. */ - if (is_gobject_property_name(priv->info, name) || - is_gobject_field_name(priv->info, name)) { - gjs_debug_jsprop(GJS_DEBUG_GOBJECT, - "Breaking out of %p resolve, '%s' is a GObject prop", - obj.get(), name.get()); - *resolved = false; + if (is_gobject_property_name(m_info, name)) + return lazy_define_gobject_property(context, obj, id, resolved, name); + + GjsAutoFieldInfo field_info = lookup_field_info(m_info, name); + if (field_info) { + bool found = false; + if (!JS_AlreadyHasOwnPropertyById(context, obj, id, &found)) + return false; + if (found) { + *resolved = false; + return true; + } + + debug_jsprop("Defining lazy GObject field", id, obj); + + unsigned flags = GJS_MODULE_PROP_FLAGS; + if (!(g_field_info_get_flags(field_info) & GI_FIELD_IS_WRITABLE)) + flags |= JSPROP_READONLY; + + JS::RootedString key(context, JSID_TO_STRING(id)); + if (!m_field_cache.putNew(key, field_info.release())) { + JS_ReportOutOfMemory(context); + return false; + } + + JS::RootedValue private_id(context, JS::StringValue(key)); + if (!gjs_define_property_dynamic( + context, obj, name, "gobject_field", &ObjectBase::field_getter, + &ObjectBase::field_setter, private_id, flags)) + return false; + + *resolved = true; return true; } @@ -909,70 +883,138 @@ object_instance_resolve(JSContext *context, * introduces the iface) */ - method_info = g_object_info_find_method_using_interfaces(priv->info, - name, - NULL); + GjsAutoFunctionInfo method_info = + g_object_info_find_method_using_interfaces(m_info, name, nullptr); /** * Search through any interfaces implemented by the GType; * this could be done better. See * https://bugzilla.gnome.org/show_bug.cgi?id=632922 */ - if (method_info == NULL) { - bool retval = object_instance_resolve_no_info(context, obj, resolved, priv, name); - return retval; - } - + if (!method_info) + return resolve_no_info(context, obj, id, resolved, name, + ConsiderOnlyMethods); #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) { gjs_debug(GJS_DEBUG_GOBJECT, "Defining method %s in prototype for %s (%s.%s)", - g_base_info_get_name( (GIBaseInfo*) method_info), - g_type_name(priv->gtype), - g_base_info_get_namespace( (GIBaseInfo*) priv->info), - g_base_info_get_name( (GIBaseInfo*) priv->info)); + method_info.name(), type_name(), ns(), this->name()); - if (gjs_define_function(context, obj, priv->gtype, method_info) == NULL) { - g_base_info_unref( (GIBaseInfo*) method_info); + if (!gjs_define_function(context, obj, m_gtype, method_info)) return false; - } *resolved = true; /* we defined the prop in obj */ } else { *resolved = false; } - g_base_info_unref( (GIBaseInfo*) method_info); return true; } -/* Set properties from args to constructor (argv[0] is supposed to be - * a hash) - * The GParameter elements in the passed-in vector must be unset by the caller, - * regardless of the return value of this function. - */ -static bool -object_instance_props_to_g_parameters(JSContext *context, - JSObject *obj, - const JS::HandleValueArray& args, - GType gtype, - std::vector& gparams) -{ - size_t ix, length; +bool ObjectPrototype::new_enumerate_impl(JSContext* cx, JS::HandleObject, + JS::MutableHandleIdVector properties, + bool only_enumerable + [[maybe_unused]]) { + unsigned n_interfaces; + GType* interfaces = g_type_interfaces(gtype(), &n_interfaces); - if (args.length() == 0 || args[0].isUndefined()) - return true; + for (unsigned k = 0; k < n_interfaces; k++) { + GjsAutoInterfaceInfo iface_info = + g_irepository_find_by_gtype(nullptr, interfaces[k]); - if (!args[0].isObject()) { - gjs_throw(context, "argument should be a hash with props to set"); - return false; + if (!iface_info) { + continue; + } + + int n_methods = g_interface_info_get_n_methods(iface_info); + int n_properties = g_interface_info_get_n_properties(iface_info); + if (!properties.reserve(properties.length() + n_methods + + n_properties)) { + JS_ReportOutOfMemory(cx); + return false; + } + + // Methods + for (int i = 0; i < n_methods; i++) { + GjsAutoFunctionInfo meth_info = + g_interface_info_get_method(iface_info, i); + GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info); + + if (flags & GI_FUNCTION_IS_METHOD) { + const char* name = meth_info.name(); + jsid id = gjs_intern_string_to_id(cx, name); + if (id == JSID_VOID) + return false; + properties.infallibleAppend(id); + } + } + + // Properties + for (int i = 0; i < n_properties; i++) { + GjsAutoPropertyInfo prop_info = + g_interface_info_get_property(iface_info, i); + + GjsAutoChar js_name = gjs_hyphen_to_underscore(prop_info.name()); + + jsid id = gjs_intern_string_to_id(cx, js_name); + if (id == JSID_VOID) + return false; + properties.infallibleAppend(id); + } + } + + g_free(interfaces); + + if (info()) { + int n_methods = g_object_info_get_n_methods(info()); + int n_properties = g_object_info_get_n_properties(info()); + if (!properties.reserve(properties.length() + n_methods + + n_properties)) { + JS_ReportOutOfMemory(cx); + return false; + } + + // Methods + for (int i = 0; i < n_methods; i++) { + GjsAutoFunctionInfo meth_info = g_object_info_get_method(info(), i); + GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info); + + if (flags & GI_FUNCTION_IS_METHOD) { + const char* name = meth_info.name(); + jsid id = gjs_intern_string_to_id(cx, name); + if (id == JSID_VOID) + return false; + properties.infallibleAppend(id); + } + } + + // Properties + for (int i = 0; i < n_properties; i++) { + GjsAutoPropertyInfo prop_info = + g_object_info_get_property(info(), i); + + GjsAutoChar js_name = gjs_hyphen_to_underscore(prop_info.name()); + jsid id = gjs_intern_string_to_id(cx, js_name); + if (id == JSID_VOID) + return false; + properties.infallibleAppend(id); + } } - JS::RootedObject props(context, &args[0].toObject()); + return true; +} + +/* Set properties from args to constructor (args[0] is supposed to be + * a hash) */ +bool ObjectPrototype::props_to_g_parameters(JSContext* context, + JS::HandleObject props, + std::vector* names, + AutoGValueVector* values) { + size_t ix, length; JS::RootedId prop_id(context); JS::RootedValue value(context); JS::Rooted ids(context, context); @@ -982,112 +1024,123 @@ object_instance_props_to_g_parameters(JSContext *context, } for (ix = 0, length = ids.length(); ix < length; ix++) { - GjsAutoJSChar name; - GParameter gparam = { NULL, { 0, }}; + GValue gvalue = G_VALUE_INIT; /* 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) || - !gjs_get_string_id(context, prop_id, &name)) + if (!JSID_IS_STRING(prop_id)) + return gjs_wrapper_throw_nonexistent_field( + context, m_gtype, gjs_debug_id(prop_id).c_str()); + + JS::RootedString js_prop_name(context, JSID_TO_STRING(prop_id)); + GParamSpec *param_spec = find_param_spec_from_id(context, js_prop_name); + if (!param_spec) return false; - switch (init_g_param_from_property(context, name, - value, - gtype, - &gparam, - true /* constructing */)) { - case NO_SUCH_G_PROPERTY: - gjs_throw(context, "No property %s on this GObject %s", - name.get(), g_type_name(gtype)); - /* fallthrough */ - case SOME_ERROR_OCCURRED: + if (!JS_GetPropertyById(context, props, prop_id, &value)) + return false; + if (value.isUndefined()) { + gjs_throw(context, "Invalid value 'undefined' for property %s in " + "object initializer.", param_spec->name); + return false; + } + + if (!(param_spec->flags & G_PARAM_WRITABLE)) + return gjs_wrapper_throw_readonly_field(context, m_gtype, + param_spec->name); + /* prevent setting the prop even in JS */ + + g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(param_spec)); + if (!gjs_value_to_g_value(context, value, &gvalue)) { + g_value_unset(&gvalue); return false; - case VALUE_WAS_SET: - default: - break; } - gparams.push_back(gparam); + names->push_back(param_spec->name); /* owned by GParamSpec in cache */ + values->push_back(gvalue); } return true; } -static GjsListLink * -object_instance_get_link(ObjectInstance *priv) -{ - return &priv->instance_link; +static void wrapped_gobj_dispose_notify( + void* data, GObject* where_the_object_was GJS_USED_VERBOSE_LIFECYCLE) { + auto *priv = static_cast(data); + priv->gobj_dispose_notify(); + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed", + where_the_object_was); } -static void -object_instance_unlink(ObjectInstance *priv) +void +ObjectInstance::gobj_dispose_notify(void) { - if (wrapped_gobject_list == priv) - wrapped_gobject_list = priv->instance_link.next(); - priv->instance_link.unlink(); + m_gobj_disposed = true; } -static void -object_instance_link(ObjectInstance *priv) -{ - if (wrapped_gobject_list) - priv->instance_link.prepend(priv, wrapped_gobject_list); - wrapped_gobject_list = priv; +void ObjectInstance::iterate_wrapped_gobjects( + const ObjectInstance::Action& action) { + ObjectInstance *link = ObjectInstance::wrapped_gobject_list; + while (link) { + ObjectInstance *next = link->next(); + action(link); + link = next; + } } -static void -wrapped_gobj_dispose_notify(gpointer data, - GObject *where_the_object_was) -{ - auto *priv = static_cast(data); +void ObjectInstance::remove_wrapped_gobjects_if( + const ObjectInstance::Predicate& predicate, + const ObjectInstance::Action& action) { + std::vector removed; + iterate_wrapped_gobjects([&predicate, &removed](ObjectInstance* link) { + if (predicate(link)) { + removed.push_back(link); + link->unlink(); + } + }); - priv->g_object_finalized = true; - object_instance_unlink(priv); - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed", - where_the_object_was); + for (ObjectInstance *priv : removed) + action(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); - } +/* + * ObjectInstance::context_dispose_notify: + * + * Callback called when the #GjsContext is disposed. It just calls + * handle_context_dispose() on every ObjectInstance. + */ +void ObjectInstance::context_dispose_notify(void*, GObject* where_the_object_was + [[maybe_unused]]) { + ObjectInstance::iterate_wrapped_gobjects( + std::mem_fn(&ObjectInstance::handle_context_dispose)); +} - priv = next; +/* + * ObjectInstance::handle_context_dispose: + * + * Called on each existing ObjectInstance when the #GjsContext is disposed. + */ +void ObjectInstance::handle_context_dispose(void) { + if (wrapper_is_rooted()) { + debug_lifecycle("Was rooted, but unrooting due to GjsContext dispose"); + discard_wrapper(); + unlink(); } } -static void -handle_toggle_down(GObject *gobj) +void +ObjectInstance::toggle_down(void) { - ObjectInstance *priv = get_object_qdata(gobj); - - 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()); + debug_lifecycle("Toggle notify DOWN"); /* Change to weak ref so the wrapper-wrappee pair can be * collected by the GC */ - if (priv->keep_alive.rooted()) { - GjsContext *context; - - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Unrooting object"); - priv->keep_alive.switch_to_unrooted(); + if (wrapper_is_rooted()) { + debug_lifecycle("Unrooting wrapper"); + GjsContextPrivate* gjs = GjsContextPrivate::from_current_context(); + switch_to_unrooted(gjs->context()); /* During a GC, the collector asks each object which other * objects that it wants to hold on to so if there's an entire @@ -1097,7 +1150,7 @@ handle_toggle_down(GObject *gobj) * * 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 + * a JS object that's wrapping 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 @@ -1105,38 +1158,32 @@ handle_toggle_down(GObject *gobj) * 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); + if (!gjs->destroying()) + gjs->schedule_gc(); } } -static void -handle_toggle_up(GObject *gobj) +void +ObjectInstance::toggle_up(void) { - ObjectInstance *priv = get_object_qdata(gobj); - /* We need to root the JSObject associated with the passed in GObject so it * doesn't get garbage collected (and lose any associated javascript state * such as custom properties). */ - if (!priv->keep_alive) /* Object already GC'd */ + if (!has_wrapper()) /* Object already GC'd */ return; - 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()); + debug_lifecycle("Toggle notify UP"); /* Change to strong ref so the wrappee keeps the wrapper alive * in case the wrapper has data in it that the app cares about */ - if (!priv->keep_alive.rooted()) { - /* 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, "Rooting object"); - auto cx = static_cast(gjs_context_get_native_context(context)); - priv->keep_alive.switch_to_rooted(cx); + if (!wrapper_is_rooted()) { + // FIXME: thread the context through somehow. Maybe by looking up the + // realm that obj belongs to. + debug_lifecycle("Rooting wrapper"); + auto* cx = GjsContextPrivate::from_current_context()->context(); + switch_to_rooted(cx); } } @@ -1146,27 +1193,23 @@ toggle_handler(GObject *gobj, { switch (direction) { case ToggleQueue::UP: - handle_toggle_up(gobj); + ObjectInstance::for_gobject(gobj)->toggle_up(); break; case ToggleQueue::DOWN: - handle_toggle_down(gobj); + ObjectInstance::for_gobject(gobj)->toggle_down(); break; default: g_assert_not_reached(); } } -static void -wrapped_gobj_toggle_notify(gpointer data, - GObject *gobj, - gboolean is_last_ref) -{ +static void wrapped_gobj_toggle_notify(void*, GObject* gobj, + gboolean is_last_ref) { bool is_main_thread; bool toggle_up_queued, toggle_down_queued; - GjsContext *context; - context = gjs_context_get_current(); - if (_gjs_context_destroying(context)) { + GjsContextPrivate* gjs = GjsContextPrivate::from_current_context(); + if (gjs->destroying()) { /* Do nothing here - we're in the process of disassociating * the objects. */ @@ -1181,8 +1224,8 @@ wrapped_gobj_toggle_notify(gpointer data, * of it is taken care by JS::Heap, which we use in GjsMaybeOwned, * so we're safe. As for sweeping, it is too late: the JS object * is dead, and attempting to keep it alive would soon crash - * the process. Plus, if we touch the JSAPI, libmozjs aborts in - * the first BeginRequest. + * the process. Plus, if we touch the JSAPI from another thread, libmozjs + * aborts in most cases when in debug mode. * Thus, we drain the toggle queue when GC starts, in order to * prevent this from happening. * In practice, a toggle up during JS finalize can only happen @@ -1201,7 +1244,7 @@ wrapped_gobj_toggle_notify(gpointer data, * weak singletons like g_bus_get_sync() objects can see toggle-ups * from different threads too. */ - is_main_thread = _gjs_context_get_is_owner_thread(context); + is_main_thread = gjs->is_owner_thread(); auto& toggle_queue = ToggleQueue::get_default(); std::tie(toggle_down_queued, toggle_up_queued) = toggle_queue.is_queued(gobj); @@ -1219,7 +1262,7 @@ wrapped_gobj_toggle_notify(gpointer data, toggle_up_queued? "up" : "down"); } - handle_toggle_down(gobj); + ObjectInstance::for_gobject(gobj)->toggle_down(); } else { toggle_queue.enqueue(gobj, ToggleQueue::DOWN, toggle_handler); } @@ -1234,22 +1277,22 @@ wrapped_gobj_toggle_notify(gpointer data, g_error("toggling up object %s that's already queued to toggle up\n", G_OBJECT_TYPE_NAME(gobj)); } - handle_toggle_up(gobj); + ObjectInstance::for_gobject(gobj)->toggle_up(); } else { toggle_queue.enqueue(gobj, ToggleQueue::UP, toggle_handler); } } } -static void -release_native_object (ObjectInstance *priv) +void +ObjectInstance::release_native_object(void) { - priv->keep_alive.reset(); - if (priv->uses_toggle_ref) - g_object_remove_toggle_ref(priv->gobj, wrapped_gobj_toggle_notify, nullptr); + discard_wrapper(); + if (m_uses_toggle_ref) + g_object_remove_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, nullptr); else - g_object_unref(priv->gobj); - priv->gobj = NULL; + g_object_unref(m_ptr); + m_ptr = nullptr; } /* At shutdown, we need to ensure we've cleared the context of any @@ -1270,140 +1313,114 @@ gjs_object_shutdown_toggle_queue(void) toggle_queue.shutdown(); } -void -gjs_object_prepare_shutdown(void) -{ +/* + * ObjectInstance::prepare_shutdown: + * + * Called when the #GjsContext is disposed, in order to release all GC roots of + * JSObjects that are held by GObjects. + */ +void ObjectInstance::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 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); + ObjectInstance::remove_wrapped_gobjects_if( + std::mem_fn(&ObjectInstance::wrapper_is_rooted), + std::mem_fn(&ObjectInstance::release_native_object)); } -static ObjectInstance * -init_object_private (JSContext *context, - JS::HandleObject object) -{ - ObjectInstance *proto_priv; - ObjectInstance *priv; - - JS_BeginRequest(context); - - priv = g_slice_new0(ObjectInstance); - new (priv) ObjectInstance(); - - GJS_INC_COUNTER(object); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - proto_priv = proto_priv_from_js(context, object); - g_assert(proto_priv != NULL); +ObjectInstance::ObjectInstance(JSContext* cx, JS::HandleObject object) + : GIWrapperInstance(cx, object) { + GTypeQuery query; + type_query_dynamic_safe(&query); + if (G_LIKELY(query.type)) + JS::AddAssociatedMemory(object, query.instance_size, + MemoryUse::GObjectInstanceStruct); - priv->gtype = proto_priv->gtype; - priv->info = proto_priv->info; - if (priv->info) - g_base_info_ref( (GIBaseInfo*) priv->info); + GJS_INC_COUNTER(object_instance); +} - gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Instance constructor of %s, " - "JS obj %p, priv %p", g_type_name(priv->gtype), - object.get(), priv); +ObjectPrototype::ObjectPrototype(GIObjectInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype) { + g_type_class_ref(gtype); - JS_EndRequest(context); - return priv; + GJS_INC_COUNTER(object_prototype); } -static void -update_heap_wrapper_weak_pointers(JSContext *cx, - JSCompartment *compartment, - gpointer data) -{ +/* + * ObjectInstance::update_heap_wrapper_weak_pointers: + * + * Private callback, called after the JS engine finishes garbage collection, and + * notifies when weak pointers need to be either moved or swept. + */ +void ObjectInstance::update_heap_wrapper_weak_pointers(JSContext*, + JS::Compartment*, + void*) { gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Weak pointer update callback, " "%zu wrapped GObject(s) to examine", - wrapped_gobject_list->instance_link.size()); - - 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.) - */ - 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); - } + ObjectInstance::num_wrapped_gobjects()); - priv = next; - } - - for (ObjectInstance *ex_object : to_be_disassociated) - disassociate_js_gobject(ex_object); + ObjectInstance::remove_wrapped_gobjects_if( + std::mem_fn(&ObjectInstance::weak_pointer_was_finalized), + std::mem_fn(&ObjectInstance::disassociate_js_gobject)); } -static void -ensure_weak_pointer_callback(JSContext *cx) +bool +ObjectInstance::weak_pointer_was_finalized(void) { - if (!weak_pointer_callback) { - JS_AddWeakPointerCompartmentCallback(cx, - update_heap_wrapper_weak_pointers, - nullptr); - weak_pointer_callback = true; + if (has_wrapper() && !wrapper_is_rooted() && 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.) + */ + debug_lifecycle("Found GObject weak pointer whose JS wrapper is about " + "to be finalized"); + return true; } + return false; } -static void -associate_js_gobject (JSContext *context, - JS::HandleObject object, - GObject *gobj) -{ - ObjectInstance *priv; - - priv = priv_from_js(context, object); - priv->uses_toggle_ref = false; - priv->gobj = gobj; +/* + * ObjectInstance::ensure_weak_pointer_callback: + * + * Private method called when adding a weak pointer for the first time. + */ +void ObjectInstance::ensure_weak_pointer_callback(JSContext* cx) { + if (!s_weak_pointer_callback) { + JS_AddWeakPointerCompartmentCallback( + cx, &ObjectInstance::update_heap_wrapper_weak_pointers, nullptr); + s_weak_pointer_callback = true; + } +} - g_assert(!priv->keep_alive.rooted()); +void +ObjectInstance::associate_js_gobject(JSContext *context, + JS::HandleObject object, + GObject *gobj) +{ + g_assert(!wrapper_is_rooted()); - set_object_qdata(gobj, priv); + m_uses_toggle_ref = false; + m_ptr = gobj; + set_object_qdata(); + m_wrapper = object; - priv->keep_alive = object; ensure_weak_pointer_callback(context); - object_instance_link(priv); + link(); - g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, priv); + g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, this); } -static void -ensure_uses_toggle_ref(JSContext *cx, - ObjectInstance *priv) +void +ObjectInstance::ensure_uses_toggle_ref(JSContext *cx) { - if (priv->uses_toggle_ref) + if (m_uses_toggle_ref) return; - g_assert(!priv->keep_alive.rooted()); + debug_lifecycle("Switching object instance to toggle ref"); + + g_assert(!wrapper_is_rooted()); /* OK, here is where things get complicated. We want the * wrapped gobj to keep the JSObject* wrapper alive, because @@ -1416,129 +1433,134 @@ ensure_uses_toggle_ref(JSContext *cx, * the wrapper to be garbage collected (and thus unref the * wrappee). */ - priv->uses_toggle_ref = true; - priv->keep_alive.switch_to_rooted(cx); - g_object_add_toggle_ref(priv->gobj, wrapped_gobj_toggle_notify, nullptr); + m_uses_toggle_ref = true; + switch_to_rooted(cx); + g_object_add_toggle_ref(m_ptr, 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_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->closures.empty()) { - /* This will also free cd, through the closure invalidation mechanism */ - GClosure *closure = *priv->closures.begin(); + g_object_unref(m_ptr); +} + +static void invalidate_closure_list(std::forward_list* closures) { + g_assert(closures); + // Can't loop directly through the items, since invalidating an item's + // closure might have the effect of removing the item from the list in the + // invalidate notifier + while (!closures->empty()) { + // This will also free the closure data, through the closure + // invalidation mechanism, but adding a temporary reference to + // ensure that the closure is still valid when calling invalidation + // notify callbacks + using GjsAutoGClosure = + GjsAutoPointer; + GjsAutoGClosure closure(closures->front(), GjsAutoTakeOwnership()); g_closure_invalidate(closure); /* Erase element if not already erased */ - priv->closures.erase(closure); + closures->remove(closure); } } -static void -disassociate_js_gobject(ObjectInstance *priv) +// Note: m_wrapper (the JS object) may already be null when this is called, if +// it was finalized while the GObject was toggled down. +void +ObjectInstance::disassociate_js_gobject(void) { bool had_toggle_down, had_toggle_up; - if (!priv->g_object_finalized) - g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv); + if (!m_gobj_disposed) + g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this); auto& toggle_queue = ToggleQueue::get_default(); - std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(priv->gobj); + std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr); if (had_toggle_down != had_toggle_up) { - 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)); + g_error( + "JS object wrapper for GObject %p (%s) is being released while " + "toggle references are still pending.", + m_ptr, type_name()); } /* Fist, remove the wrapper pointer from the wrapped GObject */ - set_object_qdata(priv->gobj, nullptr); + unset_object_qdata(); /* Now release all the resources the current wrapper has */ - invalidate_all_closures(priv); - release_native_object(priv); + invalidate_closure_list(&m_closures); + release_native_object(); /* Mark that a JS object once existed, but it doesn't any more */ - priv->js_object_finalized = true; - priv->keep_alive = nullptr; + m_wrapper_finalized = true; } -static void -clear_g_params(std::vector& params) +bool +ObjectInstance::init_impl(JSContext *context, + const JS::CallArgs& args, + JS::MutableHandleObject object) { - for (GParameter param : params) - g_value_unset(¶m.value); -} + g_assert(gtype() != G_TYPE_NONE); -static bool -object_instance_init (JSContext *context, - JS::MutableHandleObject object, - const JS::HandleValueArray& args) -{ - ObjectInstance *priv; - GType gtype; - std::vector params; - GTypeQuery query; - GObject *gobj; + if (args.length() > 1 && + !JS::WarnUTF8(context, + "Too many arguments to the constructor of %s: expected " + "1, got %u", + name(), args.length())) + return false; - priv = (ObjectInstance *) JS_GetPrivate(object); + std::vector names; + AutoGValueVector values; - gtype = priv->gtype; - g_assert(gtype != G_TYPE_NONE); + if (args.length() > 0 && !args[0].isUndefined()) { + if (!args[0].isObject()) { + gjs_throw(context, + "Argument to the constructor of %s should be an object " + "with properties to set", + name()); + return false; + } - if (G_TYPE_IS_ABSTRACT(gtype)) { - gjs_throw(context, "Cannot instantiate abstract class %s", g_type_name(gtype)); - return false; + JS::RootedObject props(context, &args[0].toObject()); + if (!m_proto->props_to_g_parameters(context, props, &names, &values)) + return false; } - if (!object_instance_props_to_g_parameters(context, object, args, - gtype, params)) { - clear_g_params(params); + if (G_TYPE_IS_ABSTRACT(gtype())) { + gjs_throw(context, + "Cannot instantiate abstract type %s", g_type_name(gtype())); return false; } - /* Mark this object in the construction stack, it - will be popped in gjs_object_custom_init() later - down. - */ - if (g_type_get_qdata(gtype, gjs_is_custom_type_quark())) { - object_init_list.emplace(context, object); + // Mark this object in the construction stack, it will be popped in + // gjs_object_custom_init() in gi/gobject.cpp. + if (is_custom_js_class()) { + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + if (!gjs->object_init_list().append(object)) { + JS_ReportOutOfMemory(context); + return false; + } } -G_GNUC_BEGIN_IGNORE_DEPRECATIONS - gobj = (GObject*) g_object_newv(gtype, params.size(), params.data()); -G_GNUC_END_IGNORE_DEPRECATIONS + g_assert(names.size() == values.size()); + GObject* gobj = g_object_new_with_properties(gtype(), values.size(), + names.data(), values.data()); - clear_g_params(params); - - ObjectInstance *other_priv = get_object_qdata(gobj); - if (other_priv && other_priv->keep_alive != object.get()) { - /* g_object_newv returned an object that's already tracked by a JS - * object. Let's assume this is a singleton like IBus.IBus and return - * the existing JS wrapper object. + ObjectInstance *other_priv = ObjectInstance::for_gobject(gobj); + if (other_priv && other_priv->m_wrapper != object.get()) { + /* g_object_new_with_properties() returned an object that's already + * tracked by a JS object. Let's assume this is a singleton like + * IBus.IBus and return the existing JS wrapper object. * * 'object' has a value that was originally created by * JS_NewObjectForConstructor in GJS_NATIVE_CONSTRUCTOR_PRELUDE, but * 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); + other_priv->ensure_uses_toggle_ref(context); + object.set(other_priv->m_wrapper); g_object_unref(gobj); /* We already own a reference */ gobj = NULL; - goto out; + return true; } - g_type_query_dynamic_safe(gtype, &query); - if (G_LIKELY (query.type)) - JS_updateMallocCounter(context, query.instance_size); - if (G_IS_INITIALLY_UNOWNED(gobj) && !g_object_is_floating(gobj)) { /* GtkWindow does not return a ref to caller of g_object_new. @@ -1554,123 +1576,99 @@ G_GNUC_END_IGNORE_DEPRECATIONS /* we should already have a ref */ } - if (priv->gobj == NULL) + if (!m_ptr) associate_js_gobject(context, object, 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", - priv->info ? g_base_info_get_name((GIBaseInfo*) priv->info) : g_type_name(gtype))); + TRACE(GJS_OBJECT_WRAPPER_NEW(this, m_ptr, ns(), name())); - out: + args.rval().setObject(*object); return true; } -GJS_NATIVE_CONSTRUCTOR_DECLARE(object_instance) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(object_instance) - bool ret; +// See GIWrapperBase::constructor() +bool ObjectInstance::constructor_impl(JSContext* context, + JS::HandleObject object, + const JS::CallArgs& argv) { JS::RootedValue initer(context); - - GJS_NATIVE_CONSTRUCTOR_PRELUDE(object_instance); - - /* Init the private variable before we do anything else. If a garbage - * collection happens when calling the init function then this object - * might be traced and we will end up dereferencing a null pointer */ - init_object_private(context, object); - - if (!gjs_object_require_property(context, object, "GObject instance", - GJS_STRING_GOBJECT_INIT, &initer)) + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + const auto& new_target = argv.newTarget(); + bool has_gtype; + + g_assert(new_target.isObject() && "new.target needs to be an object"); + JS::RootedObject rooted_target(context, &new_target.toObject()); + if (!JS_HasOwnPropertyById(context, rooted_target, gjs->atoms().gtype(), + &has_gtype)) return false; - argv.rval().setUndefined(); - ret = gjs_call_function_value(context, object, initer, argv, argv.rval()); - - if (argv.rval().isUndefined()) - argv.rval().setObject(*object); + if (!has_gtype) { + gjs_throw(context, + "Tried to construct an object without a GType; are " + "you using GObject.registerClass() when inheriting " + "from a GObject type?"); + return false; + } - return ret; + return gjs_object_require_property(context, object, "GObject instance", + gjs->atoms().init(), &initer) && + gjs->call_function(object, initer, argv, argv.rval()); } -static void -object_instance_trace(JSTracer *tracer, - JSObject *obj) -{ - ObjectInstance *priv; - - priv = (ObjectInstance *) JS_GetPrivate(obj); - if (priv == NULL) - return; - - 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 (GClosure *closure : priv->closures) +void ObjectInstance::trace_impl(JSTracer* tracer) { + for (GClosure *closure : m_closures) gjs_closure_trace(closure, tracer); } -static void -closure_invalidated(void *data, - GClosure *closure) -{ - auto priv = static_cast(data); - priv->closures.erase(closure); +void ObjectPrototype::trace_impl(JSTracer* tracer) { + m_property_cache.trace(tracer); + m_field_cache.trace(tracer); + m_unresolvable_cache.trace(tracer); + for (GClosure* closure : m_vfuncs) + gjs_closure_trace(closure, tracer); } -static void -object_instance_finalize(JSFreeOp *fop, - JSObject *obj) -{ - ObjectInstance *priv; +void ObjectInstance::finalize_impl(JSFreeOp* fop, JSObject* obj) { + GTypeQuery query; + type_query_dynamic_safe(&query); + if (G_LIKELY(query.type)) + JS::RemoveAssociatedMemory(obj, query.instance_size, + MemoryUse::GObjectInstanceStruct); - priv = (ObjectInstance *) JS_GetPrivate(obj); - 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); + GIWrapperInstance::finalize_impl(fop, obj); +} - TRACE(GJS_OBJECT_PROXY_FINALIZE(priv, priv->gobj, - priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "_gjs_private", - priv->info ? g_base_info_get_name((GIBaseInfo*) priv->info) : g_type_name(priv->gtype))); +ObjectInstance::~ObjectInstance() { + TRACE(GJS_OBJECT_WRAPPER_FINALIZE(this, m_ptr, ns(), name())); - /* This applies only to instances, not prototypes, but it's possible that - * an instance's GObject is already freed at this point. */ - invalidate_all_closures(priv); + invalidate_closure_list(&m_closures); - /* Object is instance, not prototype, AND GObject is not already freed */ - if (priv->gobj) { + /* GObject is not already freed */ + if (m_ptr) { bool had_toggle_up; bool had_toggle_down; - if (G_UNLIKELY (priv->gobj->ref_count <= 0)) { - g_error("Finalizing proxy for an already freed object of type: %s.%s\n", - 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 (G_UNLIKELY(m_ptr->ref_count <= 0)) { + g_error( + "Finalizing wrapper for an already freed object of type: " + "%s.%s\n", + ns(), name()); } auto& toggle_queue = ToggleQueue::get_default(); - std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(priv->gobj); + std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr); if (!had_toggle_up && had_toggle_down) { - g_error("Finalizing proxy for an object that's scheduled to be unrooted: %s.%s\n", - 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)); + g_error( + "Finalizing wrapper for an object that's scheduled to be " + "unrooted: %s.%s\n", + ns(), name()); } - if (!priv->g_object_finalized) - g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv); - release_native_object(priv); + if (!m_gobj_disposed) + g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this); + release_native_object(); } - if (priv->keep_alive.rooted()) { + if (wrapper_is_rooted()) { /* This happens when the refcount on the object is still >1, * for example with global objects GDK never frees like GdkDisplay, * when we close down the JS runtime. @@ -1678,35 +1676,27 @@ 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, "Unrooting object"); + debug_lifecycle("Unrooting object"); - priv->keep_alive.reset(); + discard_wrapper(); } - object_instance_unlink(priv); + unlink(); - if (priv->info) { - g_base_info_unref( (GIBaseInfo*) priv->info); - priv->info = NULL; - } + GJS_DEC_COUNTER(object_instance); +} - if (priv->klass) { - g_type_class_unref (priv->klass); - priv->klass = NULL; - } +ObjectPrototype::~ObjectPrototype() { + invalidate_closure_list(&m_vfuncs); - GJS_DEC_COUNTER(object); - priv->~ObjectInstance(); - g_slice_free(ObjectInstance, priv); + g_clear_pointer(&m_info, g_base_info_unref); + g_type_class_unref(g_type_class_peek(m_gtype)); - /* Remove the ObjectInstance pointer from the JSObject */ - JS_SetPrivate(obj, nullptr); + GJS_DEC_COUNTER(object_prototype); } -static JSObject * -gjs_lookup_object_constructor_from_info(JSContext *context, - GIObjectInfo *info, - GType gtype) -{ +JSObject* gjs_lookup_object_constructor_from_info(JSContext* context, + GIObjectInfo* info, + GType gtype) { JS::RootedObject in_object(context); const char *constructor_name; @@ -1731,8 +1721,9 @@ gjs_lookup_object_constructor_from_info(JSContext *context, we need to define it first. */ JS::RootedObject ignored(context); - gjs_define_object_class(context, in_object, NULL, gtype, &constructor, - &ignored); + if (!ObjectPrototype::define_class(context, in_object, nullptr, gtype, + &constructor, &ignored)) + return nullptr; } else { if (G_UNLIKELY (!value.isObject())) return NULL; @@ -1745,6 +1736,7 @@ gjs_lookup_object_constructor_from_info(JSContext *context, return constructor; } +GJS_JSAPI_RETURN_CONVENTION static JSObject * gjs_lookup_object_prototype_from_info(JSContext *context, GIObjectInfo *info, @@ -1756,138 +1748,158 @@ gjs_lookup_object_prototype_from_info(JSContext *context, if (G_UNLIKELY(!constructor)) return NULL; - JS::RootedValue value(context); - if (!gjs_object_get_property(context, constructor, - GJS_STRING_PROTOTYPE, &value)) - return NULL; - - if (G_UNLIKELY (!value.isObjectOrNull())) + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + JS::RootedObject prototype(context); + if (!gjs_object_require_property(context, constructor, "constructor object", + atoms.prototype(), &prototype)) return NULL; - return value.toObjectOrNull(); + return prototype; } +GJS_JSAPI_RETURN_CONVENTION static JSObject * gjs_lookup_object_prototype(JSContext *context, GType gtype) { - GIObjectInfo *info; - JSObject *proto; + GjsAutoObjectInfo info = g_irepository_find_by_gtype(nullptr, gtype); + return gjs_lookup_object_prototype_from_info(context, info, gtype); +} + +// Retrieves a GIFieldInfo for a field named @key. This is for use in +// field_getter_impl() and field_setter_not_impl(), where the field info *must* +// have been cached previously in resolve_impl() on this ObjectPrototype or one +// of its parent ObjectPrototypes. This will fail an assertion if there is no +// cached field info. +// +// The caller does not own the return value, and it can never be null. +GIFieldInfo* ObjectPrototype::lookup_cached_field_info(JSContext* cx, + JS::HandleString key) { + if (!info()) { + // Custom JS classes can't have fields, and fields on internal classes + // are not available. We must be looking up a field on a + // GObject-introspected parent. + GType parent_gtype = g_type_parent(m_gtype); + g_assert(parent_gtype != G_TYPE_INVALID && + "Custom JS class must have parent"); + ObjectPrototype* parent_proto = + ObjectPrototype::for_gtype(parent_gtype); + g_assert(parent_proto && + "Custom JS class's parent must have been accessed in JS"); + return parent_proto->lookup_cached_field_info(cx, key); + } - info = (GIObjectInfo*)g_irepository_find_by_gtype(g_irepository_get_default(), gtype); - proto = gjs_lookup_object_prototype_from_info(context, info, gtype); - if (info) - g_base_info_unref((GIBaseInfo*)info); + gjs_debug_jsprop(GJS_DEBUG_GOBJECT, + "Looking up cached field info for '%s' in '%s' prototype", + gjs_debug_string(key).c_str(), g_type_name(m_gtype)); + auto entry = m_field_cache.lookupForAdd(key); + if (entry) + return entry->value().get(); - return proto; + // We must be looking up a field defined on a parent. Look up the prototype + // object via its GIObjectInfo. + GjsAutoObjectInfo parent_info = g_object_info_get_parent(m_info); + JS::RootedObject parent_proto(cx, gjs_lookup_object_prototype_from_info( + cx, parent_info, G_TYPE_INVALID)); + ObjectPrototype* parent = ObjectPrototype::for_js(cx, parent_proto); + return parent->lookup_cached_field_info(cx, key); } -static void -do_associate_closure(ObjectInstance *priv, - GClosure *closure) -{ +void ObjectInstance::associate_closure(JSContext* cx, GClosure* closure) { + if (!is_prototype()) + to_instance()->ensure_uses_toggle_ref(cx); + /* 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); + auto already_has = std::find(m_closures.begin(), m_closures.end(), closure); + g_assert(already_has == m_closures.end() && + "This closure was already associated with this object"); + m_closures.push_front(closure); + g_closure_add_invalidate_notifier( + closure, this, &ObjectInstance::closure_invalidated_notify); +} + +void ObjectInstance::closure_invalidated_notify(void* data, GClosure* closure) { + auto* priv = static_cast(data); + priv->m_closures.remove(closure); +} + +bool ObjectBase::connect(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + if (!priv->check_is_instance(cx, "connect to signals")) + return false; + + return priv->to_instance()->connect_impl(cx, args, false); +} + +bool ObjectBase::connect_after(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + if (!priv->check_is_instance(cx, "connect to signals")) + return false; + + return priv->to_instance()->connect_impl(cx, args, true); } -static bool -real_connect_func(JSContext *context, - unsigned argc, - JS::Value *vp, - bool after) +bool +ObjectInstance::connect_impl(JSContext *context, + const JS::CallArgs& args, + bool after) { - GJS_GET_PRIV(context, argc, vp, argv, obj, ObjectInstance, priv); GClosure *closure; gulong id; guint signal_id; GQuark signal_detail; - gjs_debug_gsignal("connect obj %p priv %p argc %d", obj.get(), priv, argc); - if (priv == NULL) { - throw_priv_is_null_error(context); - return false; /* wrong class passed in */ - } - if (priv->gobj == NULL) { - /* prototype, not an instance. */ - gjs_throw(context, "Can't connect to signals on %s.%s.prototype; only on instances", - 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)); - 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; - } + gjs_debug_gsignal("connect obj %p priv %p", m_wrapper.get(), this); - ensure_uses_toggle_ref(context, priv); + if (!check_gobject_disposed("connect to any signal on")) + return true; - if (argc != 2 || !argv[0].isString() || !JS::IsCallable(&argv[1].toObject())) { - gjs_throw(context, "connect() takes two args, the signal name and the callback"); + JS::UniqueChars signal_name; + JS::RootedObject callback(context); + if (!gjs_parse_call_args(context, after ? "connect_after" : "connect", args, "so", + "signal name", &signal_name, + "callback", &callback)) return false; - } - JS::RootedString signal_str(context, argv[0].toString()); - GjsAutoJSChar signal_name = JS_EncodeStringToUTF8(context, signal_str); - if (!signal_name) + if (!JS::IsCallable(callback)) { + gjs_throw(context, "second arg must be a callback"); return false; + } - if (!g_signal_parse_name(signal_name, - G_OBJECT_TYPE(priv->gobj), - &signal_id, - &signal_detail, - true)) { + if (!g_signal_parse_name(signal_name.get(), gtype(), &signal_id, + &signal_detail, true)) { gjs_throw(context, "No signal '%s' on object '%s'", - signal_name.get(), - g_type_name(G_OBJECT_TYPE(priv->gobj))); + signal_name.get(), type_name()); return false; } - closure = gjs_closure_new_for_signal(context, &argv[1].toObject(), "signal callback", signal_id); + closure = gjs_closure_new_for_signal( + context, JS_GetObjectFunction(callback), "signal callback", signal_id); if (closure == NULL) return false; - do_associate_closure(priv, closure); + associate_closure(context, closure); - id = g_signal_connect_closure_by_id(priv->gobj, - signal_id, - signal_detail, - closure, - after); + id = g_signal_connect_closure_by_id(m_ptr, signal_id, signal_detail, + closure, after); - argv.rval().setDouble(id); + args.rval().setDouble(id); return true; } -static bool -connect_after_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - return real_connect_func(context, argc, vp, true); -} +bool ObjectBase::emit(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + if (!priv->check_is_instance(cx, "emit signal")) + return false; -static bool -connect_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - return real_connect_func(context, argc, vp, false); + return priv->to_instance()->emit_impl(cx, args); } -static bool -emit_func(JSContext *context, - unsigned argc, - JS::Value *vp) +bool +ObjectInstance::emit_impl(JSContext *context, + const JS::CallArgs& argv) { - GJS_GET_PRIV(context, argc, vp, argv, obj, ObjectInstance, priv); guint signal_id; GQuark signal_detail; GSignalQuery signal_query; @@ -1896,61 +1908,30 @@ emit_func(JSContext *context, unsigned int i; bool failed; - gjs_debug_gsignal("emit obj %p priv %p argc %d", obj.get(), priv, argc); - - if (priv == NULL) { - throw_priv_is_null_error(context); - return false; /* wrong class passed in */ - } - - if (priv->gobj == NULL) { - /* prototype, not an instance. */ - gjs_throw(context, "Can't emit signal on %s.%s.prototype; only on instances", - 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)); - return false; - } + gjs_debug_gsignal("emit obj %p priv %p argc %d", m_wrapper.get(), this, + argv.length()); - 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(); + if (!check_gobject_disposed("emit any signal on")) return true; - } - if (argc < 1 || !argv[0].isString()) { - gjs_throw(context, "emit() first arg is the signal name"); - return false; - } - - JS::RootedString signal_str(context, argv[0].toString()); - GjsAutoJSChar signal_name = JS_EncodeStringToUTF8(context, signal_str); - if (!signal_name) + JS::UniqueChars signal_name; + if (!gjs_parse_call_args(context, "emit", argv, "!s", + "signal name", &signal_name)) return false; - if (!g_signal_parse_name(signal_name, - G_OBJECT_TYPE(priv->gobj), - &signal_id, - &signal_detail, - false)) { + if (!g_signal_parse_name(signal_name.get(), gtype(), &signal_id, + &signal_detail, false)) { gjs_throw(context, "No signal '%s' on object '%s'", - signal_name.get(), - g_type_name(G_OBJECT_TYPE(priv->gobj))); + signal_name.get(), type_name()); return false; } g_signal_query(signal_id, &signal_query); - if ((argc - 1) != signal_query.n_params) { + if ((argv.length() - 1) != signal_query.n_params) { gjs_throw(context, "Signal '%s' on %s requires %d args got %d", - signal_name.get(), - g_type_name(G_OBJECT_TYPE(priv->gobj)), - signal_query.n_params, - argc - 1); + signal_name.get(), type_name(), signal_query.n_params, + argv.length() - 1); return false; } @@ -1961,8 +1942,8 @@ emit_func(JSContext *context, instance_and_args = g_newa(GValue, signal_query.n_params + 1); memset(instance_and_args, 0, sizeof(GValue) * (signal_query.n_params + 1)); - g_value_init(&instance_and_args[0], G_TYPE_FROM_INSTANCE(priv->gobj)); - g_value_set_instance(&instance_and_args[0], priv->gobj); + g_value_init(&instance_and_args[0], gtype()); + g_value_set_instance(&instance_and_args[0], m_ptr); failed = false; for (i = 0; i < signal_query.n_params; ++i) { @@ -2000,391 +1981,506 @@ emit_func(JSContext *context, return !failed; } -static bool -to_string_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, rec, obj, ObjectInstance, priv); +bool ObjectInstance::signal_match_arguments_from_object( + JSContext* cx, JS::HandleObject match_obj, GSignalMatchType* mask_out, + unsigned* signal_id_out, GQuark* detail_out, + JS::MutableHandleFunction func_out) { + g_assert(mask_out && signal_id_out && detail_out && "forgot out parameter"); - if (priv == NULL) { - throw_priv_is_null_error(context); - return false; /* wrong class passed in */ - } + int mask = 0; + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); - return _gjs_proxy_to_string_func(context, obj, - (priv->g_object_finalized) ? - "object (FINALIZED)" : "object", - (GIBaseInfo*)priv->info, priv->gtype, - priv->gobj, rec.rval()); -} + bool has_id; + unsigned signal_id = 0; + if (!JS_HasOwnPropertyById(cx, match_obj, atoms.signal_id(), &has_id)) + return false; + if (has_id) { + mask |= G_SIGNAL_MATCH_ID; -static const struct JSClassOps gjs_object_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - object_instance_get_prop, - object_instance_set_prop, - NULL, /* enumerate */ - object_instance_resolve, - nullptr, /* mayResolve */ - object_instance_finalize, - NULL, - NULL, - NULL, - object_instance_trace, -}; + JS::RootedValue value(cx); + if (!JS_GetPropertyById(cx, match_obj, atoms.signal_id(), &value)) + return false; -struct JSClass gjs_object_instance_class = { - "GObject_Object", - JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, - &gjs_object_class_ops -}; + JS::UniqueChars signal_name = gjs_string_to_utf8(cx, value); + if (!signal_name) + return false; -static bool -init_func (JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_THIS(context, argc, vp, argv, obj); - bool ret; + signal_id = g_signal_lookup(signal_name.get(), gtype()); + } - if (!do_base_typecheck(context, obj, true)) + bool has_detail; + GQuark detail = 0; + if (!JS_HasOwnPropertyById(cx, match_obj, atoms.detail(), &has_detail)) return false; + if (has_detail) { + mask |= G_SIGNAL_MATCH_DETAIL; - ret = object_instance_init(context, &obj, argv); + JS::RootedValue value(cx); + if (!JS_GetPropertyById(cx, match_obj, atoms.detail(), &value)) + return false; - if (ret) - argv.rval().setObject(*obj); + JS::UniqueChars detail_string = gjs_string_to_utf8(cx, value); + if (!detail_string) + return false; - return ret; + detail = g_quark_from_string(detail_string.get()); + } + + bool has_func; + JS::RootedFunction func(cx); + if (!JS_HasOwnPropertyById(cx, match_obj, atoms.func(), &has_func)) + return false; + if (has_func) { + mask |= G_SIGNAL_MATCH_CLOSURE; + + JS::RootedValue value(cx); + if (!JS_GetPropertyById(cx, match_obj, atoms.func(), &value)) + return false; + + if (!value.isObject() || !JS_ObjectIsFunction(&value.toObject())) { + gjs_throw(cx, "'func' property must be a function"); + return false; + } + + func = JS_GetObjectFunction(&value.toObject()); + } + + if (!has_id && !has_detail && !has_func) { + gjs_throw(cx, "Must specify at least one of signalId, detail, or func"); + return false; + } + + *mask_out = GSignalMatchType(mask); + if (has_id) + *signal_id_out = signal_id; + if (has_detail) + *detail_out = detail; + if (has_func) + func_out.set(func); + return true; } -JSPropertySpec gjs_object_instance_proto_props[] = { - JS_PS_END -}; +bool ObjectBase::signal_find(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + if (!priv->check_is_instance(cx, "find signal")) + return false; -JSFunctionSpec gjs_object_instance_proto_funcs[] = { - JS_FS("_init", init_func, 0, 0), - JS_FS("connect", connect_func, 0, 0), - JS_FS("connect_after", connect_after_func, 0, 0), - JS_FS("emit", emit_func, 0, 0), - JS_FS("toString", to_string_func, 0, 0), - JS_FS_END -}; + return priv->to_instance()->signal_find_impl(cx, args); +} -void -gjs_object_define_static_methods(JSContext *context, - JS::HandleObject constructor, - GType gtype, - GIObjectInfo *object_info) -{ - GIStructInfo *gtype_struct; - int i; - int n_methods; +bool ObjectInstance::signal_find_impl(JSContext* cx, const JS::CallArgs& args) { + gjs_debug_gsignal("[Gi.signal_find_symbol]() obj %p priv %p argc %d", + m_wrapper.get(), this, args.length()); - n_methods = g_object_info_get_n_methods(object_info); + if (!check_gobject_disposed("find any signal on")) + return true; - for (i = 0; i < n_methods; i++) { - GIFunctionInfo *meth_info; - GIFunctionInfoFlags flags; + JS::RootedObject match(cx); + if (!gjs_parse_call_args(cx, "[Gi.signal_find_symbol]", args, "o", "match", + &match)) + return false; - meth_info = g_object_info_get_method(object_info, i); - flags = g_function_info_get_flags (meth_info); + GSignalMatchType mask; + unsigned signal_id; + GQuark detail; + JS::RootedFunction func(cx); + if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id, + &detail, &func)) + return false; - /* Anything that isn't a method we put on the prototype of the - * constructor. This includes introspection - * methods, as well as the forthcoming "static methods" - * support. We may want to change this to use - * GI_FUNCTION_IS_CONSTRUCTOR and GI_FUNCTION_IS_STATIC or the - * like in the near future. - */ - if (!(flags & GI_FUNCTION_IS_METHOD)) { - if (!gjs_define_function(context, constructor, gtype, - (GICallableInfo *) meth_info)) - gjs_log_exception(context); + uint64_t handler = 0; + if (!func) { + handler = g_signal_handler_find(m_ptr, mask, signal_id, detail, nullptr, + nullptr, nullptr); + } else { + for (GClosure* candidate : m_closures) { + if (gjs_closure_get_callable(candidate) == func) { + handler = g_signal_handler_find(m_ptr, mask, signal_id, detail, + candidate, nullptr, nullptr); + if (handler != 0) + break; + } } - - g_base_info_unref((GIBaseInfo*) meth_info); } - gtype_struct = g_object_info_get_class_struct(object_info); + args.rval().setNumber(static_cast(handler)); + return true; +} - if (gtype_struct == NULL) - return; +template +static inline const char* signal_match_to_action_name(); + +template <> +inline const char* +signal_match_to_action_name<&g_signal_handlers_block_matched>() { + return "block"; +} - n_methods = g_struct_info_get_n_methods(gtype_struct); +template <> +inline const char* +signal_match_to_action_name<&g_signal_handlers_unblock_matched>() { + return "unblock"; +} - for (i = 0; i < n_methods; i++) { - GIFunctionInfo *meth_info; +template <> +inline const char* +signal_match_to_action_name<&g_signal_handlers_disconnect_matched>() { + return "disconnect"; +} - meth_info = g_struct_info_get_method(gtype_struct, i); +template +bool ObjectBase::signals_action(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + const std::string action_name = signal_match_to_action_name(); + if (!priv->check_is_instance(cx, (action_name + " signal").c_str())) + return false; - if (!gjs_define_function(context, constructor, gtype, - (GICallableInfo *) meth_info)) - gjs_log_exception(context); + return priv->to_instance()->signals_action_impl(cx, args); +} - g_base_info_unref((GIBaseInfo*) meth_info); +template +bool ObjectInstance::signals_action_impl(JSContext* cx, + const JS::CallArgs& args) { + const std::string action_name = signal_match_to_action_name(); + const std::string action_tag = "[Gi.signals_" + action_name + "_symbol]"; + gjs_debug_gsignal("[%s]() obj %p priv %p argc %d", action_tag.c_str(), + m_wrapper.get(), this, args.length()); + + if (!check_gobject_disposed((action_name + " any signal on").c_str())) { + return true; + } + JS::RootedObject match(cx); + if (!gjs_parse_call_args(cx, action_tag.c_str(), args, "o", "match", + &match)) { + return false; + } + GSignalMatchType mask; + unsigned signal_id; + GQuark detail; + JS::RootedFunction func(cx); + if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id, + &detail, &func)) { + return false; + } + unsigned n_matched = 0; + if (!func) { + n_matched = MatchFunc(m_ptr, mask, signal_id, detail, nullptr, nullptr, + nullptr); + } else { + std::vector candidates; + for (GClosure* candidate : m_closures) { + if (gjs_closure_get_callable(candidate) == func) + candidates.push_back(candidate); + } + for (GClosure* candidate : candidates) { + n_matched += MatchFunc(m_ptr, mask, signal_id, detail, candidate, + nullptr, nullptr); + } } - g_base_info_unref((GIBaseInfo*) gtype_struct); + args.rval().setNumber(n_matched); + return true; } -void -gjs_define_object_class(JSContext *context, - JS::HandleObject in_object, - GIObjectInfo *info, - GType gtype, - JS::MutableHandleObject constructor, - JS::MutableHandleObject prototype) -{ - const char *constructor_name; - JS::RootedObject parent_proto(context); - - ObjectInstance *priv; - const char *ns; - GType parent_type; +bool ObjectBase::to_string(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv); + return gjs_wrapper_to_string_func( + cx, obj, priv->to_string_kind(), priv->info(), priv->gtype(), + priv->is_prototype() ? nullptr : priv->to_instance()->ptr(), + args.rval()); +} - g_assert(in_object); - g_assert(gtype != G_TYPE_INVALID); +// Override of GIWrapperBase::to_string_kind() +const char* ObjectBase::to_string_kind(void) const { + if (is_prototype()) + return "object"; + return to_instance()->to_string_kind(); +} - /* http://egachine.berlios.de/embedding-sm-best-practice/apa.html - * http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/ - * http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/ - * - * What we want is: - * - * repoobj.Gtk.Window is constructor for a GtkWindow wrapper JSObject - * (gjs_define_object_constructor() is supposed to define Window in Gtk) - * - * Window.prototype contains the methods on Window, e.g. set_default_size() - * mywindow.__proto__ is Window.prototype - * mywindow.__proto__.__proto__ is Bin.prototype - * mywindow.__proto__.__proto__.__proto__ is Container.prototype - * - * Because Window.prototype is an instance of Window in a sense, - * Window.prototype.__proto__ is Window.prototype, just as - * mywindow.__proto__ is Window.prototype - * - * If we do "mywindow = new Window()" then we should get: - * mywindow.__proto__ == Window.prototype - * which means "mywindow instanceof Window" is true. - * - * Remember "Window.prototype" is "the __proto__ of stuff - * constructed with new Window()" - * - * __proto__ is used to search for properties if you do "this.foo" - * while __parent__ defines the scope to search if you just have - * "foo". - * - * __proto__ is used to look up properties, while .prototype is only - * relevant for constructors and is used to set __proto__ on new'd - * objects. So .prototype only makes sense on constructors. - * - * JS_SetPrototype() and JS_GetPrototype() are for __proto__. - * To set/get .prototype, just use the normal property accessors, - * or JS_InitClass() sets it up automatically. - * - * JavaScript is SO AWESOME - */ +/* + * ObjectInstance::to_string_kind: + * + * Instance-only version of GIWrapperBase::to_string_kind(). ObjectInstance + * shows a "finalized" marker in its toString() method if the wrapped GObject + * has already been finalized. + */ +const char* ObjectInstance::to_string_kind(void) const { + return m_gobj_disposed ? "object (FINALIZED)" : "object"; +} - parent_type = g_type_parent(gtype); - if (parent_type != G_TYPE_INVALID) - parent_proto = gjs_lookup_object_prototype(context, parent_type); - - ns = gjs_get_names_from_gtype_and_gi_info(gtype, (GIBaseInfo *) info, - &constructor_name); - - if (!gjs_init_class_dynamic(context, in_object, - parent_proto, - ns, constructor_name, - &gjs_object_instance_class, - gjs_object_instance_constructor, 0, - /* props of prototype */ - parent_proto ? NULL : &gjs_object_instance_proto_props[0], - /* funcs of prototype */ - parent_proto ? NULL : &gjs_object_instance_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - NULL, - prototype, - constructor)) { - g_error("Can't init class %s", constructor_name); - } - - GJS_INC_COUNTER(object); - priv = g_slice_new0(ObjectInstance); - new (priv) ObjectInstance(); - priv->info = info; - if (info) - g_base_info_ref((GIBaseInfo*) info); - priv->gtype = gtype; - priv->klass = (GTypeClass*) g_type_class_ref (gtype); - JS_SetPrivate(prototype, priv); - - 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); - - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, gtype)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, - JSPROP_PERMANENT); -} - -JSObject* -gjs_object_from_g_object(JSContext *context, - GObject *gobj) -{ - if (gobj == NULL) - return NULL; +/* + * ObjectBase::init_gobject: + * + * This is named "init_gobject()" but corresponds to "_init()" in JS. The reason + * for the name is that an "init()" method is used within SpiderMonkey to + * indicate fallible initialization that must be done before an object can be + * used, which is not the case here. + */ +bool ObjectBase::init_gobject(JSContext* context, unsigned argc, + JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(context, argc, vp, argv, obj, ObjectBase, priv); + if (!priv->check_is_instance(context, "initialize")) + return false; - ObjectInstance *priv = get_object_qdata(gobj); + return priv->to_instance()->init_impl(context, argv, &obj); +} - if (!priv) { - /* We have to create a wrapper */ - GType gtype; +// clang-format off +const struct JSClassOps ObjectBase::class_ops = { + &ObjectBase::add_property, + nullptr, // deleteProperty + nullptr, // enumerate + &ObjectBase::new_enumerate, + &ObjectBase::resolve, + nullptr, // mayResolve + &ObjectBase::finalize, + NULL, + NULL, + NULL, + &ObjectBase::trace, +}; - gjs_debug_marshal(GJS_DEBUG_GOBJECT, - "Wrapping %s with JSObject", - g_type_name_from_instance((GTypeInstance*) gobj)); +const struct JSClass ObjectBase::klass = { + "GObject_Object", + JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, + &ObjectBase::class_ops +}; - gtype = G_TYPE_FROM_INSTANCE(gobj); +JSFunctionSpec ObjectBase::proto_methods[] = { + JS_FN("_init", &ObjectBase::init_gobject, 0, 0), + JS_FN("connect", &ObjectBase::connect, 0, 0), + JS_FN("connect_after", &ObjectBase::connect_after, 0, 0), + JS_FN("emit", &ObjectBase::emit, 0, 0), + JS_FS_END +}; - JS::RootedObject proto(context, - gjs_lookup_object_prototype(context, gtype)); +JSPropertySpec ObjectBase::proto_properties[] = { + JS_STRING_SYM_PS(toStringTag, "GObject_Object", JSPROP_READONLY), + JS_PS_END}; +// clang-format on + +// Override of GIWrapperPrototype::get_parent_proto() +bool ObjectPrototype::get_parent_proto(JSContext* cx, + JS::MutableHandleObject proto) const { + GType parent_type = g_type_parent(gtype()); + if (parent_type != G_TYPE_INVALID) { + proto.set(gjs_lookup_object_prototype(cx, parent_type)); if (!proto) - return nullptr; + return false; + } + return true; +} - JS::RootedObject obj(context, - JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto)); - if (!obj) - return nullptr; +/* + * ObjectPrototype::define_class: + * @in_object: Object where the constructor is stored, typically a repo object. + * @info: Introspection info for the GObject class. + * @gtype: #GType for the GObject class. + * @constructor: Return location for the constructor object. + * @prototype: Return location for the prototype object. + * + * Define a GObject class constructor and prototype, including all the + * necessary methods and properties that are not introspected. Provides the + * constructor and prototype objects as out parameters, for convenience + * elsewhere. + */ +bool ObjectPrototype::define_class(JSContext* context, + JS::HandleObject in_object, + GIObjectInfo* info, GType gtype, + JS::MutableHandleObject constructor, + JS::MutableHandleObject prototype) { + if (!ObjectPrototype::create_class(context, in_object, info, gtype, + constructor, prototype)) + return false; - priv = init_object_private(context, obj); + // hook_up_vfunc and the signal handler matcher functions can't be included + // in gjs_object_instance_proto_funcs because they are custom symbols. + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + return JS_DefineFunctionById(context, prototype, atoms.hook_up_vfunc(), + &ObjectBase::hook_up_vfunc, 3, + GJS_MODULE_PROP_FLAGS) && + JS_DefineFunctionById(context, prototype, atoms.signal_find(), + &ObjectBase::signal_find, 1, + GJS_MODULE_PROP_FLAGS) && + JS_DefineFunctionById( + context, prototype, atoms.signals_block(), + &ObjectBase::signals_action<&g_signal_handlers_block_matched>, 1, + GJS_MODULE_PROP_FLAGS) && + JS_DefineFunctionById( + context, prototype, atoms.signals_unblock(), + &ObjectBase::signals_action<&g_signal_handlers_unblock_matched>, + 1, GJS_MODULE_PROP_FLAGS) && + JS_DefineFunctionById(context, prototype, atoms.signals_disconnect(), + &ObjectBase::signals_action< + &g_signal_handlers_disconnect_matched>, + 1, GJS_MODULE_PROP_FLAGS); +} - g_object_ref_sink(gobj); - associate_js_gobject(context, obj, gobj); +/* + * ObjectInstance::init_custom_class_from_gobject: + * + * Does all the necessary initialization for an ObjectInstance and JSObject + * wrapper, given a newly-created GObject pointer, of a GObject class that was + * created in JS with GObject.registerClass(). This is called from the GObject's + * instance init function in gobject.cpp, and that's the only reason it's a + * public method. + */ +bool ObjectInstance::init_custom_class_from_gobject(JSContext* cx, + JS::HandleObject wrapper, + GObject* gobj) { + associate_js_gobject(cx, wrapper, gobj); + + // Custom JS objects will most likely have visible state, so just do this + // from the start. + ensure_uses_toggle_ref(cx); + + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + JS::RootedValue v(cx); + if (!JS_GetPropertyById(cx, wrapper, atoms.instance_init(), &v)) + return false; - g_assert(priv->keep_alive == obj.get()); + if (v.isUndefined()) + return true; + if (!v.isObject() || !JS::IsCallable(&v.toObject())) { + gjs_throw(cx, "_instance_init property was not a function"); + return false; } - return priv->keep_alive; + JS::RootedValue ignored_rval(cx); + return JS_CallFunctionValue(cx, wrapper, v, JS::HandleValueArray::empty(), + &ignored_rval); } -GObject* -gjs_g_object_from_object(JSContext *context, - JS::HandleObject obj) -{ - ObjectInstance *priv; +/* + * ObjectInstance::new_for_gobject: + * + * Creates a new JSObject wrapper for the GObject pointer @gobj, and an + * ObjectInstance private structure to go along with it. + */ +ObjectInstance* ObjectInstance::new_for_gobject(JSContext* cx, GObject* gobj) { + g_assert(gobj && "Cannot create JSObject for null GObject pointer"); + + GType gtype = G_TYPE_FROM_INSTANCE(gobj); + gjs_debug_marshal(GJS_DEBUG_GOBJECT, "Wrapping %s with JSObject", + g_type_name(gtype)); + + JS::RootedObject proto(cx, gjs_lookup_object_prototype(cx, gtype)); + if (!proto) + return nullptr; + + JS::RootedObject obj( + cx, JS_NewObjectWithGivenProto(cx, JS_GetClass(proto), proto)); if (!obj) - return NULL; + return nullptr; - priv = priv_from_js(context, obj); - return priv->gobj; -} + ObjectInstance* priv = ObjectInstance::new_for_js_object(cx, obj); -bool -gjs_typecheck_is_object(JSContext *context, - JS::HandleObject object, - bool throw_error) -{ - return do_base_typecheck(context, object, throw_error); + g_object_ref_sink(gobj); + priv->associate_js_gobject(cx, obj, gobj); + + g_assert(priv->wrapper() == obj.get()); + + return priv; } -bool -gjs_typecheck_object(JSContext *context, - JS::HandleObject object, - GType expected_type, - bool throw_error) -{ - ObjectInstance *priv; - bool result; +/* + * ObjectInstance::wrapper_from_gobject: + * + * Gets a JSObject wrapper for the GObject pointer @gobj. If one already exists, + * then it is returned. Otherwise a new one is created with + * ObjectInstance::new_for_gobject(). + */ +JSObject* ObjectInstance::wrapper_from_gobject(JSContext* cx, GObject* gobj) { + g_assert(gobj && "Cannot get JSObject for null GObject pointer"); - if (!do_base_typecheck(context, object, throw_error)) - return false; + ObjectInstance* priv = ObjectInstance::for_gobject(gobj); + + if (!priv) { + /* We have to create a wrapper */ + priv = new_for_gobject(cx, gobj); + if (!priv) + return nullptr; + } - priv = priv_from_js(context, object); + return priv->wrapper(); +} - if (priv == NULL) { - if (throw_error) { - gjs_throw(context, - "Object instance or prototype has not been properly initialized yet. " - "Did you forget to chain-up from _init()?"); - } +// Replaces GIWrapperBase::to_c_ptr(). The GIWrapperBase version is deleted. +bool ObjectBase::to_c_ptr(JSContext* cx, JS::HandleObject obj, GObject** ptr) { + g_assert(ptr); + auto* priv = ObjectBase::for_js(cx, obj); + if (!priv || priv->is_prototype()) return false; + + ObjectInstance* instance = priv->to_instance(); + if (!instance->check_gobject_disposed("access")) { + *ptr = nullptr; + return true; } - if (priv->gobj == NULL) { - if (throw_error) { - gjs_throw(context, - "Object is %s.%s.prototype, not an object instance - cannot convert to GObject*", - 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)); - } + *ptr = instance->ptr(); + return true; +} + +// Overrides GIWrapperBase::transfer_to_gi_argument(). +bool ObjectBase::transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj, + GIArgument* arg, + GIDirection transfer_direction, + GITransfer transfer_ownership, + GType expected_gtype, + GIBaseInfo* expected_info) { + g_assert(transfer_direction != GI_DIRECTION_INOUT && + "transfer_to_gi_argument() must choose between in or out"); + if (!ObjectBase::typecheck(cx, obj, expected_info, expected_gtype)) { + gjs_arg_unset(arg); 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(); + GObject* ptr; + if (!ObjectBase::to_c_ptr(cx, obj, &ptr)) + return false; - return true; - } + gjs_arg_set(arg, ptr); - g_assert(priv->gtype == G_OBJECT_TYPE(priv->gobj)); + // Pointer can be null if object was already disposed by C code + if (!ptr) + return true; - if (expected_type != G_TYPE_NONE) - result = g_type_is_a (priv->gtype, expected_type); - else - result = true; - - if (!result && throw_error) { - if (priv->info) { - 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, JSProto_TypeError, nullptr, - "Object is of type %s - cannot convert to %s", - g_type_name(priv->gtype), - g_type_name(expected_type)); - } + if ((transfer_direction == GI_DIRECTION_IN && + transfer_ownership != GI_TRANSFER_NOTHING) || + (transfer_direction == GI_DIRECTION_OUT && + transfer_ownership == GI_TRANSFER_EVERYTHING)) { + gjs_arg_set(arg, ObjectInstance::copy_ptr(cx, expected_gtype, + gjs_arg_get(arg))); + if (!gjs_arg_get(arg)) + return false; } - return result; + return true; } +// Overrides GIWrapperInstance::typecheck_impl() +bool ObjectInstance::typecheck_impl(JSContext* cx, GIBaseInfo* expected_info, + GType expected_type) const { + g_assert(m_gobj_disposed || gtype() == G_OBJECT_TYPE(m_ptr)); + return GIWrapperInstance::typecheck_impl(cx, expected_info, expected_type); +} -static void -find_vfunc_info (JSContext *context, - GType implementor_gtype, - GIBaseInfo *vfunc_info, - const char *vfunc_name, - gpointer *implementor_vtable_ret, - GIFieldInfo **field_info_ret) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool find_vfunc_info(JSContext* context, GType implementor_gtype, + GIBaseInfo* vfunc_info, const char* vfunc_name, + void** implementor_vtable_ret, + GjsAutoFieldInfo* field_info_ret) { GType ancestor_gtype; int length, i; GIBaseInfo *ancestor_info; - GIStructInfo *struct_info; - gpointer implementor_class; + GjsAutoStructInfo struct_info; bool is_interface; - *field_info_ret = NULL; + field_info_ret->reset(); *implementor_vtable_ret = NULL; ancestor_info = g_base_info_get_container(vfunc_info); @@ -2392,16 +2488,15 @@ find_vfunc_info (JSContext *context, is_interface = g_base_info_get_type(ancestor_info) == GI_INFO_TYPE_INTERFACE; - implementor_class = g_type_class_ref(implementor_gtype); + GjsAutoTypeClass implementor_class(implementor_gtype); if (is_interface) { GTypeInstance *implementor_iface_class; implementor_iface_class = (GTypeInstance*) g_type_interface_peek(implementor_class, ancestor_gtype); if (implementor_iface_class == NULL) { - g_type_class_unref(implementor_class); gjs_throw (context, "Couldn't find GType of implementor of interface %s.", g_type_name(ancestor_gtype)); - return; + return false; } *implementor_vtable_ret = implementor_iface_class; @@ -2412,99 +2507,73 @@ find_vfunc_info (JSContext *context, *implementor_vtable_ret = implementor_class; } - g_type_class_unref(implementor_class); - length = g_struct_info_get_n_fields(struct_info); for (i = 0; i < length; i++) { - GIFieldInfo *field_info; - GITypeInfo *type_info; - - field_info = g_struct_info_get_field(struct_info, i); - - if (strcmp(g_base_info_get_name((GIBaseInfo*)field_info), vfunc_name) != 0) { - g_base_info_unref(field_info); + GjsAutoFieldInfo field_info = g_struct_info_get_field(struct_info, i); + if (strcmp(field_info.name(), vfunc_name) != 0) continue; - } - type_info = g_field_info_get_type(field_info); + GjsAutoTypeInfo type_info = g_field_info_get_type(field_info); if (g_type_info_get_tag(type_info) != GI_TYPE_TAG_INTERFACE) { /* We have a field with the same name, but it's not a callback. * There's no hope of being another field with a correct name, * so just abort early. */ - g_base_info_unref(type_info); - g_base_info_unref(field_info); - break; + return true; } else { - g_base_info_unref(type_info); - *field_info_ret = field_info; - break; + *field_info_ret = std::move(field_info); + return true; } } + return true; +} - g_base_info_unref(struct_info); +bool ObjectBase::hook_up_vfunc(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, prototype, ObjectBase, priv); + /* Normally we wouldn't assert is_prototype(), but this method can only be + * called internally so it's OK to crash if done wrongly */ + return priv->to_prototype()->hook_up_vfunc_impl(cx, args); } -static bool -gjs_hook_up_vfunc(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GjsAutoJSChar name; - JS::RootedObject object(cx), function(cx); - ObjectInstance *priv; - GType gtype, info_gtype; - GIObjectInfo *info; - GIVFuncInfo *vfunc; - gpointer implementor_vtable; - GIFieldInfo *field_info; - - if (!gjs_parse_call_args(cx, "hook_up_vfunc", argv, "oso", - "object", &object, +bool ObjectPrototype::hook_up_vfunc_impl(JSContext* cx, + const JS::CallArgs& args) { + JS::UniqueChars name; + JS::RootedObject function(cx); + if (!gjs_parse_call_args(cx, "hook_up_vfunc", args, "so", "name", &name, "function", &function)) return false; - if (!do_base_typecheck(cx, object, true)) - return false; - - priv = priv_from_js(cx, object); - gtype = priv->gtype; - info = priv->info; + args.rval().setUndefined(); /* find the first class that actually has repository information */ - info_gtype = gtype; + GIObjectInfo *info = m_info; + GType info_gtype = m_gtype; while (!info && info_gtype != G_TYPE_OBJECT) { info_gtype = g_type_parent(info_gtype); - info = g_irepository_find_by_gtype(g_irepository_get_default(), info_gtype); + info = g_irepository_find_by_gtype(nullptr, info_gtype); } /* If we don't have 'info', we don't have the base class (GObject). * This is awful, so abort now. */ g_assert(info != NULL); - argv.rval().setUndefined(); - - vfunc = find_vfunc_on_parents(info, name, NULL); + GjsAutoVFuncInfo vfunc = find_vfunc_on_parents(info, name.get(), nullptr); if (!vfunc) { guint i, n_interfaces; GType *interface_list; - GIInterfaceInfo *interface; - interface_list = g_type_interfaces(gtype, &n_interfaces); + interface_list = g_type_interfaces(m_gtype, &n_interfaces); for (i = 0; i < n_interfaces; i++) { - interface = (GIInterfaceInfo*)g_irepository_find_by_gtype(g_irepository_get_default(), - interface_list[i]); + GjsAutoInterfaceInfo interface = + g_irepository_find_by_gtype(nullptr, interface_list[i]); /* The interface doesn't have to exist -- it could be private * or dynamic. */ if (interface) { - vfunc = g_interface_info_find_vfunc(interface, name); - - g_base_info_unref((GIBaseInfo*)interface); + vfunc = g_interface_info_find_vfunc(interface, name.get()); if (vfunc) break; @@ -2520,8 +2589,13 @@ gjs_hook_up_vfunc(JSContext *cx, return false; } - find_vfunc_info(cx, gtype, vfunc, name, &implementor_vtable, &field_info); - if (field_info != NULL) { + void *implementor_vtable; + GjsAutoFieldInfo field_info; + if (!find_vfunc_info(cx, m_gtype, vfunc, name.get(), &implementor_vtable, + &field_info)) + return false; + + if (field_info) { gint offset; gpointer method_ptr; GjsCallbackTrampoline *trampoline; @@ -2529,696 +2603,40 @@ gjs_hook_up_vfunc(JSContext *cx, 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, vfunc, - GI_SCOPE_TYPE_NOTIFIED, - object, true); - - *((ffi_closure **)method_ptr) = trampoline->closure; - - g_base_info_unref(field_info); - } - - g_base_info_unref(vfunc); - return true; -} - -static gchar * -hyphen_to_underscore (gchar *string) -{ - gchar *str, *s; - str = s = g_strdup(string); - while (*(str++) != '\0') { - if (*str == '-') - *str = '_'; - } - return s; -} - -static void -gjs_object_get_gproperty (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - GjsContext *gjs_context; - JSContext *context; - gchar *underscore_name; - ObjectInstance *priv = get_object_qdata(object); - - gjs_context = gjs_context_get_current(); - context = (JSContext*) gjs_context_get_native_context(gjs_context); - - JS::RootedObject js_obj(context, priv->keep_alive); - JS::RootedValue jsvalue(context); - - underscore_name = hyphen_to_underscore((gchar *)pspec->name); - if (!JS_GetProperty(context, js_obj, underscore_name, &jsvalue) || - !gjs_value_to_g_value(context, jsvalue, value)) - gjs_log_exception(context); - g_free (underscore_name); -} - -static void -jsobj_set_gproperty(JSContext *context, - JS::HandleObject object, - const GValue *value, - GParamSpec *pspec) -{ - JS::RootedValue jsvalue(context); - gchar *underscore_name; - - if (!gjs_value_from_g_value(context, &jsvalue, value)) - return; - - underscore_name = hyphen_to_underscore((gchar *)pspec->name); - if (!JS_SetProperty(context, object, underscore_name, jsvalue)) - gjs_log_exception(context); - g_free (underscore_name); -} - -static GObject * -gjs_object_constructor (GType type, - guint n_construct_properties, - GObjectConstructParam *construct_properties) -{ - if (!object_init_list.empty()) { - GType parent_type = g_type_parent(type); - - /* The object is being constructed from JS: - * Simply chain up to the first non-gjs constructor - */ - while (G_OBJECT_CLASS(g_type_class_peek(parent_type))->constructor == gjs_object_constructor) - parent_type = g_type_parent(parent_type); - - return G_OBJECT_CLASS(g_type_class_peek(parent_type))->constructor(type, n_construct_properties, construct_properties); - } - - GjsContext *gjs_context; - JSContext *context; - JSObject *object; - ObjectInstance *priv; - - /* The object is being constructed from native code (e.g. GtkBuilder): - * Construct the JS object from the constructor, then use the GObject - * that was associated in gjs_object_custom_init() - */ - gjs_context = gjs_context_get_current(); - context = (JSContext*) gjs_context_get_native_context(gjs_context); - - JSAutoRequest ar(context); - JSAutoCompartment ac(context, gjs_get_import_global(context)); - - JS::RootedObject constructor(context, - gjs_lookup_object_constructor_from_info(context, NULL, type)); - if (!constructor) - return NULL; - - if (n_construct_properties) { - guint i; - - JS::RootedObject props_hash(context, JS_NewPlainObject(context)); - - for (i = 0; i < n_construct_properties; i++) - jsobj_set_gproperty(context, props_hash, - construct_properties[i].value, - construct_properties[i].pspec); - - JS::AutoValueArray<1> args(context); - args[0].set(JS::ObjectValue(*props_hash)); - object = JS_New(context, constructor, args); - } else { - object = JS_New(context, constructor, JS::HandleValueArray::empty()); - } - - if (!object) - return NULL; - - priv = (ObjectInstance*) JS_GetPrivate(object); - /* We only hold a toggle ref at this point, add back a ref that the - * native code can own. - */ - return G_OBJECT(g_object_ref(priv->gobj)); -} - -static void -gjs_object_set_gproperty (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec) -{ - GjsContext *gjs_context; - JSContext *context; - ObjectInstance *priv = get_object_qdata(object); - - gjs_context = gjs_context_get_current(); - context = (JSContext*) gjs_context_get_native_context(gjs_context); - - JS::RootedObject js_obj(context, priv->keep_alive); - jsobj_set_gproperty(context, js_obj, value, pspec); -} - -static bool -gjs_override_property(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GjsAutoJSChar name; - JS::RootedObject type(cx); - GParamSpec *pspec; - GParamSpec *new_pspec; - GType gtype; - - if (!gjs_parse_call_args(cx, "override_property", args, "so", - "name", &name, - "type", &type)) - return false; - - if ((gtype = gjs_gtype_get_actual_gtype(cx, type)) == G_TYPE_INVALID) { - gjs_throw(cx, "Invalid parameter type was not a GType"); - return false; - } - - if (g_type_is_a(gtype, G_TYPE_INTERFACE)) { - GTypeInterface *interface_type = - (GTypeInterface *) g_type_default_interface_ref(gtype); - pspec = g_object_interface_find_property(interface_type, name); - g_type_default_interface_unref(interface_type); - } else { - GTypeClass *class_type = (GTypeClass *) g_type_class_ref(gtype); - pspec = g_object_class_find_property(G_OBJECT_CLASS(class_type), name); - g_type_class_unref(class_type); - } - - if (pspec == NULL) { - gjs_throw(cx, "No such property '%s' to override on type '%s'", - name.get(), g_type_name(gtype)); - return false; - } - - new_pspec = g_param_spec_override(name, pspec); - - g_param_spec_set_qdata(new_pspec, gjs_is_custom_property_quark(), GINT_TO_POINTER(1)); - - args.rval().setObject(*gjs_param_from_g_param(cx, new_pspec)); - g_param_spec_unref(new_pspec); - - return true; -} - -static void -gjs_interface_init(GTypeInterface *g_iface, - gpointer iface_data) -{ - GType gtype = G_TYPE_FROM_INTERFACE(g_iface); - - auto found = class_init_properties.find(gtype); - if (found == class_init_properties.end()) - return; - - 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()); - } - - class_init_properties.erase(found); -} - -static void -gjs_object_class_init(GObjectClass *klass, - gpointer user_data) -{ - 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; - - 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 -gjs_object_custom_init(GTypeInstance *instance, - gpointer klass) -{ - GjsContext *gjs_context; - JSContext *context; - ObjectInstance *priv; - - if (object_init_list.empty()) - return; - - gjs_context = gjs_context_get_current(); - context = (JSContext*) gjs_context_get_native_context(gjs_context); - - JS::RootedObject object(context, object_init_list.top().get()); - priv = (ObjectInstance*) JS_GetPrivate(object); - - if (priv->gtype != G_TYPE_FROM_INSTANCE (instance)) { - /* This is not the most derived instance_init function, - do nothing. - */ - return; - } - - object_init_list.pop(); - - 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)) { - gjs_log_exception(context); - return; - } - - if (!v.isObject()) - return; - - JS::RootedValue r(context); - if (!JS_CallFunctionValue(context, object, v, - JS::HandleValueArray::empty(), &r)) - gjs_log_exception(context); -} - -static inline void -gjs_add_interface(GType instance_type, - GType interface_type) -{ - static GInterfaceInfo interface_vtable = { NULL, NULL, NULL }; - - g_type_add_interface_static(instance_type, - interface_type, - &interface_vtable); -} - -static bool -validate_interfaces_and_properties_args(JSContext *cx, - JS::HandleObject interfaces, - JS::HandleObject properties, - uint32_t *n_interfaces, - uint32_t *n_properties) -{ - guint32 n_int, n_prop; - bool is_array; - - if (!JS_IsArrayObject(cx, interfaces, &is_array)) - return false; - if (!is_array) { - gjs_throw(cx, "Invalid parameter interfaces (expected Array)"); - return false; - } - - if (!JS_GetArrayLength(cx, interfaces, &n_int)) - return false; - - if (!JS_IsArrayObject(cx, properties, &is_array)) - return false; - if (!is_array) { - gjs_throw(cx, "Invalid parameter properties (expected Array)"); - return false; - } - - if (!JS_GetArrayLength(cx, properties, &n_prop)) - return false; - - if (n_interfaces != NULL) - *n_interfaces = n_int; - if (n_properties != NULL) - *n_properties = n_prop; - return true; -} - -static bool -get_interface_gtypes(JSContext *cx, - JS::HandleObject interfaces, - uint32_t n_interfaces, - GType *iface_types) -{ - guint32 i; - - for (i = 0; i < n_interfaces; i++) { - JS::RootedValue iface_val(cx); - GType iface_type; - - if (!JS_GetElement(cx, interfaces, i, &iface_val)) - return false; - - if (!iface_val.isObject()) { - gjs_throw (cx, "Invalid parameter interfaces (element %d was not a GType)", i); - return false; - } - - JS::RootedObject iface(cx, &iface_val.toObject()); - iface_type = gjs_gtype_get_actual_gtype(cx, iface); - if (iface_type == G_TYPE_INVALID) { - gjs_throw (cx, "Invalid parameter interfaces (element %d was not a GType)", i); + if (!js::IsFunctionObject(function)) { + gjs_throw(cx, "Tried to deal with a vfunc that wasn't a function"); return false; } - - iface_types[i] = iface_type; - } - return true; -} - -static bool -save_properties_for_class_init(JSContext *cx, - JS::HandleObject properties, - uint32_t n_properties, - GType gtype) -{ - 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)) + JS::RootedFunction func(cx, JS_GetObjectFunction(function)); + trampoline = gjs_callback_trampoline_new( + cx, func, vfunc, GI_SCOPE_TYPE_NOTIFIED, true, true); + if (!trampoline) return false; - if (!prop_val.isObject()) { - gjs_throw(cx, "Invalid parameter, expected object"); - return false; - } + // This is traced, and will be cleared from the list when the closure is + // invalidated + g_assert(std::find(m_vfuncs.begin(), m_vfuncs.end(), + trampoline->js_function) == m_vfuncs.end() && + "This vfunc was already associated with this class"); + m_vfuncs.push_front(trampoline->js_function); + g_closure_add_invalidate_notifier( + trampoline->js_function, this, + &ObjectPrototype::vfunc_invalidated_notify); + g_closure_add_invalidate_notifier( + trampoline->js_function, trampoline, [](void* data, GClosure*) { + auto* trampoline = static_cast(data); + gjs_callback_trampoline_unref(trampoline); + }); - prop_obj = &prop_val.toObject(); - if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, true)) - return false; - - 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; -} - -static bool -gjs_register_interface(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - GjsAutoJSChar name; - guint32 i, n_interfaces, n_properties; - GType *iface_types; - GType interface_type; - GTypeInfo type_info = { - sizeof (GTypeInterface), /* class_size */ - - (GBaseInitFunc) NULL, - (GBaseFinalizeFunc) NULL, - - (GClassInitFunc) gjs_interface_init, - (GClassFinalizeFunc) NULL, - NULL, /* class_data */ - - 0, /* instance_size */ - 0, /* n_preallocs */ - NULL, /* instance_init */ - }; - - JS::RootedObject interfaces(cx), properties(cx); - if (!gjs_parse_call_args(cx, "register_interface", args, "soo", - "name", &name, - "interfaces", &interfaces, - "properties", &properties)) - return false; - - if (!validate_interfaces_and_properties_args(cx, interfaces, properties, - &n_interfaces, &n_properties)) - return false; - - iface_types = (GType *) g_alloca(sizeof(GType) * n_interfaces); - - /* We do interface addition in two passes so that any failure - is caught early, before registering the GType (which we can't undo) */ - if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types)) - return false; - - if (g_type_from_name(name) != G_TYPE_INVALID) { - gjs_throw(cx, "Type name %s is already registered", name.get()); - return false; - } - - interface_type = g_type_register_static(G_TYPE_INTERFACE, name, &type_info, - (GTypeFlags) 0); - - g_type_set_qdata(interface_type, gjs_is_custom_type_quark(), GINT_TO_POINTER(1)); - - if (!save_properties_for_class_init(cx, properties, n_properties, interface_type)) - return false; - - for (i = 0; i < n_interfaces; i++) - g_type_interface_add_prerequisite(interface_type, iface_types[i]); - - /* create a custom JSClass */ - JS::RootedObject module(cx, gjs_lookup_private_namespace(cx)); - if (!module) - return false; /* error will have been thrown already */ - - JS::RootedObject constructor(cx); - gjs_define_interface_class(cx, module, NULL, interface_type, &constructor); - - args.rval().setObject(*constructor); - 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; - GType instance_type, parent_type; - GTypeQuery query; - ObjectInstance *parent_priv; - GTypeInfo type_info = { - 0, /* class_size */ - - gjs_object_base_init, - gjs_object_base_finalize, - - (GClassInitFunc) gjs_object_class_init, - (GClassFinalizeFunc) NULL, - NULL, /* class_data */ - - 0, /* instance_size */ - 0, /* n_preallocs */ - gjs_object_custom_init, - }; - guint32 i, n_interfaces, n_properties; - GType *iface_types; - - JSAutoRequest ar(cx); - - JS::RootedObject parent(cx), interfaces(cx), properties(cx); - if (!gjs_parse_call_args(cx, "register_type", argv, "osoo", - "parent", &parent, - "name", &name, - "interfaces", &interfaces, - "properties", &properties)) - return false; - - if (!parent) - return false; - - if (!do_base_typecheck(cx, parent, true)) - return false; - - if (!validate_interfaces_and_properties_args(cx, interfaces, properties, - &n_interfaces, &n_properties)) - return false; - - iface_types = (GType*) g_alloca(sizeof(GType) * n_interfaces); - - /* We do interface addition in two passes so that any failure - is caught early, before registering the GType (which we can't undo) */ - if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types)) - return false; - - if (g_type_from_name(name) != G_TYPE_INVALID) { - gjs_throw(cx, "Type name %s is already registered", name.get()); - return false; - } - - parent_priv = priv_from_js(cx, parent); - - /* We checked parent above, in do_base_typecheck() */ - g_assert(parent_priv != NULL); - - parent_type = parent_priv->gtype; - - g_type_query_dynamic_safe(parent_type, &query); - if (G_UNLIKELY (query.type == 0)) { - gjs_throw (cx, "Cannot inherit from a non-gjs dynamic type [bug 687184]"); - return false; - } - - type_info.class_size = query.class_size; - type_info.instance_size = query.instance_size; - - instance_type = g_type_register_static(parent_type, name, &type_info, - (GTypeFlags) 0); - - g_type_set_qdata (instance_type, gjs_is_custom_type_quark(), GINT_TO_POINTER (1)); - - if (!save_properties_for_class_init(cx, properties, n_properties, instance_type)) - return false; - - for (i = 0; i < n_interfaces; i++) - gjs_add_interface(instance_type, iface_types[i]); - - /* create a custom JSClass */ - 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); - - return true; -} - -static bool -gjs_signal_new(JSContext *cx, - unsigned argc, - JS::Value *vp) -{ - JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); - GType gtype; - GjsAutoJSChar signal_name; - GSignalAccumulator accumulator; - gint signal_id; - guint i, n_parameters; - GType *params, return_type; - - if (argc != 6) - return false; - - JSAutoRequest ar(cx); - - if (!gjs_string_to_utf8(cx, argv[1], &signal_name)) - return false; - - JS::RootedObject obj(cx, &argv[0].toObject()); - if (!gjs_typecheck_gtype(cx, obj, true)) - return false; - - /* we only support standard accumulators for now */ - switch (argv[3].toInt32()) { - case 1: - accumulator = g_signal_accumulator_first_wins; - break; - case 2: - accumulator = g_signal_accumulator_true_handled; - break; - case 0: - default: - accumulator = NULL; - } - - JS::RootedObject gtype_obj(cx, &argv[4].toObject()); - return_type = gjs_gtype_get_actual_gtype(cx, gtype_obj); - - if (accumulator == g_signal_accumulator_true_handled && return_type != G_TYPE_BOOLEAN) { - gjs_throw (cx, "GObject.SignalAccumulator.TRUE_HANDLED can only be used with boolean signals"); - return false; - } - - JS::RootedObject params_obj(cx, &argv[5].toObject()); - if (!JS_GetArrayLength(cx, params_obj, &n_parameters)) - return false; - - params = g_newa(GType, n_parameters); - JS::RootedValue gtype_val(cx); - for (i = 0; i < n_parameters; i++) { - if (!JS_GetElement(cx, params_obj, i, >ype_val) || - !gtype_val.isObject()) { - gjs_throw(cx, "Invalid signal parameter number %d", i); - return false; - } - - JS::RootedObject gjs_gtype(cx, >ype_val.toObject()); - params[i] = gjs_gtype_get_actual_gtype(cx, gjs_gtype); + *((ffi_closure **)method_ptr) = trampoline->closure; } - gtype = gjs_gtype_get_actual_gtype(cx, obj); - - signal_id = g_signal_newv(signal_name, - gtype, - (GSignalFlags) argv[2].toInt32(), /* signal_flags */ - NULL, /* class closure */ - accumulator, - NULL, /* accu_data */ - g_cclosure_marshal_generic, - return_type, /* return type */ - n_parameters, - params); - - argv.rval().setInt32(signal_id); return true; } -static JSFunctionSpec module_funcs[] = { - JS_FS("override_property", gjs_override_property, 2, GJS_MODULE_PROP_FLAGS), - JS_FS("register_interface", gjs_register_interface, 3, GJS_MODULE_PROP_FLAGS), - JS_FS("register_type", gjs_register_type, 4, GJS_MODULE_PROP_FLAGS), - JS_FS("hook_up_vfunc", gjs_hook_up_vfunc, 3, GJS_MODULE_PROP_FLAGS), - JS_FS("signal_new", gjs_signal_new, 6, GJS_MODULE_PROP_FLAGS), - JS_FS_END, -}; - -bool -gjs_define_private_gi_stuff(JSContext *cx, - JS::MutableHandleObject module) -{ - module.set(JS_NewPlainObject(cx)); - return JS_DefineFunctions(cx, module, &module_funcs[0]); +void ObjectPrototype::vfunc_invalidated_notify(void* data, GClosure* closure) { + auto* priv = static_cast(data); + priv->m_vfuncs.remove(closure); } bool @@ -3227,38 +2645,14 @@ gjs_lookup_object_constructor(JSContext *context, JS::MutableHandleValue value_p) { JSObject *constructor; - GIObjectInfo *object_info; - object_info = (GIObjectInfo*)g_irepository_find_by_gtype(NULL, gtype); - - g_assert(object_info == NULL || - g_base_info_get_type((GIBaseInfo*)object_info) == - GI_INFO_TYPE_OBJECT); + GjsAutoObjectInfo object_info = g_irepository_find_by_gtype(nullptr, gtype); constructor = gjs_lookup_object_constructor_from_info(context, object_info, gtype); if (G_UNLIKELY (constructor == NULL)) return false; - if (object_info) - g_base_info_unref((GIBaseInfo*)object_info); - 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 d1b5fbe..9f04993 100644 --- a/gi/object.h +++ b/gi/object.h @@ -21,61 +21,491 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_OBJECT_H__ -#define __GJS_OBJECT_H__ +#ifndef GI_OBJECT_H_ +#define GI_OBJECT_H_ -#include +#include + +#include // for size_t + +#include +#include +#include -#include #include +#include +#include + +#include // for GCHashMap +#include +#include +#include +#include +#include // for JSID_IS_ATOM, JSID_TO_ATOM +#include // for HashGeneric, HashNumber +#include // for DefaultHasher +#include // for MOZ_LIKELY + +#include "gi/wrapperutils.h" +#include "cjs/jsapi-util-root.h" #include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "util/log.h" + +class GjsAtoms; +class JSTracer; +namespace JS { +class CallArgs; +} +namespace js { +class SystemAllocPolicy; +} +class ObjectInstance; +class ObjectPrototype; + +class GjsListLink { + private: + ObjectInstance* m_prev; + ObjectInstance* m_next; + + public: + [[nodiscard]] ObjectInstance* prev() const { return m_prev; } + [[nodiscard]] ObjectInstance* next() const { return m_next; } + + void prepend(ObjectInstance* this_instance, ObjectInstance* head); + void unlink(void); + [[nodiscard]] size_t size() const; +}; + +struct AutoGValueVector : public std::vector { + ~AutoGValueVector() { + for (GValue value : *this) + g_value_unset(&value); + } +}; + +/* + * ObjectBase: + * + * Specialization of GIWrapperBase for GObject instances. See the documentation + * in wrapperutils.h. + * + * It's important that ObjectBase and ObjectInstance not grow in size without a + * very good reason. There can be tens, maybe hundreds of thousands of these + * objects alive in a typical gnome-shell run, so even 8 more bytes will add up. + * It's less critical that ObjectPrototype stay small, since only one of these + * is allocated per GType. + */ +class ObjectBase + : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit ObjectBase(ObjectPrototype* proto = nullptr) + : GIWrapperBase(proto) {} + + public: + using SignalMatchFunc = guint(gpointer, GSignalMatchType, guint, GQuark, + GClosure*, gpointer, gpointer); + static const GjsDebugTopic debug_topic = GJS_DEBUG_GOBJECT; + static constexpr const char* debug_tag = "GObject"; + + static const struct JSClassOps class_ops; + static const struct JSClass klass; + static JSFunctionSpec proto_methods[]; + static JSPropertySpec proto_properties[]; + + static GObject* to_c_ptr(JSContext* cx, JS::HandleObject obj) = delete; + GJS_JSAPI_RETURN_CONVENTION + static bool to_c_ptr(JSContext* cx, JS::HandleObject obj, GObject** ptr); + GJS_JSAPI_RETURN_CONVENTION + static bool transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj, + GIArgument* arg, + GIDirection transfer_direction, + GITransfer transfer_ownership, + GType expected_gtype, + GIBaseInfo* expected_info = nullptr); + + private: + // This is used in debug methods only. + [[nodiscard]] const void* jsobj_addr() const; + + /* Helper methods */ + + protected: + void debug_lifecycle(const char* message) const { + GIWrapperBase::debug_lifecycle(jsobj_addr(), message); + } + + [[nodiscard]] bool id_is_never_lazy(jsid name, const GjsAtoms& atoms); + [[nodiscard]] bool is_custom_js_class(); + + public: + void type_query_dynamic_safe(GTypeQuery* query); + + GJS_JSAPI_RETURN_CONVENTION + static bool typecheck(JSContext* cx, JS::HandleObject obj, + GIObjectInfo* expected_info, GType expected_gtype); + [[nodiscard]] static bool typecheck(JSContext* cx, JS::HandleObject obj, + GIObjectInfo* expected_info, + GType expected_gtype, + GjsTypecheckNoThrow no_throw) { + return GIWrapperBase::typecheck(cx, obj, expected_info, expected_gtype, + no_throw); + } + + /* JSClass operations */ + + static bool add_property(JSContext* cx, JS::HandleObject obj, + JS::HandleId id, JS::HandleValue value); + + /* JS property getters/setters */ + + public: + GJS_JSAPI_RETURN_CONVENTION + static bool prop_getter(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool field_getter(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool prop_setter(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool field_setter(JSContext* cx, unsigned argc, JS::Value* vp); + + /* JS methods */ + + GJS_JSAPI_RETURN_CONVENTION + static bool connect(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool connect_after(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool emit(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool signal_find(JSContext* cx, unsigned argc, JS::Value* vp); + template + GJS_JSAPI_RETURN_CONVENTION static bool signals_action(JSContext* cx, + unsigned argc, + JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp); + [[nodiscard]] const char* to_string_kind() const; + GJS_JSAPI_RETURN_CONVENTION + static bool init_gobject(JSContext* cx, unsigned argc, JS::Value* vp); + GJS_JSAPI_RETURN_CONVENTION + static bool hook_up_vfunc(JSContext* cx, unsigned argc, JS::Value* vp); + + /* Quarks */ + + [[nodiscard]] static GQuark custom_type_quark(); + [[nodiscard]] static GQuark custom_property_quark(); +}; + +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1614220 +struct IdHasher { + typedef jsid Lookup; + static mozilla::HashNumber hash(jsid id) { + if (MOZ_LIKELY(JSID_IS_ATOM(id))) + return js::DefaultHasher::hash(JSID_TO_ATOM(id)); + if (JSID_IS_SYMBOL(id)) + return js::DefaultHasher::hash(JSID_TO_SYMBOL(id)); + return mozilla::HashGeneric(JSID_BITS(id)); + } + static bool match(jsid id1, jsid id2) { return id1 == id2; } +}; + +class ObjectPrototype + : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; + + using PropertyCache = + JS::GCHashMap, GjsAutoParam, + js::DefaultHasher, js::SystemAllocPolicy>; + using FieldCache = + JS::GCHashMap, GjsAutoInfo, + js::DefaultHasher, js::SystemAllocPolicy>; + using NegativeLookupCache = + JS::GCHashSet, IdHasher, js::SystemAllocPolicy>; + + PropertyCache m_property_cache; + FieldCache m_field_cache; + NegativeLookupCache m_unresolvable_cache; + // a list of vfunc GClosures installed on this prototype, used when tracing + std::forward_list m_vfuncs; + + ObjectPrototype(GIObjectInfo* info, GType gtype); + ~ObjectPrototype(); + + static constexpr InfoType::Tag info_type_tag = InfoType::Object; + + public: + [[nodiscard]] static ObjectPrototype* for_gtype(GType gtype); + + /* Helper methods */ + private: + GJS_JSAPI_RETURN_CONVENTION + bool get_parent_proto(JSContext* cx, JS::MutableHandleObject proto) const; + + [[nodiscard]] bool is_vfunc_unchanged(GIVFuncInfo* info); + static void vfunc_invalidated_notify(void* data, GClosure* closure); + + GJS_JSAPI_RETURN_CONVENTION + bool lazy_define_gobject_property(JSContext* cx, JS::HandleObject obj, + JS::HandleId id, bool* resolved, + const char* name); + + enum ResolveWhat { ConsiderOnlyMethods, ConsiderMethodsAndProperties }; + GJS_JSAPI_RETURN_CONVENTION + bool resolve_no_info(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolved, const char* name, + ResolveWhat resolve_props); + GJS_JSAPI_RETURN_CONVENTION + bool uncached_resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* name, bool* resolved); -G_BEGIN_DECLS + public: + void set_type_qdata(void); + GJS_JSAPI_RETURN_CONVENTION + GParamSpec* find_param_spec_from_id(JSContext* cx, JS::HandleString key); + GJS_JSAPI_RETURN_CONVENTION + GIFieldInfo* lookup_cached_field_info(JSContext* cx, JS::HandleString key); + GJS_JSAPI_RETURN_CONVENTION + bool props_to_g_parameters(JSContext* cx, JS::HandleObject props, + std::vector* names, + AutoGValueVector* values); -void gjs_define_object_class(JSContext *context, - JS::HandleObject in_object, - GIObjectInfo *info, - GType gtype, + GJS_JSAPI_RETURN_CONVENTION + static bool define_class(JSContext* cx, JS::HandleObject in_object, + GIObjectInfo* info, GType gtype, JS::MutableHandleObject constructor, JS::MutableHandleObject prototype); -bool gjs_lookup_object_constructor(JSContext *context, - GType gtype, - JS::MutableHandleValue value_p); + void ref_vfuncs(void) { + for (GClosure* closure : m_vfuncs) + g_closure_ref(closure); + } + void unref_vfuncs(void) { + for (GClosure* closure : m_vfuncs) + g_closure_unref(closure); + } -JSObject* gjs_object_from_g_object (JSContext *context, - GObject *gobj); + /* JSClass operations */ + private: + GJS_JSAPI_RETURN_CONVENTION + bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* prop_name, bool* resolved); -GObject *gjs_g_object_from_object(JSContext *context, - JS::HandleObject obj); + GJS_JSAPI_RETURN_CONVENTION + bool new_enumerate_impl(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool only_enumerable); + void trace_impl(JSTracer* tracer); -bool gjs_typecheck_object(JSContext *context, - JS::HandleObject obj, - GType expected_type, - bool throw_error); + /* JS methods */ + public: + GJS_JSAPI_RETURN_CONVENTION + bool hook_up_vfunc_impl(JSContext* cx, const JS::CallArgs& args); +}; -bool gjs_typecheck_is_object(JSContext *context, - JS::HandleObject obj, - bool throw_error); +class ObjectInstance : public GIWrapperInstance { + friend class GIWrapperInstance; + friend class GIWrapperBase; + friend class ObjectBase; // for add_property, prop_getter, etc. -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); + // GIWrapperInstance::m_ptr may be null in ObjectInstance. + + GjsMaybeOwned m_wrapper; + // a list of all GClosures installed on this object (from signal connections + // and scope-notify callbacks passed to methods), used when tracing + std::forward_list m_closures; + GjsListLink m_instance_link; + + bool m_wrapper_finalized : 1; + bool m_gobj_disposed : 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 m_uses_toggle_ref : 1; + + static bool s_weak_pointer_callback; + + /* Constructors */ + + private: + ObjectInstance(JSContext* cx, JS::HandleObject obj); + ~ObjectInstance(); + + GJS_JSAPI_RETURN_CONVENTION + static ObjectInstance* new_for_gobject(JSContext* cx, GObject* gobj); + + // Extra method to get an existing ObjectInstance from qdata + + public: + [[nodiscard]] static ObjectInstance* for_gobject(GObject* gobj); + + /* Accessors */ + + private: + [[nodiscard]] bool has_wrapper() const { return !!m_wrapper; } + + public: + [[nodiscard]] JSObject* wrapper() const { return m_wrapper; } + + /* Methods to manipulate the JS object wrapper */ + + private: + void discard_wrapper(void) { m_wrapper.reset(); } + void switch_to_rooted(JSContext* cx) { m_wrapper.switch_to_rooted(cx); } + void switch_to_unrooted(JSContext* cx) { m_wrapper.switch_to_unrooted(cx); } + [[nodiscard]] bool update_after_gc() { return m_wrapper.update_after_gc(); } + [[nodiscard]] bool wrapper_is_rooted() const { return m_wrapper.rooted(); } + void release_native_object(void); + void associate_js_gobject(JSContext* cx, JS::HandleObject obj, + GObject* gobj); + void disassociate_js_gobject(void); + void handle_context_dispose(void); + [[nodiscard]] bool weak_pointer_was_finalized(); + static void ensure_weak_pointer_callback(JSContext* cx); + static void update_heap_wrapper_weak_pointers(JSContext* cx, + JS::Compartment* compartment, + void* data); + + public: + void toggle_down(void); + void toggle_up(void); + + GJS_JSAPI_RETURN_CONVENTION + static JSObject* wrapper_from_gobject(JSContext* cx, GObject* ptr); + + /* Methods to manipulate the list of closures */ + + private: + static void closure_invalidated_notify(void* data, GClosure* closure); -void gjs_object_define_static_methods(JSContext *context, - JS::HandleObject constructor, - GType gtype, - GIObjectInfo *object_info); + public: + void associate_closure(JSContext* cx, GClosure* closure); -bool gjs_define_private_gi_stuff(JSContext *cx, - JS::MutableHandleObject module); + /* Helper methods */ -bool gjs_object_associate_closure(JSContext *cx, - JS::HandleObject obj, - GClosure *closure); + private: + void set_object_qdata(void); + void unset_object_qdata(void); + void check_js_object_finalized(void); + void ensure_uses_toggle_ref(JSContext* cx); + [[nodiscard]] bool check_gobject_disposed(const char* for_what) const; + GJS_JSAPI_RETURN_CONVENTION + bool signal_match_arguments_from_object(JSContext* cx, + JS::HandleObject props_obj, + GSignalMatchType* mask_out, + unsigned* signal_id_out, + GQuark* detail_out, + JS::MutableHandleFunction func_out); -G_END_DECLS + public: + static GObject* copy_ptr(JSContext*, GType, void* ptr) { + return G_OBJECT(g_object_ref(G_OBJECT(ptr))); + } + + GJS_JSAPI_RETURN_CONVENTION + bool init_custom_class_from_gobject(JSContext* cx, JS::HandleObject wrapper, + GObject* gobj); + + /* Methods to manipulate the linked list of instances */ + + private: + static ObjectInstance* wrapped_gobject_list; + [[nodiscard]] ObjectInstance* next() const { + return m_instance_link.next(); + } + void link(void); + void unlink(void); + [[nodiscard]] static size_t num_wrapped_gobjects() { + return wrapped_gobject_list + ? wrapped_gobject_list->m_instance_link.size() + : 0; + } + using Action = std::function; + using Predicate = std::function; + static void iterate_wrapped_gobjects(const Action& action); + static void remove_wrapped_gobjects_if(const Predicate& predicate, + const Action& action); + + public: + [[nodiscard]] GjsListLink* get_link() { return &m_instance_link; } + static void prepare_shutdown(void); + + /* JSClass operations */ + + private: + GJS_JSAPI_RETURN_CONVENTION + bool add_property_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + JS::HandleValue value); + void finalize_impl(JSFreeOp* fop, JSObject* obj); + void trace_impl(JSTracer* trc); + + /* JS property getters/setters */ + + private: + GJS_JSAPI_RETURN_CONVENTION + bool prop_getter_impl(JSContext* cx, JS::HandleString name, + JS::MutableHandleValue rval); + GJS_JSAPI_RETURN_CONVENTION + bool field_getter_impl(JSContext* cx, JS::HandleString name, + JS::MutableHandleValue rval); + GJS_JSAPI_RETURN_CONVENTION + bool prop_setter_impl(JSContext* cx, JS::HandleString name, + JS::HandleValue value); + GJS_JSAPI_RETURN_CONVENTION + bool field_setter_not_impl(JSContext* cx, JS::HandleString name); + + // JS constructor + + GJS_JSAPI_RETURN_CONVENTION + bool constructor_impl(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args); + + /* JS methods */ + + private: + GJS_JSAPI_RETURN_CONVENTION + bool connect_impl(JSContext* cx, const JS::CallArgs& args, bool after); + GJS_JSAPI_RETURN_CONVENTION + bool emit_impl(JSContext* cx, const JS::CallArgs& args); + GJS_JSAPI_RETURN_CONVENTION + bool signal_find_impl(JSContext* cx, const JS::CallArgs& args); + template + GJS_JSAPI_RETURN_CONVENTION bool signals_action_impl( + JSContext* cx, const JS::CallArgs& args); + GJS_JSAPI_RETURN_CONVENTION + bool init_impl(JSContext* cx, const JS::CallArgs& args, + JS::MutableHandleObject obj); + [[nodiscard]] const char* to_string_kind() const; + + GJS_JSAPI_RETURN_CONVENTION + bool typecheck_impl(JSContext* cx, GIBaseInfo* expected_info, + GType expected_type) const; + + /* Notification callbacks */ + + public: + void gobj_dispose_notify(void); + static void context_dispose_notify(void* data, + GObject* where_the_object_was); +}; + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_lookup_object_constructor(JSContext *context, + GType gtype, + JS::MutableHandleValue value_p); +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_lookup_object_constructor_from_info(JSContext* cx, + GIObjectInfo* info, + GType gtype); + +void gjs_object_clear_toggles(void); +void gjs_object_shutdown_toggle_queue(void); -#endif /* __GJS_OBJECT_H__ */ +#endif // GI_OBJECT_H_ diff --git a/gi/param.cpp b/gi/param.cpp index 8bcc591..366fed7 100644 --- a/gi/param.cpp +++ b/gi/param.cpp @@ -23,89 +23,81 @@ #include -#include - -#include "param.h" -#include "arg.h" -#include "object.h" -#include "repo.h" -#include "gtype.h" -#include "function.h" +#include +#include + +#include +#include +#include +#include +#include // for UniqueChars +#include // for JS_GetClass, JS_GetPropertyById +#include // for JSProto_TypeError + +#include "gi/function.h" +#include "gi/param.h" +#include "gi/repo.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" #include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" - -#include - -typedef struct { - GParamSpec *gparam; /* NULL if we are the prototype and not an instance */ -} Param; +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" extern struct JSClass gjs_param_class; -GJS_DEFINE_PRIV_FROM_JS(Param, gjs_param_class) +GJS_DEFINE_PRIV_FROM_JS(GParamSpec, gjs_param_class) /* * The *resolved out parameter, on success, should be false to indicate that id * was not resolved; and true if id was resolved. */ +GJS_JSAPI_RETURN_CONVENTION static bool param_resolve(JSContext *context, JS::HandleObject obj, JS::HandleId id, bool *resolved) { - GIObjectInfo *info = NULL; - GIFunctionInfo *method_info; - Param *priv; - bool ret = false; - - priv = priv_from_js(context, obj); - if (priv != NULL) { + if (!priv_from_js(context, obj)) { /* instance, not prototype */ *resolved = false; return true; } - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { + JS::UniqueChars name; + if (!gjs_get_string_id(context, id, &name)) + return false; + if (!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); + GjsAutoObjectInfo info = g_irepository_find_by_gtype(nullptr, G_TYPE_PARAM); + GjsAutoFunctionInfo method_info = + g_object_info_find_method(info, name.get()); - if (method_info == NULL) { + if (!method_info) { *resolved = false; - ret = true; - goto out; + 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) { gjs_debug(GJS_DEBUG_GOBJECT, "Defining method %s in prototype for GObject.ParamSpec", - g_base_info_get_name( (GIBaseInfo*) method_info)); + method_info.name()); - if (gjs_define_function(context, obj, G_TYPE_PARAM, method_info) == NULL) { - g_base_info_unref( (GIBaseInfo*) method_info); - goto out; - } + if (!gjs_define_function(context, obj, G_TYPE_PARAM, method_info)) + return false; *resolved = true; /* we defined the prop in obj */ } - g_base_info_unref( (GIBaseInfo*) method_info); - - ret = true; - out: - if (info != NULL) - g_base_info_unref( (GIBaseInfo*)info); - - return ret; + return true; } GJS_NATIVE_CONSTRUCTOR_DECLARE(param) @@ -117,25 +109,15 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(param) return true; } -static void -param_finalize(JSFreeOp *fop, - JSObject *obj) -{ - Param *priv; - - priv = (Param*) JS_GetPrivate(obj); - gjs_debug_lifecycle(GJS_DEBUG_GPARAM, - "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) +static void param_finalize(JSFreeOp*, JSObject* obj) { + GjsAutoParam param = static_cast(JS_GetPrivate(obj)); + gjs_debug_lifecycle(GJS_DEBUG_GPARAM, "finalize, obj %p priv %p", obj, + param.get()); + if (!param) return; /* wrong class? */ - if (priv->gparam) { - g_param_spec_unref(priv->gparam); - priv->gparam = NULL; - } - GJS_DEC_COUNTER(param); - g_slice_free(Param, priv); + JS_SetPrivate(obj, nullptr); } @@ -144,15 +126,13 @@ param_finalize(JSFreeOp *fop, * class have. */ static const struct JSClassOps gjs_param_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate param_resolve, - nullptr, /* mayResolve */ - param_finalize -}; + nullptr, // mayResolve + param_finalize}; struct JSClass gjs_param_class = { "GObject_ParamSpec", @@ -172,74 +152,63 @@ static JSFunctionSpec gjs_param_constructor_funcs[] = { JS_FS_END }; +GJS_JSAPI_RETURN_CONVENTION static JSObject* gjs_lookup_param_prototype(JSContext *context) { - JS::RootedId gobject_name(context, gjs_intern_string_to_id(context, "GObject")); - JS::RootedObject in_object(context, - gjs_lookup_namespace_object_by_name(context, gobject_name)); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + JS::RootedObject in_object( + context, gjs_lookup_namespace_object_by_name(context, atoms.gobject())); if (G_UNLIKELY (!in_object)) - return NULL; + return nullptr; JS::RootedValue value(context); - if (!JS_GetProperty(context, in_object, "ParamSpec", &value)) - return NULL; - - if (G_UNLIKELY (!value.isObject())) - return NULL; + if (!JS_GetPropertyById(context, in_object, atoms.param_spec(), &value) || + G_UNLIKELY(!value.isObject())) + return nullptr; JS::RootedObject constructor(context, &value.toObject()); g_assert(constructor); - if (!gjs_object_get_property(context, constructor, - GJS_STRING_PROTOTYPE, &value)) - return NULL; - - if (G_UNLIKELY (!value.isObjectOrNull())) - return NULL; + if (!JS_GetPropertyById(context, constructor, atoms.prototype(), &value) || + G_UNLIKELY(!value.isObjectOrNull())) + return nullptr; return value.toObjectOrNull(); } -void +bool gjs_define_param_class(JSContext *context, JS::HandleObject in_object) { const char *constructor_name; JS::RootedObject prototype(context), constructor(context); - GIObjectInfo *info; constructor_name = "ParamSpec"; - if (!gjs_init_class_dynamic(context, in_object, nullptr, "GObject", - constructor_name, - &gjs_param_class, - gjs_param_constructor, 0, - /* props of prototype */ - &gjs_param_proto_props[0], - /* funcs of prototype */ - &gjs_param_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - gjs_param_constructor_funcs, - &prototype, - &constructor)) { - g_error("Can't init class %s", constructor_name); - } + if (!gjs_init_class_dynamic( + context, in_object, nullptr, "GObject", constructor_name, + &gjs_param_class, gjs_param_constructor, 0, + gjs_param_proto_props, // props of prototype + gjs_param_proto_funcs, // funcs of prototype + nullptr, // props of constructor, MyConstructor.myprop + gjs_param_constructor_funcs, // funcs of constructor + &prototype, &constructor)) + return false; - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, G_TYPE_PARAM)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, JSPROP_PERMANENT); + if (!gjs_wrapper_define_gtype_prop(context, constructor, G_TYPE_PARAM)) + return false; - info = (GIObjectInfo*)g_irepository_find_by_gtype(g_irepository_get_default(), G_TYPE_PARAM); - gjs_object_define_static_methods(context, constructor, G_TYPE_PARAM, info); - g_base_info_unref( (GIBaseInfo*) info); + GjsAutoObjectInfo info = g_irepository_find_by_gtype(nullptr, G_TYPE_PARAM); + if (!gjs_define_static_methods(context, constructor, + G_TYPE_PARAM, info)) + return false; gjs_debug(GJS_DEBUG_GPARAM, "Defined class %s prototype is %p class %p in object %p", constructor_name, prototype.get(), JS_GetClass(prototype), in_object.get()); + return true; } JSObject* @@ -247,10 +216,9 @@ gjs_param_from_g_param(JSContext *context, GParamSpec *gparam) { JSObject *obj; - Param *priv; - if (gparam == NULL) - return NULL; + if (!gparam) + return nullptr; gjs_debug(GJS_DEBUG_GPARAM, "Wrapping %s '%s' on %s with JSObject", @@ -263,14 +231,12 @@ gjs_param_from_g_param(JSContext *context, obj = JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto); GJS_INC_COUNTER(param); - priv = g_slice_new0(Param); - JS_SetPrivate(obj, priv); - priv->gparam = gparam; + JS_SetPrivate(obj, gparam); g_param_spec_ref (gparam); gjs_debug(GJS_DEBUG_GPARAM, - "JSObject created with param instance %p type %s", - priv->gparam, g_type_name(G_TYPE_FROM_INSTANCE((GTypeInstance*) priv->gparam))); + "JSObject created with param instance %p type %s", gparam, + g_type_name(G_TYPE_FROM_INSTANCE(gparam))); return obj; } @@ -279,14 +245,10 @@ GParamSpec* gjs_g_param_from_param(JSContext *context, JS::HandleObject obj) { - Param *priv; - if (!obj) - return NULL; - - priv = priv_from_js(context, obj); + return nullptr; - return priv->gparam; + return priv_from_js(context, obj); } bool @@ -295,15 +257,14 @@ gjs_typecheck_param(JSContext *context, GType expected_type, bool throw_error) { - Param *priv; bool result; if (!do_base_typecheck(context, object, throw_error)) return false; - priv = priv_from_js(context, object); + GParamSpec* param = priv_from_js(context, object); - if (priv->gparam == NULL) { + if (!param) { if (throw_error) { gjs_throw_custom(context, JSProto_TypeError, nullptr, "Object is GObject.ParamSpec.prototype, not an object instance - " @@ -314,14 +275,14 @@ gjs_typecheck_param(JSContext *context, } if (expected_type != G_TYPE_NONE) - result = g_type_is_a (G_TYPE_FROM_INSTANCE (priv->gparam), expected_type); + result = g_type_is_a(G_TYPE_FROM_INSTANCE(param), expected_type); else result = true; if (!result && throw_error) { 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(G_TYPE_FROM_INSTANCE(param)), g_type_name(expected_type)); } diff --git a/gi/param.h b/gi/param.h index 4202b7e..e630064 100644 --- a/gi/param.h +++ b/gi/param.h @@ -21,30 +21,30 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_PARAM_H__ -#define __GJS_PARAM_H__ +#ifndef GI_PARAM_H_ +#define GI_PARAM_H_ -#include -#include -#include -#include "cjs/jsapi-util.h" +#include -G_BEGIN_DECLS +#include -void gjs_define_param_class(JSContext *context, +#include + +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_param_class(JSContext *context, JS::HandleObject in_object); +GJS_JSAPI_RETURN_CONVENTION GParamSpec *gjs_g_param_from_param (JSContext *context, JS::HandleObject obj); +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_param_from_g_param (JSContext *context, GParamSpec *param); -bool gjs_typecheck_param(JSContext *context, - JS::HandleObject obj, - GType expected_type, - bool throw_error); - -G_END_DECLS +[[nodiscard]] bool gjs_typecheck_param(JSContext* cx, JS::HandleObject obj, + GType expected_type, bool throw_error); -#endif /* __GJS_PARAM_H__ */ +#endif // GI_PARAM_H_ diff --git a/gi/private.cpp b/gi/private.cpp new file mode 100644 index 0000000..80c3691 --- /dev/null +++ b/gi/private.cpp @@ -0,0 +1,456 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2008 litl, LLC + * Copyright (c) 2018 Philip Chimento + * + * 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 // for JS::GetArrayLength, +#include +#include // for JSID_TO_SYMBOL +#include +#include +#include +#include // for UniqueChars +#include // for JS_GetElement + +#include "gi/gobject.h" +#include "gi/gtype.h" +#include "gi/interface.h" +#include "gi/object.h" +#include "gi/param.h" +#include "gi/private.h" +#include "gi/repo.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util-args.h" +#include "cjs/jsapi-util.h" + +/* gi/private.cpp - private "imports._gi" module with operations that we need + * to use from JS in order to create GObject classes, but should not be exposed + * to client code. + */ + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_override_property(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + JS::UniqueChars name; + JS::RootedObject type(cx); + + if (!gjs_parse_call_args(cx, "override_property", args, "so", "name", &name, + "type", &type)) + return false; + + GType gtype; + if (!gjs_gtype_get_actual_gtype(cx, type, >ype)) + return false; + if (gtype == G_TYPE_INVALID) { + gjs_throw(cx, "Invalid parameter type was not a GType"); + return false; + } + + GParamSpec* pspec; + if (g_type_is_a(gtype, G_TYPE_INTERFACE)) { + auto* interface_type = + static_cast(g_type_default_interface_ref(gtype)); + pspec = g_object_interface_find_property(interface_type, name.get()); + g_type_default_interface_unref(interface_type); + } else { + GjsAutoTypeClass class_type(gtype); + pspec = g_object_class_find_property(class_type, name.get()); + } + + if (!pspec) { + gjs_throw(cx, "No such property '%s' to override on type '%s'", + name.get(), g_type_name(gtype)); + return false; + } + + GjsAutoParam new_pspec = g_param_spec_override(name.get(), pspec); + + g_param_spec_set_qdata(new_pspec, ObjectBase::custom_property_quark(), + GINT_TO_POINTER(1)); + + args.rval().setObject(*gjs_param_from_g_param(cx, new_pspec)); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool validate_interfaces_and_properties_args(JSContext* cx, + JS::HandleObject interfaces, + JS::HandleObject properties, + uint32_t* n_interfaces, + uint32_t* n_properties) { + bool is_array; + if (!JS::IsArrayObject(cx, interfaces, &is_array)) + return false; + if (!is_array) { + gjs_throw(cx, "Invalid parameter interfaces (expected Array)"); + return false; + } + + uint32_t n_int; + if (!JS::GetArrayLength(cx, interfaces, &n_int)) + return false; + + if (!JS::IsArrayObject(cx, properties, &is_array)) + return false; + if (!is_array) { + gjs_throw(cx, "Invalid parameter properties (expected Array)"); + return false; + } + + uint32_t n_prop; + if (!JS::GetArrayLength(cx, properties, &n_prop)) + return false; + + if (n_interfaces) + *n_interfaces = n_int; + if (n_properties) + *n_properties = n_prop; + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool save_properties_for_class_init(JSContext* cx, + JS::HandleObject properties, + uint32_t n_properties, GType gtype) { + AutoParamArray 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()) { + gjs_throw(cx, "Invalid parameter, expected object"); + return false; + } + + prop_obj = &prop_val.toObject(); + if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, true)) + return false; + + properties_native.emplace_back( + g_param_spec_ref(gjs_g_param_from_param(cx, prop_obj))); + } + push_class_init_properties(gtype, &properties_native); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool get_interface_gtypes(JSContext* cx, JS::HandleObject interfaces, + uint32_t n_interfaces, GType* iface_types) { + for (uint32_t ix = 0; ix < n_interfaces; ix++) { + JS::RootedValue iface_val(cx); + if (!JS_GetElement(cx, interfaces, ix, &iface_val)) + return false; + + if (!iface_val.isObject()) { + gjs_throw( + cx, "Invalid parameter interfaces (element %d was not a GType)", + ix); + return false; + } + + JS::RootedObject iface(cx, &iface_val.toObject()); + GType iface_type; + if (!gjs_gtype_get_actual_gtype(cx, iface, &iface_type)) + return false; + if (iface_type == G_TYPE_INVALID) { + gjs_throw( + cx, "Invalid parameter interfaces (element %d was not a GType)", + ix); + return false; + } + + iface_types[ix] = iface_type; + } + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_register_interface(JSContext* cx, unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::UniqueChars name; + JS::RootedObject interfaces(cx), properties(cx); + if (!gjs_parse_call_args(cx, "register_interface", args, "soo", "name", + &name, "interfaces", &interfaces, "properties", + &properties)) + return false; + + uint32_t n_interfaces, n_properties; + if (!validate_interfaces_and_properties_args(cx, interfaces, properties, + &n_interfaces, &n_properties)) + return false; + + GType* iface_types = g_newa(GType, n_interfaces); + + /* We do interface addition in two passes so that any failure + is caught early, before registering the GType (which we can't undo) */ + if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types)) + return false; + + if (g_type_from_name(name.get()) != G_TYPE_INVALID) { + gjs_throw(cx, "Type name %s is already registered", name.get()); + return false; + } + + GTypeInfo type_info = gjs_gobject_interface_info; + GType interface_type = g_type_register_static(G_TYPE_INTERFACE, name.get(), + &type_info, GTypeFlags(0)); + + g_type_set_qdata(interface_type, ObjectBase::custom_type_quark(), + GINT_TO_POINTER(1)); + + if (!save_properties_for_class_init(cx, properties, n_properties, + interface_type)) + return false; + + for (uint32_t ix = 0; ix < n_interfaces; ix++) + g_type_interface_add_prerequisite(interface_type, iface_types[ix]); + + /* create a custom JSClass */ + JS::RootedObject module(cx, gjs_lookup_private_namespace(cx)); + if (!module) + return false; // error will have been thrown already + + JS::RootedObject constructor(cx), ignored_prototype(cx); + if (!InterfacePrototype::create_class(cx, module, nullptr, interface_type, + &constructor, &ignored_prototype)) + return false; + + args.rval().setObject(*constructor); + return true; +} + +static inline void gjs_add_interface(GType instance_type, + GType interface_type) { + static GInterfaceInfo interface_vtable{nullptr, nullptr, nullptr}; + g_type_add_interface_static(instance_type, interface_type, + &interface_vtable); +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + + JS::UniqueChars name; + GTypeFlags type_flags; + JS::RootedObject parent(cx), interfaces(cx), properties(cx); + if (!gjs_parse_call_args(cx, "register_type", argv, "osioo", "parent", + &parent, "name", &name, "flags", &type_flags, + "interfaces", &interfaces, + "properties", &properties)) + return false; + + if (!parent) + return false; + + /* Don't pass the argv to it, as otherwise we will log about the callee + * while we only care about the parent object type. */ + auto* parent_priv = ObjectBase::for_js_typecheck(cx, parent); + if (!parent_priv) + return false; + + uint32_t n_interfaces, n_properties; + if (!validate_interfaces_and_properties_args(cx, interfaces, properties, + &n_interfaces, &n_properties)) + return false; + + auto* iface_types = + static_cast(g_alloca(sizeof(GType) * n_interfaces)); + + /* We do interface addition in two passes so that any failure + is caught early, before registering the GType (which we can't undo) */ + if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types)) + return false; + + if (g_type_from_name(name.get()) != G_TYPE_INVALID) { + gjs_throw(cx, "Type name %s is already registered", name.get()); + return false; + } + + /* We checked parent above, in ObjectBase::for_js_typecheck() */ + g_assert(parent_priv); + + GTypeQuery query; + parent_priv->type_query_dynamic_safe(&query); + if (G_UNLIKELY(query.type == 0)) { + gjs_throw(cx, + "Cannot inherit from a non-gjs dynamic type [bug 687184]"); + return false; + } + + GTypeInfo type_info = gjs_gobject_class_info; + type_info.class_size = query.class_size; + type_info.instance_size = query.instance_size; + + GType instance_type = g_type_register_static( + parent_priv->gtype(), name.get(), &type_info, type_flags); + + g_type_set_qdata(instance_type, ObjectBase::custom_type_quark(), + GINT_TO_POINTER(1)); + + if (!save_properties_for_class_init(cx, properties, n_properties, + instance_type)) + return false; + + for (uint32_t ix = 0; ix < n_interfaces; ix++) + gjs_add_interface(instance_type, iface_types[ix]); + + /* create a custom JSClass */ + JS::RootedObject module(cx, gjs_lookup_private_namespace(cx)); + JS::RootedObject constructor(cx), prototype(cx); + if (!ObjectPrototype::define_class(cx, module, nullptr, instance_type, + &constructor, &prototype)) + return false; + + auto* priv = ObjectPrototype::for_js(cx, prototype); + priv->set_type_qdata(); + + argv.rval().setObject(*constructor); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_signal_new(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + JS::UniqueChars signal_name; + int32_t flags, accumulator_enum; + JS::RootedObject gtype_obj(cx), return_gtype_obj(cx), params_obj(cx); + if (!gjs_parse_call_args(cx, "signal_new", args, "osiioo", "gtype", + >ype_obj, "signal name", &signal_name, "flags", + &flags, "accumulator", &accumulator_enum, + "return gtype", &return_gtype_obj, "params", + ¶ms_obj)) + return false; + + if (!gjs_typecheck_gtype(cx, gtype_obj, true)) + return false; + + /* we only support standard accumulators for now */ + GSignalAccumulator accumulator; + switch (accumulator_enum) { + case 1: + accumulator = g_signal_accumulator_first_wins; + break; + case 2: + accumulator = g_signal_accumulator_true_handled; + break; + case 0: + default: + accumulator = nullptr; + } + + GType return_type; + if (!gjs_gtype_get_actual_gtype(cx, return_gtype_obj, &return_type)) + return false; + + if (accumulator == g_signal_accumulator_true_handled && + return_type != G_TYPE_BOOLEAN) { + gjs_throw(cx, + "GObject.SignalAccumulator.TRUE_HANDLED can only be used " + "with boolean signals"); + return false; + } + + uint32_t n_parameters; + if (!JS::GetArrayLength(cx, params_obj, &n_parameters)) + return false; + + GType* params = g_newa(GType, n_parameters); + JS::RootedValue gtype_val(cx); + for (uint32_t ix = 0; ix < n_parameters; ix++) { + if (!JS_GetElement(cx, params_obj, ix, >ype_val) || + !gtype_val.isObject()) { + gjs_throw(cx, "Invalid signal parameter number %d", ix); + return false; + } + + JS::RootedObject gjs_gtype(cx, >ype_val.toObject()); + if (!gjs_gtype_get_actual_gtype(cx, gjs_gtype, ¶ms[ix])) + return false; + } + + GType gtype; + if (!gjs_gtype_get_actual_gtype(cx, gtype_obj, >ype)) + return false; + + unsigned signal_id = g_signal_newv( + signal_name.get(), gtype, GSignalFlags(flags), + /* class closure */ nullptr, accumulator, /* accu_data */ nullptr, + /* c_marshaller */ nullptr, return_type, n_parameters, params); + + // FIXME: what if ID is greater than int32 max? + args.rval().setInt32(signal_id); + return true; +} + +template +GJS_JSAPI_RETURN_CONVENTION static bool symbol_getter(JSContext* cx, + unsigned argc, + JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + args.rval().setSymbol(JSID_TO_SYMBOL((atoms.*member)())); + return true; +} + +static JSFunctionSpec module_funcs[] = { + JS_FN("override_property", gjs_override_property, 2, GJS_MODULE_PROP_FLAGS), + JS_FN("register_interface", gjs_register_interface, 3, + GJS_MODULE_PROP_FLAGS), + JS_FN("register_type", gjs_register_type, 4, GJS_MODULE_PROP_FLAGS), + JS_FN("signal_new", gjs_signal_new, 6, GJS_MODULE_PROP_FLAGS), + JS_FS_END, +}; + +static JSPropertySpec module_props[] = { + JS_PSG("hook_up_vfunc_symbol", symbol_getter<&GjsAtoms::hook_up_vfunc>, + GJS_MODULE_PROP_FLAGS), + JS_PSG("signal_find_symbol", symbol_getter<&GjsAtoms::signal_find>, + GJS_MODULE_PROP_FLAGS), + JS_PSG("signals_block_symbol", symbol_getter<&GjsAtoms::signals_block>, + GJS_MODULE_PROP_FLAGS), + JS_PSG("signals_unblock_symbol", symbol_getter<&GjsAtoms::signals_unblock>, + GJS_MODULE_PROP_FLAGS), + JS_PSG("signals_disconnect_symbol", + symbol_getter<&GjsAtoms::signals_disconnect>, GJS_MODULE_PROP_FLAGS), + JS_PS_END}; + +bool gjs_define_private_gi_stuff(JSContext* cx, + JS::MutableHandleObject module) { + module.set(JS_NewPlainObject(cx)); + return JS_DefineFunctions(cx, module, module_funcs) && + JS_DefineProperties(cx, module, module_props); +} diff --git a/util/glib.h b/gi/private.h similarity index 82% rename from util/glib.h rename to gi/private.h index 3334dfb..fc93318 100644 --- a/util/glib.h +++ b/gi/private.h @@ -21,16 +21,16 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_UTIL_GLIB_H__ -#define __GJS_UTIL_GLIB_H__ +#ifndef GI_PRIVATE_H_ +#define GI_PRIVATE_H_ -#include +#include -G_BEGIN_DECLS +#include -char** gjs_g_strv_concat (char ***strv_array, - int len); +#include "cjs/macros.h" -G_END_DECLS +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_private_gi_stuff(JSContext* cx, JS::MutableHandleObject module); -#endif /* __GJS_UTIL_GLIB_H__ */ +#endif // GI_PRIVATE_H_ diff --git a/gi/proxyutils.cpp b/gi/proxyutils.cpp deleted file mode 100644 index d90aa4d..0000000 --- a/gi/proxyutils.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* - * Copyright (c) 2012 Red Hat, 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. - */ - -#include - -#include - -#include "proxyutils.h" - -/* Default spidermonkey toString is worthless. Replace it - * with something that gives us both the introspection name - * and a memory address. - */ -bool -_gjs_proxy_to_string_func(JSContext *context, - JSObject *this_obj, - const char *objtype, - GIBaseInfo *info, - GType gtype, - gpointer native_address, - JS::MutableHandleValue rval) -{ - GString *buf; - bool ret = false; - - buf = g_string_new(""); - g_string_append_c(buf, '['); - g_string_append(buf, objtype); - if (native_address == NULL) - g_string_append(buf, " prototype of"); - else - g_string_append(buf, " instance proxy"); - - if (info != NULL) { - g_string_append_printf(buf, " GIName:%s.%s", - g_base_info_get_namespace(info), - g_base_info_get_name(info)); - } else { - g_string_append(buf, " GType:"); - g_string_append(buf, g_type_name(gtype)); - } - - 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, rval)) - goto out; - - ret = true; - out: - g_string_free (buf, true); - return ret; -} diff --git a/gi/repo.cpp b/gi/repo.cpp index 8260459..a55718b 100644 --- a/gi/repo.cpp +++ b/gi/repo.cpp @@ -23,27 +23,43 @@ #include -#include "repo.h" -#include "ns.h" -#include "function.h" -#include "object.h" -#include "param.h" -#include "boxed.h" -#include "union.h" -#include "enumeration.h" -#include "arg.h" -#include "foreign.h" -#include "fundamental.h" -#include "interface.h" -#include "gerror.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" - -#include +#include +#include // for strlen #include -#include +#include +#include + +#include +#include // for JSID_IS_STRING, JSID_VOID +#include // for JSPROP_PERMANENT, JSPROP_RESOLVING +#include +#include +#include +#include // for UniqueChars +#include +#include +#include // for JS_DefinePropertyById, JS_GetProp... + +#include "gi/arg.h" +#include "gi/boxed.h" +#include "gi/enumeration.h" +#include "gi/function.h" +#include "gi/fundamental.h" +#include "gi/gerror.h" +#include "gi/interface.h" +#include "gi/ns.h" +#include "gi/object.h" +#include "gi/param.h" +#include "gi/repo.h" +#include "gi/union.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/global.h" +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" typedef struct { void *dummy; @@ -54,21 +70,19 @@ extern struct JSClass gjs_repo_class; GJS_DEFINE_PRIV_FROM_JS(Repo, gjs_repo_class) +GJS_JSAPI_RETURN_CONVENTION static bool lookup_override_function(JSContext *, JS::HandleId, JS::MutableHandleValue); -static bool -get_version_for_ns (JSContext *context, - JS::HandleObject repo_obj, - JS::HandleId ns_id, - GjsAutoJSChar *version) -{ +GJS_JSAPI_RETURN_CONVENTION +static bool get_version_for_ns(JSContext* context, JS::HandleObject repo_obj, + JS::HandleId ns_id, JS::UniqueChars* version) { JS::RootedObject versions(context); bool found; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); - if (!gjs_object_require_property(context, repo_obj, - "GI repository object", - GJS_STRING_GI_VERSIONS, &versions)) + if (!gjs_object_require_property(context, repo_obj, "GI repository object", + atoms.versions(), &versions)) return false; if (!JS_AlreadyHasOwnPropertyById(context, versions, ns_id, &found)) @@ -80,40 +94,41 @@ get_version_for_ns (JSContext *context, return gjs_object_require_property(context, versions, NULL, ns_id, version); } -static bool -resolve_namespace_object(JSContext *context, - JS::HandleObject repo_obj, - JS::HandleId ns_id, - const char *ns_name) -{ - GIRepository *repo; +GJS_JSAPI_RETURN_CONVENTION +static bool resolve_namespace_object(JSContext* context, + JS::HandleObject repo_obj, + JS::HandleId ns_id) { GError *error; - JSAutoRequest ar(context); - - GjsAutoJSChar version; + JS::UniqueChars version; if (!get_version_for_ns(context, repo_obj, ns_id, &version)) return false; - repo = g_irepository_get_default(); - GList *versions = g_irepository_enumerate_versions(repo, ns_name); + JS::UniqueChars ns_name; + if (!gjs_get_string_id(context, ns_id, &ns_name)) + return false; + if (!ns_name) { + gjs_throw(context, "Requiring invalid namespace on imports.gi"); + return false; + } + + GList* versions = g_irepository_enumerate_versions(nullptr, ns_name.get()); unsigned nversions = g_list_length(versions); if (nversions > 1 && !version && - !g_irepository_is_registered(repo, ns_name, NULL)) { - GjsAutoChar warn_text = g_strdup_printf("Requiring %s but it has %u " - "versions available; use " - "imports.gi.versions to pick one", - ns_name, nversions); - JS_ReportWarningUTF8(context, "%s", warn_text.get()); - } + !g_irepository_is_registered(nullptr, ns_name.get(), nullptr) && + !JS::WarnUTF8(context, + "Requiring %s but it has %u versions available; use " + "imports.gi.versions to pick one", + ns_name.get(), nversions)) + return false; g_list_free_full(versions, g_free); error = NULL; - g_irepository_require(repo, ns_name, version, (GIRepositoryLoadFlags) 0, &error); + g_irepository_require(nullptr, ns_name.get(), version.get(), + GIRepositoryLoadFlags(0), &error); if (error != NULL) { - gjs_throw(context, - "Requiring %s, version %s: %s", - ns_name, version ? version.get() : "none", error->message); + gjs_throw(context, "Requiring %s, version %s: %s", ns_name.get(), + version ? version.get() : "none", error->message); g_error_free(error); return false; @@ -123,13 +138,14 @@ resolve_namespace_object(JSContext *context, * with the given namespace name, pointing to that namespace * in the repo. */ - JS::RootedObject gi_namespace(context, gjs_create_ns(context, ns_name)); + JS::RootedObject gi_namespace(context, + gjs_create_ns(context, ns_name.get())); /* Define the property early, to avoid reentrancy issues if the override module looks for namespaces that import this */ - if (!JS_DefineProperty(context, repo_obj, ns_name, gi_namespace, - GJS_MODULE_PROP_FLAGS)) - g_error("no memory to define ns property"); + if (!JS_DefinePropertyById(context, repo_obj, ns_id, gi_namespace, + GJS_MODULE_PROP_FLAGS)) + return false; JS::RootedValue override(context); if (!lookup_override_function(context, ns_id, &override)) @@ -143,10 +159,11 @@ resolve_namespace_object(JSContext *context, return false; gjs_debug(GJS_DEBUG_GNAMESPACE, - "Defined namespace '%s' %p in GIRepository %p", ns_name, + "Defined namespace '%s' %p in GIRepository %p", ns_name.get(), gi_namespace.get(), repo_obj.get()); - gjs_schedule_gc_if_needed(context); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + gjs->schedule_gc_if_needed(); return true; } @@ -154,6 +171,7 @@ resolve_namespace_object(JSContext *context, * The *resolved out parameter, on success, should be false to indicate that id * was not resolved; and true if id was resolved. */ +GJS_JSAPI_RETURN_CONVENTION static bool repo_resolve(JSContext *context, JS::HandleObject obj, @@ -167,10 +185,9 @@ repo_resolve(JSContext *context, return true; /* not resolved, but no error */ } - JSFlatString *str = JSID_TO_FLAT_STRING(id); /* let Object.prototype resolve these */ - if (JS_FlatStringEqualsAscii(str, "valueOf") || - JS_FlatStringEqualsAscii(str, "toString")) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + if (id == atoms.to_string() || id == atoms.value_of()) { *resolved = false; return true; } @@ -185,15 +202,13 @@ repo_resolve(JSContext *context, return true; } - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { + if (!JSID_IS_STRING(id)) { *resolved = false; return true; } - if (!resolve_namespace_object(context, obj, id, name)) { + if (!resolve_namespace_object(context, obj, id)) return false; - } *resolved = true; return true; @@ -201,10 +216,7 @@ repo_resolve(JSContext *context, GJS_NATIVE_CONSTRUCTOR_DEFINE_ABSTRACT(repo) -static void -repo_finalize(JSFreeOp *fop, - JSObject *obj) -{ +static void repo_finalize(JSFreeOp*, JSObject* obj) { Repo *priv; priv = (Repo*) JS_GetPrivate(obj); @@ -222,15 +234,13 @@ repo_finalize(JSFreeOp *fop, * class have. */ static const struct JSClassOps gjs_repo_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate repo_resolve, - nullptr, /* mayResolve */ - repo_finalize -}; + nullptr, // mayResolve + repo_finalize}; struct JSClass gjs_repo_class = { "GIRepository", /* means "new GIRepository()" works */ @@ -238,12 +248,18 @@ struct JSClass gjs_repo_class = { &gjs_repo_class_ops, }; -static JSPropertySpec *gjs_repo_proto_props = nullptr; +// clang-format off +static const JSPropertySpec gjs_repo_proto_props[] = { + JS_STRING_SYM_PS(toStringTag, "GIRepository", JSPROP_READONLY), + JS_PS_END}; +// clang-format on + static JSFunctionSpec *gjs_repo_proto_funcs = nullptr; static JSFunctionSpec *gjs_repo_static_funcs = nullptr; GJS_DEFINE_PROTO_FUNCS(repo) +GJS_JSAPI_RETURN_CONVENTION static JSObject* repo_new(JSContext *context) { @@ -256,7 +272,7 @@ repo_new(JSContext *context) JS::RootedObject repo(context, JS_NewObjectWithGivenProto(context, &gjs_repo_class, proto)); if (repo == nullptr) - g_error("No memory to create repo object"); + return nullptr; priv = g_slice_new0(Repo); @@ -268,28 +284,30 @@ repo_new(JSContext *context) gjs_debug_lifecycle(GJS_DEBUG_GREPO, "repo constructor, obj %p priv %p", repo.get(), priv); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject versions(context, JS_NewPlainObject(context)); - gjs_object_define_property(context, repo, GJS_STRING_GI_VERSIONS, - versions, JSPROP_PERMANENT | JSPROP_RESOLVING); + if (!JS_DefinePropertyById(context, repo, atoms.versions(), versions, + JSPROP_PERMANENT | JSPROP_RESOLVING)) + return nullptr; /* GLib/GObject/Gio are fixed at 2.0, since we depend on them * internally. */ JS::RootedString two_point_oh(context, JS_NewStringCopyZ(context, "2.0")); - if (!JS_DefineProperty(context, versions, "GLib", two_point_oh, - JSPROP_PERMANENT)) + if (!JS_DefinePropertyById(context, versions, atoms.glib(), two_point_oh, + JSPROP_PERMANENT)) return nullptr; - if (!JS_DefineProperty(context, versions, "GObject", two_point_oh, - JSPROP_PERMANENT)) + if (!JS_DefinePropertyById(context, versions, atoms.gobject(), two_point_oh, + JSPROP_PERMANENT)) return nullptr; - if (!JS_DefineProperty(context, versions, "Gio", two_point_oh, - JSPROP_PERMANENT)) + if (!JS_DefinePropertyById(context, versions, atoms.gio(), two_point_oh, + JSPROP_PERMANENT)) return nullptr; JS::RootedObject private_ns(context, JS_NewPlainObject(context)); - gjs_object_define_property(context, repo, - GJS_STRING_PRIVATE_NS_MARKER, private_ns, - JSPROP_PERMANENT | JSPROP_RESOLVING); + if (!JS_DefinePropertyById(context, repo, atoms.private_ns_marker(), + private_ns, JSPROP_PERMANENT | JSPROP_RESOLVING)) + return nullptr; return repo; } @@ -302,34 +320,36 @@ gjs_define_repo(JSContext *cx, return true; } +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_value_from_constant_info(JSContext* cx, GIConstantInfo* info, + JS::MutableHandleValue value) { + GIArgument garg; + g_constant_info_get_value(info, &garg); + + GjsAutoTypeInfo type_info = g_constant_info_get_type(info); + + bool ok = gjs_value_from_g_argument(cx, value, type_info, &garg, true); + + g_constant_info_free_value(info, &garg); + return ok; +} + +GJS_JSAPI_RETURN_CONVENTION static bool gjs_define_constant(JSContext *context, JS::HandleObject in_object, GIConstantInfo *info) { JS::RootedValue value(context); - GArgument garg = { 0, }; - GITypeInfo *type_info; const char *name; - bool ret = false; - - type_info = g_constant_info_get_type(info); - g_constant_info_get_value(info, &garg); - if (!gjs_value_from_g_argument(context, &value, type_info, &garg, true)) - goto out; + if (!gjs_value_from_constant_info(context, info, &value)) + return false; name = g_base_info_get_name((GIBaseInfo*) info); - if (JS_DefineProperty(context, in_object, - name, value, - GJS_MODULE_PROP_FLAGS)) - ret = true; - - out: - g_constant_info_free_value (info, &garg); - g_base_info_unref((GIBaseInfo*) type_info); - return ret; + return JS_DefineProperty(context, in_object, name, value, + GJS_MODULE_PROP_FLAGS); } #if GJS_VERBOSE_ENABLE_GI_USAGE @@ -425,21 +445,18 @@ gjs_define_info(JSContext *context, gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)info); if (g_type_is_a (gtype, G_TYPE_PARAM)) { - gjs_define_param_class(context, in_object); + if (!gjs_define_param_class(context, in_object)) + return false; } else if (g_type_is_a (gtype, G_TYPE_OBJECT)) { JS::RootedObject ignored1(context), ignored2(context); - gjs_define_object_class(context, in_object, info, gtype, - &ignored1, &ignored2); + if (!ObjectPrototype::define_class(context, in_object, info, + gtype, &ignored1, &ignored2)) + return false; } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { - JS::RootedObject ignored1(context), ignored2(context); - if (!gjs_define_fundamental_class(context, in_object, - (GIObjectInfo*)info, - &ignored1, &ignored2)) { - gjs_throw (context, - "Unsupported fundamental class creation for type %s", - g_type_name(gtype)); + JS::RootedObject ignored(context); + if (!FundamentalPrototype::define_class(context, in_object, + info, &ignored)) return false; - } } else { gjs_throw (context, "Unsupported type %s, deriving from fundamental %s", @@ -460,7 +477,8 @@ gjs_define_info(JSContext *context, /* Fall through */ case GI_INFO_TYPE_BOXED: - gjs_define_boxed_class(context, in_object, (GIBoxedInfo*) info); + if (!BoxedPrototype::define_class(context, in_object, info)) + return false; break; case GI_INFO_TYPE_UNION: if (!gjs_define_union_class(context, in_object, (GIUnionInfo*) info)) @@ -469,10 +487,11 @@ gjs_define_info(JSContext *context, case GI_INFO_TYPE_ENUM: if (g_enum_info_get_error_domain((GIEnumInfo*) info)) { /* define as GError subclass */ - gjs_define_error_class(context, in_object, (GIEnumInfo*) info); + if (!ErrorPrototype::define_class(context, in_object, info)) + return false; break; } - /* fall through */ + [[fallthrough]]; case GI_INFO_TYPE_FLAGS: if (!gjs_define_enumeration(context, in_object, (GIEnumInfo*) info)) @@ -484,11 +503,12 @@ gjs_define_info(JSContext *context, break; case GI_INFO_TYPE_INTERFACE: { - JS::RootedObject ignored(context); - gjs_define_interface_class(context, in_object, - (GIInterfaceInfo *) info, - g_registered_type_info_get_g_type((GIRegisteredTypeInfo *) info), - &ignored); + JS::RootedObject ignored1(context), ignored2(context); + if (!InterfacePrototype::create_class( + context, in_object, info, + g_registered_type_info_get_g_type(info), &ignored1, + &ignored2)) + return false; } break; case GI_INFO_TYPE_INVALID: @@ -517,9 +537,9 @@ gjs_define_info(JSContext *context, JSObject* gjs_lookup_private_namespace(JSContext *context) { - JS::HandleId ns_name = - gjs_context_get_const_string(context, GJS_STRING_PRIVATE_NS_MARKER); - return gjs_lookup_namespace_object_by_name(context, ns_name); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + return gjs_lookup_namespace_object_by_name(context, + atoms.private_ns_marker()); } /* Get the namespace object that the GIBaseInfo should be inside */ @@ -539,16 +559,16 @@ gjs_lookup_namespace_object(JSContext *context, } JS::RootedId ns_name(context, gjs_intern_string_to_id(context, ns)); + if (ns_name == JSID_VOID) + return nullptr; return gjs_lookup_namespace_object_by_name(context, ns_name); } /* Check if an exception's 'name' property is equal to compare_name. Ignores - * all errors that might arise. Requires request. */ -static bool -error_has_name(JSContext *cx, - JS::HandleValue thrown_value, - JSString *compare_name) -{ + * all errors that might arise. */ +[[nodiscard]] static bool error_has_name(JSContext* cx, + JS::HandleValue thrown_value, + JSString* compare_name) { if (!thrown_value.isObject()) return false; @@ -556,8 +576,9 @@ error_has_name(JSContext *cx, JS::RootedObject exc(cx, &thrown_value.toObject()); JS::RootedValue exc_name(cx); bool retval = false; + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); - if (!gjs_object_get_property(cx, exc, GJS_STRING_NAME, &exc_name)) + if (!JS_GetPropertyById(cx, exc, atoms.name(), &exc_name)) goto out; int32_t cmp_result; @@ -572,23 +593,24 @@ out: return retval; } +GJS_JSAPI_RETURN_CONVENTION static bool lookup_override_function(JSContext *cx, JS::HandleId ns_name, JS::MutableHandleValue function) { - JSAutoRequest ar(cx); JS::AutoSaveExceptionState saved_exc(cx); - JS::RootedValue importer(cx, gjs_get_global_slot(cx, GJS_GLOBAL_SLOT_IMPORTS)); + JS::RootedObject global(cx, gjs_get_import_global(cx)); + JS::RootedValue importer( + cx, gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS)); g_assert(importer.isObject()); JS::RootedObject overridespkg(cx), module(cx); JS::RootedObject importer_obj(cx, &importer.toObject()); - + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); if (!gjs_object_require_property(cx, importer_obj, "importer", - GJS_STRING_GI_OVERRIDES, - &overridespkg)) + atoms.overrides(), &overridespkg)) goto fail; if (!gjs_object_require_property(cx, overridespkg, @@ -608,7 +630,7 @@ lookup_override_function(JSContext *cx, } if (!gjs_object_require_property(cx, module, "override module", - GJS_STRING_GOBJECT_INIT, function) || + atoms.init(), function) || !function.isObjectOrNull()) { gjs_throw(cx, "Unexpected value for _init in overrides module"); goto fail; @@ -624,16 +646,15 @@ JSObject* gjs_lookup_namespace_object_by_name(JSContext *context, JS::HandleId ns_name) { - JSAutoRequest ar(context); - - JS::RootedValue importer(context, - gjs_get_global_slot(context, GJS_GLOBAL_SLOT_IMPORTS)); + JS::RootedObject global(context, gjs_get_import_global(context)); + JS::RootedValue importer( + context, gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS)); g_assert(importer.isObject()); JS::RootedObject repo(context), importer_obj(context, &importer.toObject()); - + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); if (!gjs_object_require_property(context, importer_obj, "importer", - GJS_STRING_GI_MODULE, &repo)) { + atoms.gi(), &repo)) { gjs_log_exception(context); gjs_throw(context, "No gi property in importer"); return NULL; @@ -697,32 +718,6 @@ gjs_info_type_name(GIInfoType type) } } -char* -gjs_camel_from_hyphen(const char *hyphen_name) -{ - GString *s; - const char *p; - bool next_upper; - - s = g_string_sized_new(strlen(hyphen_name) + 1); - - next_upper = false; - for (p = hyphen_name; *p; ++p) { - if (*p == '-' || *p == '_') { - next_upper = true; - } else { - if (next_upper) { - g_string_append_c(s, g_ascii_toupper(*p)); - next_upper = false; - } else { - g_string_append_c(s, *p); - } - } - } - - return g_string_free(s, false); -} - char* gjs_hyphen_from_camel(const char *camel_name) { @@ -761,8 +756,12 @@ gjs_lookup_generic_constructor(JSContext *context, if (!JS_GetProperty(context, in_object, constructor_name, &value)) return NULL; - if (G_UNLIKELY (!value.isObject())) + if (G_UNLIKELY(!value.isObject())) { + gjs_throw(context, + "Constructor of %s.%s was the wrong type, expected an object", + g_base_info_get_namespace(info), constructor_name); return NULL; + } return &value.toObject(); } @@ -776,13 +775,26 @@ gjs_lookup_generic_prototype(JSContext *context, if (G_UNLIKELY(!constructor)) return NULL; + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue value(context); - if (!gjs_object_get_property(context, constructor, - GJS_STRING_PROTOTYPE, &value)) + if (!JS_GetPropertyById(context, constructor, atoms.prototype(), &value)) return NULL; - if (G_UNLIKELY (!value.isObjectOrNull())) + if (G_UNLIKELY(!value.isObject())) { + gjs_throw(context, + "Prototype of %s.%s was the wrong type, expected an object", + g_base_info_get_namespace(info), g_base_info_get_name(info)); return NULL; + } + + return &value.toObject(); +} + +JSObject* gjs_new_object_with_generic_prototype(JSContext* cx, + GIBaseInfo* info) { + JS::RootedObject proto(cx, gjs_lookup_generic_prototype(cx, info)); + if (!proto) + return nullptr; - return value.toObjectOrNull(); + return JS_NewObjectWithGivenProto(cx, JS_GetClass(proto), proto); } diff --git a/gi/repo.h b/gi/repo.h index 61bfc8e..ff7dbeb 100644 --- a/gi/repo.h +++ b/gi/repo.h @@ -21,48 +21,53 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_REPO_H__ -#define __GJS_REPO_H__ +#ifndef GI_REPO_H_ +#define GI_REPO_H_ -#include -#include +#include #include -#include "cjs/jsapi-wrapper.h" -#include +#include -G_BEGIN_DECLS +#include "cjs/macros.h" +#include "util/log.h" +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_repo(JSContext *cx, JS::MutableHandleObject repo); -const char* gjs_info_type_name (GIInfoType type); +[[nodiscard]] const char* gjs_info_type_name(GIInfoType type); +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_lookup_private_namespace (JSContext *context); +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_lookup_namespace_object (JSContext *context, GIBaseInfo *info); +GJS_JSAPI_RETURN_CONVENTION JSObject *gjs_lookup_namespace_object_by_name(JSContext *context, JS::HandleId name); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_lookup_generic_constructor (JSContext *context, GIBaseInfo *info); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_lookup_generic_prototype (JSContext *context, GIBaseInfo *info); +GJS_JSAPI_RETURN_CONVENTION +JSObject* gjs_new_object_with_generic_prototype(JSContext* cx, + GIBaseInfo* info); +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_info(JSContext *context, JS::HandleObject in_object, GIBaseInfo *info, bool *defined); -char* gjs_camel_from_hyphen (const char *hyphen_name); -char* gjs_hyphen_from_camel (const char *camel_name); - +[[nodiscard]] char* gjs_hyphen_from_camel(const char* camel_name); #if GJS_VERBOSE_ENABLE_GI_USAGE void _gjs_log_info_usage(GIBaseInfo *info); #endif -G_END_DECLS - -#endif /* __GJS_REPO_H__ */ +#endif // GI_REPO_H_ diff --git a/gi/toggle.cpp b/gi/toggle.cpp index 9e402be..3807ae1 100644 --- a/gi/toggle.cpp +++ b/gi/toggle.cpp @@ -23,17 +23,28 @@ * Authored by: Philip Chimento , */ -#include +#include // for find_if #include #include +#include // for pair + #include +#include -#include "toggle.h" +#include "gi/toggle.h" std::deque::iterator -ToggleQueue::find_operation_locked(GObject *gobj, - ToggleQueue::Direction direction) -{ +ToggleQueue::find_operation_locked(const GObject *gobj, + ToggleQueue::Direction direction) { + return std::find_if(q.begin(), q.end(), + [gobj, direction](const Item& item)->bool { + return item.gobj == gobj && item.direction == direction; + }); +} + +std::deque::const_iterator +ToggleQueue::find_operation_locked(const GObject *gobj, + ToggleQueue::Direction direction) const { return std::find_if(q.begin(), q.end(), [gobj, direction](const Item& item)->bool { return item.gobj == gobj && item.direction == direction; @@ -41,7 +52,7 @@ ToggleQueue::find_operation_locked(GObject *gobj, } bool -ToggleQueue::find_and_erase_operation_locked(GObject *gobj, +ToggleQueue::find_and_erase_operation_locked(const GObject *gobj, ToggleQueue::Direction direction) { auto pos = find_operation_locked(gobj, direction); @@ -71,7 +82,7 @@ ToggleQueue::idle_destroy_notify(void *data) } std::pair -ToggleQueue::is_queued(GObject *gobj) +ToggleQueue::is_queued(GObject *gobj) const { std::lock_guard hold(lock); bool has_toggle_down = find_operation_locked(gobj, DOWN) != q.end(); @@ -112,7 +123,7 @@ ToggleQueue::handle_toggle(Handler handler) debug("handle", item.gobj); if (item.needs_unref) g_object_unref(item.gobj); - + return true; } diff --git a/gi/toggle.h b/gi/toggle.h index f03b6ea..49f61b7 100644 --- a/gi/toggle.h +++ b/gi/toggle.h @@ -23,13 +23,16 @@ * Authored by: Philip Chimento , */ -#ifndef GJS_TOGGLE_H -#define GJS_TOGGLE_H +#ifndef GI_TOGGLE_H_ +#define GI_TOGGLE_H_ #include #include #include +#include // for pair + #include +#include #include "util/log.h" @@ -52,7 +55,7 @@ private: unsigned needs_unref : 1; }; - std::mutex lock; + mutable std::mutex lock; std::deque q; std::atomic_bool m_shutdown = ATOMIC_VAR_INIT(false); @@ -60,21 +63,27 @@ private: Handler m_toggle_handler; /* No-op unless GJS_VERBOSE_ENABLE_LIFECYCLE is defined to 1. */ - inline void debug(const char *did, void *what) { + inline void debug(const char* did GJS_USED_VERBOSE_LIFECYCLE, + const void* what GJS_USED_VERBOSE_LIFECYCLE) { 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); + [[nodiscard]] std::deque::iterator find_operation_locked( + const GObject* gobj, Direction direction); + + [[nodiscard]] std::deque::const_iterator find_operation_locked( + const GObject* gobj, Direction direction) const; + + [[nodiscard]] bool find_and_erase_operation_locked(const GObject* gobj, + Direction direction); static gboolean idle_handle_toggle(void *data); static void idle_destroy_notify(void *data); -public: + public: /* These two functions return a pair DOWN, UP signifying whether toggles * are / were queued. is_queued() just checks and does not modify. */ - std::pair is_queued(GObject *gobj); + [[nodiscard]] std::pair is_queued(GObject* gobj) const; /* Cancels pending toggles and returns whether any were queued. */ std::pair cancel(GObject *gobj); @@ -93,11 +102,10 @@ public: Direction direction, Handler handler); - static ToggleQueue& - get_default(void) { + [[nodiscard]] static ToggleQueue& get_default() { static ToggleQueue the_singleton; return the_singleton; } }; -#endif /* GJS_TOGGLE_H */ +#endif // GI_TOGGLE_H_ diff --git a/gi/union.cpp b/gi/union.cpp index b9d29cd..14bcd2f 100644 --- a/gi/union.cpp +++ b/gi/union.cpp @@ -23,103 +23,67 @@ #include -#include - -/* include first for logging related #define used in repo.h */ -#include - -#include "union.h" -#include "arg.h" -#include "object.h" -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-wrapper.h" -#include "cjs/mem.h" -#include "repo.h" -#include "proxyutils.h" -#include "function.h" -#include "gtype.h" #include -typedef struct { - GIUnionInfo *info; - void *gboxed; /* NULL if we are the prototype and not an instance */ - GType gtype; -} Union; - -extern struct JSClass gjs_union_class; +#include +#include +#include +#include +#include + +#include "gi/arg-inl.h" +#include "gi/function.h" +#include "gi/repo.h" +#include "gi/union.h" +#include "cjs/jsapi-util.h" +#include "cjs/mem-private.h" +#include "util/log.h" + +UnionPrototype::UnionPrototype(GIUnionInfo* info, GType gtype) + : GIWrapperPrototype(info, gtype) { + GJS_INC_COUNTER(union_prototype); +} -GJS_DEFINE_PRIV_FROM_JS(Union, gjs_union_class) +UnionPrototype::~UnionPrototype(void) { GJS_DEC_COUNTER(union_prototype); } -/* - * The *resolved out parameter, on success, should be false to indicate that id - * was not resolved; and true if id was resolved. - */ -static bool -union_resolve(JSContext *context, - JS::HandleObject obj, - JS::HandleId id, - bool *resolved) -{ - 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 */ - - if (priv->gboxed != NULL) { - /* 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; - } +UnionInstance::UnionInstance(JSContext* cx, JS::HandleObject obj) + : GIWrapperInstance(cx, obj) { + GJS_INC_COUNTER(union_instance); +} - GjsAutoJSChar name; - if (!gjs_get_string_id(context, id, &name)) { - *resolved = false; - return true; /* not resolved, but no error */ +UnionInstance::~UnionInstance(void) { + if (m_ptr) { + g_boxed_free(gtype(), m_ptr); + m_ptr = nullptr; } + GJS_DEC_COUNTER(union_instance); +} - /* We are the prototype, so look for methods and other class properties */ - GIFunctionInfo *method_info; - - method_info = g_union_info_find_method((GIUnionInfo*) priv->info, - name); - - if (method_info != NULL) { - const char *method_name; +// See GIWrapperBase::resolve(). +bool UnionPrototype::resolve_impl(JSContext* context, JS::HandleObject obj, + JS::HandleId, const char* prop_name, + bool* resolved) { + // Look for methods and other class properties + GjsAutoFunctionInfo method_info = + g_union_info_find_method(info(), prop_name); + if (method_info) { #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)); + method_info.name(), ns(), name()); /* obj is union proto */ - if (gjs_define_function(context, obj, - g_registered_type_info_get_g_type(priv->info), - method_info) == NULL) { - g_base_info_unref( (GIBaseInfo*) method_info); + if (!gjs_define_function(context, obj, gtype(), method_info)) return false; - } *resolved = true; /* we defined the prop in object_proto */ } else { *resolved = false; } - - g_base_info_unref( (GIBaseInfo*) method_info); } else { *resolved = false; } @@ -127,11 +91,9 @@ union_resolve(JSContext *context, return true; } -static void* -union_new(JSContext *context, - JS::HandleObject obj, /* "this" for constructor */ - GIUnionInfo *info) -{ +GJS_JSAPI_RETURN_CONVENTION +static void* union_new(JSContext* context, JS::HandleObject this_obj, + const JS::CallArgs& args, GIUnionInfo* info) { int n_methods; int i; @@ -140,184 +102,72 @@ union_new(JSContext *context, n_methods = g_union_info_get_n_methods(info); for (i = 0; i < n_methods; ++i) { - GIFunctionInfo *func_info; GIFunctionInfoFlags flags; - func_info = g_union_info_get_method(info, i); + GjsAutoFunctionInfo func_info = g_union_info_get_method(info, i); flags = g_function_info_get_flags(func_info); if ((flags & GI_FUNCTION_IS_CONSTRUCTOR) != 0 && g_callable_info_get_n_args((GICallableInfo*) func_info) == 0) { - - JS::RootedValue rval(context, JS::NullValue()); - - gjs_invoke_c_function_uncached(context, func_info, obj, - JS::HandleValueArray::empty(), &rval); - - g_base_info_unref((GIBaseInfo*) func_info); - - /* We are somewhat wasteful here; invoke_c_function() above - * creates a JSObject wrapper for the union that we immediately - * discard. - */ - if (rval.isNull()) { - return NULL; - } else { - JS::RootedObject rval_obj(context, &rval.toObject()); - return gjs_c_union_from_union(context, rval_obj); + GIArgument rval; + if (!gjs_invoke_constructor_from_c(context, func_info, this_obj, + args, &rval)) + return nullptr; + + if (!gjs_arg_get(&rval)) { + gjs_throw(context, + "Unable to construct union type %s as its" + "constructor function returned null", + g_base_info_get_name(info)); + return nullptr; } - } - g_base_info_unref((GIBaseInfo*) func_info); + return gjs_arg_get(&rval); + } } gjs_throw(context, "Unable to construct union type %s since it has no zero-args , can only wrap an existing one", g_base_info_get_name((GIBaseInfo*) info)); - return NULL; + return nullptr; } -GJS_NATIVE_CONSTRUCTOR_DECLARE(union) -{ - GJS_NATIVE_CONSTRUCTOR_VARIABLES(union) - Union *priv; - Union *proto_priv; - JS::RootedObject proto(context); - void *gboxed; - - GJS_NATIVE_CONSTRUCTOR_PRELUDE(union); - - priv = g_slice_new0(Union); - - GJS_INC_COUNTER(boxed); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "union constructor, obj %p priv %p", - object.get(), priv); - - JS_GetPrototype(context, object, &proto); - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, "union instance __proto__ is %p", - proto.get()); - - /* If we're the prototype, then post-construct we'll fill in priv->info. - * If we are not the prototype, though, then we'll get ->info from the - * prototype and then create a GObject if we don't have one already. - */ - proto_priv = priv_from_js(context, proto); - if (proto_priv == NULL) { - gjs_debug(GJS_DEBUG_GBOXED, - "Bad prototype set on union? Must match JSClass of object. JS error should have been reported."); - return false; - } - - priv->info = proto_priv->info; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->gtype = proto_priv->gtype; - - /* union_new happens to be implemented by calling - * gjs_invoke_c_function(), which returns a JS::Value. - * The returned "gboxed" here is owned by that JS::Value, - * not by us. - */ - gboxed = union_new(context, object, priv->info); - - if (gboxed == NULL) { +// See GIWrapperBase::constructor(). +bool UnionInstance::constructor_impl(JSContext* context, + JS::HandleObject object, + const JS::CallArgs& args) { + if (args.length() > 0 && + !JS::WarnUTF8(context, "Arguments to constructor of %s ignored", + name())) return false; - } - - /* Because "gboxed" is owned by a JS::Value and will - * be garbage collected, we make a copy here to be - * owned by us. - */ - priv->gboxed = g_boxed_copy(priv->gtype, gboxed); - - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "JSObject created with union instance %p type %s", - priv->gboxed, g_type_name(priv->gtype)); - - GJS_NATIVE_CONSTRUCTOR_FINISH(union); - - return true; -} - -static void -union_finalize(JSFreeOp *fop, - JSObject *obj) -{ - Union *priv; - - priv = (Union*) JS_GetPrivate(obj); - gjs_debug_lifecycle(GJS_DEBUG_GBOXED, - "finalize, obj %p priv %p", obj, priv); - if (priv == NULL) - return; /* wrong class? */ - - if (priv->gboxed) { - g_boxed_free(g_registered_type_info_get_g_type( (GIRegisteredTypeInfo*) priv->info), - priv->gboxed); - priv->gboxed = NULL; - } - - if (priv->info) { - g_base_info_unref( (GIBaseInfo*) priv->info); - priv->info = NULL; - } - GJS_DEC_COUNTER(boxed); - g_slice_free(Union, priv); + m_ptr = union_new(context, object, args, info()); + return !!m_ptr; } -static bool -to_string_func(JSContext *context, - unsigned argc, - JS::Value *vp) -{ - GJS_GET_PRIV(context, argc, vp, rec, obj, Union, priv); - return _gjs_proxy_to_string_func(context, obj, "union", - (GIBaseInfo*)priv->info, priv->gtype, - priv->gboxed, rec.rval()); -} - -/* The bizarre thing about this vtable is that it applies to both - * instances of the object, and to the prototype that instances of the - * class have. - */ -static const struct JSClassOps gjs_union_class_ops = { - NULL, /* addProperty */ - NULL, /* deleteProperty */ - NULL, /* getProperty */ - NULL, /* setProperty */ - NULL, /* enumerate */ - union_resolve, - nullptr, /* mayResolve */ - union_finalize +// clang-format off +const struct JSClassOps UnionBase::class_ops = { + nullptr, // addProperty + nullptr, // deleteProperty + nullptr, // enumerate + nullptr, // newEnumerate + &UnionBase::resolve, + nullptr, // mayResolve + &UnionBase::finalize, }; -struct JSClass gjs_union_class = { +const struct JSClass UnionBase::klass = { "GObject_Union", JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE, - &gjs_union_class_ops -}; - -JSPropertySpec gjs_union_proto_props[] = { - JS_PS_END -}; - -JSFunctionSpec gjs_union_proto_funcs[] = { - JS_FS("toString", to_string_func, 0, 0), - JS_FS_END + &UnionBase::class_ops }; +// clang-format on bool gjs_define_union_class(JSContext *context, JS::HandleObject in_object, GIUnionInfo *info) { - const char *constructor_name; - Union *priv; GType gtype; JS::RootedObject prototype(context), constructor(context); @@ -330,47 +180,8 @@ gjs_define_union_class(JSContext *context, return false; } - /* See the comment in gjs_define_object_class() for an - * explanation of how this all works; Union is pretty much the - * same as Object. - */ - - constructor_name = g_base_info_get_name( (GIBaseInfo*) info); - - if (!gjs_init_class_dynamic(context, in_object, nullptr, - g_base_info_get_namespace( (GIBaseInfo*) info), - constructor_name, - &gjs_union_class, - gjs_union_constructor, 0, - /* props of prototype */ - &gjs_union_proto_props[0], - /* funcs of prototype */ - &gjs_union_proto_funcs[0], - /* props of constructor, MyConstructor.myprop */ - NULL, - /* funcs of constructor, MyConstructor.myfunc() */ - NULL, - &prototype, - &constructor)) { - g_error("Can't init class %s", constructor_name); - } - - GJS_INC_COUNTER(boxed); - priv = g_slice_new0(Union); - priv->info = info; - g_base_info_ref( (GIBaseInfo*) priv->info); - priv->gtype = gtype; - JS_SetPrivate(prototype, priv); - - gjs_debug(GJS_DEBUG_GBOXED, "Defined class %s prototype is %p class %p in object %p", - constructor_name, prototype.get(), JS_GetClass(prototype), - in_object.get()); - - JS::RootedObject gtype_obj(context, - gjs_gtype_create_gtype_wrapper(context, gtype)); - JS_DefineProperty(context, constructor, "$gtype", gtype_obj, JSPROP_PERMANENT); - - return true; + return !!UnionPrototype::create_class(context, in_object, info, gtype, + &constructor, &prototype); } JSObject* @@ -378,12 +189,10 @@ gjs_union_from_c_union(JSContext *context, GIUnionInfo *info, void *gboxed) { - JSObject *obj; - Union *priv; GType gtype; - if (gboxed == NULL) - return NULL; + if (!gboxed) + return nullptr; /* For certain unions, we may be able to relax this in the future by * directly allocating union memory, as we do for structures in boxed.c @@ -391,92 +200,30 @@ gjs_union_from_c_union(JSContext *context, gtype = g_registered_type_info_get_g_type( (GIRegisteredTypeInfo*) info); if (gtype == G_TYPE_NONE) { gjs_throw(context, "Unions must currently be registered as boxed types"); - return NULL; + return nullptr; } gjs_debug_marshal(GJS_DEBUG_GBOXED, "Wrapping union %s %p with JSObject", g_base_info_get_name((GIBaseInfo *)info), gboxed); - JS::RootedObject proto(context, - gjs_lookup_generic_prototype(context, (GIUnionInfo*) info)); - - obj = JS_NewObjectWithGivenProto(context, JS_GetClass(proto), proto); - - GJS_INC_COUNTER(boxed); - priv = g_slice_new0(Union); - JS_SetPrivate(obj, priv); - priv->info = info; - g_base_info_ref( (GIBaseInfo *) priv->info); - priv->gtype = gtype; - priv->gboxed = g_boxed_copy(gtype, gboxed); - - return obj; -} - -void* -gjs_c_union_from_union(JSContext *context, - JS::HandleObject obj) -{ - Union *priv; - + JS::RootedObject obj(context, + gjs_new_object_with_generic_prototype(context, info)); if (!obj) - return NULL; + return nullptr; - priv = priv_from_js(context, obj); + UnionInstance* priv = UnionInstance::new_for_js_object(context, obj); + priv->copy_union(gboxed); - return priv->gboxed; + return obj; } -bool -gjs_typecheck_union(JSContext *context, - JS::HandleObject object, - GIStructInfo *expected_info, - GType expected_type, - bool throw_error) -{ - Union *priv; - bool result; - - if (!do_base_typecheck(context, object, throw_error)) - return false; - - priv = priv_from_js(context, object); - - if (priv->gboxed == NULL) { - if (throw_error) { - 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)); - } - - return false; - } - - if (expected_type != G_TYPE_NONE) - result = g_type_is_a (priv->gtype, expected_type); - else if (expected_info != NULL) - result = g_base_info_equal((GIBaseInfo*) priv->info, (GIBaseInfo*) expected_info); - else - result = true; - - if (!result && throw_error) { - if (expected_info != 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, 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)); - } - } +void* UnionInstance::copy_ptr(JSContext* cx, GType gtype, void* ptr) { + if (g_type_is_a(gtype, G_TYPE_BOXED)) + return g_boxed_copy(gtype, ptr); - return result; + gjs_throw(cx, + "Can't transfer ownership of a union type not registered as " + "boxed"); + return nullptr; } diff --git a/gi/union.h b/gi/union.h index 5d0c46d..7b792c4 100644 --- a/gi/union.h +++ b/gi/union.h @@ -21,32 +21,98 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_UNION_H__ -#define __GJS_UNION_H__ +#ifndef GI_UNION_H_ +#define GI_UNION_H_ + +#include -#include -#include #include -#include "cjs/jsapi-util.h" +#include + +#include + +#include "gi/wrapperutils.h" +#include "cjs/macros.h" +#include "util/log.h" + +namespace JS { +class CallArgs; +} +struct JSClass; +struct JSClassOps; +class UnionPrototype; +class UnionInstance; + +class UnionBase + : public GIWrapperBase { + friend class GIWrapperBase; + + protected: + explicit UnionBase(UnionPrototype* proto = nullptr) + : GIWrapperBase(proto) {} + ~UnionBase(void) {} + + static const GjsDebugTopic debug_topic = GJS_DEBUG_GBOXED; + static constexpr const char* debug_tag = "union"; + + static const JSClassOps class_ops; + static const JSClass klass; + + [[nodiscard]] static const char* to_string_kind(void) { return "union"; } +}; + +class UnionPrototype : public GIWrapperPrototype { + friend class GIWrapperPrototype; + friend class GIWrapperBase; -G_BEGIN_DECLS + static constexpr InfoType::Tag info_type_tag = InfoType::Union; + explicit UnionPrototype(GIUnionInfo* info, GType gtype); + ~UnionPrototype(void); + + GJS_JSAPI_RETURN_CONVENTION + bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + const char* prop_name, bool* resolved); + + // Overrides GIWrapperPrototype::constructor_nargs(). + [[nodiscard]] unsigned constructor_nargs(void) const { return 0; } +}; + +class UnionInstance + : public GIWrapperInstance { + friend class GIWrapperInstance; + friend class GIWrapperBase; + + explicit UnionInstance(JSContext* cx, JS::HandleObject obj); + ~UnionInstance(void); + + GJS_JSAPI_RETURN_CONVENTION + bool constructor_impl(JSContext* cx, JS::HandleObject obj, + const JS::CallArgs& args); + + public: + /* + * UnionInstance::copy_union: + * + * Allocate a new union pointer using g_boxed_copy(), from a raw union + * pointer. + */ + void copy_union(void* ptr) { m_ptr = g_boxed_copy(gtype(), ptr); } + + GJS_JSAPI_RETURN_CONVENTION + static void* copy_ptr(JSContext* cx, GType gtype, void* ptr); +}; + +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_union_class(JSContext *context, JS::HandleObject in_object, GIUnionInfo *info); -void *gjs_c_union_from_union(JSContext *context, - JS::HandleObject obj); - +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_union_from_c_union (JSContext *context, GIUnionInfo *info, void *gboxed); -bool gjs_typecheck_union (JSContext *context, - JS::HandleObject obj, - GIStructInfo *expected_info, - GType expected_type, - bool throw_error); - -G_END_DECLS -#endif /* __GJS_UNION_H__ */ +#endif // GI_UNION_H_ diff --git a/gi/utils-inl.h b/gi/utils-inl.h new file mode 100644 index 0000000..4e060c7 --- /dev/null +++ b/gi/utils-inl.h @@ -0,0 +1,42 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later + * + * Copyright (c) 2020 Marco Trevisan + */ + +#pragma once + +#include + +#include // IWYU pragma: keep + +template +constexpr void* gjs_int_to_pointer(T v) { + static_assert(std::is_integral_v, "Need integer value"); + + if constexpr (std::is_signed_v) + return reinterpret_cast(static_cast(v)); + else + return reinterpret_cast(static_cast(v)); +} + +template +constexpr T gjs_pointer_to_int(void* p) { + static_assert(std::is_integral_v, "Need integer value"); + + if constexpr (std::is_signed_v) + return static_cast(reinterpret_cast(p)); + else + return static_cast(reinterpret_cast(p)); +} + +template <> +inline void* gjs_int_to_pointer(bool v) { + return gjs_int_to_pointer(!!v); +} + +template <> +inline bool gjs_pointer_to_int(void* p) { + return !!gjs_pointer_to_int(p); +} diff --git a/gi/value.cpp b/gi/value.cpp index d2fc3bd..da3d397 100644 --- a/gi/value.cpp +++ b/gi/value.cpp @@ -23,24 +23,45 @@ #include -#include - -#include "foreign.h" -#include "value.h" -#include "closure.h" -#include "arg.h" -#include "param.h" -#include "object.h" -#include "fundamental.h" -#include "boxed.h" -#include "union.h" -#include "gtype.h" -#include "gerror.h" -#include "cjs/context-private.h" -#include "cjs/jsapi-wrapper.h" +#include // for SCHAR_MAX, SCHAR_MIN, UCHAR_MAX +#include +#include // for memset #include +#include +#include + +#include +#include +#include // for RootedVector +#include +#include +#include // for UniqueChars +#include +#include +#include // for InformalValueTypeName, JS_ClearPendingException +#include + +#include "gi/arg-inl.h" +#include "gi/arg.h" +#include "gi/boxed.h" +#include "gi/closure.h" +#include "gi/foreign.h" +#include "gi/fundamental.h" +#include "gi/gerror.h" +#include "gi/gtype.h" +#include "gi/object.h" +#include "gi/param.h" +#include "gi/union.h" +#include "gi/value.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/context.h" +#include "cjs/jsapi-util.h" +#include "util/log.h" +GJS_JSAPI_RETURN_CONVENTION static bool gjs_value_from_g_value_internal(JSContext *context, JS::MutableHandleValue value_p, const GValue *gvalue, @@ -53,9 +74,8 @@ static bool gjs_value_from_g_value_internal(JSContext *context, * only works for signals on introspected GObjects, not signals on GJS-defined * GObjects nor standalone closures. The return value must be unreffed. */ -static GISignalInfo * -get_signal_info_if_available(GSignalQuery *signal_query) -{ +[[nodiscard]] static GISignalInfo* get_signal_info_if_available( + GSignalQuery* signal_query) { GIBaseInfo *obj; GIInfoType info_type; GISignalInfo *signal_info = NULL; @@ -83,6 +103,7 @@ get_signal_info_if_available(GSignalQuery *signal_query) * Fill in value_p with a JS array, converted from a C array stored as a pointer * in array_value, with its length stored in array_length_value. */ +GJS_JSAPI_RETURN_CONVENTION static bool gjs_value_from_array_and_length_values(JSContext *context, JS::MutableHandleValue value_p, @@ -104,7 +125,7 @@ gjs_value_from_array_and_length_values(JSContext *context, signal_query, array_length_arg_n)) return false; - array_arg.v_pointer = g_value_get_pointer(array_value); + gjs_arg_set(&array_arg, g_value_get_pointer(array_value)); return gjs_value_from_explicit_array(context, value_p, array_type_info, &array_arg, array_length.toInt32()); @@ -119,7 +140,6 @@ closure_marshal(GClosure *closure, gpointer marshal_data) { JSContext *context; - JSObject *obj; unsigned i; GSignalQuery signal_query = { 0, }; GISignalInfo *signal_info; @@ -137,7 +157,8 @@ closure_marshal(GClosure *closure, } context = gjs_closure_get_context(closure); - if (G_UNLIKELY(_gjs_context_is_sweeping(context))) { + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + if (G_UNLIKELY(gjs->sweeping())) { GSignalInvocationHint *hint = (GSignalInvocationHint*) invocation_hint; g_critical("Attempting to call back into JSAPI during the sweeping phase of GC. " @@ -158,9 +179,8 @@ closure_marshal(GClosure *closure, return; } - obj = gjs_closure_get_callable(closure); - JSAutoRequest ar(context); - JSAutoCompartment ac(context, obj); + JSFunction* func = gjs_closure_get_callable(closure); + JSAutoRealm ar(context, JS_GetFunctionObject(func)); if (marshal_data) { /* we are used for a signal handler */ @@ -216,7 +236,7 @@ closure_marshal(GClosure *closure, g_base_info_unref((GIBaseInfo *)signal_info); } - JS::AutoValueVector argv(context); + JS::RootedValueVector argv(context); /* May end up being less */ if (!argv.reserve(n_param_values)) g_error("Unable to reserve space"); @@ -260,8 +280,7 @@ closure_marshal(GClosure *closure, return; } - if (!argv.append(argv_to_append)) - g_error("Unable to append to vector"); + argv.infallibleAppend(argv_to_append); } for (i = 1; i < n_param_values; i++) @@ -269,7 +288,8 @@ closure_marshal(GClosure *closure, g_base_info_unref((GIBaseInfo *)type_info_for[i]); JS::RootedValue rval(context); - gjs_closure_invoke(closure, nullptr, argv, &rval, false); + mozilla::Unused << gjs_closure_invoke(closure, nullptr, argv, &rval, false); + // Any exception now pending, is handled when returning control to JS if (return_value != NULL) { if (rval.isUndefined()) { @@ -286,12 +306,8 @@ closure_marshal(GClosure *closure, } } -GClosure* -gjs_closure_new_for_signal(JSContext *context, - JSObject *callable, - const char *description, - guint signal_id) -{ +GClosure* gjs_closure_new_for_signal(JSContext* context, JSFunction* callable, + const char* description, guint signal_id) { GClosure *closure; closure = gjs_closure_new(context, callable, description, false); @@ -301,11 +317,8 @@ gjs_closure_new_for_signal(JSContext *context, return closure; } -GClosure* -gjs_closure_new_marshaled (JSContext *context, - JSObject *callable, - const char *description) -{ +GClosure* gjs_closure_new_marshaled(JSContext* context, JSFunction* callable, + const char* description) { GClosure *closure; closure = gjs_closure_new(context, callable, description, true); @@ -315,31 +328,38 @@ gjs_closure_new_marshaled (JSContext *context, return closure; } -static GType -gjs_value_guess_g_type(JSContext *context, - JS::Value value) -{ - if (value.isNull()) - return G_TYPE_POINTER; - - if (value.isString()) - return G_TYPE_STRING; - - if (value.isInt32()) - return G_TYPE_INT; - - if (value.isDouble()) - return G_TYPE_DOUBLE; - - if (value.isBoolean()) - return G_TYPE_BOOLEAN; +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_value_guess_g_type(JSContext* context, JS::Value value, + GType* gtype_out) { + g_assert(gtype_out && "Invalid return location"); + if (value.isNull()) { + *gtype_out = G_TYPE_POINTER; + return true; + } + if (value.isString()) { + *gtype_out = G_TYPE_STRING; + return true; + } + if (value.isInt32()) { + *gtype_out = G_TYPE_INT; + return true; + } + if (value.isDouble()) { + *gtype_out = G_TYPE_DOUBLE; + return true; + } + if (value.isBoolean()) { + *gtype_out = G_TYPE_BOOLEAN; + return true; + } if (value.isObject()) { JS::RootedObject obj(context, &value.toObject()); - return gjs_gtype_get_actual_gtype(context, obj); + return gjs_gtype_get_actual_gtype(context, obj, gtype_out); } - return G_TYPE_INVALID; + *gtype_out = G_TYPE_INVALID; + return true; } static bool @@ -355,6 +375,7 @@ throw_expect_type(JSContext *cx, return false; /* for convenience */ } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_value_to_g_value_internal(JSContext *context, JS::HandleValue value, @@ -366,7 +387,8 @@ gjs_value_to_g_value_internal(JSContext *context, gtype = G_VALUE_TYPE(gvalue); if (gtype == 0) { - gtype = gjs_value_guess_g_type(context, value); + if (!gjs_value_guess_g_type(context, value, >ype)) + return false; if (gtype == G_TYPE_INVALID) { gjs_throw(context, "Could not guess unspecified GValue type"); @@ -393,11 +415,11 @@ gjs_value_to_g_value_internal(JSContext *context, g_value_set_string(gvalue, NULL); } else if (value.isString()) { JS::RootedString str(context, value.toString()); - GjsAutoJSChar utf8_string = JS_EncodeStringToUTF8(context, str); + JS::UniqueChars utf8_string(JS_EncodeStringToUTF8(context, str)); if (!utf8_string) return false; - g_value_take_string(gvalue, utf8_string.copy()); + g_value_set_string(gvalue, utf8_string.get()); } else { return throw_expect_type(context, value, "string"); } @@ -454,32 +476,31 @@ gjs_value_to_g_value_internal(JSContext *context, /* nothing to do */ } else if (value.isObject()) { JS::RootedObject obj(context, &value.toObject()); - - if (!gjs_typecheck_object(context, obj, gtype, true)) + if (!ObjectBase::typecheck(context, obj, nullptr, gtype) || + !ObjectBase::to_c_ptr(context, obj, &gobj)) return false; - - gobj = gjs_g_object_from_object(context, obj); + if (!gobj) + return true; // treat disposed object as if value.isNull() } else { return throw_expect_type(context, value, "object", gtype); } g_value_set_object(gvalue, gobj); } else if (gtype == G_TYPE_STRV) { - bool found_length; - if (value.isNull()) { /* do nothing */ - } else { + } else if (value.isObject()) { + bool found_length; + + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject array_obj(context, &value.toObject()); - if (gjs_object_has_property(context, array_obj, - GJS_STRING_LENGTH, &found_length) && + if (JS_HasPropertyById(context, array_obj, atoms.length(), + &found_length) && found_length) { guint32 length; - if (!gjs_object_require_converted_property(context, array_obj, - NULL, - GJS_STRING_LENGTH, - &length)) { + if (!gjs_object_require_converted_property( + context, array_obj, nullptr, atoms.length(), &length)) { JS_ClearPendingException(context); return throw_expect_type(context, value, "strv"); } else { @@ -497,6 +518,8 @@ gjs_value_to_g_value_internal(JSContext *context, } else { return throw_expect_type(context, value, "strv"); } + } else { + return throw_expect_type(context, value, "strv"); } } else if (g_type_is_a(gtype, G_TYPE_BOXED)) { void *gboxed; @@ -509,6 +532,22 @@ gjs_value_to_g_value_internal(JSContext *context, if (g_type_is_a(gtype, G_TYPE_VALUE)) { GValue nested_gvalue = G_VALUE_INIT; + /* explicitly handle values that are already GValues + to avoid infinite recursion */ + if (value.isObject()) { + JS::RootedObject obj(context, &value.toObject()); + GType guessed_gtype; + + if (!gjs_value_guess_g_type(context, value, &guessed_gtype)) + return false; + + if (guessed_gtype == G_TYPE_VALUE) { + gboxed = BoxedBase::to_c_ptr(context, obj); + g_value_set_boxed(gvalue, gboxed); + return true; + } + } + if (!gjs_value_to_g_value(context, value, &nested_gvalue)) return false; @@ -522,10 +561,9 @@ gjs_value_to_g_value_internal(JSContext *context, if (g_type_is_a(gtype, G_TYPE_ERROR)) { /* special case GError */ - if (!gjs_typecheck_gerror(context, obj, true)) + gboxed = ErrorBase::to_c_ptr(context, obj); + if (!gboxed) return false; - - gboxed = gjs_gerror_from_error(context, obj); } else { GIBaseInfo *registered = g_irepository_find_by_gtype (NULL, gtype); @@ -546,7 +584,7 @@ gjs_value_to_g_value_internal(JSContext *context, true, &arg)) return false; - gboxed = arg.v_pointer; + gboxed = gjs_arg_get(&arg); } } @@ -558,14 +596,17 @@ gjs_value_to_g_value_internal(JSContext *context, loading the typelib. */ if (!gboxed) { - if (gjs_typecheck_union(context, obj, NULL, gtype, false)) { - gboxed = gjs_c_union_from_union(context, obj); + if (UnionBase::typecheck(context, obj, nullptr, gtype, + GjsTypecheckNoThrow())) { + gboxed = UnionBase::to_c_ptr(context, obj); } else { - if (!gjs_typecheck_boxed(context, obj, NULL, gtype, true)) + if (!BoxedBase::typecheck(context, obj, nullptr, gtype)) return false; - gboxed = gjs_c_struct_from_boxed(context, obj); + gboxed = BoxedBase::to_c_ptr(context, obj); } + if (!gboxed) + return false; } } } else { @@ -584,10 +625,12 @@ gjs_value_to_g_value_internal(JSContext *context, } else if (value.isObject()) { JS::RootedObject obj(context, &value.toObject()); - if (!gjs_typecheck_boxed(context, obj, NULL, G_TYPE_VARIANT, true)) + if (!BoxedBase::typecheck(context, obj, nullptr, G_TYPE_VARIANT)) return false; - variant = (GVariant*) gjs_c_struct_from_boxed(context, obj); + variant = BoxedBase::to_c_ptr(context, obj); + if (!variant) + return false; } else { return throw_expect_type(context, value, "boxed type", gtype); } @@ -598,12 +641,10 @@ gjs_value_to_g_value_internal(JSContext *context, if (JS::ToInt64(context, value, &value_int64)) { GEnumValue *v; - gpointer gtype_class = g_type_class_ref(gtype); + GjsAutoTypeClass enum_class(gtype); /* See arg.c:_gjs_enum_to_int() */ - v = g_enum_get_value(G_ENUM_CLASS(gtype_class), - (int)value_int64); - g_type_class_unref(gtype_class); + v = g_enum_get_value(enum_class, (int)value_int64); if (v == NULL) { gjs_throw(context, "%d is not a valid value for enumeration %s", @@ -652,7 +693,8 @@ gjs_value_to_g_value_internal(JSContext *context, return throw_expect_type(context, value, "GType object"); JS::RootedObject obj(context, &value.toObject()); - type = gjs_gtype_get_actual_gtype(context, obj); + if (!gjs_gtype_get_actual_gtype(context, obj, &type)) + return false; g_value_set_gtype(gvalue, type); } else if (g_type_is_a(gtype, G_TYPE_POINTER)) { if (value.isNull()) { @@ -677,6 +719,15 @@ gjs_value_to_g_value_internal(JSContext *context, } else { return throw_expect_type(context, value, "integer"); } + } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { + // The gtype is none of the above, it should be derived from a custom + // fundamental type. + if (!value.isObject()) + return throw_expect_type(context, value, "object", gtype); + + JS::RootedObject fundamental_object(context, &value.toObject()); + if (!FundamentalBase::to_gvalue(context, fundamental_object, gvalue)) + return false; } else { gjs_debug(GJS_DEBUG_GCLOSURE, "JS::Value is number %d gtype fundamental %d transformable to int %d from int %d", value.isNumber(), @@ -709,30 +760,24 @@ gjs_value_to_g_value_no_copy(JSContext *context, return gjs_value_to_g_value_internal(context, value, gvalue, true); } -static JS::Value -convert_int_to_enum (GType gtype, - int v) -{ +[[nodiscard]] static JS::Value convert_int_to_enum(GType gtype, int v) { double v_double; if (v > 0 && v < G_MAXINT) { /* Optimize the unambiguous case */ v_double = v; } else { - GIBaseInfo *info; - /* Need to distinguish between negative integers and unsigned integers */ - - info = g_irepository_find_by_gtype(g_irepository_get_default(), gtype); + GjsAutoEnumInfo info = g_irepository_find_by_gtype(nullptr, gtype); g_assert (info); - v_double = _gjs_enum_from_int ((GIEnumInfo *)info, v); - g_base_info_unref(info); + v_double = _gjs_enum_from_int(info, v); } return JS::NumberValue(v_double); } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_value_from_g_value_internal(JSContext *context, JS::MutableHandleValue value_p, @@ -790,12 +835,17 @@ gjs_value_from_g_value_internal(JSContext *context, value_p.setBoolean(!!v); } else if (g_type_is_a(gtype, G_TYPE_OBJECT) || g_type_is_a(gtype, G_TYPE_INTERFACE)) { GObject *gobj; - JSObject *obj; gobj = (GObject*) g_value_get_object(gvalue); - obj = gjs_object_from_g_object(context, gobj); - value_p.setObjectOrNull(obj); + if (gobj) { + JSObject* obj = ObjectInstance::wrapper_from_gobject(context, gobj); + if (!obj) + return false; + value_p.setObject(*obj); + } else { + value_p.setNull(); + } } else if (gtype == G_TYPE_STRV) { if (!gjs_array_from_strv (context, value_p, @@ -812,8 +862,6 @@ gjs_value_from_g_value_internal(JSContext *context, return false; } else if (g_type_is_a(gtype, G_TYPE_BOXED) || g_type_is_a(gtype, G_TYPE_VARIANT)) { - GjsBoxedCreationFlags boxed_flags; - GIBaseInfo *info; void *gboxed; JSObject *obj; @@ -821,13 +869,21 @@ gjs_value_from_g_value_internal(JSContext *context, gboxed = g_value_get_boxed(gvalue); else gboxed = g_value_get_variant(gvalue); - boxed_flags = GJS_BOXED_CREATION_NONE; + + if (!gboxed) { + gjs_debug_marshal(GJS_DEBUG_GCLOSURE, + "Converting null boxed pointer to JS::Value"); + value_p.setNull(); + return true; + } /* special case GError */ if (g_type_is_a(gtype, G_TYPE_ERROR)) { - obj = gjs_error_from_gerror(context, (GError*) gboxed, false); - value_p.setObjectOrNull(obj); - + obj = ErrorInstance::object_for_c_ptr(context, + static_cast(gboxed)); + if (!obj) + return false; + value_p.setObject(*obj); return true; } @@ -839,43 +895,38 @@ gjs_value_from_g_value_internal(JSContext *context, /* The only way to differentiate unions and structs is from * their g-i info as both GBoxed */ - info = g_irepository_find_by_gtype(g_irepository_get_default(), - gtype); - if (info == NULL) { + GjsAutoBaseInfo info = g_irepository_find_by_gtype(nullptr, gtype); + if (!info) { gjs_throw(context, "No introspection information found for %s", g_type_name(gtype)); return false; } - if (g_base_info_get_type(info) == GI_INFO_TYPE_STRUCT && - g_struct_info_is_foreign((GIStructInfo*)info)) { - bool ret; + if (info.type() == GI_INFO_TYPE_STRUCT && + g_struct_info_is_foreign(info)) { GIArgument arg; - arg.v_pointer = gboxed; - ret = gjs_struct_foreign_convert_from_g_argument(context, value_p, info, &arg); - g_base_info_unref(info); - return ret; + gjs_arg_set(&arg, gboxed); + return gjs_struct_foreign_convert_from_g_argument(context, value_p, + info, &arg); } - GIInfoType type = g_base_info_get_type(info); + GIInfoType type = info.type(); if (type == GI_INFO_TYPE_BOXED || type == GI_INFO_TYPE_STRUCT) { if (no_copy) - boxed_flags = (GjsBoxedCreationFlags) (boxed_flags | GJS_BOXED_CREATION_NO_COPY); - obj = gjs_boxed_from_c_struct(context, (GIStructInfo *)info, gboxed, boxed_flags); + obj = BoxedInstance::new_for_c_struct(context, info, gboxed, + BoxedInstance::NoCopy()); + else + obj = BoxedInstance::new_for_c_struct(context, info, gboxed); } else if (type == GI_INFO_TYPE_UNION) { - obj = gjs_union_from_c_union(context, (GIUnionInfo *)info, gboxed); + obj = gjs_union_from_c_union(context, info, gboxed); } else { - gjs_throw(context, - "Unexpected introspection type %d for %s", - g_base_info_get_type(info), - g_type_name(gtype)); - g_base_info_unref(info); + gjs_throw(context, "Unexpected introspection type %d for %s", + info.type(), g_type_name(gtype)); return false; } value_p.setObjectOrNull(obj); - g_base_info_unref(info); } else if (g_type_is_a(gtype, G_TYPE_ENUM)) { value_p.set(convert_int_to_enum(gtype, g_value_get_enum(gvalue))); } else if (g_type_is_a(gtype, G_TYPE_PARAM)) { @@ -906,7 +957,7 @@ gjs_value_from_g_value_internal(JSContext *context, " calling gjs_value_from_g_value_internal()", g_type_info_get_array_length(&type_info) == -1)); - arg.v_pointer = g_value_get_pointer(gvalue); + gjs_arg_set(&arg, g_value_get_pointer(gvalue)); res = gjs_value_from_g_argument(context, value_p, &type_info, &arg, true); @@ -942,8 +993,8 @@ gjs_value_from_g_value_internal(JSContext *context, } else if (G_TYPE_IS_INSTANTIATABLE(gtype)) { /* The gtype is none of the above, it should be a custom fundamental type. */ - JSObject *obj; - obj = gjs_fundamental_from_g_value(context, (const GValue*)gvalue, gtype); + JSObject* obj = + FundamentalInstance::object_for_gvalue(context, gvalue, gtype); if (obj == NULL) return false; else diff --git a/gi/value.h b/gi/value.h index b4d6a8b..3150a56 100644 --- a/gi/value.h +++ b/gi/value.h @@ -21,34 +21,37 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_VALUE_H__ -#define __GJS_VALUE_H__ +#ifndef GI_VALUE_H_ +#define GI_VALUE_H_ + +#include -#include #include -#include "cjs/jsapi-util.h" -G_BEGIN_DECLS +#include + +#include "cjs/macros.h" +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_to_g_value (JSContext *context, JS::HandleValue value, GValue *gvalue); +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_to_g_value_no_copy (JSContext *context, JS::HandleValue value, GValue *gvalue); +GJS_JSAPI_RETURN_CONVENTION bool gjs_value_from_g_value(JSContext *context, JS::MutableHandleValue value_p, const GValue *gvalue); -GClosure* gjs_closure_new_marshaled (JSContext *context, - JSObject *callable, - const char *description); -GClosure* gjs_closure_new_for_signal (JSContext *context, - JSObject *callable, - const char *description, - guint signal_id); - -G_END_DECLS +[[nodiscard]] GClosure* gjs_closure_new_marshaled(JSContext* cx, + JSFunction* callable, + const char* description); +[[nodiscard]] GClosure* gjs_closure_new_for_signal(JSContext* cx, + JSFunction* callable, + const char* description, + unsigned signal_id); -#endif /* __GJS_VALUE_H__ */ +#endif // GI_VALUE_H_ diff --git a/gi/wrapperutils.cpp b/gi/wrapperutils.cpp new file mode 100644 index 0000000..5dc7d47 --- /dev/null +++ b/gi/wrapperutils.cpp @@ -0,0 +1,206 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2012 Red Hat, 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. + */ + +#include + +#include +#include + +#include // for JSPROP_PERMANENT +#include +#include // for JS_DefinePropertyById + +#include "gi/function.h" +#include "gi/gtype.h" +#include "gi/wrapperutils.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-util.h" + +/* Default spidermonkey toString is worthless. Replace it + * with something that gives us both the introspection name + * and a memory address. + */ +bool gjs_wrapper_to_string_func(JSContext* context, JSObject* this_obj, + const char* objtype, GIBaseInfo* info, + GType gtype, const void* native_address, + JS::MutableHandleValue rval) { + GString *buf; + bool ret = false; + + buf = g_string_new(""); + g_string_append_c(buf, '['); + g_string_append(buf, objtype); + if (!native_address) + g_string_append(buf, " prototype of"); + else + g_string_append(buf, " instance wrapper"); + + if (info) { + g_string_append_printf(buf, " GIName:%s.%s", + g_base_info_get_namespace(info), + g_base_info_get_name(info)); + } else { + g_string_append(buf, " GType:"); + g_string_append(buf, g_type_name(gtype)); + } + + g_string_append_printf(buf, " jsobj@%p", this_obj); + if (native_address) + g_string_append_printf(buf, " native@%p", native_address); + + g_string_append_c(buf, ']'); + + if (!gjs_string_from_utf8(context, buf->str, rval)) + goto out; + + ret = true; + out: + g_string_free(buf, true); + return ret; +} + +bool gjs_wrapper_throw_nonexistent_field(JSContext* cx, GType gtype, + const char* field_name) { + gjs_throw(cx, "No property %s on %s", field_name, g_type_name(gtype)); + return false; +} + +bool gjs_wrapper_throw_readonly_field(JSContext* cx, GType gtype, + const char* field_name) { + gjs_throw(cx, "Property %s.%s is not writable", g_type_name(gtype), + field_name); + return false; +} + +bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor, + GType gtype) { + JS::RootedObject gtype_obj(cx, gjs_gtype_create_gtype_wrapper(cx, gtype)); + if (!gtype_obj) + return false; + + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + return JS_DefinePropertyById(cx, constructor, atoms.gtype(), gtype_obj, + JSPROP_PERMANENT); +} + +// These policies work around having separate g_foo_info_get_n_methods() and +// g_foo_info_get_method() functions for different GIInfoTypes. It's not +// possible to use GIFooInfo* as the template parameter, because the GIFooInfo +// structs are all typedefs of GIBaseInfo. It's also not possible to use the +// GIInfoType enum value as the template parameter, because GI_INFO_TYPE_BOXED +// could be either a GIStructInfo or GIUnionInfo. +template +static inline GIStructInfo* no_type_struct(InfoT*) { + return nullptr; +} + +template > +struct InfoMethodsPolicy { + static constexpr decltype(NMethods) n_methods = NMethods; + static constexpr decltype(Method) method = Method; + static constexpr decltype(TypeStruct) type_struct = TypeStruct; +}; + +template <> +struct InfoMethodsPolicy + : InfoMethodsPolicy {}; +template <> +struct InfoMethodsPolicy + : InfoMethodsPolicy< + InfoType::Interface, GIInterfaceInfo, &g_interface_info_get_n_methods, + &g_interface_info_get_method, &g_interface_info_get_iface_struct> {}; +template <> +struct InfoMethodsPolicy + : InfoMethodsPolicy {}; +template <> +struct InfoMethodsPolicy + : InfoMethodsPolicy {}; +template <> +struct InfoMethodsPolicy + : InfoMethodsPolicy { +}; + +template +bool gjs_define_static_methods(JSContext* cx, JS::HandleObject constructor, + GType gtype, GIBaseInfo* info) { + int n_methods = InfoMethodsPolicy::n_methods(info); + + for (int ix = 0; ix < n_methods; ix++) { + GjsAutoFunctionInfo meth_info = + InfoMethodsPolicy::method(info, ix); + GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info); + + // Anything that isn't a method we put on the constructor. This + // includes introspection methods, as well as static + // methods. We may want to change this to use + // GI_FUNCTION_IS_CONSTRUCTOR and GI_FUNCTION_IS_STATIC or the like + // in the future. + if (!(flags & GI_FUNCTION_IS_METHOD)) { + if (!gjs_define_function(cx, constructor, gtype, meth_info)) + return false; + } + } + + // Also define class/interface methods if there is a gtype struct + + GjsAutoStructInfo type_struct = InfoMethodsPolicy::type_struct(info); + // Not an error for it to be null even in the case of Object and Interface; + // documentation says g_object_info_get_class_struct() and + // g_interface_info_get_iface_struct() can validly return a null pointer. + if (!type_struct) + return true; + + n_methods = g_struct_info_get_n_methods(type_struct); + + for (int ix = 0; ix < n_methods; ix++) { + GjsAutoFunctionInfo meth_info = + g_struct_info_get_method(type_struct, ix); + + if (!gjs_define_function(cx, constructor, gtype, meth_info)) + return false; + } + + return true; +} + +// All possible instantiations are needed +template bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); +template bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); +template bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); +template bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); +template bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); diff --git a/gi/wrapperutils.h b/gi/wrapperutils.h new file mode 100644 index 0000000..f17161a --- /dev/null +++ b/gi/wrapperutils.h @@ -0,0 +1,1134 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * Copyright (c) 2008 litl, LLC + * Copyright (c) 2018 Philip Chimento + * + * 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. + */ + +#ifndef GI_WRAPPERUTILS_H_ +#define GI_WRAPPERUTILS_H_ + +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include // for UniqueChars +#include // for JS_GetPrivate, JS_SetPrivate, JS_Ge... +#include // for JSProto_TypeError + +#include "gi/arg-inl.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" +#include "cjs/jsapi-class.h" // IWYU pragma: keep +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "util/log.h" + +struct JSFunctionSpec; +struct JSPropertySpec; +class JSTracer; + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_wrapper_to_string_func(JSContext* cx, JSObject* this_obj, + const char* objtype, GIBaseInfo* info, + GType gtype, const void* native_address, + JS::MutableHandleValue ret); + +bool gjs_wrapper_throw_nonexistent_field(JSContext* cx, GType gtype, + const char* field_name); + +bool gjs_wrapper_throw_readonly_field(JSContext* cx, GType gtype, + const char* field_name); + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor, + GType gtype); + +namespace InfoType { +enum Tag { Enum, Interface, Object, Struct, Union }; +} + +namespace MemoryUse { +constexpr JS::MemoryUse GObjectInstanceStruct = JS::MemoryUse::Embedding1; +} + +struct GjsTypecheckNoThrow {}; + +/* + * gjs_define_static_methods: + * + * Defines all static methods from @info on @constructor. Also includes class + * methods for GIObjectInfo, and interface methods for GIInterfaceInfo. + */ +template +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_static_methods( + JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info); + +/* + * GJS_GET_WRAPPER_PRIV: + * @cx: JSContext pointer passed into JSNative function + * @argc: Number of arguments passed into JSNative function + * @vp: Argument value array passed into JSNative function + * @args: Name for JS::CallArgs variable defined by this code snippet + * @thisobj: Name for JS::RootedObject variable referring to function's this + * @type: Type of private data + * @priv: Name for private data variable defined by this code snippet + * + * A convenience macro for getting the private data from GJS classes using + * GIWrapper. + * Throws an error and returns false if the 'this' object is not the right type. + * Use in any JSNative function. + */ +#define GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, thisobj, type, priv) \ + GJS_GET_THIS(cx, argc, vp, args, thisobj); \ + type* priv = type::for_js_typecheck(cx, thisobj, args); \ + if (!priv) \ + return false; + +/* + * GIWrapperBase: + * + * In most different kinds of C pointer that we expose to JS through GObject + * Introspection (boxed, fundamental, gerror, interface, object, union), we want + * to have different private structures for the prototype JS object and the JS + * objects representing instances. Both should inherit from a base structure for + * their common functionality. + * + * This is mainly for memory reasons. We need to keep track of the GIBaseInfo* + * and GType for each dynamically created class, but we don't need to duplicate + * that information (16 bytes on x64 systems) for every instance. In some cases + * there can also be other information that's only used on the prototype. + * + * So, to conserve memory, we split the private structures in FooInstance and + * FooPrototype, which both inherit from FooBase. All the repeated code in these + * structures lives in GIWrapperBase, GIWrapperPrototype, and GIWrapperInstance. + * + * The m_proto member needs a bit of explanation, as this is used to implement + * an unusual form of polymorphism. Sadly, we cannot have virtual methods in + * FooBase, because SpiderMonkey can be compiled with or without RTTI, so we + * cannot count on being able to cast FooBase to FooInstance or FooPrototype + * with dynamic_cast<>, and the vtable would take up just as much space anyway. + * Instead, we use the CRTP technique, and distinguish between FooInstance and + * FooPrototype using the m_proto member, which will be null for FooPrototype. + * Instead of casting, we have the to_prototype() and to_instance() methods + * which will give you a pointer if the FooBase is of the correct type (and + * assert if not.) + * + * The CRTP requires inheriting classes to declare themselves friends of the + * parent class, so that the parent class can call their private methods. + * + * For more information about the CRTP, the Wikipedia article is informative: + * https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern + */ +template +class GIWrapperBase { + protected: + // nullptr if this Base is a Prototype; points to the corresponding + // Prototype if this Base is an Instance. + Prototype* m_proto; + + explicit GIWrapperBase(Prototype* proto = nullptr) : m_proto(proto) {} + ~GIWrapperBase(void) {} + + // These three can be overridden in subclasses. See define_jsclass(). + static constexpr JSPropertySpec* proto_properties = nullptr; + static constexpr JSPropertySpec* static_properties = nullptr; + static constexpr JSFunctionSpec* proto_methods = nullptr; + static constexpr JSFunctionSpec* static_methods = nullptr; + + // Methods to get an existing Base + + public: + /* + * GIWrapperBase::for_js: + * + * Gets the Base belonging to a particular JS object wrapper. Checks that + * the wrapper object has the right JSClass (Base::klass) and returns null + * if not. */ + [[nodiscard]] static Base* for_js(JSContext* cx, JS::HandleObject wrapper) { + return static_cast( + JS_GetInstancePrivate(cx, wrapper, &Base::klass, nullptr)); + } + + /* + * GIWrapperBase::check_jsclass: + * + * Checks if the given wrapper object has the right JSClass (Base::klass). + */ + [[nodiscard]] static bool check_jsclass(JSContext* cx, + JS::HandleObject wrapper) { + return !!for_js(cx, wrapper); + } + + /* + * GIWrapperBase::for_js_typecheck: + * + * Like for_js(), only throws a JS exception if the wrapper object has the + * wrong class. Use in JSNative functions, where you have access to a + * JS::CallArgs. The exception message will mention args.callee. + * + * The second overload can be used when you don't have access to an + * instance of JS::CallArgs. The exception message will be generic. + */ + GJS_JSAPI_RETURN_CONVENTION + static Base* for_js_typecheck( + JSContext* cx, JS::HandleObject wrapper, + JS::CallArgs& args) { // NOLINT(runtime/references) + return static_cast( + JS_GetInstancePrivate(cx, wrapper, &Base::klass, &args)); + } + GJS_JSAPI_RETURN_CONVENTION + static Base* for_js_typecheck(JSContext* cx, JS::HandleObject wrapper) { + if (!gjs_typecheck_instance(cx, wrapper, &Base::klass, true)) + return nullptr; + return for_js(cx, wrapper); + } + + /* + * GIWrapperBase::for_js_nocheck: + * + * Use when you don't have a JSContext* available. This method is infallible + * and cannot trigger a GC, so it's safe to use from finalize() and trace(). + * (It can return null if no private data has been set yet on the wrapper.) + */ + [[nodiscard]] static Base* for_js_nocheck(JSObject* wrapper) { + return static_cast(JS_GetPrivate(wrapper)); + } + + // Methods implementing our CRTP polymorphism scheme follow below. We don't + // use standard C++ polymorphism because that would occupy another 8 bytes + // for a vtable. + + /* + * GIWrapperBase::is_prototype: + * + * Returns whether this Base is actually a Prototype (true) or an Instance + * (false). + */ + [[nodiscard]] bool is_prototype() const { return !m_proto; } + + /* + * GIWrapperBase::to_prototype: + * GIWrapperBase::to_instance: + * + * These methods assert that this Base is of the correct subclass. If you + * don't want to assert, then either check beforehand with is_prototype(), + * or use get_prototype(). + */ + [[nodiscard]] Prototype* to_prototype() { + g_assert(is_prototype()); + return reinterpret_cast(this); + } + [[nodiscard]] const Prototype* to_prototype() const { + g_assert(is_prototype()); + return reinterpret_cast(this); + } + [[nodiscard]] Instance* to_instance() { + g_assert(!is_prototype()); + return reinterpret_cast(this); + } + [[nodiscard]] const Instance* to_instance() const { + g_assert(!is_prototype()); + return reinterpret_cast(this); + } + + /* + * GIWrapperBase::get_prototype: + * + * get_prototype() doesn't assert. If you call it on a Prototype, it returns + * you the same object cast to the correct type; if you call it on an + * Instance, it returns you the Prototype belonging to the corresponding JS + * prototype. + */ + [[nodiscard]] Prototype* get_prototype() { + return is_prototype() ? to_prototype() : m_proto; + } + [[nodiscard]] const Prototype* get_prototype() const { + return is_prototype() ? to_prototype() : m_proto; + } + + // Accessors for Prototype members follow below. Both Instance and Prototype + // should be able to access the GIFooInfo and the GType, but for space + // reasons we store them only on Prototype. + + [[nodiscard]] GIBaseInfo* info() const { return get_prototype()->info(); } + [[nodiscard]] GType gtype() const { return get_prototype()->gtype(); } + + // The next three methods are operations derived from the GIFooInfo. + + [[nodiscard]] const char* type_name() const { return g_type_name(gtype()); } + [[nodiscard]] const char* ns() const { + return info() ? g_base_info_get_namespace(info()) : ""; + } + [[nodiscard]] const char* name() const { + return info() ? g_base_info_get_name(info()) : type_name(); + } + + private: + // Accessor for Instance member. Used only in debug methods and toString(). + [[nodiscard]] const void* ptr_addr() const { + return is_prototype() ? nullptr : to_instance()->ptr(); + } + + // Debug methods + + protected: + void debug_lifecycle(const char* message GJS_USED_VERBOSE_LIFECYCLE) const { + gjs_debug_lifecycle( + Base::debug_topic, "[%p: %s pointer %p - %s.%s (%s)] %s", this, + Base::debug_tag, ptr_addr(), ns(), name(), type_name(), message); + } + void debug_lifecycle(const void* obj GJS_USED_VERBOSE_LIFECYCLE, + const char* message GJS_USED_VERBOSE_LIFECYCLE) const { + gjs_debug_lifecycle( + Base::debug_topic, + "[%p: %s pointer %p - JS wrapper %p - %s.%s (%s)] %s", this, + Base::debug_tag, ptr_addr(), obj, ns(), name(), type_name(), + message); + } + void debug_jsprop(const char* message GJS_USED_VERBOSE_PROPS, + const char* id GJS_USED_VERBOSE_PROPS, + const void* obj GJS_USED_VERBOSE_PROPS) const { + gjs_debug_jsprop( + Base::debug_topic, + "[%p: %s pointer %p - JS wrapper %p - %s.%s (%s)] %s '%s'", this, + Base::debug_tag, ptr_addr(), obj, ns(), name(), type_name(), + message, id); + } + void debug_jsprop(const char* message, jsid id, const void* obj) const { + debug_jsprop(message, gjs_debug_id(id).c_str(), obj); + } + void debug_jsprop(const char* message, JSString* id, + const void* obj) const { + debug_jsprop(message, gjs_debug_string(id).c_str(), obj); + } + static void debug_jsprop_static(const char* message GJS_USED_VERBOSE_PROPS, + jsid id GJS_USED_VERBOSE_PROPS, + const void* obj GJS_USED_VERBOSE_PROPS) { + gjs_debug_jsprop(Base::debug_topic, + "[%s JS wrapper %p] %s '%s', no instance associated", + Base::debug_tag, obj, message, + gjs_debug_id(id).c_str()); + } + + // JS class operations, used only in the JSClassOps struct + + /* + * GIWrapperBase::new_enumerate: + * + * Include this in the Base::klass vtable if the class should support + * lazy enumeration (listing all of the lazy properties that can be defined + * in resolve().) If it is included, then there must be a corresponding + * Prototype::new_enumerate_impl() method. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool new_enumerate(JSContext* cx, JS::HandleObject obj, + JS::MutableHandleIdVector properties, + bool only_enumerable) { + Base* priv = Base::for_js(cx, obj); + + priv->debug_jsprop("Enumerate hook", "(all)", obj); + + if (!priv->is_prototype()) { + // Instances don't have any methods or properties. + // Spidermonkey will call new_enumerate on the prototype next. + return true; + } + + return priv->to_prototype()->new_enumerate_impl(cx, obj, properties, + only_enumerable); + } + + private: + /* + * GIWrapperBase::id_is_never_lazy: + * + * Returns true if @id should never be treated as a lazy property. The + * JSResolveOp for an instance is called for every property not defined, + * even if it's one of the functions or properties we're adding to the + * prototype manually, such as toString(). + * + * Override this and chain up if you have Base::resolve in your JSClassOps + * vtable, and have overridden Base::proto_properties or + * Base::proto_methods. You should add any identifiers in the override that + * you have added to the prototype object. + */ + [[nodiscard]] static bool id_is_never_lazy(jsid id, const GjsAtoms& atoms) { + // toString() is always defined somewhere on the prototype chain, so it + // is never a lazy property. + return id == atoms.to_string(); + } + + protected: + /* + * GIWrapperBase::resolve: + * + * Include this in the Base::klass vtable if the class should support lazy + * properties. If it is included, then there must be a corresponding + * Prototype::resolve_impl() method. + * + * The *resolved out parameter, on success, should be false to indicate that + * id was not resolved; and true if id was resolved. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* resolved) { + Base* priv = Base::for_js(cx, obj); + + if (!priv) { + // This catches a case in Object where the private struct isn't set + // until the initializer is called, so just defer to prototype + // chains in this case. + // + // This isn't too bad: either you get undefined if the field doesn't + // exist on any of the prototype chains, or whatever code will run + // afterwards will fail because of the "!priv" check there. + debug_jsprop_static("Resolve hook", id, obj); + *resolved = false; + return true; + } + + priv->debug_jsprop("Resolve hook", id, obj); + + if (!priv->is_prototype()) { + // 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 property accessors, not this resolve hook. + *resolved = false; + return true; + } + + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (id_is_never_lazy(id, atoms)) { + *resolved = false; + return true; + } + + // A GObject-introspection lazy property will always be a string, so + // also bail out if trying to resolve an integer or symbol property. + JS::UniqueChars prop_name; + if (!gjs_get_string_id(cx, id, &prop_name)) + return false; + if (!prop_name) { + *resolved = false; + return true; // not resolved, but no error + } + + return priv->to_prototype()->resolve_impl(cx, obj, id, prop_name.get(), + resolved); + } + + /* + * GIWrapperBase::finalize: + * + * This should always be included in the Base::klass vtable. The destructors + * of Prototype and Instance will be called in the finalize hook. It is not + * necessary to include a finalize_impl() function in Prototype or Instance. + * Any needed finalization should be done in ~Prototype() and ~Instance(). + */ + static void finalize(JSFreeOp* fop, JSObject* obj) { + Base* priv = Base::for_js_nocheck(obj); + if (!priv) + return; // construction didn't finish + + // Call only GIWrapperBase's original method here, not any overrides; + // e.g., we don't want to deal with a read barrier in ObjectInstance. + static_cast(priv)->debug_lifecycle(obj, "Finalize"); + + if (priv->is_prototype()) + priv->to_prototype()->finalize_impl(fop, obj); + else + priv->to_instance()->finalize_impl(fop, obj); + + // Remove the pointer from the JSObject + JS_SetPrivate(obj, nullptr); + } + + /* + * GIWrapperBase::trace: + * + * This should be included in the Base::klass vtable if any of the Base, + * Prototype or Instance structures contain any members that the JS garbage + * collector must trace. Each struct containing such members must override + * GIWrapperBase::trace_impl(), GIWrapperPrototype::trace_impl(), and/or + * GIWrapperInstance::trace_impl() in order to perform the trace. + */ + static void trace(JSTracer* trc, JSObject* obj) { + Base* priv = Base::for_js_nocheck(obj); + if (!priv) + return; + + // Don't log in trace(). That would overrun even the most verbose logs. + + if (priv->is_prototype()) + priv->to_prototype()->trace_impl(trc); + else + priv->to_instance()->trace_impl(trc); + + priv->trace_impl(trc); + } + + /* + * GIWrapperBase::trace_impl: + * Override if necessary. See trace(). + */ + void trace_impl(JSTracer*) {} + + // JSNative methods + + /* + * GIWrapperBase::constructor: + * + * C++ implementation of the JS constructor passed to JS_InitClass(). Only + * called on instances, never on prototypes. This method contains the + * functionality common to all GI wrapper classes. There must be a + * corresponding Instance::constructor_impl method containing the rest of + * the functionality. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + + if (!args.isConstructing()) { + gjs_throw_constructor_error(cx); + return false; + } + JS::RootedObject obj( + cx, JS_NewObjectForConstructor(cx, &Base::klass, args)); + if (!obj) + return false; + + JS::RootedObject proto(cx); + if (!JS_GetPrototype(cx, obj, &proto)) + return false; + if (JS_GetClass(proto) != &Base::klass) { + gjs_throw(cx, "Tried to construct an object without a GType"); + return false; + } + + args.rval().setUndefined(); + + Instance* priv = Instance::new_for_js_object(cx, obj); + + if (!priv->constructor_impl(cx, obj, args)) + return false; + + static_cast(priv)->debug_lifecycle(obj, + "JSObject created"); + gjs_debug_lifecycle(Base::debug_topic, "m_proto is %p", + priv->get_prototype()); + + // We may need to return a value different from obj (for example because + // we delegate to another constructor) + if (args.rval().isUndefined()) + args.rval().setObject(*obj); + return true; + } + + /* + * GIWrapperBase::to_string: + * + * JSNative method connected to the toString() method in JS. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp) { + GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, Base, priv); + return gjs_wrapper_to_string_func( + cx, obj, static_cast(priv)->to_string_kind(), + priv->info(), priv->gtype(), priv->ptr_addr(), args.rval()); + } + + // Helper methods + + public: + /* + * GIWrapperBase::check_is_instance: + * @for_what: string used in the exception message if an exception is thrown + * + * Used in JSNative methods to ensure the passed-in JS object is an instance + * and not the prototype. Throws a JS exception if the prototype is passed + * in. + */ + GJS_JSAPI_RETURN_CONVENTION + bool check_is_instance(JSContext* cx, const char* for_what) const { + if (!is_prototype()) + return true; + gjs_throw(cx, "Can't %s on %s.%s.prototype; only on instances", + for_what, ns(), name()); + return false; + } + + /* + * GIWrapperBase::to_c_ptr: + * + * Returns the underlying C pointer of the wrapped object, or throws a JS + * exception if that is not possible (for example, the passed-in JS object + * is the prototype.) + * + * Includes a JS typecheck (but without any extra typecheck of the GType or + * introspection info that you would get from GIWrapperBase::typecheck(), so + * if you want that you still have to do the typecheck before calling this + * method.) + */ + template + GJS_JSAPI_RETURN_CONVENTION static T* to_c_ptr(JSContext* cx, + JS::HandleObject obj) { + Base* priv = Base::for_js_typecheck(cx, obj); + if (!priv || !priv->check_is_instance(cx, "get a C pointer")) + return nullptr; + + return static_cast(priv->to_instance()->ptr()); + } + + /* + * GIWrapperBase::transfer_to_gi_argument: + * @arg: #GIArgument to fill with the value from @obj + * @transfer_direction: Either %GI_DIRECTION_IN or %GI_DIRECTION_OUT + * @transfer_ownership: #GITransfer value specifying whether @arg should + * copy or acquire a reference to the value or not + * @expected_gtype: #GType to perform a typecheck with + * @expected_info: Introspection info to perform a typecheck with + * + * Prepares @arg for passing the value from @obj into C code. It will get a + * C pointer from @obj and assign it to @arg's pointer field, taking a + * reference with GIWrapperInstance::copy_ptr() if @transfer_direction and + * @transfer_ownership indicate that it should. + * + * Includes a typecheck using GIWrapperBase::typecheck(), to which + * @expected_gtype and @expected_info are passed. + * + * If returning false, then @arg's pointer field is null. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj, + GIArgument* arg, + GIDirection transfer_direction, + GITransfer transfer_ownership, + GType expected_gtype, + GIBaseInfo* expected_info = nullptr) { + g_assert(transfer_direction != GI_DIRECTION_INOUT && + "transfer_to_gi_argument() must choose between in or out"); + + if (!Base::typecheck(cx, obj, expected_info, expected_gtype)) { + gjs_arg_unset(arg); + return false; + } + + gjs_arg_set(arg, Base::to_c_ptr(cx, obj)); + if (!gjs_arg_get(arg)) + return false; + + if ((transfer_direction == GI_DIRECTION_IN && + transfer_ownership != GI_TRANSFER_NOTHING) || + (transfer_direction == GI_DIRECTION_OUT && + transfer_ownership == GI_TRANSFER_EVERYTHING)) { + gjs_arg_set(arg, Instance::copy_ptr(cx, expected_gtype, + gjs_arg_get(arg))); + if (!gjs_arg_get(arg)) + return false; + } + + return true; + } + + // Public typecheck API + + /* + * GIWrapperBase::typecheck: + * @expected_info: (nullable): GI info to check + * @expected_type: (nullable): GType to check + * + * Checks not only that the JS object is of the correct JSClass (like + * for_js_typecheck() does); but also that the object is an instance, not + * the protptype; and that the instance's wrapped pointer is of the correct + * GType or GI info. + * + * The overload with a GjsTypecheckNoThrow parameter will not throw a JS + * exception if the prototype is passed in or the typecheck fails. + */ + GJS_JSAPI_RETURN_CONVENTION + static bool typecheck(JSContext* cx, JS::HandleObject object, + GIBaseInfo* expected_info, GType expected_gtype) { + Base* priv = Base::for_js_typecheck(cx, object); + if (!priv || !priv->check_is_instance(cx, "convert to pointer")) + return false; + + if (priv->to_instance()->typecheck_impl(cx, expected_info, + expected_gtype)) + return true; + + if (expected_info) { + gjs_throw_custom( + cx, JSProto_TypeError, nullptr, + "Object is of type %s.%s - cannot convert to %s.%s", priv->ns(), + priv->name(), g_base_info_get_namespace(expected_info), + g_base_info_get_name(expected_info)); + } else { + gjs_throw_custom(cx, JSProto_TypeError, nullptr, + "Object is of type %s.%s - cannot convert to %s", + priv->ns(), priv->name(), + g_type_name(expected_gtype)); + } + + return false; + } + [[nodiscard]] static bool typecheck(JSContext* cx, JS::HandleObject object, + GIBaseInfo* expected_info, + GType expected_gtype, + GjsTypecheckNoThrow) { + Base* priv = Base::for_js(cx, object); + if (!priv || priv->is_prototype()) + return false; + + return priv->to_instance()->typecheck_impl(cx, expected_info, + expected_gtype); + } + + // Deleting these constructors and assignment operators will also delete + // them from derived classes. + GIWrapperBase(const GIWrapperBase& other) = delete; + GIWrapperBase(GIWrapperBase&& other) = delete; + GIWrapperBase& operator=(const GIWrapperBase& other) = delete; + GIWrapperBase& operator=(GIWrapperBase&& other) = delete; +}; + +/* + * GIWrapperPrototype: + * + * The specialization of GIWrapperBase which becomes the private data of JS + * prototype objects. For example, it is the parent class of BoxedPrototype. + * + * Classes inheriting from GIWrapperPrototype must declare "friend class + * GIWrapperBase" as well as the normal CRTP requirement of "friend class + * GIWrapperPrototype", because of the unusual polymorphism scheme, in order for + * Base to call methods such as trace_impl(). + */ +template +class GIWrapperPrototype : public Base { + protected: + // m_info may be null in the case of JS-defined types, or internal types + // not exposed through introspection, such as GLocalFile. Not all subclasses + // of GIWrapperPrototype support this. Object and Interface support it in + // any case. + Info* m_info; + GType m_gtype; + + explicit GIWrapperPrototype(Info* info, GType gtype) + : Base(), + m_info(info ? g_base_info_ref(info) : nullptr), + m_gtype(gtype) { + Base::debug_lifecycle("Prototype constructor"); + } + + ~GIWrapperPrototype(void) { g_clear_pointer(&m_info, g_base_info_unref); } + + /* + * GIWrapperPrototype::init: + * + * Performs any initialization that cannot be done in the constructor of + * GIWrapperPrototype, either because it can fail, or because it can cause a + * garbage collection. + * + * This default implementation does nothing. Override in a subclass if + * necessary. + */ + GJS_JSAPI_RETURN_CONVENTION + bool init(JSContext*) { return true; } + + // The following four methods are private because they are used only in + // create_class(). + + private: + /* + * GIWrapperPrototype::parent_proto: + * + * Returns in @proto the parent class's prototype object, or nullptr if + * there is none. + * + * This default implementation is for GObject introspection types that can't + * inherit in JS, like Boxed and Union. Override this if the type can + * inherit in JS. + */ + GJS_JSAPI_RETURN_CONVENTION + bool get_parent_proto(JSContext*, JS::MutableHandleObject proto) const { + proto.set(nullptr); + return true; + } + + /* + * GIWrapperPrototype::constructor_nargs: + * + * Override this if the type's constructor takes other than 1 argument. + */ + [[nodiscard]] unsigned constructor_nargs() const { return 1; } + + /* + * GIWrapperPrototype::define_jsclass: + * @in_object: JSObject on which to define the class constructor as a + * property + * @parent_proto: (nullable): prototype of the prototype + * @constructor: return location for the constructor function object + * @prototype: return location for the prototype object + * + * Defines a JS class with constructor and prototype, and optionally defines + * properties and methods on the prototype object, and methods on the + * constructor object. + * + * By default no properties or methods are defined, but derived classes can + * override the GIWrapperBase::proto_properties, + * GIWrapperBase::proto_methods, and GIWrapperBase::static_methods members. + * Static properties would also be possible but are not used anywhere in GJS + * so are not implemented yet. + * + * Note: no prototype methods are defined if @parent_proto is null. + * + * Here is a refresher comment on the difference between __proto__ and + * prototype that has been in the GJS codebase since forever: + * + * https://web.archive.org/web/20100716231157/http://egachine.berlios.de/embedding-sm-best-practice/apa.html + * https://www.sitepoint.com/javascript-inheritance/ + * http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/ + * + * What we want is: repoobj.Gtk.Window is constructor for a GtkWindow + * wrapper JSObject (gjs_define_object_class() is supposed to define Window + * in Gtk.) + * + * Window.prototype contains the methods on Window, e.g. set_default_size() + * mywindow.__proto__ is Window.prototype + * mywindow.__proto__.__proto__ is Bin.prototype + * mywindow.__proto__.__proto__.__proto__ is Container.prototype + * + * Because Window.prototype is an instance of Window in a sense, + * Window.prototype.__proto__ is Window.prototype, just as + * mywindow.__proto__ is Window.prototype + * + * If we do "mywindow = new Window()" then we should get: + * mywindow.__proto__ == Window.prototype + * which means "mywindow instanceof Window" is true. + * + * Remember "Window.prototype" is "the __proto__ of stuff constructed with + * new Window()" + * + * __proto__ is used to search for properties if you do "this.foo", while + * .prototype is only relevant for constructors and is used to set __proto__ + * on new'd objects. So .prototype only makes sense on constructors. + * + * JS_SetPrototype() and JS_GetPrototype() are for __proto__. To set/get + * .prototype, just use the normal property accessors, or JS_InitClass() + * sets it up automatically. + */ + GJS_JSAPI_RETURN_CONVENTION + bool define_jsclass(JSContext* cx, JS::HandleObject in_object, + JS::HandleObject parent_proto, + JS::MutableHandleObject constructor, + JS::MutableHandleObject prototype) { + // The GI namespace is only used to set the JSClass->name field (exposed + // by Object.prototype.toString, for example). We can safely set + // "unknown" if this is a custom or internal JS class with no GI + // namespace, as in that case the name is already globally unique (it's + // a GType name). + const char* gi_namespace = Base::info() ? Base::ns() : "unknown"; + + unsigned nargs = static_cast(this)->constructor_nargs(); + + if (!gjs_init_class_dynamic( + cx, in_object, parent_proto, gi_namespace, Base::name(), + &Base::klass, &Base::constructor, nargs, Base::proto_properties, + parent_proto ? nullptr : Base::proto_methods, + Base::static_properties, Base::static_methods, prototype, + constructor)) + return false; + + gjs_debug(Base::debug_topic, + "Defined class for %s (%s), prototype %p, " + "JSClass %p, in object %p", + Base::name(), Base::type_name(), prototype.get(), + JS_GetClass(prototype), in_object.get()); + + return true; + } + + /* + * GIWrapperPrototype::define_static_methods: + * + * Defines all introspectable static methods on @constructor, including + * class methods for objects, and interface methods for interfaces. See + * gjs_define_static_methods() for details. + * + * It requires Prototype to have an info_type_tag member to indicate + * the correct template specialization of gjs_define_static_methods(). + */ + GJS_JSAPI_RETURN_CONVENTION + bool define_static_methods(JSContext* cx, JS::HandleObject constructor) { + if (!info()) + return true; // no introspection means no methods to define + return gjs_define_static_methods( + cx, constructor, m_gtype, m_info); + } + + public: + /** + * GIWrapperPrototype::create_class: + * @in_object: JSObject on which to define the class constructor as a + * property + * @info: (nullable): Introspection info for the class, or null if the class + * has been defined in JS + * @gtype: GType for the class + * @constructor: return location for the constructor function object + * @prototype: return location for the prototype object + * + * Creates a JS class that wraps a GI pointer, by defining its constructor + * function and prototype object. The prototype object is given an instance + * of GIWrapperPrototype as its private data, which is also returned. + * Basically treat this method as the public constructor. + * + * Also defines all the requested methods and properties on the prototype + * and constructor objects (see define_jsclass()), as well as a `$gtype` + * property and a toString() method. + * + * This method can be overridden and chained up to if the derived class + * needs to define more properties on the constructor or prototype objects, + * e.g. eager GI properties. + */ + GJS_JSAPI_RETURN_CONVENTION + static Prototype* create_class(JSContext* cx, JS::HandleObject in_object, + Info* info, GType gtype, + JS::MutableHandleObject constructor, + JS::MutableHandleObject prototype) { + g_assert(in_object); + g_assert(gtype != G_TYPE_INVALID); + + // We have to keep the Prototype in an arcbox because some of its + // members are needed in some Instance destructors, e.g. m_gtype to + // figure out how to free the Instance's m_ptr, and m_info to figure out + // how many bytes to free if it is allocated directly. Storing a + // refcount on the prototype is cheaper than storing pointers to m_info + // and m_gtype on each instance. + auto* priv = g_atomic_rc_box_new0(Prototype); + new (priv) Prototype(info, gtype); + if (!priv->init(cx)) + return nullptr; + + JS::RootedObject parent_proto(cx); + if (!priv->get_parent_proto(cx, &parent_proto) || + !priv->define_jsclass(cx, in_object, parent_proto, constructor, + prototype)) + return nullptr; + + // Init the private variable of @private before we do anything else. If + // a garbage collection or error happens subsequently, then this object + // might be traced and we would end up dereferencing a null pointer. + JS_SetPrivate(prototype, priv); + + if (!gjs_wrapper_define_gtype_prop(cx, constructor, gtype)) + return nullptr; + + // Every class has a toString() with C++ implementation, so define that + // without requiring it to be listed in Base::proto_methods + if (!parent_proto) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); + if (!JS_DefineFunctionById(cx, prototype, atoms.to_string(), + &Base::to_string, 0, + GJS_MODULE_PROP_FLAGS)) + return nullptr; + } + + if (!priv->define_static_methods(cx, constructor)) + return nullptr; + + return priv; + } + + // Methods to get an existing Prototype + + /* + * GIWrapperPrototype::for_js: + * + * Like Base::for_js(), but asserts that the returned private struct is a + * Prototype and not an Instance. + */ + [[nodiscard]] static Prototype* for_js(JSContext* cx, + JS::HandleObject wrapper) { + return Base::for_js(cx, wrapper)->to_prototype(); + } + + /* + * GIWrapperPrototype::for_js_prototype: + * + * Gets the Prototype private data from to @wrapper.prototype. Cannot return + * null, and asserts so. + */ + [[nodiscard]] static Prototype* for_js_prototype(JSContext* cx, + JS::HandleObject wrapper) { + JS::RootedObject proto(cx); + JS_GetPrototype(cx, wrapper, &proto); + Base* retval = Base::for_js(cx, proto); + g_assert(retval); + return retval->to_prototype(); + } + + // Accessors + + [[nodiscard]] Info* info() const { return m_info; } + [[nodiscard]] GType gtype() const { return m_gtype; } + + // Helper methods + + private: + static void destroy_notify(void* ptr) { + static_cast(ptr)->~Prototype(); + } + + public: + Prototype* acquire(void) { + g_atomic_rc_box_acquire(this); + return static_cast(this); + } + + void release(void) { g_atomic_rc_box_release_full(this, &destroy_notify); } + + // JSClass operations + + protected: + void finalize_impl(JSFreeOp*, JSObject*) { release(); } + + // Override if necessary + void trace_impl(JSTracer*) {} +}; + +/* + * GIWrapperInstance: + * + * The specialization of GIWrapperBase which becomes the private data of JS + * instance objects. For example, it is the parent class of BoxedInstance. + * + * Classes inheriting from GIWrapperInstance must declare "friend class + * GIWrapperBase" as well as the normal CRTP requirement of "friend class + * GIWrapperInstance", because of the unusual polymorphism scheme, in order for + * Base to call methods such as trace_impl(). + */ +template +class GIWrapperInstance : public Base { + protected: + Wrapped* m_ptr; + + explicit GIWrapperInstance(JSContext* cx, JS::HandleObject obj) + : Base(Prototype::for_js_prototype(cx, obj)) { + Base::m_proto->acquire(); + Base::GIWrapperBase::debug_lifecycle(obj, "Instance constructor"); + } + ~GIWrapperInstance(void) { Base::m_proto->release(); } + + public: + /* + * GIWrapperInstance::new_for_js_object: + * + * Creates a GIWrapperInstance and associates it with @obj as its private + * data. This is called by the JS constructor. Uses the slice allocator. + */ + [[nodiscard]] static Instance* new_for_js_object(JSContext* cx, + JS::HandleObject obj) { + g_assert(!JS_GetPrivate(obj)); + auto* priv = g_slice_new0(Instance); + new (priv) Instance(cx, obj); + + // Init the private variable before we do anything else. If a garbage + // collection happens when calling the constructor, then this object + // might be traced and we would end up dereferencing a null pointer. + JS_SetPrivate(obj, priv); + + return priv; + } + + // Method to get an existing Instance + + /* + * GIWrapperInstance::for_js: + * + * Like Base::for_js(), but asserts that the returned private struct is an + * Instance and not a Prototype. + */ + [[nodiscard]] static Instance* for_js(JSContext* cx, + JS::HandleObject wrapper) { + return Base::for_js(cx, wrapper)->to_instance(); + } + + // Accessors + + [[nodiscard]] Wrapped* ptr() const { return m_ptr; } + /* + * GIWrapperInstance::raw_ptr: + * + * Like ptr(), but returns a byte pointer for use in byte arithmetic. + */ + [[nodiscard]] uint8_t* raw_ptr() const { + return reinterpret_cast(m_ptr); + } + + // JSClass operations + + protected: + void finalize_impl(JSFreeOp*, JSObject*) { + static_cast(this)->~Instance(); + g_slice_free(Instance, this); + } + + // Override if necessary + void trace_impl(JSTracer*) {} + + // Helper methods + + /* + * GIWrapperInstance::typecheck_impl: + * + * See GIWrapperBase::typecheck(). Checks that the instance's wrapped + * pointer is of the correct GType or GI info. Does not throw a JS + * exception. + * + * It's possible to override typecheck_impl() if you need an extra step in + * the check. + */ + [[nodiscard]] bool typecheck_impl(JSContext*, GIBaseInfo* expected_info, + GType expected_gtype) const { + if (expected_gtype != G_TYPE_NONE) + return g_type_is_a(Base::gtype(), expected_gtype); + else if (expected_info) + return g_base_info_equal(Base::info(), expected_info); + return true; + } +}; + +#endif // GI_WRAPPERUTILS_H_ diff --git a/git.mk b/git.mk deleted file mode 100644 index facca55..0000000 --- a/git.mk +++ /dev/null @@ -1,359 +0,0 @@ -# git.mk, a small Makefile to autogenerate .gitignore files -# for autotools-based projects. -# -# Copyright 2009, Red Hat, Inc. -# Copyright 2010,2011,2012,2013 Behdad Esfahbod -# Written by Behdad Esfahbod -# -# Copying and distribution of this file, with or without modification, -# is permitted in any medium without royalty provided the copyright -# notice and this notice are preserved. -# -# The latest version of this file can be downloaded from: -GIT_MK_URL = https://raw.githubusercontent.com/behdad/git.mk/master/git.mk -# -# Bugs, etc, should be reported upstream at: -# https://github.com/behdad/git.mk -# -# To use in your project, import this file in your git repo's toplevel, -# then do "make -f git.mk". This modifies all Makefile.am files in -# your project to -include git.mk. Remember to add that line to new -# Makefile.am files you create in your project, or just rerun the -# "make -f git.mk". -# -# This enables automatic .gitignore generation. If you need to ignore -# more files, add them to the GITIGNOREFILES variable in your Makefile.am. -# But think twice before doing that. If a file has to be in .gitignore, -# chances are very high that it's a generated file and should be in one -# of MOSTLYCLEANFILES, CLEANFILES, DISTCLEANFILES, or MAINTAINERCLEANFILES. -# -# The only case that you need to manually add a file to GITIGNOREFILES is -# when remove files in one of mostlyclean-local, clean-local, distclean-local, -# or maintainer-clean-local make targets. -# -# Note that for files like editor backup, etc, there are better places to -# ignore them. See "man gitignore". -# -# If "make maintainer-clean" removes the files but they are not recognized -# by this script (that is, if "git status" shows untracked files still), send -# me the output of "git status" as well as your Makefile.am and Makefile for -# the directories involved and I'll diagnose. -# -# For a list of toplevel files that should be in MAINTAINERCLEANFILES, see -# Makefile.am.sample in the git.mk git repo. -# -# Don't EXTRA_DIST this file. It is supposed to only live in git clones, -# not tarballs. It serves no useful purpose in tarballs and clutters the -# build dir. -# -# This file knows how to handle autoconf, automake, libtool, gtk-doc, -# gnome-doc-utils, yelp.m4, mallard, intltool, gsettings, dejagnu, appdata, -# appstream, hotdoc. -# -# This makefile provides the following targets: -# -# - all: "make all" will build all gitignore files. -# - gitignore: makes all gitignore files in the current dir and subdirs. -# - .gitignore: make gitignore file for the current dir. -# - gitignore-recurse: makes all gitignore files in the subdirs. -# -# KNOWN ISSUES: -# -# - Recursive configure doesn't work as $(top_srcdir)/git.mk inside the -# submodule doesn't find us. If you have configure.{in,ac} files in -# subdirs, add a proxy git.mk file in those dirs that simply does: -# "include $(top_srcdir)/../git.mk". Add more ..'s to your taste. -# And add those files to git. See vte/gnome-pty-helper/git.mk for -# example. -# - - - -############################################################################### -# Variables user modules may want to add to toplevel MAINTAINERCLEANFILES: -############################################################################### - -# -# Most autotools-using modules should be fine including this variable in their -# toplevel MAINTAINERCLEANFILES: -GITIGNORE_MAINTAINERCLEANFILES_TOPLEVEL = \ - $(srcdir)/aclocal.m4 \ - $(srcdir)/autoscan.log \ - $(srcdir)/configure.scan \ - `AUX_DIR=$(srcdir)/$$(cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_AUX_DIR:$$1' ./configure.ac); \ - test "x$$AUX_DIR" = "x$(srcdir)/" && AUX_DIR=$(srcdir); \ - for x in \ - ar-lib \ - compile \ - config.guess \ - config.rpath \ - config.sub \ - depcomp \ - install-sh \ - ltmain.sh \ - missing \ - mkinstalldirs \ - test-driver \ - ylwrap \ - ; do echo "$$AUX_DIR/$$x"; done` \ - `cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_HEADERS:$$1' ./configure.ac | \ - head -n 1 | while read f; do echo "$(srcdir)/$$f.in"; done` -# -# All modules should also be fine including the following variable, which -# removes automake-generated Makefile.in files: -GITIGNORE_MAINTAINERCLEANFILES_MAKEFILE_IN = \ - `cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_FILES:$$1' ./configure.ac | \ - while read f; do \ - case $$f in Makefile|*/Makefile) \ - test -f "$(srcdir)/$$f.am" && echo "$(srcdir)/$$f.in";; esac; \ - done` -# -# Modules that use libtool and use AC_CONFIG_MACRO_DIR() may also include this, -# though it's harmless to include regardless. -GITIGNORE_MAINTAINERCLEANFILES_M4_LIBTOOL = \ - `MACRO_DIR=$(srcdir)/$$(cd $(top_srcdir); $(AUTOCONF) --trace 'AC_CONFIG_MACRO_DIR:$$1' ./configure.ac); \ - if test "x$$MACRO_DIR" != "x$(srcdir)/"; then \ - for x in \ - libtool.m4 \ - ltoptions.m4 \ - ltsugar.m4 \ - ltversion.m4 \ - lt~obsolete.m4 \ - ; do echo "$$MACRO_DIR/$$x"; done; \ - fi` - - - -############################################################################### -# Default rule is to install ourselves in all Makefile.am files: -############################################################################### - -git-all: git-mk-install - -git-mk-install: - @echo "Installing git makefile" - @any_failed=; \ - find "`test -z "$(top_srcdir)" && echo . || echo "$(top_srcdir)"`" -name Makefile.am | while read x; do \ - if grep 'include .*/git.mk' $$x >/dev/null; then \ - echo "$$x already includes git.mk"; \ - else \ - failed=; \ - echo "Updating $$x"; \ - { cat $$x; \ - echo ''; \ - echo '-include $$(top_srcdir)/git.mk'; \ - } > $$x.tmp || failed=1; \ - if test x$$failed = x; then \ - mv $$x.tmp $$x || failed=1; \ - fi; \ - if test x$$failed = x; then : else \ - echo "Failed updating $$x"; >&2 \ - any_failed=1; \ - fi; \ - fi; done; test -z "$$any_failed" - -git-mk-update: - wget $(GIT_MK_URL) -O $(top_srcdir)/git.mk - -.PHONY: git-all git-mk-install git-mk-update - - - -############################################################################### -# Actual .gitignore generation: -############################################################################### - -$(srcdir)/.gitignore: Makefile.am $(top_srcdir)/git.mk - @echo "git.mk: Generating $@" - @{ \ - if test "x$(DOC_MODULE)" = x -o "x$(DOC_MAIN_SGML_FILE)" = x; then :; else \ - for x in \ - $(DOC_MODULE)-decl-list.txt \ - $(DOC_MODULE)-decl.txt \ - tmpl/$(DOC_MODULE)-unused.sgml \ - "tmpl/*.bak" \ - $(REPORT_FILES) \ - $(DOC_MODULE).pdf \ - xml html \ - ; do echo "/$$x"; done; \ - FLAVOR=$$(cd $(top_srcdir); $(AUTOCONF) --trace 'GTK_DOC_CHECK:$$2' ./configure.ac); \ - case $$FLAVOR in *no-tmpl*) echo /tmpl;; esac; \ - if echo "$(SCAN_OPTIONS)" | grep -q "\-\-rebuild-types"; then \ - echo "/$(DOC_MODULE).types"; \ - fi; \ - if echo "$(SCAN_OPTIONS)" | grep -q "\-\-rebuild-sections"; then \ - echo "/$(DOC_MODULE)-sections.txt"; \ - fi; \ - if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \ - for x in \ - $(SETUP_FILES) \ - $(DOC_MODULE).types \ - ; do echo "/$$x"; done; \ - fi; \ - fi; \ - if test "x$(DOC_MODULE)$(DOC_ID)" = x -o "x$(DOC_LINGUAS)" = x; then :; else \ - for lc in $(DOC_LINGUAS); do \ - for x in \ - $(if $(DOC_MODULE),$(DOC_MODULE).xml) \ - $(DOC_PAGES) \ - $(DOC_INCLUDES) \ - ; do echo "/$$lc/$$x"; done; \ - done; \ - for x in \ - $(_DOC_OMF_ALL) \ - $(_DOC_DSK_ALL) \ - $(_DOC_HTML_ALL) \ - $(_DOC_MOFILES) \ - $(DOC_H_FILE) \ - "*/.xml2po.mo" \ - "*/*.omf.out" \ - ; do echo /$$x; done; \ - fi; \ - if test "x$(HOTDOC)" = x; then :; else \ - $(foreach project, $(HOTDOC_PROJECTS),echo "/$(call HOTDOC_TARGET,$(project))"; \ - echo "/$(shell $(call HOTDOC_PROJECT_COMMAND,$(project)) --get-conf-path output)" ; \ - echo "/$(shell $(call HOTDOC_PROJECT_COMMAND,$(project)) --get-private-folder)" ; \ - ) \ - for x in \ - .hotdoc.d \ - ; do echo "/$$x"; done; \ - fi; \ - if test "x$(HELP_ID)" = x -o "x$(HELP_LINGUAS)" = x; then :; else \ - for lc in $(HELP_LINGUAS); do \ - for x in \ - $(HELP_FILES) \ - "$$lc.stamp" \ - "$$lc.mo" \ - ; do echo "/$$lc/$$x"; done; \ - done; \ - fi; \ - if test "x$(gsettings_SCHEMAS)" = x; then :; else \ - for x in \ - $(gsettings_SCHEMAS:.xml=.valid) \ - $(gsettings__enum_file) \ - ; do echo "/$$x"; done; \ - fi; \ - if test "x$(appdata_XML)" = x; then :; else \ - for x in \ - $(appdata_XML:.xml=.valid) \ - ; do echo "/$$x"; done; \ - fi; \ - if test "x$(appstream_XML)" = x; then :; else \ - for x in \ - $(appstream_XML:.xml=.valid) \ - ; do echo "/$$x"; done; \ - fi; \ - if test -f $(srcdir)/po/Makefile.in.in; then \ - for x in \ - ABOUT-NLS \ - po/Makefile.in.in \ - po/Makefile.in.in~ \ - po/Makefile.in \ - po/Makefile \ - po/Makevars.template \ - po/POTFILES \ - po/Rules-quot \ - po/stamp-it \ - po/stamp-po \ - po/.intltool-merge-cache \ - "po/*.gmo" \ - "po/*.header" \ - "po/*.mo" \ - "po/*.sed" \ - "po/*.sin" \ - po/$(GETTEXT_PACKAGE).pot \ - intltool-extract.in \ - intltool-merge.in \ - intltool-update.in \ - ; do echo "/$$x"; done; \ - fi; \ - if test -f $(srcdir)/configure; then \ - for x in \ - autom4te.cache \ - configure \ - config.h \ - stamp-h1 \ - libtool \ - config.lt \ - ; do echo "/$$x"; done; \ - fi; \ - if test "x$(DEJATOOL)" = x; then :; else \ - for x in \ - $(DEJATOOL) \ - ; do echo "/$$x.sum"; echo "/$$x.log"; done; \ - echo /site.exp; \ - fi; \ - if test "x$(am__dirstamp)" = x; then :; else \ - echo "$(am__dirstamp)"; \ - fi; \ - if test "x$(findstring libtool,$(LTCOMPILE))" = x -a "x$(findstring libtool,$(LTCXXCOMPILE))" = x -a "x$(GTKDOC_RUN)" = x; then :; else \ - for x in \ - "*.lo" \ - ".libs" "_libs" \ - ; do echo "$$x"; done; \ - fi; \ - for x in \ - .gitignore \ - $(GITIGNOREFILES) \ - $(CLEANFILES) \ - $(PROGRAMS) $(check_PROGRAMS) $(EXTRA_PROGRAMS) \ - $(LIBRARIES) $(check_LIBRARIES) $(EXTRA_LIBRARIES) \ - $(LTLIBRARIES) $(check_LTLIBRARIES) $(EXTRA_LTLIBRARIES) \ - so_locations \ - $(MOSTLYCLEANFILES) \ - $(TEST_LOGS) \ - $(TEST_LOGS:.log=.trs) \ - $(TEST_SUITE_LOG) \ - $(TESTS:=.test) \ - "*.gcda" \ - "*.gcno" \ - $(DISTCLEANFILES) \ - $(am__CONFIG_DISTCLEAN_FILES) \ - $(CONFIG_CLEAN_FILES) \ - TAGS ID GTAGS GRTAGS GSYMS GPATH tags \ - "*.tab.c" \ - $(MAINTAINERCLEANFILES) \ - $(BUILT_SOURCES) \ - $(patsubst %.vala,%.c,$(filter %.vala,$(SOURCES))) \ - $(filter %_vala.stamp,$(DIST_COMMON)) \ - $(filter %.vapi,$(DIST_COMMON)) \ - $(filter $(addprefix %,$(notdir $(patsubst %.vapi,%.h,$(filter %.vapi,$(DIST_COMMON))))),$(DIST_COMMON)) \ - Makefile \ - Makefile.in \ - "*.orig" \ - "*.rej" \ - "*.bak" \ - "*~" \ - ".*.sw[nop]" \ - ".dirstamp" \ - ; do echo "/$$x"; done; \ - for x in \ - "*.$(OBJEXT)" \ - $(DEPDIR) \ - ; do echo "$$x"; done; \ - } | \ - sed "s@^/`echo "$(srcdir)" | sed 's/\(.\)/[\1]/g'`/@/@" | \ - sed 's@/[.]/@/@g' | \ - LC_ALL=C sort | uniq > $@.tmp && \ - mv $@.tmp $@; - -all: $(srcdir)/.gitignore gitignore-recurse-maybe -gitignore: $(srcdir)/.gitignore gitignore-recurse - -gitignore-recurse-maybe: - @for subdir in $(DIST_SUBDIRS); do \ - case " $(SUBDIRS) " in \ - *" $$subdir "*) :;; \ - *) test "$$subdir" = . -o -e "$$subdir/.git" || (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) gitignore || echo "Skipping $$subdir");; \ - esac; \ - done -gitignore-recurse: - @for subdir in $(DIST_SUBDIRS); do \ - test "$$subdir" = . -o -e "$$subdir/.git" || (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) gitignore || echo "Skipping $$subdir"); \ - done - -maintainer-clean: gitignore-clean -gitignore-clean: - -rm -f $(srcdir)/.gitignore - -.PHONY: gitignore-clean gitignore gitignore-recurse gitignore-recurse-maybe diff --git a/gjs-modules-srcs.mk b/gjs-modules-srcs.mk deleted file mode 100644 index ca08fe7..0000000 --- a/gjs-modules-srcs.mk +++ /dev/null @@ -1,34 +0,0 @@ -module_console_srcs = \ - modules/console.h \ - modules/console.cpp \ - $(NULL) - -module_resource_srcs = \ - modules-resources.c \ - modules-resources.h \ - $(NULL) - -module_system_srcs = \ - modules/system.h \ - modules/system.cpp \ - $(NULL) - -module_cairo_srcs = \ - modules/cairo-private.h \ - modules/cairo-module.h \ - modules/cairo-region.cpp \ - modules/cairo-context.cpp \ - modules/cairo-path.cpp \ - modules/cairo-surface.cpp \ - modules/cairo-image-surface.cpp \ - modules/cairo-ps-surface.cpp \ - modules/cairo-pdf-surface.cpp \ - modules/cairo-svg-surface.cpp \ - modules/cairo-pattern.cpp \ - modules/cairo-gradient.cpp \ - modules/cairo-linear-gradient.cpp \ - modules/cairo-radial-gradient.cpp \ - modules/cairo-surface-pattern.cpp \ - modules/cairo-solid-pattern.cpp \ - modules/cairo.cpp \ - $(NULL) diff --git a/gjs-srcs.mk b/gjs-srcs.mk deleted file mode 100644 index 3ca1dfa..0000000 --- a/gjs-srcs.mk +++ /dev/null @@ -1,113 +0,0 @@ -gjs_public_headers = \ - cjs/context.h \ - cjs/coverage.h \ - cjs/gjs.h \ - cjs/macros.h \ - cjs/profiler.h \ - util/error.h \ - $(NULL) - -# For historical reasons, some files live in gi/ -# Some headers in the following list were formerly -# public - -gjs_srcs = \ - gi/arg.cpp \ - gi/arg.h \ - gi/boxed.cpp \ - gi/boxed.h \ - gi/closure.cpp \ - gi/closure.h \ - gi/enumeration.cpp \ - gi/enumeration.h \ - gi/foreign.cpp \ - gi/foreign.h \ - gi/fundamental.cpp \ - gi/fundamental.h \ - gi/function.cpp \ - gi/function.h \ - gi/gerror.cpp \ - gi/gerror.h \ - gi/gjs_gi_trace.h \ - gi/gtype.cpp \ - gi/gtype.h \ - gi/interface.cpp \ - gi/interface.h \ - gi/ns.cpp \ - gi/ns.h \ - gi/object.cpp \ - gi/object.h \ - gi/param.cpp \ - gi/param.h \ - gi/proxyutils.cpp \ - gi/proxyutils.h \ - gi/repo.cpp \ - gi/repo.h \ - gi/toggle.cpp \ - gi/toggle.h \ - gi/union.cpp \ - gi/union.h \ - gi/value.cpp \ - gi/value.h \ - cjs/byteArray.cpp \ - cjs/byteArray.h \ - cjs/context.cpp \ - cjs/context-private.h \ - cjs/coverage.cpp \ - cjs/engine.cpp \ - cjs/engine.h \ - cjs/global.cpp \ - cjs/global.h \ - cjs/importer.cpp \ - cjs/importer.h \ - cjs/jsapi-class.h \ - cjs/jsapi-dynamic-class.cpp \ - cjs/jsapi-util.cpp \ - cjs/jsapi-util.h \ - cjs/jsapi-util-args.h \ - cjs/jsapi-util-error.cpp \ - cjs/jsapi-util-root.h \ - cjs/jsapi-util-string.cpp \ - cjs/jsapi-wrapper.h \ - cjs/mem.h \ - cjs/mem.cpp \ - cjs/module.h \ - 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/log.cpp \ - util/log.h \ - util/misc.cpp \ - util/misc.h \ - $(NULL) - -# These files were part of a separate library -gjs_private_srcs = \ - libgjs-private/gjs-gdbus-wrapper.cpp \ - libgjs-private/gjs-gdbus-wrapper.h \ - libgjs-private/gjs-util.cpp \ - libgjs-private/gjs-util.h \ - libgjs-private/gjs-gtk-util.h \ - $(NULL) - -gjs_gtk_private_srcs = \ - libgjs-private/gjs-gtk-util.c \ - $(NULL) - -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 023841a..67c5106 100644 --- a/gjs.doap +++ b/gjs.doap @@ -13,11 +13,10 @@ GNOME JavaScript bindings - - - - - + + + + diff --git a/installed-tests/debugger-test.sh b/installed-tests/debugger-test.sh new file mode 100755 index 0000000..b74dec7 --- /dev/null +++ b/installed-tests/debugger-test.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then + gjs="$TOP_BUILDDIR/cjs-console" +else + gjs=cjs-console +fi + +echo 1..1 + +DEBUGGER_SCRIPT="$1" +JS_SCRIPT="$1.js" +EXPECTED_OUTPUT="$1.output" +THE_DIFF=$("$gjs" -d "$JS_SCRIPT" < "$DEBUGGER_SCRIPT" | sed \ + -e "s#$1#$(basename $1)#g" \ + -e "s/0x[0-9a-f]\{4,16\}/0xADDR/g" \ + | diff -u "$EXPECTED_OUTPUT" -) +if test $? -ne 0; then + echo "not ok 1 - $1 # command failed" + exit 1 +fi + +if test -n "$THE_DIFF"; then + echo "not ok 1 - $1" + echo "$THE_DIFF" | while read line; do echo "#$line"; done +else + echo "ok 1 - $1" +fi diff --git a/installed-tests/debugger.test.in b/installed-tests/debugger.test.in new file mode 100644 index 0000000..57e01ac --- /dev/null +++ b/installed-tests/debugger.test.in @@ -0,0 +1,4 @@ +[Test] +Type=session +Exec=@installed_tests_execdir@/debugger-test.sh @installed_tests_execdir@/debugger/@name@ +Output=TAP diff --git a/installed-tests/debugger/.eslintrc.yml b/installed-tests/debugger/.eslintrc.yml new file mode 100644 index 0000000..9643be5 --- /dev/null +++ b/installed-tests/debugger/.eslintrc.yml @@ -0,0 +1,2 @@ +rules: + no-debugger: 'off' diff --git a/installed-tests/debugger/backtrace.debugger b/installed-tests/debugger/backtrace.debugger new file mode 100644 index 0000000..7b86c07 --- /dev/null +++ b/installed-tests/debugger/backtrace.debugger @@ -0,0 +1,6 @@ +backtrace +c +bt +c +where +q diff --git a/installed-tests/debugger/backtrace.debugger.js b/installed-tests/debugger/backtrace.debugger.js new file mode 100644 index 0000000..b5e9bc7 --- /dev/null +++ b/installed-tests/debugger/backtrace.debugger.js @@ -0,0 +1,8 @@ +debugger; +[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]].forEach(array => { + debugger; + array.forEach(num => { + debugger; + print(num); + }); +}); diff --git a/installed-tests/debugger/backtrace.debugger.output b/installed-tests/debugger/backtrace.debugger.output new file mode 100644 index 0000000..021be8a --- /dev/null +++ b/installed-tests/debugger/backtrace.debugger.output @@ -0,0 +1,14 @@ +GJS debugger. Type "help" for help +db> backtrace +#0 toplevel at backtrace.debugger.js:1:0 +db> c +Debugger statement, toplevel at backtrace.debugger.js:1:0 +db> bt +#0 toplevel at backtrace.debugger.js:1:0 +db> c +Debugger statement, ([object Array], 0, [object Array]) at backtrace.debugger.js:3:4 +db> where +#0 ([object Array], 0, [object Array]) at backtrace.debugger.js:3:4 +#1 toplevel at backtrace.debugger.js:2:36 +db> q +Program exited with code 0 diff --git a/installed-tests/debugger/breakpoint.debugger b/installed-tests/debugger/breakpoint.debugger new file mode 100644 index 0000000..7a14f47 --- /dev/null +++ b/installed-tests/debugger/breakpoint.debugger @@ -0,0 +1,7 @@ +breakpoint 2 +break 4 +b 6 +c +c +c +c diff --git a/installed-tests/debugger/breakpoint.debugger.js b/installed-tests/debugger/breakpoint.debugger.js new file mode 100644 index 0000000..1ca4030 --- /dev/null +++ b/installed-tests/debugger/breakpoint.debugger.js @@ -0,0 +1,7 @@ +print('1'); +print('2'); +function foo() { + print('Function foo'); +} +print('3'); +foo(); diff --git a/installed-tests/debugger/breakpoint.debugger.output b/installed-tests/debugger/breakpoint.debugger.output new file mode 100644 index 0000000..1d9bc49 --- /dev/null +++ b/installed-tests/debugger/breakpoint.debugger.output @@ -0,0 +1,19 @@ +GJS debugger. Type "help" for help +db> breakpoint 2 +Breakpoint 1 at breakpoint.debugger.js:2:0 +db> break 4 +Breakpoint 2 at breakpoint.debugger.js:4:4 +db> b 6 +Breakpoint 3 at breakpoint.debugger.js:6:0 +db> c +1 +Breakpoint 1, toplevel at breakpoint.debugger.js:2:0 +db> c +2 +Breakpoint 3, toplevel at breakpoint.debugger.js:6:0 +db> c +3 +Breakpoint 2, foo() at breakpoint.debugger.js:4:4 +db> c +Function foo +Program exited with code 0 diff --git a/installed-tests/debugger/continue.debugger b/installed-tests/debugger/continue.debugger new file mode 100644 index 0000000..bdbcae4 --- /dev/null +++ b/installed-tests/debugger/continue.debugger @@ -0,0 +1,3 @@ +continue +cont +c diff --git a/installed-tests/debugger/continue.debugger.js b/installed-tests/debugger/continue.debugger.js new file mode 100644 index 0000000..2af2400 --- /dev/null +++ b/installed-tests/debugger/continue.debugger.js @@ -0,0 +1,2 @@ +debugger; +debugger; diff --git a/installed-tests/debugger/continue.debugger.output b/installed-tests/debugger/continue.debugger.output new file mode 100644 index 0000000..66d5c11 --- /dev/null +++ b/installed-tests/debugger/continue.debugger.output @@ -0,0 +1,7 @@ +GJS debugger. Type "help" for help +db> continue +Debugger statement, toplevel at continue.debugger.js:1:0 +db> cont +Debugger statement, toplevel at continue.debugger.js:2:0 +db> c +Program exited with code 0 diff --git a/installed-tests/debugger/delete.debugger b/installed-tests/debugger/delete.debugger new file mode 100644 index 0000000..9fc4560 --- /dev/null +++ b/installed-tests/debugger/delete.debugger @@ -0,0 +1,10 @@ +b 2 +b 3 +b 4 +b 5 +# Check that breakpoint 4 still remains after deleting 1-3 +delete 1 +del 2 +d 3 +c +c diff --git a/installed-tests/debugger/delete.debugger.js b/installed-tests/debugger/delete.debugger.js new file mode 100644 index 0000000..9f6b2cd --- /dev/null +++ b/installed-tests/debugger/delete.debugger.js @@ -0,0 +1,5 @@ +print('1'); +print('2'); +print('3'); +print('4'); +print('5'); diff --git a/installed-tests/debugger/delete.debugger.output b/installed-tests/debugger/delete.debugger.output new file mode 100644 index 0000000..af6ee6d --- /dev/null +++ b/installed-tests/debugger/delete.debugger.output @@ -0,0 +1,25 @@ +GJS debugger. Type "help" for help +db> b 2 +Breakpoint 1 at delete.debugger.js:2:0 +db> b 3 +Breakpoint 2 at delete.debugger.js:3:0 +db> b 4 +Breakpoint 3 at delete.debugger.js:4:0 +db> b 5 +Breakpoint 4 at delete.debugger.js:5:0 +db> # Check that breakpoint 4 still remains after deleting 1-3 +db> delete 1 +Breakpoint 1 at delete.debugger.js:2:0 deleted +db> del 2 +Breakpoint 2 at delete.debugger.js:3:0 deleted +db> d 3 +Breakpoint 3 at delete.debugger.js:4:0 deleted +db> c +1 +2 +3 +4 +Breakpoint 4, toplevel at delete.debugger.js:5:0 +db> c +5 +Program exited with code 0 diff --git a/installed-tests/debugger/detach.debugger b/installed-tests/debugger/detach.debugger new file mode 100644 index 0000000..8ddea54 --- /dev/null +++ b/installed-tests/debugger/detach.debugger @@ -0,0 +1 @@ +detach diff --git a/installed-tests/debugger/detach.debugger.js b/installed-tests/debugger/detach.debugger.js new file mode 100644 index 0000000..10ed45c --- /dev/null +++ b/installed-tests/debugger/detach.debugger.js @@ -0,0 +1 @@ +print('hi'); diff --git a/installed-tests/debugger/detach.debugger.output b/installed-tests/debugger/detach.debugger.output new file mode 100644 index 0000000..94199c3 --- /dev/null +++ b/installed-tests/debugger/detach.debugger.output @@ -0,0 +1,4 @@ +GJS debugger. Type "help" for help +db> detach +hi +Program exited with code 0 diff --git a/installed-tests/debugger/down-up.debugger b/installed-tests/debugger/down-up.debugger new file mode 100644 index 0000000..0b5a0e0 --- /dev/null +++ b/installed-tests/debugger/down-up.debugger @@ -0,0 +1,12 @@ +c +down +up +up +up +up +up +down +dn +dn +dn +c diff --git a/installed-tests/debugger/down-up.debugger.js b/installed-tests/debugger/down-up.debugger.js new file mode 100644 index 0000000..534099d --- /dev/null +++ b/installed-tests/debugger/down-up.debugger.js @@ -0,0 +1,17 @@ +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + d(); +} + +function d() { + debugger; +} + +a(); diff --git a/installed-tests/debugger/down-up.debugger.output b/installed-tests/debugger/down-up.debugger.output new file mode 100644 index 0000000..3f2fe43 --- /dev/null +++ b/installed-tests/debugger/down-up.debugger.output @@ -0,0 +1,25 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, d() at down-up.debugger.js:14:4 +db> down +Youngest frame selected; you cannot go down. +db> up +#1 c() at down-up.debugger.js:10:4 +db> up +#2 b() at down-up.debugger.js:6:4 +db> up +#3 a() at down-up.debugger.js:2:4 +db> up +#4 toplevel at down-up.debugger.js:17:0 +db> up +Initial frame selected; you cannot go up. +db> down +#3 a() at down-up.debugger.js:2:4 +db> dn +#2 b() at down-up.debugger.js:6:4 +db> dn +#1 c() at down-up.debugger.js:10:4 +db> dn +#0 d() at down-up.debugger.js:14:4 +db> c +Program exited with code 0 diff --git a/installed-tests/debugger/finish.debugger b/installed-tests/debugger/finish.debugger new file mode 100644 index 0000000..344ebe5 --- /dev/null +++ b/installed-tests/debugger/finish.debugger @@ -0,0 +1,5 @@ +c +finish +c +fin +c diff --git a/installed-tests/debugger/finish.debugger.js b/installed-tests/debugger/finish.debugger.js new file mode 100644 index 0000000..f8f8d01 --- /dev/null +++ b/installed-tests/debugger/finish.debugger.js @@ -0,0 +1,16 @@ +function foo() { + print('Print me'); + debugger; + print('Print me also'); +} + +function bar() { + print('Print me'); + debugger; + print('Print me also'); + return 5; +} + +foo(); +bar(); +print('Print me at the end'); diff --git a/installed-tests/debugger/finish.debugger.output b/installed-tests/debugger/finish.debugger.output new file mode 100644 index 0000000..2253b97 --- /dev/null +++ b/installed-tests/debugger/finish.debugger.output @@ -0,0 +1,21 @@ +GJS debugger. Type "help" for help +db> c +Print me +Debugger statement, foo() at finish.debugger.js:3:4 +db> finish +Run till exit from foo() at finish.debugger.js:3:4 +Print me also +No value returned. +toplevel at finish.debugger.js:14:0 +db> c +Print me +Debugger statement, bar() at finish.debugger.js:9:4 +db> fin +Run till exit from bar() at finish.debugger.js:9:4 +Print me also +Value returned is: +$1 = 5 +toplevel at finish.debugger.js:15:0 +db> c +Print me at the end +Program exited with code 0 diff --git a/installed-tests/debugger/frame.debugger b/installed-tests/debugger/frame.debugger new file mode 100644 index 0000000..8ef8ed3 --- /dev/null +++ b/installed-tests/debugger/frame.debugger @@ -0,0 +1,4 @@ +c +frame 2 +f 1 +c diff --git a/installed-tests/debugger/frame.debugger.js b/installed-tests/debugger/frame.debugger.js new file mode 100644 index 0000000..0b6509b --- /dev/null +++ b/installed-tests/debugger/frame.debugger.js @@ -0,0 +1,9 @@ +function a() { + b(); +} + +function b() { + debugger; +} + +a(); diff --git a/installed-tests/debugger/frame.debugger.output b/installed-tests/debugger/frame.debugger.output new file mode 100644 index 0000000..e0853ae --- /dev/null +++ b/installed-tests/debugger/frame.debugger.output @@ -0,0 +1,9 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, b() at frame.debugger.js:6:4 +db> frame 2 +#2 toplevel at frame.debugger.js:9:0 +db> f 1 +#1 a() at frame.debugger.js:2:4 +db> c +Program exited with code 0 diff --git a/installed-tests/debugger/keys.debugger b/installed-tests/debugger/keys.debugger new file mode 100644 index 0000000..473b212 --- /dev/null +++ b/installed-tests/debugger/keys.debugger @@ -0,0 +1,4 @@ +c +keys a +k a +c diff --git a/installed-tests/debugger/keys.debugger.js b/installed-tests/debugger/keys.debugger.js new file mode 100644 index 0000000..e5f57b1 --- /dev/null +++ b/installed-tests/debugger/keys.debugger.js @@ -0,0 +1,7 @@ +const a = { + foo: 1, + bar: null, + tres: undefined, +}; +debugger; +void a; diff --git a/installed-tests/debugger/keys.debugger.output b/installed-tests/debugger/keys.debugger.output new file mode 100644 index 0000000..42df6ae --- /dev/null +++ b/installed-tests/debugger/keys.debugger.output @@ -0,0 +1,19 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, toplevel at keys.debugger.js:6:0 +db> keys a +$1 = [object Array] +[ + "foo", + "bar", + "tres" +] +db> k a +$2 = [object Array] +[ + "foo", + "bar", + "tres" +] +db> c +Program exited with code 0 diff --git a/installed-tests/debugger/next.debugger b/installed-tests/debugger/next.debugger new file mode 100644 index 0000000..beb49d5 --- /dev/null +++ b/installed-tests/debugger/next.debugger @@ -0,0 +1,9 @@ +c +next +n +n +n +n +n +n +n diff --git a/installed-tests/debugger/next.debugger.js b/installed-tests/debugger/next.debugger.js new file mode 100644 index 0000000..4162f2b --- /dev/null +++ b/installed-tests/debugger/next.debugger.js @@ -0,0 +1,11 @@ +function a() { + debugger; + b(); + print('A line in a'); +} + +function b() { + print('A line in b'); +} + +a(); diff --git a/installed-tests/debugger/next.debugger.output b/installed-tests/debugger/next.debugger.output new file mode 100644 index 0000000..bb76e34 --- /dev/null +++ b/installed-tests/debugger/next.debugger.output @@ -0,0 +1,25 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, a() at next.debugger.js:2:4 +db> next +a() at next.debugger.js:2:4 +db> n +a() at next.debugger.js:3:4 +A line in b +db> n +a() at next.debugger.js:4:4 +A line in a +db> n +a() at next.debugger.js:5:0 +No value returned. +db> n +a() at next.debugger.js:5:0 +toplevel at next.debugger.js:11:0 +db> n +toplevel at next.debugger.js:11:0 +db> n +toplevel at next.debugger.js:12:0 +No value returned. +db> n +toplevel at next.debugger.js:12:0 +Program exited with code 0 diff --git a/installed-tests/debugger/print.debugger b/installed-tests/debugger/print.debugger new file mode 100644 index 0000000..2dd0387 --- /dev/null +++ b/installed-tests/debugger/print.debugger @@ -0,0 +1,21 @@ +c +# Simple types +print a +p b +p c +p d +p e +p f +p g +# Objects +print h +print/b h +print/p h +p i +p/b i +p j +p k +p/b k +p l +p m +c diff --git a/installed-tests/debugger/print.debugger.js b/installed-tests/debugger/print.debugger.js new file mode 100644 index 0000000..05ee0d4 --- /dev/null +++ b/installed-tests/debugger/print.debugger.js @@ -0,0 +1,16 @@ +const {GObject} = imports.gi; +const a = undefined; +const b = null; +const c = 42; +const d = 'some string'; +const e = false; +const f = true; +const g = Symbol('foobar'); +const h = [1, 'money', 2, 'show', {three: 'to', 'get ready': 'go cat go'}]; +const i = {some: 'plain object', that: 'has keys'}; +const j = new Set([5, 6, 7]); +const k = class J {}; +const l = new GObject.Object(); +const m = new Error('message'); +debugger; +void (a, b, c, d, e, f, g, h, i, j, k, l, m); diff --git a/installed-tests/debugger/print.debugger.output b/installed-tests/debugger/print.debugger.output new file mode 100644 index 0000000..36614f4 --- /dev/null +++ b/installed-tests/debugger/print.debugger.output @@ -0,0 +1,85 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, toplevel at print.debugger.js:15:0 +db> # Simple types +db> print a +$1 = undefined +db> p b +$2 = null +db> p c +$3 = 42 +db> p d +$4 = "some string" +db> p e +$5 = false +db> p f +$6 = true +db> p g +$7 = Symbol("foobar") +db> # Objects +db> print h +$8 = [object Array] +[ + 1, + "money", + 2, + "show", + { + "three": "to", + "get ready": "go cat go" + } +] +db> print/b h +$9 = [object Array] +{ + "0": 1, + "1": "money", + "2": 2, + "3": "show", + "4": "(...)", + "length": 5 +} +db> print/p h +$10 = [object Array] +[ + 1, + "money", + 2, + "show", + { + "three": "to", + "get ready": "go cat go" + } +] +db> p i +$11 = [object Object] +{ + "some": "plain object", + "that": "has keys" +} +db> p/b i +$12 = [object Object] +{ + "some": "plain object", + "that": "has keys" +} +db> p j +$13 = [object Set] +{} +db> p k +$14 = [object Function] +db> p/b k +$15 = [object Function] +{ + "prototype": "(...)", + "length": 0, + "name": "J" +} +db> p l +$16 = [object GObject_Object] +[object instance wrapper GIName:GObject.Object jsobj@0xADDR native@0xADDR] +db> p m +$17 = [object Error] +Error: message +db> c +Program exited with code 0 diff --git a/installed-tests/debugger/quit.debugger b/installed-tests/debugger/quit.debugger new file mode 100644 index 0000000..bca70f3 --- /dev/null +++ b/installed-tests/debugger/quit.debugger @@ -0,0 +1 @@ +q diff --git a/installed-tests/debugger/quit.debugger.js b/installed-tests/debugger/quit.debugger.js new file mode 100644 index 0000000..10ed45c --- /dev/null +++ b/installed-tests/debugger/quit.debugger.js @@ -0,0 +1 @@ +print('hi'); diff --git a/installed-tests/debugger/quit.debugger.output b/installed-tests/debugger/quit.debugger.output new file mode 100644 index 0000000..7d691b2 --- /dev/null +++ b/installed-tests/debugger/quit.debugger.output @@ -0,0 +1,3 @@ +GJS debugger. Type "help" for help +db> q +Program exited with code 0 diff --git a/installed-tests/debugger/return.debugger b/installed-tests/debugger/return.debugger new file mode 100644 index 0000000..4613817 --- /dev/null +++ b/installed-tests/debugger/return.debugger @@ -0,0 +1,8 @@ +b 2 +b 6 +b 10 +c +return +ret 5 +ret `${4 * 10 + 2} is the answer` +c diff --git a/installed-tests/debugger/return.debugger.js b/installed-tests/debugger/return.debugger.js new file mode 100644 index 0000000..4133a18 --- /dev/null +++ b/installed-tests/debugger/return.debugger.js @@ -0,0 +1,15 @@ +function func1() { + return 1; +} + +function func2() { + return 2; +} + +function func3() { + return 3; +} + +print(func1()); +print(func2()); +print(func3()); diff --git a/installed-tests/debugger/return.debugger.output b/installed-tests/debugger/return.debugger.output new file mode 100644 index 0000000..7d7ee44 --- /dev/null +++ b/installed-tests/debugger/return.debugger.output @@ -0,0 +1,18 @@ +GJS debugger. Type "help" for help +db> b 2 +Breakpoint 1 at return.debugger.js:2:4 +db> b 6 +Breakpoint 2 at return.debugger.js:6:4 +db> b 10 +Breakpoint 3 at return.debugger.js:10:4 +db> c +Breakpoint 1, func1() at return.debugger.js:2:4 +db> return +undefined +Breakpoint 2, func2() at return.debugger.js:6:4 +db> ret 5 +5 +Breakpoint 3, func3() at return.debugger.js:10:4 +db> ret `${4 * 10 + 2} is the answer` +42 is the answer +Program exited with code 0 diff --git a/installed-tests/debugger/set.debugger b/installed-tests/debugger/set.debugger new file mode 100644 index 0000000..a8331d7 --- /dev/null +++ b/installed-tests/debugger/set.debugger @@ -0,0 +1,21 @@ +# Currently the only option is "pretty" for pretty-printing. Set doesn't yet +# allow setting variables in the program. +c +p a +set pretty 0 +p a +set pretty 1 +p a +set pretty off +p a +set pretty on +p a +set pretty false +p a +set pretty true +p a +set pretty no +p a +set pretty yes +p a +q diff --git a/installed-tests/debugger/set.debugger.js b/installed-tests/debugger/set.debugger.js new file mode 100644 index 0000000..e2a5f5a --- /dev/null +++ b/installed-tests/debugger/set.debugger.js @@ -0,0 +1,3 @@ +const a = {}; +debugger; +void a; diff --git a/installed-tests/debugger/set.debugger.output b/installed-tests/debugger/set.debugger.output new file mode 100644 index 0000000..93d95fe --- /dev/null +++ b/installed-tests/debugger/set.debugger.output @@ -0,0 +1,38 @@ +GJS debugger. Type "help" for help +db> # Currently the only option is "pretty" for pretty-printing. Set doesn't yet +db> # allow setting variables in the program. +db> c +Debugger statement, toplevel at set.debugger.js:2:0 +db> p a +$1 = [object Object] +{} +db> set pretty 0 +db> p a +$2 = [object Object] +db> set pretty 1 +db> p a +$3 = [object Object] +{} +db> set pretty off +db> p a +$4 = [object Object] +db> set pretty on +db> p a +$5 = [object Object] +{} +db> set pretty false +db> p a +$6 = [object Object] +db> set pretty true +db> p a +$7 = [object Object] +{} +db> set pretty no +db> p a +$8 = [object Object] +db> set pretty yes +db> p a +$9 = [object Object] +{} +db> q +Program exited with code 0 diff --git a/installed-tests/debugger/step.debugger b/installed-tests/debugger/step.debugger new file mode 100644 index 0000000..df19715 --- /dev/null +++ b/installed-tests/debugger/step.debugger @@ -0,0 +1,12 @@ +s +s +s +s +s +s +s +s +s +s +s +s diff --git a/installed-tests/debugger/step.debugger.js b/installed-tests/debugger/step.debugger.js new file mode 100644 index 0000000..454406e --- /dev/null +++ b/installed-tests/debugger/step.debugger.js @@ -0,0 +1,10 @@ +function a() { + b(); + print('A line in a'); +} + +function b() { + print('A line in b'); +} + +a(); diff --git a/installed-tests/debugger/step.debugger.output b/installed-tests/debugger/step.debugger.output new file mode 100644 index 0000000..806e5e9 --- /dev/null +++ b/installed-tests/debugger/step.debugger.output @@ -0,0 +1,35 @@ +GJS debugger. Type "help" for help +db> s +toplevel at step.debugger.js:10:0 +entered frame: a() at step.debugger.js:2:4 +db> s +a() at step.debugger.js:2:4 +entered frame: b() at step.debugger.js:7:4 +db> s +b() at step.debugger.js:7:4 +A line in b +db> s +b() at step.debugger.js:8:0 +No value returned. +db> s +b() at step.debugger.js:8:0 +a() at step.debugger.js:2:4 +db> s +a() at step.debugger.js:2:4 +db> s +a() at step.debugger.js:3:4 +A line in a +db> s +a() at step.debugger.js:4:0 +No value returned. +db> s +a() at step.debugger.js:4:0 +toplevel at step.debugger.js:10:0 +db> s +toplevel at step.debugger.js:10:0 +db> s +toplevel at step.debugger.js:11:0 +No value returned. +db> s +toplevel at step.debugger.js:11:0 +Program exited with code 0 diff --git a/installed-tests/debugger/throw.debugger b/installed-tests/debugger/throw.debugger new file mode 100644 index 0000000..0c74e67 --- /dev/null +++ b/installed-tests/debugger/throw.debugger @@ -0,0 +1,4 @@ +c +throw 'foobar' + 3.14; +fin +throw diff --git a/installed-tests/debugger/throw.debugger.js b/installed-tests/debugger/throw.debugger.js new file mode 100644 index 0000000..2ff9443 --- /dev/null +++ b/installed-tests/debugger/throw.debugger.js @@ -0,0 +1,10 @@ +function a() { + debugger; + return 5; +} + +try { + a(); +} catch (e) { + print(`Exception: ${e}`); +} diff --git a/installed-tests/debugger/throw.debugger.output b/installed-tests/debugger/throw.debugger.output new file mode 100644 index 0000000..248827d --- /dev/null +++ b/installed-tests/debugger/throw.debugger.output @@ -0,0 +1,20 @@ +GJS debugger. Type "help" for help +db> c +Debugger statement, a() at throw.debugger.js:2:4 +db> throw 'foobar' + 3.14; +Unwinding due to exception. (Type 'c' to continue unwinding.) +#0 a() at throw.debugger.js:2:4 +Exception value is: +$1 = "foobar3.14" +db> fin +Run till exit from a() at throw.debugger.js:2:4 +Frame terminated by exception: +$2 = "foobar3.14" +(To rethrow it, type 'throw'.) +Unwinding due to exception. (Type 'c' to continue unwinding.) +#0 toplevel at throw.debugger.js:7:4 +Exception value is: +$3 = "foobar3.14" +db> throw +Exception: foobar3.14 +Program exited with code 0 diff --git a/installed-tests/debugger/until.debugger b/installed-tests/debugger/until.debugger new file mode 100644 index 0000000..fb07788 --- /dev/null +++ b/installed-tests/debugger/until.debugger @@ -0,0 +1,4 @@ +until 3 +upto 5 +u 7 +c diff --git a/installed-tests/debugger/until.debugger.js b/installed-tests/debugger/until.debugger.js new file mode 100644 index 0000000..8a7b067 --- /dev/null +++ b/installed-tests/debugger/until.debugger.js @@ -0,0 +1,7 @@ +print('1'); +print('2'); +print('3'); +(function () { + print('4'); +})(); +print('5'); diff --git a/installed-tests/debugger/until.debugger.output b/installed-tests/debugger/until.debugger.output new file mode 100644 index 0000000..b2a7c4c --- /dev/null +++ b/installed-tests/debugger/until.debugger.output @@ -0,0 +1,17 @@ +GJS debugger. Type "help" for help +db> until 3 +toplevel at until.debugger.js:1:0 +1 +2 +db> upto 5 +toplevel at until.debugger.js:3:0 +3 +entered frame: () at until.debugger.js:5:4 +db> u 7 +() at until.debugger.js:5:4 +4 +No value returned. +toplevel at until.debugger.js:7:0 +db> c +5 +Program exited with code 0 diff --git a/installed-tests/extra/cjs.supp b/installed-tests/extra/cjs.supp new file mode 100644 index 0000000..396fac5 --- /dev/null +++ b/installed-tests/extra/cjs.supp @@ -0,0 +1,127 @@ +# Valgrind suppressions file for GJS +# This is intended to be used in addition to GLib's glib.supp file. + +# SpiderMonkey leaks + +{ + mozjs-thread-stack-init + Memcheck:Leak + match-leak-kinds: possible + fun:calloc + fun:_dl_allocate_tls + fun:pthread_create@@GLIBC_2.2.5 + fun:_ZN7mozilla9TimeStamp20ComputeProcessUptimeEv + fun:_ZN7mozilla9TimeStamp15ProcessCreationEPb + fun:_ZN2JS6detail25InitWithFailureDiagnosticEb + fun:_Z7JS_Initv +} + +# Various things that I don't believe are related to GJS + +{ + gtk-style-context + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:g_malloc + ... + fun:gtk_css_node_declaration_make_writable + ... + fun:gtk_style_constructed +} + +{ + gtk-style-context2 + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:g_malloc + ... + fun:gtk_css_node_declaration_make_writable_resize + ... + fun:gtk_style_constructed +} + +# https://bugs.freedesktop.org/show_bug.cgi?id=105466 +{ + freedesktop-bug-105466 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:FcConfigSubstituteWithPat + fun:_cairo_ft_resolve_pattern + fun:_cairo_ft_font_face_get_implementation + fun:cairo_scaled_font_create + fun:_cairo_gstate_ensure_scaled_font + fun:_cairo_gstate_get_scaled_font + fun:_cairo_default_context_get_scaled_font + fun:cairo_show_text +} + +# Data that Cairo keeps around for the process lifetime +# This could be freed by calling cairo_debug_reset_static_data(), but it's +# not a good idea to call that function in production, because certain versions +# of Cairo have bugs that cause it to fail assertions and crash. +{ + cairo-static-data + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:FcPatternDuplicate + fun:_cairo_ft_font_face_create_for_pattern + fun:_cairo_ft_font_face_create_for_toy + fun:_cairo_toy_font_face_create_impl_face + fun:_cairo_toy_font_face_init + fun:cairo_toy_font_face_create + ... + fun:_cairo_gstate_ensure_font_face + fun:_cairo_gstate_ensure_scaled_font + fun:_cairo_gstate_get_scaled_font + fun:_cairo_default_context_get_scaled_font + ... + fun:cairo_show_text +} + +# https://gitlab.gnome.org/GNOME/gobject-introspection/issues/265 +{ + gobject-introspection-default-repository + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + ... + fun:build_typelib_key + fun:register_internal +} + +# Workaround for https://github.com/mesonbuild/meson/issues/4427 +# When fixed, valgrind should already not trace bash +{ + bash-workaround + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:xmalloc + fun:set_default_locale + fun:main +} + +# https://gitlab.gnome.org/GNOME/glib/-/issues/1911 +{ + g-type-register-static + Memcheck:Leak + match-leak-kinds:possible + fun:malloc + ... + fun:g_type_register_static +} + +{ + g-type-register-static-calloc + Memcheck:Leak + match-leak-kinds:possible + fun:calloc + ... + fun:g_type_register_static +} diff --git a/installed-tests/extra/lsan.supp b/installed-tests/extra/lsan.supp index 4eadd49..9b0105c 100644 --- a/installed-tests/extra/lsan.supp +++ b/installed-tests/extra/lsan.supp @@ -1,5 +1,9 @@ # SpiderMonkey leaks a mutex for each GC helper thread. leak:js::HelperThread::threadLoop -# We leak a small wrapper in GJS for each registered GType. -leak:gjs_gtype_create_gtype_wrapper +# https://bugs.freedesktop.org/show_bug.cgi?id=105466 +leak:libfontconfig.so.1 + +# https://bugzilla.mozilla.org/show_bug.cgi?id=1478679 +leak:js::coverage::LCovSource::writeScript +leak:js/src/util/Text.cpp diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml new file mode 100644 index 0000000..14b7a03 --- /dev/null +++ b/installed-tests/js/.eslintrc.yml @@ -0,0 +1,29 @@ +--- +env: + jasmine: true +rules: + no-restricted-globals: + - error + - name: fdescribe + message: Do not commit fdescribe(). Use describe() instead. + - name: fit + message: Do not commit fit(). Use it() instead. + no-restricted-syntax: + - error + - selector: CallExpression[callee.name="it"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead + - selector: CallExpression[callee.name="describe"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead + - selector: CallExpression[callee.name="beforeEach"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead + - selector: CallExpression[callee.name="afterEach"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead + - selector: CallExpression[callee.name="beforeAll"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead + - selector: CallExpression[callee.name="afterAll"] > ArrowFunctionExpression + message: Arrow functions can mess up some Jasmine APIs. Use function () instead +globals: + clearInterval: writable + clearTimeout: writable + setInterval: writable + setTimeout: writable diff --git a/installed-tests/js/complex.ui b/installed-tests/js/complex3.ui similarity index 91% rename from installed-tests/js/complex.ui rename to installed-tests/js/complex3.ui index 4096a4a..b5136fb 100644 --- a/installed-tests/js/complex.ui +++ b/installed-tests/js/complex3.ui @@ -17,6 +17,7 @@ Complex as well! True + diff --git a/installed-tests/js/complex4.ui b/installed-tests/js/complex4.ui new file mode 100644 index 0000000..f355785 --- /dev/null +++ b/installed-tests/js/complex4.ui @@ -0,0 +1,26 @@ + + + + diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml index 15bd330..e0060c8 100644 --- a/installed-tests/js/jsunit.gresources.xml +++ b/installed-tests/js/jsunit.gresources.xml @@ -1,20 +1,22 @@ - complex.ui + complex3.ui + complex4.ui jasmine.js minijasmine.js modules/alwaysThrows.js + modules/badOverrides/GIMarshallingTests.js + modules/badOverrides/Gio.js + modules/badOverrides/Regress.js + modules/badOverrides/WarnLib.js modules/foobar.js modules/lexicalScope.js modules/modunicode.js modules/mutualImport/a.js modules/mutualImport/b.js - modules/subA/subB/__init__.js modules/overrides/GIMarshallingTests.js - modules/overrides/Gio.js - modules/overrides/Regress.js - modules/overrides/WarnLib.js + modules/subA/subB/__init__.js modules/subA/subB/baz.js modules/subA/subB/foobar.js diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build new file mode 100644 index 0000000..651577f --- /dev/null +++ b/installed-tests/js/meson.build @@ -0,0 +1,192 @@ +### Jasmine tests ############################################################## + +jsunit_resources_files = gnome.compile_resources('jsunit-resources', + 'jsunit.gresources.xml', c_name: 'jsunit_resources') + +minijasmine = executable('minijasmine', '../minijasmine.cpp', + jsunit_resources_files, dependencies: libgjs_dep, + cpp_args: [ + '-DINSTTESTDIR="@0@"'.format(installed_tests_execdir), + ], + include_directories: top_include, + install: get_option('installed_tests'), install_dir: installed_tests_execdir) + +gidatadir = gi.get_pkgconfig_variable('gidatadir') +gi_tests = gidatadir / 'tests' + +test_gir_extra_c_args = [] +test_gir_warning_c_args = [] + +if cc.get_argument_syntax() == 'msvc' + # We need to ensure the symbols in the test DLLs export in clang-cl builds + test_gir_extra_c_args += ['-D_GI_EXTERN=__declspec(dllexport)extern'] +else + # These consist of external code (from gobject-introspection) so they should not + # error out even when building with -Werror + test_gir_warning_c_args += ['-Wno-error'] +endif + +regress_dependencies = [glib, gobject, gio] +regress_gir_includes = ['Gio-2.0'] +regress_gir_c_args = test_gir_extra_c_args +if build_cairo + regress_gir_includes += 'cairo-1.0' + regress_dependencies += [cairo, cairo_gobject] +else + regress_gir_c_args += ['-D_GI_DISABLE_CAIRO'] +endif + +regress_sources = [ + gi_tests / 'regress.c', + gi_tests / 'regress.h', +] +libregress = library('regress', regress_sources, + c_args: regress_gir_c_args + test_gir_warning_c_args, + dependencies: regress_dependencies, install: get_option('installed_tests'), + install_dir: installed_tests_execdir) +regress_gir = gnome.generate_gir(libregress, includes: regress_gir_includes, + sources: regress_sources, namespace: 'Regress', nsversion: '1.0', + identifier_prefix: 'Regress', symbol_prefix: 'regress_', + extra_args: ['--warn-all', '--warn-error'] + regress_gir_c_args, + install: get_option('installed_tests'), install_dir_gir: false, + install_dir_typelib: installed_tests_execdir) +regress_typelib = regress_gir[1] + +warnlib_sources = [ + gi_tests / 'warnlib.c', + gi_tests / 'warnlib.h', +] +libwarnlib = library('warnlib', warnlib_sources, + c_args: test_gir_warning_c_args + test_gir_extra_c_args, + dependencies: [glib, gobject, gio], install: get_option('installed_tests'), + include_directories: top_include, + install_dir: installed_tests_execdir) +# This should have --warn-all turned off, but there is currently no way to do so +# in gnome.generate_gir(). See https://github.com/mesonbuild/meson/issues/5876 +warnlib_gir = gnome.generate_gir(libwarnlib, includes: ['Gio-2.0'], + sources: warnlib_sources, namespace: 'WarnLib', nsversion: '1.0', + symbol_prefix: 'warnlib_', header: 'warnlib.h', + install: get_option('installed_tests'), install_dir_gir: false, + install_dir_typelib: installed_tests_execdir) +warnlib_typelib = warnlib_gir[1] + +gimarshallingtests_sources = [ + gi_tests / 'gimarshallingtests.c', + gi_tests / 'gimarshallingtests.h', +] +libgimarshallingtests = library('gimarshallingtests', + gimarshallingtests_sources, dependencies: [glib, gobject, gio], + c_args: test_gir_extra_c_args + test_gir_warning_c_args, + install: get_option('installed_tests'), install_dir: installed_tests_execdir) +gimarshallingtests_gir = gnome.generate_gir(libgimarshallingtests, + includes: ['Gio-2.0'], sources: gimarshallingtests_sources, + namespace: 'GIMarshallingTests', nsversion: '1.0', + symbol_prefix: 'gi_marshalling_tests_', extra_args: '--warn-error', + install: get_option('installed_tests'), install_dir_gir: false, + install_dir_typelib: installed_tests_execdir) +gimarshallingtests_typelib = gimarshallingtests_gir[1] + +jasmine_tests = [ + 'self', + 'ByteArray', + 'Exceptions', + 'Format', + 'Fundamental', + 'Gettext', + 'GIMarshalling', + 'Gio', + 'GLib', + 'GObject', + 'GObjectClass', + 'GObjectInterface', + 'GTypeClass', + 'Importer', + 'Introspection', + 'Lang', + 'LegacyByteArray', + 'LegacyClass', + 'LegacyGObject', + 'Mainloop', + 'Namespace', + 'Package', + 'ParamSpec', + 'Print', + 'Regress', + 'Signals', + 'System', + 'Tweener', + 'WarnLib', +] + +if build_cairo + jasmine_tests += 'Cairo' +endif + +if not get_option('skip_gtk_tests') + jasmine_tests += [ + 'Gtk3', + 'GObjectDestructionAccess', + 'LegacyGtk', + ] + + if have_gtk4 + jasmine_tests += 'Gtk4' + endif +endif + +installed_js_tests_dir = installed_tests_execdir / 'js' + +gschemas_compiled = gnome.compile_schemas( + depend_files: 'org.cinnamon.CjsTest.gschema.xml') + +foreach test : jasmine_tests + test_file = files('test@0@.js'.format(test)) + + test(test, minijasmine, args: test_file, + depends: [ + gschemas_compiled, + gimarshallingtests_typelib, + regress_typelib, + warnlib_typelib, + ], + env: tests_environment, protocol: 'tap', suite: 'JS') + + test_description_subst = { + 'name': 'test@0@.js'.format(test), + 'installed_tests_execdir': installed_tests_execdir, + } + test_description = configure_file(configuration: test_description_subst, + input: '../minijasmine.test.in', + output: 'test@0@.test'.format(test), + install: get_option('installed_tests'), + install_dir: installed_tests_metadir) + + if get_option('installed_tests') + install_data(test_file, install_dir: installed_js_tests_dir) + endif +endforeach + +# testGDBus.js is separate, because it can be skipped, and during build should +# be run using dbus-run-session + +if not get_option('skip_dbus_tests') + test_file = files('testGDBus.js') + bus_config = files('../../test/test-bus.conf') + test('GDBus', dbus_run_session, + args: ['--config-file', bus_config, '--', minijasmine, test_file], + env: tests_environment, protocol: 'tap', suite: 'dbus') +endif + +gdbus_test_description_subst = { + 'name': 'testGDBus.js', + 'installed_tests_execdir': installed_tests_execdir, +} +gdbus_test_description = configure_file( + configuration: gdbus_test_description_subst, + input: '../minijasmine.test.in', output: 'testGDBus.test', + install: get_option('installed_tests'), + install_dir: installed_tests_metadir) + +if get_option('installed_tests') + install_data('testGDBus.js', install_dir: installed_js_tests_dir) +endif diff --git a/installed-tests/js/minijasmine.js b/installed-tests/js/minijasmine.js index 6af74e3..a424680 100644 --- a/installed-tests/js/minijasmine.js +++ b/installed-tests/js/minijasmine.js @@ -1,7 +1,6 @@ #!/usr/bin/env gjs const GLib = imports.gi.GLib; -const Lang = imports.lang; function _removeNewlines(str) { let allNewlines = /\n/g; @@ -31,20 +30,20 @@ function _clearTimeoutInternal(id) { } // Install the browser setTimeout/setInterval API on the global object -window.setTimeout = _setTimeoutInternal.bind(undefined, GLib.SOURCE_REMOVE); -window.setInterval = _setTimeoutInternal.bind(undefined, GLib.SOURCE_CONTINUE); -window.clearTimeout = window.clearInterval = _clearTimeoutInternal; +globalThis.setTimeout = _setTimeoutInternal.bind(undefined, GLib.SOURCE_REMOVE); +globalThis.setInterval = _setTimeoutInternal.bind(undefined, GLib.SOURCE_CONTINUE); +globalThis.clearTimeout = globalThis.clearInterval = _clearTimeoutInternal; let jasmineRequire = imports.jasmine.getJasmineRequireObj(); let jasmineCore = jasmineRequire.core(jasmineRequire); -window._jasmineEnv = jasmineCore.getEnv(); +globalThis._jasmineEnv = jasmineCore.getEnv(); -window._jasmineMain = GLib.MainLoop.new(null, false); -window._jasmineRetval = 0; +globalThis._jasmineMain = GLib.MainLoop.new(null, false); +globalThis._jasmineRetval = 0; // Install Jasmine API on the global object -let jasmineInterface = jasmineRequire.interface(jasmineCore, window._jasmineEnv); -Lang.copyProperties(jasmineInterface, window); +let jasmineInterface = jasmineRequire.interface(jasmineCore, globalThis._jasmineEnv); +Object.assign(globalThis, jasmineInterface); // Reporter that outputs according to the Test Anything Protocol // See http://testanything.org/tap-specification.html @@ -55,29 +54,28 @@ class TapReporter { } jasmineStarted(info) { - print('1..' + info.totalSpecsDefined); + print(`1..${info.totalSpecsDefined}`); } jasmineDone() { this._failedSuites.forEach(failure => { failure.failedExpectations.forEach(result => { print('not ok - An error was thrown outside a test'); - print('# ' + result.message); + print(`# ${result.message}`); }); }); - window._jasmineMain.quit(); + globalThis._jasmineMain.quit(); } suiteDone(result) { if (result.failedExpectations && result.failedExpectations.length > 0) { - window._jasmineRetval = 1; + globalThis._jasmineRetval = 1; this._failedSuites.push(result); } - if (result.status === 'disabled') { + if (result.status === 'disabled') print('# Suite was disabled:', result.fullName); - } } specStarted() { @@ -85,30 +83,37 @@ class TapReporter { } specDone(result) { - let tap_report; + let tapReport; if (result.status === 'failed') { - window._jasmineRetval = 1; - tap_report = 'not ok'; + globalThis._jasmineRetval = 1; + tapReport = 'not ok'; } else { - tap_report = 'ok'; + tapReport = 'ok'; } - tap_report += ' ' + this._specCount + ' ' + result.fullName; + tapReport += ` ${this._specCount} ${result.fullName}`; if (result.status === 'pending' || result.status === 'disabled') { let reason = result.pendingReason || result.status; - tap_report += ' # SKIP ' + reason; + tapReport += ` # SKIP ${reason}`; } - print(tap_report); + print(tapReport); // Print additional diagnostic info on failure if (result.status === 'failed' && result.failedExpectations) { - result.failedExpectations.forEach((failedExpectation) => { + result.failedExpectations.forEach(failedExpectation => { print('# Message:', _removeNewlines(failedExpectation.message)); print('# Stack:'); let stackTrace = _filterStack(failedExpectation.stack).trim(); - print(stackTrace.split('\n').map((str) => '# ' + str).join('\n')); + print(stackTrace.split('\n').map(str => `# ${str}`).join('\n')); }); } } } -window._jasmineEnv.addReporter(new TapReporter()); +globalThis._jasmineEnv.addReporter(new TapReporter()); + +// If we're running the tests in certain JS_GC_ZEAL modes, then some will time +// out if the CI machine is under a certain load. In that case increase the +// default timeout. +const gcZeal = GLib.getenv('JS_GC_ZEAL'); +if (gcZeal && (gcZeal === '2' || gcZeal.startsWith('2,') || gcZeal === '4')) + jasmine.DEFAULT_TIMEOUT_INTERVAL *= 5; diff --git a/installed-tests/js/modules/alwaysThrows.js b/installed-tests/js/modules/alwaysThrows.js index 50af1ff..13f7b22 100644 --- a/installed-tests/js/modules/alwaysThrows.js +++ b/installed-tests/js/modules/alwaysThrows.js @@ -1,4 +1,4 @@ // line 0 // line 1 // line 2 -throw new Error("This is an error that always happens on line 3"); +throw new Error('This is an error that always happens on line 3'); diff --git a/installed-tests/js/modules/badOverrides/.eslintrc.yml b/installed-tests/js/modules/badOverrides/.eslintrc.yml new file mode 100644 index 0000000..c65c160 --- /dev/null +++ b/installed-tests/js/modules/badOverrides/.eslintrc.yml @@ -0,0 +1,6 @@ +--- +rules: + no-throw-literal: 'off' # these are intended to be bad code + no-unused-vars: + - error + - varsIgnorePattern: ^_init$ diff --git a/installed-tests/js/modules/badOverrides/GIMarshallingTests.js b/installed-tests/js/modules/badOverrides/GIMarshallingTests.js new file mode 100644 index 0000000..6264d3e --- /dev/null +++ b/installed-tests/js/modules/badOverrides/GIMarshallingTests.js @@ -0,0 +1,3 @@ +// Sabotage the import of imports.gi.GIMarshallingTests! + +throw '💩'; diff --git a/installed-tests/js/modules/overrides/Gio.js b/installed-tests/js/modules/badOverrides/Gio.js similarity index 100% rename from installed-tests/js/modules/overrides/Gio.js rename to installed-tests/js/modules/badOverrides/Gio.js diff --git a/installed-tests/js/modules/overrides/Regress.js b/installed-tests/js/modules/badOverrides/Regress.js similarity index 100% rename from installed-tests/js/modules/overrides/Regress.js rename to installed-tests/js/modules/badOverrides/Regress.js diff --git a/installed-tests/js/modules/overrides/WarnLib.js b/installed-tests/js/modules/badOverrides/WarnLib.js similarity index 100% rename from installed-tests/js/modules/overrides/WarnLib.js rename to installed-tests/js/modules/badOverrides/WarnLib.js diff --git a/installed-tests/js/modules/foobar.js b/installed-tests/js/modules/foobar.js index 4ac0a24..44b3dea 100644 --- a/installed-tests/js/modules/foobar.js +++ b/installed-tests/js/modules/foobar.js @@ -1,7 +1,10 @@ // simple test module (used by testImporter.js) -var foo = "This is foo"; -var bar = "This is bar"; +/* eslint no-redeclare: ["error", { "builtinGlobals": false }] */ // for toString +/* exported bar, foo, testToString, toString */ + +var foo = 'This is foo'; +var bar = 'This is bar'; var toString = x => x; diff --git a/installed-tests/js/modules/modunicode.js b/installed-tests/js/modules/modunicode.js index 560f5c8..5503345 100644 --- a/installed-tests/js/modules/modunicode.js +++ b/installed-tests/js/modules/modunicode.js @@ -1,3 +1,4 @@ +/* exported uval */ // This file is written in UTF-8. -var uval = "const ♥ utf8"; +var uval = 'const ♥ utf8'; diff --git a/installed-tests/js/modules/mutualImport/a.js b/installed-tests/js/modules/mutualImport/a.js index c5fd069..253d3c2 100644 --- a/installed-tests/js/modules/mutualImport/a.js +++ b/installed-tests/js/modules/mutualImport/a.js @@ -1,3 +1,5 @@ +/* exported getCount, getCountViaB, incrementCount */ + const B = imports.mutualImport.b; let count = 0; diff --git a/installed-tests/js/modules/mutualImport/b.js b/installed-tests/js/modules/mutualImport/b.js index b6443b1..d481760 100644 --- a/installed-tests/js/modules/mutualImport/b.js +++ b/installed-tests/js/modules/mutualImport/b.js @@ -1,3 +1,5 @@ +/* exported getCount */ + const A = imports.mutualImport.a; function getCount() { diff --git a/installed-tests/js/modules/overrides/.eslintrc.yml b/installed-tests/js/modules/overrides/.eslintrc.yml new file mode 100644 index 0000000..189476b --- /dev/null +++ b/installed-tests/js/modules/overrides/.eslintrc.yml @@ -0,0 +1,5 @@ +--- +rules: + no-unused-vars: + - error + - varsIgnorePattern: ^_init$ diff --git a/installed-tests/js/modules/overrides/GIMarshallingTests.js b/installed-tests/js/modules/overrides/GIMarshallingTests.js index 6264d3e..7fee428 100644 --- a/installed-tests/js/modules/overrides/GIMarshallingTests.js +++ b/installed-tests/js/modules/overrides/GIMarshallingTests.js @@ -1,3 +1,24 @@ -// Sabotage the import of imports.gi.GIMarshallingTests! +function _init() { + const GIMarshallingTests = this; -throw '💩'; + GIMarshallingTests.OVERRIDES_CONSTANT = 7; + + GIMarshallingTests.OverridesStruct.prototype._real_method = + GIMarshallingTests.OverridesStruct.prototype.method; + GIMarshallingTests.OverridesStruct.prototype.method = function () { + return this._real_method() / 7; + }; + + GIMarshallingTests.OverridesObject.prototype._realInit = + GIMarshallingTests.OverridesObject.prototype._init; + GIMarshallingTests.OverridesObject.prototype._init = function (num, ...args) { + this._realInit(...args); + this.num = num; + }; + + GIMarshallingTests.OverridesObject.prototype._realMethod = + GIMarshallingTests.OverridesObject.prototype.method; + GIMarshallingTests.OverridesObject.prototype.method = function () { + return this._realMethod() / 7; + }; +} diff --git a/installed-tests/js/modules/subA/subB/__init__.js b/installed-tests/js/modules/subA/subB/__init__.js index 15bab86..671cffc 100644 --- a/installed-tests/js/modules/subA/subB/__init__.js +++ b/installed-tests/js/modules/subA/subB/__init__.js @@ -1,5 +1,7 @@ +/* exported ImporterClass, testImporterFunction */ + function testImporterFunction() { - return "__init__ function tested"; + return '__init__ function tested'; } function ImporterClass() { @@ -7,11 +9,11 @@ function ImporterClass() { } ImporterClass.prototype = { - _init : function() { - this._a = "__init__ class tested"; + _init() { + this._a = '__init__ class tested'; }, - testMethod : function() { + testMethod() { return this._a; - } + }, }; diff --git a/installed-tests/js/modules/subA/subB/foobar.js b/installed-tests/js/modules/subA/subB/foobar.js index cdd5cad..92c69b3 100644 --- a/installed-tests/js/modules/subA/subB/foobar.js +++ b/installed-tests/js/modules/subA/subB/foobar.js @@ -1,4 +1,6 @@ // simple test module (used by testImporter.js) -var foo = "This is foo"; -var bar = "This is bar"; +/* exported bar, foo */ + +var foo = 'This is foo'; +var bar = 'This is bar'; diff --git a/installed-tests/js/org.cinnamon.CjsTest.gschema.xml b/installed-tests/js/org.cinnamon.CjsTest.gschema.xml new file mode 100644 index 0000000..4e8615c --- /dev/null +++ b/installed-tests/js/org.cinnamon.CjsTest.gschema.xml @@ -0,0 +1,21 @@ + + + + + (-1, -1) + + + false + + + false + + + + + + + 10 + + + diff --git a/installed-tests/js/testByteArray.js b/installed-tests/js/testByteArray.js index 57ec948..227101f 100644 --- a/installed-tests/js/testByteArray.js +++ b/installed-tests/js/testByteArray.js @@ -1,93 +1,7 @@ const ByteArray = imports.byteArray; +const {GIMarshallingTests, GLib} = imports.gi; describe('Byte array', function () { - it('has length 0 for empty array', function () { - let a = new ByteArray.ByteArray(); - expect(a.length).toEqual(0); - }); - - describe('initially sized to 10', function () { - let a; - beforeEach(function () { - a = new ByteArray.ByteArray(10); - }); - - it('has length 10', function () { - expect(a.length).toEqual(10); - }); - - it('is initialized to zeroes', function () { - for (let i = 0; i < a.length; ++i) { - expect(a[i]).toEqual(0); - } - }); - }); - - it('assigns values correctly', function () { - let a = new ByteArray.ByteArray(256); - - for (let i = 0; i < a.length; ++i) { - a[i] = 255 - i; - } - - for (let i = 0; i < a.length; ++i) { - expect(a[i]).toEqual(255 - i); - } - }); - - describe('assignment past end', function () { - let a; - beforeEach(function () { - a = new ByteArray.ByteArray(); - a[2] = 5; - }); - - it('implicitly lengthens the array', function () { - expect(a.length).toEqual(3); - expect(a[2]).toEqual(5); - }); - - it('implicitly creates zero bytes', function () { - expect(a[0]).toEqual(0); - expect(a[1]).toEqual(0); - }); - }); - - it('changes the length when assigning to length property', function () { - let a = new ByteArray.ByteArray(20); - expect(a.length).toEqual(20); - a.length = 5; - expect(a.length).toEqual(5); - }); - - describe('conversions', function () { - let a; - beforeEach(function () { - a = new ByteArray.ByteArray(); - a[0] = 255; - }); - - it('gives a byte 5 when assigning 5', function () { - a[0] = 5; - expect(a[0]).toEqual(5); - }); - - it('gives a byte 0 when assigning null', function () { - a[0] = null; - expect(a[0]).toEqual(0); - }); - - it('gives a byte 0 when assigning undefined', function () { - a[0] = undefined; - expect(a[0]).toEqual(0); - }); - - it('rounds off when assigning a double', function () { - a[0] = 3.14; - expect(a[0]).toEqual(3); - }); - }); - it('can be created from a string', function () { let a = ByteArray.fromString('abcd'); expect(a.length).toEqual(4); @@ -112,20 +26,69 @@ describe('Byte array', function () { [0xe2, 0x85, 0x9c].forEach((val, ix) => expect(a[ix]).toEqual(val)); }); - it('can be created from an array', function () { - let a = ByteArray.fromArray([ 1, 2, 3, 4 ]); - expect(a.length).toEqual(4); - [1, 2, 3, 4].forEach((val, ix) => expect(a[ix]).toEqual(val)); - }); - it('can be converted to a string of ASCII characters', function () { - let a = new ByteArray.ByteArray(); + let a = new Uint8Array(4); a[0] = 97; a[1] = 98; a[2] = 99; a[3] = 100; - let s = a.toString(); + let s = ByteArray.toString(a); + expect(s.length).toEqual(4); + expect(s).toEqual('abcd'); + }); + + it('can be converted to a string of UTF-8 characters even if it ends with a 0', function () { + const a = Uint8Array.of(97, 98, 99, 100, 0); + const s = ByteArray.toString(a); + expect(s.length).toEqual(4); + expect(s).toEqual('abcd'); + }); + + it('can be converted to a string of encoded characters even with a 0 byte', function () { + const a = Uint8Array.of(97, 98, 99, 100, 0); + const s = ByteArray.toString(a, 'LATIN1'); expect(s.length).toEqual(4); expect(s).toEqual('abcd'); }); + + it('stops converting to a string at an embedded 0 byte', function () { + const a = Uint8Array.of(97, 98, 0, 99, 100); + const s = ByteArray.toString(a); + expect(s.length).toEqual(2); + expect(s).toEqual('ab'); + }); + + it('deals gracefully with a 0-length array', function () { + const a = new Uint8Array(0); + expect(ByteArray.toString(a)).toEqual(''); + expect(ByteArray.toGBytes(a).get_size()).toEqual(0); + }); + + it('deals gracefully with a non Uint8Array', function () { + const a = [97, 98, 99, 100, 0]; + expect(() => ByteArray.toString(a)).toThrow(); + expect(() => ByteArray.toGBytes(a)).toThrow(); + }); + + describe('legacy toString() behavior', function () { + beforeEach(function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + 'Some code called array.toString()*'); + }); + + it('is preserved when created from a string', function () { + let a = ByteArray.fromString('⅜'); + expect(a.toString()).toEqual('⅜'); + }); + + it('is preserved when marshalled from GI', function () { + let a = GIMarshallingTests.bytearray_full_return(); + expect(a.toString()).toEqual(''); + }); + + afterEach(function () { + GLib.test_assert_expected_messages_internal('Cjs', + 'testByteArray.js', 0, 'testToStringCompatibility'); + }); + }); }); diff --git a/installed-tests/js/testCairo.js b/installed-tests/js/testCairo.js index db20de0..78f0efb 100644 --- a/installed-tests/js/testCairo.js +++ b/installed-tests/js/testCairo.js @@ -2,22 +2,16 @@ imports.gi.versions.Gdk = '3.0'; imports.gi.versions.Gtk = '3.0'; const Cairo = imports.cairo; -const Gdk = imports.gi.Gdk; -const Gtk = imports.gi.Gtk; -const Regress = imports.gi.Regress; +const {Gdk, GIMarshallingTests, GLib, Gtk, Regress} = imports.gi; function _ts(obj) { return obj.toString().slice(8, -1); } describe('Cairo', function () { - beforeAll(function () { - Gtk.init(null); - }); - let cr, surface; beforeEach(function () { - surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 10, 10); cr = new Cairo.Context(surface); }); @@ -137,6 +131,8 @@ describe('Cairo', function () { cr.paint(); cr.paintWithAlpha(1); + cr.setDash([1, 0.5], 1); + cr.stroke(); cr.strokePreserve(); @@ -150,10 +146,10 @@ describe('Cairo', function () { cr.rotate(180); cr.identityMatrix(); - cr.showText("foobar"); + cr.showText('foobar'); cr.moveTo(0, 0); - cr.setDash([1, 0.5], 1); + cr.setDash([], 1); cr.lineTo(1, 0); cr.lineTo(1, 1); cr.lineTo(0, 1); @@ -165,15 +161,12 @@ describe('Cairo', function () { }).not.toThrow(); }); - it('can be marshalled through a signal handler', function () { - let o = new Regress.TestObj(); - let foreignSpy = jasmine.createSpy('sig-with-foreign-struct'); - o.connect('sig-with-foreign-struct', foreignSpy); - o.emit_sig_with_foreign_struct(); - expect(foreignSpy).toHaveBeenCalledWith(o, cr); - }); - it('has methods when created from a C function', function () { + if (GLib.getenv('ENABLE_GTK') !== 'yes') { + pending('GTK disabled'); + return; + } + Gtk.init(null); let win = new Gtk.OffscreenWindow(); let da = new Gtk.DrawingArea(); win.add(da); @@ -185,6 +178,13 @@ describe('Cairo', function () { }); }); + describe('pattern', function () { + it('has typechecks', function () { + expect(() => cr.setSource({})).toThrow(); + expect(() => cr.setSource(surface)).toThrow(); + }); + }); + describe('solid pattern', function () { it('can be created from RGB static method', function () { let p1 = Cairo.SolidPattern.createRGB(1, 2, 3); @@ -227,6 +227,74 @@ describe('Cairo', function () { expect(_ts(cr.getSource())).toEqual('RadialGradient'); }); }); + + describe('path', function () { + it('has typechecks', function () { + expect(() => cr.appendPath({})).toThrow(); + expect(() => cr.appendPath(surface)).toThrow(); + }); + }); + + describe('surface', function () { + it('has typechecks', function () { + expect(() => new Cairo.Context({})).toThrow(); + const pattern = new Cairo.SurfacePattern(surface); + expect(() => new Cairo.Context(pattern)).toThrow(); + }); + }); + + describe('GI test suite', function () { + describe('for context', function () { + it('can be marshalled as a return value', function () { + const outCr = Regress.test_cairo_context_full_return(); + const outSurface = outCr.getTarget(); + expect(outSurface.getFormat()).toEqual(Cairo.Format.ARGB32); + expect(outSurface.getWidth()).toEqual(10); + expect(outSurface.getHeight()).toEqual(10); + }); + + it('can be marshalled as an in parameter', function () { + expect(() => Regress.test_cairo_context_none_in(cr)).not.toThrow(); + }); + }); + + describe('for surface', function () { + ['none', 'full'].forEach(transfer => { + it(`can be marshalled as a transfer-${transfer} return value`, function () { + const outSurface = Regress[`test_cairo_surface_${transfer}_return`](); + expect(outSurface.getFormat()).toEqual(Cairo.Format.ARGB32); + expect(outSurface.getWidth()).toEqual(10); + expect(outSurface.getHeight()).toEqual(10); + }); + }); + + it('can be marshalled as an in parameter', function () { + expect(() => Regress.test_cairo_surface_none_in(surface)).not.toThrow(); + }); + + it('can be marshalled as an out parameter', function () { + const outSurface = Regress.test_cairo_surface_full_out(); + expect(outSurface.getFormat()).toEqual(Cairo.Format.ARGB32); + expect(outSurface.getWidth()).toEqual(10); + expect(outSurface.getHeight()).toEqual(10); + }); + }); + + it('can be marshalled through a signal handler', function () { + let o = new Regress.TestObj(); + let foreignSpy = jasmine.createSpy('sig-with-foreign-struct'); + o.connect('sig-with-foreign-struct', foreignSpy); + o.emit_sig_with_foreign_struct(); + expect(foreignSpy).toHaveBeenCalledWith(o, cr); + }); + + it('can have its type inferred as a foreign struct', function () { + expect(() => GIMarshallingTests.gvalue_in_with_type(cr, Cairo.Context)) + .not.toThrow(); + expect(() => GIMarshallingTests.gvalue_in_with_type(surface, Cairo.Surface)) + .not.toThrow(); + }); + }); }); describe('Cairo imported via GI', function () { diff --git a/installed-tests/js/testEverythingBasic.js b/installed-tests/js/testEverythingBasic.js deleted file mode 100644 index 8849fc2..0000000 --- a/installed-tests/js/testEverythingBasic.js +++ /dev/null @@ -1,755 +0,0 @@ -const Regress = imports.gi.Regress; -const WarnLib = imports.gi.WarnLib; - -// We use Gio to have some objects that we know exist -imports.gi.versions.Gdk = '3.0'; -const Gdk = imports.gi.Gdk; -const GLib = imports.gi.GLib; -const Gio = imports.gi.Gio; -const GObject = imports.gi.GObject; - -describe('Life, the Universe and Everything', function () { - it('includes booleans', function () { - expect(Regress.test_boolean(false)).toBe(false); - expect(Regress.test_boolean(true)).toBe(true); - }); - - [8, 16, 32, 64].forEach(bits => { - it('includes ' + bits + '-bit integers', function () { - let method = 'test_int' + bits; - expect(Regress[method](42)).toBe(42); - expect(Regress[method](-42)).toBe(-42); - }); - - it('includes unsigned ' + bits + '-bit integers', function () { - let method = 'test_uint' + bits; - expect(Regress[method](42)).toBe(42); - }); - }); - - ['short', 'int', 'long', 'ssize', 'float', 'double'].forEach(type => { - it('includes ' + type + 's', function () { - let method = 'test_' + type; - expect(Regress[method](42)).toBe(42); - expect(Regress[method](-42)).toBe(-42); - }); - }); - - ['ushort', 'uint', 'ulong', 'size'].forEach(type => { - it('includes ' + type + 's', function () { - let method = 'test_' + type; - expect(Regress[method](42)).toBe(42); - }); - }); - - it('includes wide characters', function () { - expect(Regress.test_unichar('c')).toBe('c'); - expect(Regress.test_unichar('')).toBe(''); - expect(Regress.test_unichar('\u2665')).toBe('\u2665'); - }); - - it('includes time_t', function () { - let now = Math.floor(new Date().getTime() / 1000); - let bounced = Math.floor(Regress.test_timet(now)); - expect(bounced).toEqual(now); - }); - - describe('Limits', function () { - const Limits = { - '8': { - MIN: -128, - MAX: 127, - UMAX: 255, - }, - '16': { - MIN: -32767 - 1, - MAX: 32767, - UMAX: 65535, - }, - '32': { - MIN: -2147483647 - 1, - MAX: 2147483647, - UMAX: 4294967295, - }, - '64': { - MIN: -9223372036854775807 - 1, - MAX: 9223372036854775807, - UMAX: 18446744073709551615, - }, - }; - - const skip = { - 'UMAX64': true, // FAIL: expected 18446744073709552000, got 0 - 'MAX64': true, // FAIL: expected 9223372036854776000, got -9223372036854776000 - }; - - function run_test(bytes, limit, method_stem) { - if(skip[limit + bytes]) - pending("This test doesn't work"); - - if (bytes === '64') - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - "*cannot be safely stored*"); - - let val = Limits[bytes][limit]; - expect(Regress[method_stem + bytes](val)).toBe(val); - - if (bytes === '64') - GLib.test_assert_expected_messages_internal('Gjs', - 'testEverythingBasic.js', 0, 'Ignore message'); - } - ['8', '16', '32', '64'].forEach(bytes => { - it('marshals max value of unsigned ' + bytes + '-bit integers', function () { - run_test(bytes, 'UMAX', 'test_uint'); - }); - - it('marshals min value of signed ' + bytes + '-bit integers', function () { - run_test(bytes, 'MIN', 'test_int'); - }); - - it('marshals max value of signed ' + bytes + '-bit integers', function () { - run_test(bytes, 'MAX', 'test_int'); - }); - }); - - it('warns when conversion is lossy', function () { - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - "*cannot be safely stored*"); - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - "*cannot be safely stored*"); - void GLib.MAXINT64; - void GLib.MAXUINT64; - GLib.test_assert_expected_messages_internal('Gjs', - 'testEverythingBasic.js', 0, - 'Limits warns when conversion is lossy'); - }); - }); - - describe('No implicit conversion to unsigned', function () { - ['uint8', 'uint16', 'uint32', 'uint64', 'uint', 'size'].forEach(type => { - it('for ' + type, function () { - expect(() => Regress['test_' + type](-42)).toThrow(); - }); - }); - }); - - it('throws when constructor called without new', function () { - expect(() => Gio.AppLaunchContext()) - .toThrowError(/Constructor called as normal method/); - }); - - describe('String arrays', function () { - it('marshalling in', function () { - expect(Regress.test_strv_in(['1', '2', '3'])).toBeTruthy(); - // Second two are deliberately not strings - expect(() => Regress.test_strv_in(['1', 2, 3])).toThrow(); - }); - - it('marshalling out', function () { - expect(Regress.test_strv_out()) - .toEqual(['thanks', 'for', 'all', 'the', 'fish']); - }); - - it('marshalling out with container transfer', function () { - expect(Regress.test_strv_out_container()).toEqual(['1', '2', '3']); - }); - }); - - it('in after out', function () { - const str = "hello"; - let len = Regress.test_int_out_utf8(str); - expect(len).toEqual(str.length); - }); - - describe('UTF-8 strings', function () { - const CONST_STR = "const \u2665 utf8"; - const NONCONST_STR = "nonconst \u2665 utf8"; - - it('as return types', function () { - expect(Regress.test_utf8_const_return()).toEqual(CONST_STR); - expect(Regress.test_utf8_nonconst_return()).toEqual(NONCONST_STR); - }); - - it('as in parameters', function () { - Regress.test_utf8_const_in(CONST_STR); - }); - - it('as out parameters', function () { - expect(Regress.test_utf8_out()).toEqual(NONCONST_STR); - }); - - // FIXME: this is broken due to a change in gobject-introspection. - xit('as in-out parameters', function () { - expect(Regress.test_utf8_inout(CONST_STR)).toEqual(NONCONST_STR); - }).pend('https://bugzilla.gnome.org/show_bug.cgi?id=736517'); - }); - - it('return values in filename encoding', function () { - let filenames = Regress.test_filename_return(); - expect(filenames).toEqual(['\u00e5\u00e4\u00f6', '/etc/fstab']); - }); - - it('static methods', function () { - let v = Regress.TestObj.new_from_file("/enoent"); - expect(v instanceof Regress.TestObj).toBeTruthy(); - }); - - it('closures', function () { - let callback = jasmine.createSpy('callback').and.returnValue(42); - expect(Regress.test_closure(callback)).toEqual(42); - expect(callback).toHaveBeenCalledWith(); - }); - - it('closures with one argument', function () { - let callback = jasmine.createSpy('callback') - .and.callFake(someValue => someValue); - expect(Regress.test_closure_one_arg(callback, 42)).toEqual(42); - expect(callback).toHaveBeenCalledWith(42); - }); - - it('callbacks', function () { - let callback = jasmine.createSpy('callback').and.returnValue(42); - expect(Regress.test_callback(callback)).toEqual(42); - }); - - it('null / undefined callback', function () { - expect(Regress.test_callback(null)).toEqual(0); - expect(() => Regress.test_callback(undefined)).toThrow(); - }); - - it('array callbacks', function () { - let callback = jasmine.createSpy('callback').and.returnValue(7); - expect(Regress.test_array_callback(callback)).toEqual(14); - expect(callback).toHaveBeenCalledWith([-1, 0, 1, 2], ["one", "two", "three"]); - }); - - it('null array callback', function () { - expect(() => Regress.test_array_callback(null)).toThrow(); - }); - - it('callback with transfer-full return value', function () { - function callback() { - return Regress.TestObj.new_from_file("/enoent"); - } - Regress.test_callback_return_full(callback); - }); - - it('callback with destroy-notify', function () { - let testObj = { - test: function (data) { return data; }, - }; - spyOn(testObj, 'test').and.callThrough(); - expect(Regress.test_callback_destroy_notify(function () { - return testObj.test(42); - }.bind(testObj))).toEqual(42); - expect(testObj.test).toHaveBeenCalledTimes(1); - expect(Regress.test_callback_thaw_notifications()).toEqual(42); - }); - - it('async callback', function () { - Regress.test_callback_async(() => 44); - expect(Regress.test_callback_thaw_async()).toEqual(44); - }); - - describe('GValue boxing and unboxing', function () { - it('integer in', function () { - expect(Regress.test_int_value_arg(42)).toEqual(42); - }); - - it('integer out', function () { - expect(Regress.test_value_return(42)).toEqual(42); - }); - - it('strv in', function () { - expect(Regress.test_strv_in_gvalue()).toEqual(['one', 'two', 'three']); - }); - - const GVALUE_HASH_TABLE = { - 'integer': 12, - 'boolean': true, - 'string': 'some text', - 'strings': ['first', 'second', 'third'], - 'flags': Regress.TestFlags.FLAG1 | Regress.TestFlags.FLAG3, - 'enum': Regress.TestEnum.VALUE2, - }; - - it('hashtable with GValue value type out', function () { - expect(Regress.test_ghash_gvalue_return()).toEqual(GVALUE_HASH_TABLE); - }); - }); - - ['glist', 'gslist'].forEach(list => { - describe(list + ' types', function () { - const STR_LIST = ['1', '2', '3']; - - it('return with transfer-none', function () { - expect(Regress['test_' + list + '_nothing_return']()).toEqual(STR_LIST); - expect(Regress['test_' + list + '_nothing_return2']()).toEqual(STR_LIST); - }); - - it('return with transfer-container', function () { - expect(Regress['test_' + list + '_container_return']()).toEqual(STR_LIST); - }); - - it('return with transfer-full', function () { - expect(Regress['test_' + list + '_everything_return']()).toEqual(STR_LIST); - }); - - it('in with transfer-none', function () { - Regress['test_' + list + '_nothing_in'](STR_LIST); - Regress['test_' + list + '_nothing_in2'](STR_LIST); - }); - - xit('in with transfer-container', function () { - Regress['test_' + list + '_container_in'](STR_LIST); - }).pend('Function not added to gobject-introspection test suite yet'); - }); - }); - - ['int', 'gint8', 'gint16', 'gint32', 'gint64'].forEach(inttype => { - it('arrays of ' + inttype + ' in', function () { - expect(Regress['test_array_' + inttype + '_in']([1, 2, 3, 4])).toEqual(10); - }); - }); - - it('implicit conversions from strings to int arrays', function () { - expect(Regress.test_array_gint8_in("\x01\x02\x03\x04")).toEqual(10); - expect(Regress.test_array_gint16_in("\x01\x02\x03\x04")).toEqual(10); - expect(Regress.test_array_gint16_in("\u0100\u0200\u0300\u0400")).toEqual(2560); - }); - - it('GType arrays', function () { - expect(Regress.test_array_gtype_in([Gio.SimpleAction, Gio.Icon, GObject.TYPE_BOXED])) - .toEqual('[GSimpleAction,GIcon,GBoxed,]'); - expect(() => Regress.test_array_gtype_in(42)).toThrow(); - expect(() => Regress.test_array_gtype_in([undefined])).toThrow(); - // 80 is G_TYPE_OBJECT, but we don't want it to work - expect(() => Regress.test_array_gtype_in([80])).toThrow(); - }); - - it('out arrays of integers', function () { - expect(Regress.test_array_int_out()).toEqual([0, 1, 2, 3, 4]); - - let array = Regress.test_array_fixed_size_int_out(); - expect(array[0]).toEqual(0); - expect(array[4]).toEqual(4); - array = Regress.test_array_fixed_size_int_return(); - expect(array[0]).toEqual(0); - expect(array[4]).toEqual(4); - - expect(Regress.test_array_int_none_out()).toEqual([1, 2, 3, 4, 5]); - - expect(Regress.test_array_int_full_out()).toEqual([0, 1, 2, 3, 4]); - - expect(Regress.test_array_int_null_out()).toEqual([]); - }); - - it('null in-array', function () { - Regress.test_array_int_null_in(null); - }); - - it('out arrays of structs', function () { - let array = Regress.test_array_struct_out(); - let ints = array.map(struct => struct.some_int); - expect(ints).toEqual([22, 33, 44]); - }); - - describe('GHash type', function () {; - const EXPECTED_HASH = { baz: 'bat', foo: 'bar', qux: 'quux' }; - - it('null GHash in', function () { - Regress.test_ghash_null_in(null); - }); - - it('null GHash out', function () { - expect(Regress.test_ghash_null_return()).toBeNull(); - }); - - it('out GHash', function () { - expect(Regress.test_ghash_nothing_return()).toEqual(EXPECTED_HASH); - expect(Regress.test_ghash_nothing_return2()).toEqual(EXPECTED_HASH); - expect(Regress.test_ghash_container_return()).toEqual(EXPECTED_HASH); - expect(Regress.test_ghash_everything_return()).toEqual(EXPECTED_HASH); - }); - - it('in GHash', function () { - Regress.test_ghash_nothing_in(EXPECTED_HASH); - Regress.test_ghash_nothing_in2(EXPECTED_HASH); - }); - - it('nested GHash', function () { - const EXPECTED_NESTED_HASH = { wibble: EXPECTED_HASH }; - - expect(Regress.test_ghash_nested_everything_return()) - .toEqual(EXPECTED_NESTED_HASH); - expect(Regress.test_ghash_nested_everything_return2()) - .toEqual(EXPECTED_NESTED_HASH); - }); - }); - - it('enum parameter', function () { - expect(Regress.test_enum_param(Regress.TestEnum.VALUE1)).toEqual('value1'); - expect(Regress.test_enum_param(Regress.TestEnum.VALUE3)).toEqual('value3'); - }); - - it('unsigned enum parameter', function () { - expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE1)) - .toEqual('value1'); - expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE2)) - .toEqual('value2'); - }); - - it('enum has a $gtype property', function () { - expect(Regress.TestEnumUnsigned.$gtype).toBeDefined(); - }); - - it('enum $gtype property is enumerable', function () { - expect('$gtype' in Regress.TestEnumUnsigned).toBeTruthy(); - }); - - it('Number converts error to quark', function () { - expect(Regress.TestError.quark()).toEqual(Number(Regress.TestError)); - }); - - it('converts enum to string', function () { - expect(Regress.TestEnum.param(Regress.TestEnum.VALUE4)).toEqual('value4'); - }); - - describe('Object-valued GProperty', function () { - let o1, t1, t2; - beforeEach(function () { - o1 = new GObject.Object(); - t1 = new Regress.TestObj({bare: o1}); - t2 = new Regress.TestSubObj(); - t2.bare = o1; - }); - - it('marshals correctly in the getter', function () { - expect(t1.bare).toBe(o1); - }); - - it('marshals correctly when inherited', function () { - expect(t2.bare).toBe(o1); - }); - - it('marshals into setter function', function () { - let o2 = new GObject.Object(); - t2.set_bare(o2); - expect(t2.bare).toBe(o2); - }); - - it('marshals null', function () { - t2.unset_bare(); - expect(t2.bare).toBeNull(); - }); - }); - - describe('Signal connection', function () { - let o; - beforeEach(function () { - o = new Regress.TestObj(); - }); - - it('calls correct handlers with correct arguments', function () { - let handler = jasmine.createSpy('handler'); - let handlerId = o.connect('test', handler); - handler.and.callFake(() => o.disconnect(handlerId)); - - o.emit('test'); - expect(handler).toHaveBeenCalledTimes(1); - expect(handler).toHaveBeenCalledWith(o); - - handler.calls.reset(); - o.emit('test'); - expect(handler).not.toHaveBeenCalled(); - }); - - it('throws errors for invalid signals', function () { - expect(() => o.connect('invalid-signal', o => {})).toThrow(); - expect(() => o.emit('invalid-signal')).toThrow(); - }); - - it('signal handler with static scope arg gets arg passed by reference', function () { - let b = new Regress.TestSimpleBoxedA({ - some_int: 42, - some_int8: 43, - some_double: 42.5, - some_enum: Regress.TestEnum.VALUE3, - }); - o.connect('test-with-static-scope-arg', (signalObject, signalArg) => { - signalArg.some_int = 44; - }); - o.emit('test-with-static-scope-arg', b); - expect(b.some_int).toEqual(44); - }); - - it('signal with array len parameter is not passed correct array and no length arg', function (done) { - o.connect('sig-with-array-len-prop', (signalObj, signalArray, shouldBeUndefined) => { - expect(shouldBeUndefined).not.toBeDefined(); - expect(signalArray).toEqual([0, 1, 2, 3, 4]); - done(); - }); - o.emit_sig_with_array_len_prop(); - }); - - xit('can pass parameter to signal with array len parameter via emit', function (done) { - o.connect('sig-with-array-len-prop', (signalObj, signalArray) => { - expect(signalArray).toEqual([0, 1, 2, 3, 4]); - done(); - }); - o.emit('sig-with-array-len-prop', [0, 1, 2, 3, 4]); - }).pend('Not yet implemented'); - - xit('can pass null to signal with array len parameter', function () { - let handler = jasmine.createSpy('handler'); - o.connect('sig-with-array-len-prop', handler); - o.emit('sig-with-array-len-prop', null); - expect(handler).toHaveBeenCalledWith([jasmine.any(Object), null]); - }).pend('Not yet implemented'); - }); - - describe('Signal alternative syntax', function () { - let o, handler; - beforeEach(function () { - handler = jasmine.createSpy('handler'); - o = new Regress.TestObj(); - let handlerId = GObject.signal_connect(o, 'test', handler); - handler.and.callFake(() => - GObject.signal_handler_disconnect(o, handlerId)); - - GObject.signal_emit_by_name(o, 'test'); - }); - - it('handler is called with the right object', function () { - expect(handler).toHaveBeenCalledTimes(1); - expect(handler).toHaveBeenCalledWith(o); - }); - - it('disconnected handler is not called', function () { - handler.calls.reset(); - GObject.signal_emit_by_name(o, 'test'); - expect(handler).not.toHaveBeenCalled(); - }); - }); - - it('torture signature 0', function () { - let [y, z, q] = Regress.test_torture_signature_0(42, 'foo', 7); - expect(Math.floor(y)).toEqual(42); - expect(z).toEqual(84); - expect(q).toEqual(10); - }); - - it('torture signature 1 fail', function () { - expect(() => Regress.test_torture_signature_1(42, 'foo', 7)).toThrow(); - }); - - it('torture signature 1 success', function () { - let [, y, z, q] = Regress.test_torture_signature_1(11, 'barbaz', 8); - expect(Math.floor(y)).toEqual(11); - expect(z).toEqual(22); - expect(q).toEqual(14); - }); - - it('torture signature 2', function () { - let [y, z, q] = Regress.test_torture_signature_2(42, () => 0, 'foo', 7); - expect(Math.floor(y)).toEqual(42); - expect(z).toEqual(84); - expect(q).toEqual(10); - }); - - describe('Object torture signature', function () { - let o; - beforeEach(function () { - o = new Regress.TestObj(); - }); - - it('0', function () { - let [y, z, q] = o.torture_signature_0(42, 'foo', 7); - expect(Math.floor(y)).toEqual(42); - expect(z).toEqual(84); - expect(q).toEqual(10); - }); - - it('1 fail', function () { - expect(() => o.torture_signature_1(42, 'foo', 7)).toThrow(); - }); - - it('1 success', function () { - let [, y, z, q] = o.torture_signature_1(11, 'barbaz', 8); - expect(Math.floor(y)).toEqual(11); - expect(z).toEqual(22); - expect(q).toEqual(14); - }); - }); - - // Cannot access the variant contents, for now - it('integer GVariant', function () { - let ivar = Regress.test_gvariant_i(); - expect(ivar.get_type_string()).toEqual('i'); - expect(ivar.equal(GLib.Variant.new_int32(1))).toBeTruthy(); - }); - - it('string GVariant', function () { - let svar = Regress.test_gvariant_s(); - expect(String.fromCharCode(svar.classify())).toEqual('s'); - expect(svar.get_string()[0]).toEqual('one'); - }); - - it('a{sv} GVariant', function () { - let asvvar = Regress.test_gvariant_asv(); - expect(asvvar.n_children()).toEqual(2); - }); - - it('as Variant', function () { - let asvar = Regress.test_gvariant_as(); - expect(asvar.get_strv()).toEqual(['one', 'two', 'three']); - }); - - it('error enum names match error quarks', function () { - expect(Number(Gio.IOErrorEnum)).toEqual(Gio.io_error_quark()); - }); - - describe('thrown GError', function () { - let err; - beforeEach(function () { - try { - let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist"); - file.read(null); - } catch (x) { - err = x; - } - }); - - it('is an instance of error enum type', function () { - expect(err instanceof Gio.IOErrorEnum).toBeTruthy(); - }); - - it('matches error domain and code', function () { - expect(err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND)) - .toBeTruthy(); - }); - - it('has properties for domain and code', function () { - expect(err.domain).toEqual(Gio.io_error_quark()); - expect(err.code).toEqual(Gio.IOErrorEnum.NOT_FOUND); - }); - }); - - it('GError callback', function (done) { - Regress.test_gerror_callback(e => { - expect(e instanceof Gio.IOErrorEnum).toBeTruthy(); - expect(e.domain).toEqual(Gio.io_error_quark()); - expect(e.code).toEqual(Gio.IOErrorEnum.NOT_SUPPORTED); - done(); - }); - }); - - it('owned GError callback', function (done) { - Regress.test_owned_gerror_callback(e => { - expect(e instanceof Gio.IOErrorEnum).toBeTruthy(); - expect(e.domain).toEqual(Gio.io_error_quark()); - expect(e.code).toEqual(Gio.IOErrorEnum.PERMISSION_DENIED); - done(); - }); - }); - - // Calling matches() on an unpaired error used to JSUnit.assert: - // https://bugzilla.gnome.org/show_bug.cgi?id=689482 - it('bug 689482', function () { - try { - WarnLib.throw_unpaired(); - fail(); - } catch (e) { - expect(e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)).toBeFalsy(); - } - }); - - it('correctly converts a NULL strv in a GValue to an empty array', function() { - let v = Regress.test_null_strv_in_gvalue(); - expect(v.length).toEqual(0); - }); - - describe('wrong type for GObject', function () { - let wrongObject, wrongBoxed, subclassObject; - beforeEach(function () { - wrongObject = new Gio.SimpleAction(); - wrongBoxed = new GLib.KeyFile(); - subclassObject = new Regress.TestSubObj(); - }); - - // Everything.func_obj_null_in expects a Everything.TestObj - it('function does not accept a GObject of the wrong type', function () { - expect(() => Regress.func_obj_null_in(wrongObject)).toThrow(); - }); - - it('function does not accept a GBoxed instead of GObject', function () { - expect(() => Regress.func_obj_null_in(wrongBoxed)).toThrow(); - }); - - it('function does not accept returned GObject of the wrong type', function () { - let wrongReturnedObject = Gio.File.new_for_path('/'); - expect(() => Regress.func_obj_null_in(wrongReturnedObject)).toThrow(); - }); - - it('function accepts GObject of subclass of expected type', function () { - expect(() => Regress.func_obj_null_in(subclassObject)).not.toThrow(); - }); - - it('method cannot be called on a GObject of the wrong type', function () { - expect(() => Regress.TestObj.prototype.instance_method.call(wrongObject)) - .toThrow(); - }); - - it('method cannot be called on a GBoxed', function () { - expect(() => Regress.TestObj.prototype.instance_method.call(wrongBoxed)) - .toThrow(); - }); - - it('method can be called on a GObject of subclass of expected type', function () { - expect(() => Regress.TestObj.prototype.instance_method.call(subclassObject)) - .not.toThrow(); - }); - }); - - describe('wrong type for GBoxed', function () { - let simpleBoxed, wrongObject, wrongBoxed; - beforeEach(function () { - simpleBoxed = new Regress.TestSimpleBoxedA(); - wrongObject = new Gio.SimpleAction(); - wrongBoxed = new GLib.KeyFile(); - }); - - // simpleBoxed.equals expects a Everything.TestSimpleBoxedA - it('function does not accept a GObject of the wrong type', function () { - expect(() => simpleBoxed.equals(wrongObject)).toThrow(); - }); - - it('function does not accept a GBoxed of the wrong type', function () { - expect(() => simpleBoxed.equals(wrongBoxed)).toThrow(); - }); - - it('function does accept a GBoxed of the correct type', function () { - expect(simpleBoxed.equals(simpleBoxed)).toBeTruthy(); - }); - - it('method cannot be called on a GObject', function () { - expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(wrongObject)) - .toThrow(); - }); - - it('method cannot be called on a GBoxed of the wrong type', function () { - expect(() => Regress.TestSimpleBoxedA.protoype.copy.call(wrongBoxed)) - .toThrow(); - }); - - it('method can be called on correct GBoxed type', function () { - expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(simpleBoxed)) - .not.toThrow(); - }); - }); - - it('presents GdkAtom as string', function () { - expect(Gdk.Atom.intern('CLIPBOARD', false)).toBe('CLIPBOARD'); - expect(Gdk.Atom.intern('NONE', false)).toBe(null); - }); -}); diff --git a/installed-tests/js/testEverythingEncapsulated.js b/installed-tests/js/testEverythingEncapsulated.js deleted file mode 100644 index 1039f0e..0000000 --- a/installed-tests/js/testEverythingEncapsulated.js +++ /dev/null @@ -1,290 +0,0 @@ -const GLib = imports.gi.GLib; -const Regress = imports.gi.Regress; -const System = imports.system; - -describe('Introspected structs', function () { - let struct; - - describe('simple', function () { - beforeEach(function () { - struct = new Regress.TestStructA(); - struct.some_int = 42; - struct.some_int8 = 43; - struct.some_double = 42.5; - struct.some_enum = Regress.TestEnum.VALUE3; - }); - - it('sets fields correctly', function () { - expect(struct.some_int).toEqual(42); - expect(struct.some_int8).toEqual(43); - expect(struct.some_double).toEqual(42.5); - expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - - it('can clone', function () { - let b = struct.clone(); - expect(b.some_int).toEqual(42); - expect(b.some_int8).toEqual(43); - expect(b.some_double).toEqual(42.5); - expect(b.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - }); - - describe('nested', function () { - beforeEach(function () { - struct = new Regress.TestStructB(); - struct.some_int8 = 43; - struct.nested_a.some_int8 = 66; - }); - - it('sets fields correctly', function () { - expect(struct.some_int8).toEqual(43); - expect(struct.nested_a.some_int8).toEqual(66); - }); - - it('can clone', function () { - let b = struct.clone(); - expect(b.some_int8).toEqual(43); - expect(b.nested_a.some_int8).toEqual(66); - }); - }); - - describe('constructors', function () { - beforeEach(function () { - struct = new Regress.TestStructA({ - some_int: 42, - some_int8: 43, - some_double: 42.5, - some_enum: Regress.TestEnum.VALUE3, - }); - }); - - it('"copies" an object from a hash of field values', function () { - expect(struct.some_int).toEqual(42); - expect(struct.some_int8).toEqual(43); - expect(struct.some_double).toEqual(42.5); - expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - - it('catches bad field names', function () { - expect(() => new Regress.TestStructA({ junk: 42 })).toThrow(); - }); - - it('copies an object from another object of the same type', function () { - let copy = new Regress.TestStructA(struct); - expect(copy.some_int).toEqual(42); - expect(copy.some_int8).toEqual(43); - expect(copy.some_double).toEqual(42.5); - expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - }); - - it('containing fixed array', function () { - let struct = new Regress.TestStructFixedArray(); - struct.frob(); - expect(struct.just_int).toEqual(7); - expect(struct.array).toEqual([42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); - }); -}); - -describe('Introspected boxed types', function () { - let simple_boxed; - - it('sets fields correctly', function () { - simple_boxed = new Regress.TestSimpleBoxedA(); - simple_boxed.some_int = 42; - simple_boxed.some_int8 = 43; - simple_boxed.some_double = 42.5; - simple_boxed.some_enum = Regress.TestEnum.VALUE3; - expect(simple_boxed.some_int).toEqual(42); - expect(simple_boxed.some_int8).toEqual(43); - expect(simple_boxed.some_double).toEqual(42.5); - expect(simple_boxed.some_enum).toEqual(Regress.TestEnum.VALUE3); - - let boxed = new Regress.TestBoxed(); - boxed.some_int8 = 42; - expect(boxed.some_int8).toEqual(42); - }); - - describe('copy constructors', function () { - beforeEach(function () { - simple_boxed = new Regress.TestSimpleBoxedA({ - some_int: 42, - some_int8: 43, - some_double: 42.5, - some_enum: Regress.TestEnum.VALUE3, - }); - }); - - it('"copies" an object from a hash of field values', function () { - expect(simple_boxed.some_int).toEqual(42); - expect(simple_boxed.some_int8).toEqual(43); - expect(simple_boxed.some_double).toEqual(42.5); - expect(simple_boxed.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - - it('catches bad field names', function () { - expect(() => new Regress.TestSimpleBoxedA({ junk: 42 })).toThrow(); - }); - - it('copies an object from another object of the same type', function () { - let copy = new Regress.TestSimpleBoxedA(simple_boxed); - expect(copy instanceof Regress.TestSimpleBoxedA).toBeTruthy(); - expect(copy.some_int).toEqual(42); - expect(copy.some_int8).toEqual(43); - expect(copy.some_double).toEqual(42.5); - expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3); - }); - }); - - describe('nested', function () { - beforeEach(function () { - simple_boxed = new Regress.TestSimpleBoxedB(); - }); - - it('reads fields and nested fields', function () { - simple_boxed.some_int8 = 42; - simple_boxed.nested_a.some_int = 43; - expect(simple_boxed.some_int8).toEqual(42); - expect(simple_boxed.nested_a.some_int).toEqual(43); - }); - - it('assigns nested struct field from an instance', function () { - simple_boxed.nested_a = new Regress.TestSimpleBoxedA({ some_int: 53 }); - expect(simple_boxed.nested_a.some_int).toEqual(53); - }); - - it('assigns nested struct field directly from a hash of field values', function () { - simple_boxed.nested_a = { some_int: 63 }; - expect(simple_boxed.nested_a.some_int).toEqual(63); - }); - }); - - it('constructs with a nested hash of field values', function () { - let simple2 = new Regress.TestSimpleBoxedB({ - some_int8: 42, - nested_a: { - some_int: 43, - some_int8: 44, - some_double: 43.5 - } - }); - expect(simple2.some_int8).toEqual(42); - expect(simple2.nested_a.some_int).toEqual(43); - expect(simple2.nested_a.some_int8).toEqual(44); - expect(simple2.nested_a.some_double).toEqual(43.5); - }); - - it('constructs using a custom constructor', function () { - let boxed = new Regress.TestBoxedD('abcd', 8); - expect(boxed.get_magic()).toEqual(12); - }); - - // RegressTestBoxedB has a constructor that takes multiple - // arguments, but since it is directly allocatable, we keep - // the old style of passing an hash of fields. - // The two real world structs that have this behavior are - // Clutter.Color and Clutter.ActorBox. - it('constructs using a custom constructor in backwards compatibility mode', function () { - let boxed = new Regress.TestBoxedB({ some_int8: 7, some_long: 5 }); - expect(boxed.some_int8).toEqual(7); - expect(boxed.some_long).toEqual(5); - }); -}); - -describe('Introspected GObject', function () { - let obj; - beforeEach(function () { - obj = new Regress.TestObj({ - // These properties have backing public fields with different names - int: 42, - float: 3.1416, - double: 2.71828, - }); - }); - - it('can access fields with simple types', function () { - // Compare the values gotten through the GObject property getters to the - // values of the backing fields - expect(obj.some_int8).toEqual(obj.int); - expect(obj.some_float).toEqual(obj.float); - expect(obj.some_double).toEqual(obj.double); - }); - - it('cannot access fields with complex types (GI limitation)', function () { - expect(() => obj.parent_instance).toThrow(); - expect(() => obj.function_ptr).toThrow(); - }); - - it('silently does not set read-only fields', function () { - obj.some_int8 = 41; - expect(obj.some_int8).toEqual(42); - expect(obj.int).toEqual(42); - }); - - it('throws an error in strict mode when setting a read-only field', function () { - 'use strict'; - expect(() => obj.some_int8 = 41).toThrow(); - }); - - it('has normal Object methods', function () { - obj.ownprop = 'foo'; - expect(obj.hasOwnProperty('ownprop')).toBeTruthy(); - }); - - // This test is not meant to be normative; a GObject behaving like this is - // doing something unsupported. However, we have been handling this so far - // in a certain way, and we don't want to break user code because of badly - // behaved libraries. This test ensures that any change to the behaviour - // must be intentional. - it('resolves properties when they are shadowed by methods', function () { - expect(obj.name_conflict).toEqual(42); - expect(obj.name_conflict instanceof Function).toBeFalsy(); - }); -}); - -describe('Introspected function length', function () { - let obj; - beforeEach(function () { - obj = new Regress.TestObj(); - }); - - it('skips over instance parameters of methods', function () { - expect(obj.set_bare.length).toEqual(1); - }); - - it('skips over out and GError parameters', function () { - expect(obj.torture_signature_1.length).toEqual(3); - }); - - it('does not skip over inout parameters', function () { - expect(obj.skip_return_val.length).toEqual(5); - }); - - xit('skips over parameters annotated with skip', function () { - expect(obj.skip_param.length).toEqual(4); - }).pend('Not implemented yet'); - - it('gives number of arguments for static methods', function () { - expect(Regress.TestObj.new_from_file.length).toEqual(1); - }); - - it('skips over destroy-notify and user-data parameters', function () { - expect(Regress.TestObj.new_callback.length).toEqual(1); - }); -}); - -describe('Garbage collection of introspected objects', function () { - // This tests a regression that would very rarely crash, but - // when run under valgrind this code would show use-after-free. - it('collects objects properly with signals connected', function (done) { - function orphanObject() { - let obj = new Regress.TestObj(); - obj.connect('notify', () => {}); - } - - orphanObject(); - System.gc(); - GLib.idle_add(GLib.PRIORITY_LOW, () => done()); - }); -}); diff --git a/installed-tests/js/testExceptions.js b/installed-tests/js/testExceptions.js index c6388bb..a92d55a 100644 --- a/installed-tests/js/testExceptions.js +++ b/installed-tests/js/testExceptions.js @@ -1,18 +1,16 @@ -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; +const {GIMarshallingTests, Gio, GLib, GObject} = imports.gi; const Foo = GObject.registerClass({ Properties: { - 'prop': GObject.ParamSpec.string('prop', '', '', GObject.ParamFlags.READWRITE, '') + 'prop': GObject.ParamSpec.string('prop', '', '', GObject.ParamFlags.READWRITE, ''), }, }, class Foo extends GObject.Object { set prop(v) { - throw new Error('set'); + throw new Error('set'); } get prop() { - throw new Error('get'); + throw new Error('get'); } }); @@ -20,13 +18,13 @@ const Bar = GObject.registerClass({ Properties: { 'prop': GObject.ParamSpec.string('prop', '', '', GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, ''), - } + }, }, class Bar extends GObject.Object {}); describe('Exceptions', function () { it('are thrown from property setter', function () { let foo = new Foo(); - expect(() => foo.prop = 'bar').toThrowError(/set/); + expect(() => (foo.prop = 'bar')).toThrowError(/set/); }); it('are thrown from property getter', function () { @@ -37,12 +35,12 @@ describe('Exceptions', function () { // FIXME: In the next cases the errors aren't thrown but logged it('are logged from constructor', function () { - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'JS ERROR: Error: set*'); - new Foo({ prop: 'bar' }); + new Foo({prop: 'bar'}); - GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testExceptions.js', 0, 'testExceptionInPropertySetterFromConstructor'); }); @@ -53,13 +51,13 @@ describe('Exceptions', function () { bar.bind_property('prop', foo, 'prop', GObject.BindingFlags.DEFAULT); - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'JS ERROR: Error: set*'); // wake up the binding so that g_object_set() is called on foo bar.notify('prop'); - GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testExceptions.js', 0, 'testExceptionInPropertySetterWithBinding'); }); @@ -70,20 +68,20 @@ describe('Exceptions', function () { foo.bind_property('prop', bar, 'prop', GObject.BindingFlags.DEFAULT); - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'JS ERROR: Error: get*'); // wake up the binding so that g_object_get() is called on foo foo.notify('prop'); - GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testExceptions.js', 0, 'testExceptionInPropertyGetterWithBinding'); }); }); describe('logError', function () { afterEach(function () { - GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', + GLib.test_assert_expected_messages_internal('Cjs', 'testExceptions.js', 0, 'testGErrorMessages'); }); @@ -93,7 +91,7 @@ describe('logError', function () { try { let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist"); file.read(null); - } catch(e) { + } catch (e) { logError(e); } }); @@ -102,8 +100,8 @@ describe('logError', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, 'JS ERROR: Gio.IOErrorEnum: a message\nmarker@*'); try { - throw new Gio.IOErrorEnum({ message: 'a message', code: 0 }); - } catch(e) { + throw new Gio.IOErrorEnum({message: 'a message', code: 0}); + } catch (e) { logError(e); } }); @@ -111,21 +109,27 @@ describe('logError', function () { it('also logs an error for a created GError that is not thrown', function marker() { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, 'JS ERROR: Gio.IOErrorEnum: a message\nmarker@*'); - logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 })); + logError(new Gio.IOErrorEnum({message: 'a message', code: 0})); }); - it('logs an error with no stack trace for an error created with the GLib.Error constructor', function () { + it('logs an error created with the GLib.Error constructor', function marker() { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: Gio.IOErrorEnum: a message'); + 'JS ERROR: Gio.IOErrorEnum: a message\nmarker@*'); logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message')); }); - it('logs the quark for a JS-created GError type', function () { + it('logs the quark for a JS-created GError type', function marker() { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: GLib.Error my-error: a message'); + 'JS ERROR: GLib.Error my-error: a message\nmarker@*'); logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message')); }); + it('logs with stack for a GError created from a C struct', function marker() { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + 'JS ERROR: GLib.Error gi-marshalling-tests-gerror-domain: gi-marshalling-tests-gerror-message\nmarker@*'); + logError(GIMarshallingTests.gerror_return()); + }); + // Now with prefix it('logs an error with a prefix if given', function () { @@ -134,7 +138,7 @@ describe('logError', function () { try { let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist"); file.read(null); - } catch(e) { + } catch (e) { logError(e, 'prefix'); } }); @@ -143,28 +147,20 @@ describe('logError', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, 'JS ERROR: prefix: Gio.IOErrorEnum: a message\nmarker@*'); try { - throw new Gio.IOErrorEnum({ message: 'a message', code: 0 }); - } catch(e) { + throw new Gio.IOErrorEnum({message: 'a message', code: 0}); + } catch (e) { logError(e, 'prefix'); } }); - it('logs a non-thrown error with prefix', function marker() { + it('logs a SyntaxError', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: prefix: Gio.IOErrorEnum: a message\nmarker@*'); - logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 }), 'prefix'); - }); - - it('logs a GLib.Error with prefix', function () { - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: prefix: Gio.IOErrorEnum: a message'); - logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message'), 'prefix'); - }); - - it('logs a JS-created GLib.Error with prefix', function () { - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: prefix: GLib.Error my-error: a message'); - logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message'), 'prefix'); + 'JS ERROR: SyntaxError:*'); + try { + Reflect.parse('!@#$%^&'); + } catch (e) { + logError(e); + } }); }); @@ -178,4 +174,30 @@ describe('Exception from function with too few arguments', function () { let file = Gio.File.new_for_path('foo'); expect(() => file.read()).toThrowError(/Gio\.File\.read/); }); -}); \ No newline at end of file +}); + +describe('thrown GError', function () { + let err; + beforeEach(function () { + try { + let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist"); + file.read(null); + } catch (x) { + err = x; + } + }); + + it('is an instance of error enum type', function () { + expect(err).toEqual(jasmine.any(Gio.IOErrorEnum)); + }); + + it('matches error domain and code', function () { + expect(err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND)) + .toBeTruthy(); + }); + + it('has properties for domain and code', function () { + expect(err.domain).toEqual(Gio.io_error_quark()); + expect(err.code).toEqual(Gio.IOErrorEnum.NOT_FOUND); + }); +}); diff --git a/installed-tests/js/testFundamental.js b/installed-tests/js/testFundamental.js index fc08de0..6384793 100644 --- a/installed-tests/js/testFundamental.js +++ b/installed-tests/js/testFundamental.js @@ -1,11 +1,8 @@ -const Regress = imports.gi.Regress; +const {GObject, Regress} = imports.gi; describe('Fundamental type support', function () { - it('constructs a subtype of a fundamental type', function () { - expect(() => new Regress.TestFundamentalSubObject('plop')).not.toThrow(); - }); - - it('constructs a subtype of a hidden (no introspection data) fundamental type', function() { - expect(() => Regress.test_create_fundamental_hidden_class_instance()).not.toThrow(); + it('can marshal a subtype of a custom fundamental type into a GValue', function () { + const fund = new Regress.TestFundamentalSubObject('plop'); + expect(() => GObject.strdup_value_contents(fund)).not.toThrow(); }); }); diff --git a/installed-tests/js/testGDBus.js b/installed-tests/js/testGDBus.js index c76c0e1..17811b7 100644 --- a/installed-tests/js/testGDBus.js +++ b/installed-tests/js/testGDBus.js @@ -1,92 +1,110 @@ -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; +const ByteArray = imports.byteArray; +const {Gio, CjsPrivate, GLib} = imports.gi; /* The methods list with their signatures. * - * *** NOTE: If you add stuff here, you need to update testIntrospectReal + * *** NOTE: If you add stuff here, you need to update the Test class below. */ -var TestIface = ' \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -'; - +var TestIface = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +const PROP_READ_ONLY_INITIAL_VALUE = Math.random(); const PROP_READ_WRITE_INITIAL_VALUE = 58; -const PROP_WRITE_ONLY_INITIAL_VALUE = "Initial value"; +const PROP_WRITE_ONLY_INITIAL_VALUE = 'Initial value'; /* Test is the actual object exporting the dbus methods */ class Test { constructor() { + this._propReadOnly = PROP_READ_ONLY_INITIAL_VALUE; this._propWriteOnly = PROP_WRITE_ONLY_INITIAL_VALUE; this._propReadWrite = PROP_READ_WRITE_INITIAL_VALUE; @@ -94,20 +112,19 @@ class Test { this._impl.export(Gio.DBus.session, '/org/gnome/gjs/Test'); } - frobateStuff(args) { - return { hello: new GLib.Variant('s', 'world') }; + frobateStuff() { + return {hello: new GLib.Variant('s', 'world')}; } nonJsonFrobateStuff(i) { - if (i == 42) { - return "42 it is!"; - } else { - return "Oops"; - } + if (i === 42) + return '42 it is!'; + else + return 'Oops'; } alwaysThrowException() { - throw Error("Exception!"); + throw Error('Exception!'); } thisDoesNotExist() { @@ -115,15 +132,15 @@ class Test { } noInParameter() { - return "Yes!"; + return 'Yes!'; } multipleInArgs(a, b, c, d, e) { - return a + " " + b + " " + c + " " + d + " " + e; + return `${a} ${b} ${c} ${d} ${e}`; } emitSignal() { - this._impl.emit_signal('signalFoo', GLib.Variant.new('(s)', [ "foobar" ])); + this._impl.emit_signal('signalFoo', GLib.Variant.new('(s)', ['foobar'])); } noReturnValue() { @@ -135,22 +152,22 @@ class Test { * multipleOutValues is "sss", while oneArrayOut is "as" */ multipleOutValues() { - return [ "Hello", "World", "!" ]; + return ['Hello', 'World', '!']; } oneArrayOut() { - return [ "Hello", "World", "!" ]; + return ['Hello', 'World', '!']; } /* Same thing again. In this case multipleArrayOut is "asas", * while arrayOfArrayOut is "aas". */ multipleArrayOut() { - return [[ "Hello", "World" ], [ "World", "Hello" ]]; + return [['Hello', 'World'], ['World', 'Hello']]; } arrayOfArrayOut() { - return [[ "Hello", "World" ], [ "World", "Hello" ]]; + return [['Hello', 'World'], ['World', 'Hello']]; } arrayOutBadSig() { @@ -173,15 +190,15 @@ class Test { * the input arguments */ echoAsync(parameters, invocation) { var [someString, someInt] = parameters; - GLib.idle_add(GLib.PRIORITY_DEFAULT, function() { + GLib.idle_add(GLib.PRIORITY_DEFAULT, function () { invocation.return_value(new GLib.Variant('(si)', [someString, someInt])); return false; }); } - // boolean + // double get PropReadOnly() { - return true; + return this._propReadOnly; } // string @@ -195,34 +212,84 @@ class Test { } set PropReadWrite(value) { - this._propReadWrite = value.deep_unpack(); + this._propReadWrite = value.deepUnpack(); } structArray() { return [[128, 123456], [42, 654321]]; } + + fdIn(fdIndex, fdList) { + const fd = fdList.get(fdIndex); + const stream = new Gio.UnixInputStream({fd, closeFd: true}); + const bytes = stream.read_bytes(4096, null); + return bytes; + } + + // Same as fdIn(), but implemented asynchronously + fdIn2Async([fdIndex], invocation, fdList) { + const fd = fdList.get(fdIndex); + const stream = new Gio.UnixInputStream({fd, closeFd: true}); + stream.read_bytes_async(4096, GLib.PRIORITY_DEFAULT, null, (obj, res) => { + const bytes = obj.read_bytes_finish(res); + invocation.return_value(new GLib.Variant('(ay)', [bytes])); + }); + } + + fdOut(bytes) { + const fd = CjsPrivate.open_bytes(bytes); + const fdList = Gio.UnixFDList.new_from_array([fd]); + return [0, fdList]; + } + + fdOut2Async([bytes], invocation) { + GLib.idle_add(GLib.PRIORITY_DEFAULT, function () { + const fd = CjsPrivate.open_bytes(bytes); + const fdList = Gio.UnixFDList.new_from_array([fd]); + invocation.return_value_with_unix_fd_list(new GLib.Variant('(h)', [0]), + fdList); + return GLib.SOURCE_REMOVE; + }); + } } const ProxyClass = Gio.DBusProxy.makeProxyWrapper(TestIface); describe('Exported DBus object', function () { - var own_name_id; + let ownNameID; var test; var proxy; let loop; + function waitForServerProperty(property, value = undefined, timeout = 500) { + let waitId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout, () => { + waitId = 0; + throw new Error(`Timeout waiting for property ${property} expired`); + }); + + while (waitId && (!test[property] || + value !== undefined && test[property] !== value)) + loop.get_context().iteration(true); + + if (waitId) + GLib.source_remove(waitId); + + expect(waitId).not.toBe(0); + return test[property]; + } + beforeAll(function () { loop = new GLib.MainLoop(null, false); test = new Test(); - own_name_id = Gio.DBus.session.own_name('org.gnome.gjs.Test', + ownNameID = Gio.DBus.session.own_name('org.gnome.gjs.Test', Gio.BusNameOwnerFlags.NONE, name => { - log("Acquired name " + name); + log(`Acquired name ${name}`); loop.quit(); }, name => { - log("Lost name " + name); + log(`Lost name ${name}`); }); loop.run(); new ProxyClass(Gio.DBus.session, 'org.gnome.gjs.Test', @@ -232,14 +299,15 @@ describe('Exported DBus object', function () { proxy = obj; expect(proxy).not.toBeNull(); loop.quit(); - }); + }, + Gio.DBusProxyFlags.NONE); loop.run(); }); afterAll(function () { // Not really needed, but if we don't cleanup // memory checking will complain - Gio.DBus.session.unown_name(own_name_id); + Gio.DBus.session.unown_name(ownNameID); }); beforeEach(function () { @@ -249,7 +317,7 @@ describe('Exported DBus object', function () { it('can call a remote method', function () { proxy.frobateStuffRemote({}, ([result], excp) => { expect(excp).toBeNull(); - expect(result.hello.deep_unpack()).toEqual('world'); + expect(result.hello.deepUnpack()).toEqual('world'); loop.quit(); }); loop.run(); @@ -274,7 +342,7 @@ describe('Exported DBus object', function () { otherProxy.frobateStuffRemote({}, ([result], excp) => { expect(excp).toBeNull(); - expect(result.hello.deep_unpack()).toEqual('world'); + expect(result.hello.deepUnpack()).toEqual('world'); loop.quit(); }); loop.run(); @@ -286,7 +354,7 @@ describe('Exported DBus object', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, 'JS ERROR: Exception in method call: alwaysThrowException: *'); - proxy.alwaysThrowExceptionRemote({}, function(result, excp) { + proxy.alwaysThrowExceptionRemote({}, function (result, excp) { expect(excp).not.toBeNull(); loop.quit(); }); @@ -302,7 +370,7 @@ describe('Exported DBus object', function () { // argument destructuring will not propagate across the FFI boundary // and the main loop will never quit. // https://bugzilla.gnome.org/show_bug.cgi?id=729015 - proxy.alwaysThrowExceptionRemote({}, function([a, b, c], excp) { + proxy.alwaysThrowExceptionRemote({}, function ([a, b, c], excp) { expect(a).not.toBeDefined(); expect(b).not.toBeDefined(); expect(c).not.toBeDefined(); @@ -376,7 +444,7 @@ describe('Exported DBus object', function () { }); it('can call a remote method with multiple return values', function () { - proxy.multipleOutValuesRemote(function(result, excp) { + proxy.multipleOutValuesRemote(function (result, excp) { expect(result).toEqual(['Hello', 'World', '!']); expect(excp).toBeNull(); loop.quit(); @@ -414,7 +482,7 @@ describe('Exported DBus object', function () { }); it('handles a bad signature by throwing an exception', function () { - proxy.arrayOutBadSigRemote(function(result, excp) { + proxy.arrayOutBadSigRemote(function (result, excp) { expect(excp).not.toBeNull(); loop.quit(); }); @@ -422,11 +490,11 @@ describe('Exported DBus object', function () { }); it('can call a remote method that is implemented asynchronously', function () { - let someString = "Hello world!"; + let someString = 'Hello world!'; let someInt = 42; proxy.echoRemote(someString, someInt, - function(result, excp) { + function (result, excp) { expect(excp).toBeNull(); expect(result).toEqual([someString, someInt]); loop.quit(); @@ -435,9 +503,7 @@ describe('Exported DBus object', function () { }); it('can send and receive bytes from a remote method', function () { - let loop = GLib.MainLoop.new(null, false); - - let someBytes = [ 0, 63, 234 ]; + let someBytes = [0, 63, 234]; someBytes.forEach(b => { proxy.byteEchoRemote(b, ([result], excp) => { expect(excp).toBeNull(); @@ -472,16 +538,124 @@ describe('Exported DBus object', function () { expect(result).not.toBeNull(); // verify the fractional part was dropped off int - expect(result['anInteger'].deep_unpack()).toEqual(10); + expect(result['anInteger'].deepUnpack()).toEqual(10); // and not dropped off a double - expect(result['aDoubleBeforeAndAfter'].deep_unpack()).toEqual(10.5); + expect(result['aDoubleBeforeAndAfter'].deepUnpack()).toEqual(10.5); // check without type conversion - expect(result['aDouble'].deep_unpack()).toBe(10.0); + expect(result['aDouble'].deepUnpack()).toBe(10.0); loop.quit(); }); loop.run(); }); + + it('can call a remote method with a Unix FD', function (done) { + const expectedBytes = ByteArray.fromString('some bytes'); + const fd = CjsPrivate.open_bytes(expectedBytes); + const fdList = Gio.UnixFDList.new_from_array([fd]); + proxy.fdInRemote(0, fdList, ([bytes], exc, outFdList) => { + expect(exc).toBeNull(); + expect(outFdList).toBeNull(); + expect(bytes).toEqual(expectedBytes); + done(); + }); + }); + + it('can call an asynchronously implemented remote method with a Unix FD', function (done) { + const expectedBytes = ByteArray.fromString('some bytes'); + const fd = CjsPrivate.open_bytes(expectedBytes); + const fdList = Gio.UnixFDList.new_from_array([fd]); + proxy.fdIn2Remote(0, fdList, ([bytes], exc, outFdList) => { + expect(exc).toBeNull(); + expect(outFdList).toBeNull(); + expect(bytes).toEqual(expectedBytes); + done(); + }); + }); + + function readBytesFromFdSync(fd) { + const stream = new Gio.UnixInputStream({fd, closeFd: true}); + const bytes = stream.read_bytes(4096, null); + return ByteArray.fromGBytes(bytes); + } + + it('can call a remote method that returns a Unix FD', function (done) { + const expectedBytes = ByteArray.fromString('some bytes'); + proxy.fdOutRemote(expectedBytes, ([fdIndex], exc, outFdList) => { + expect(exc).toBeNull(); + const bytes = readBytesFromFdSync(outFdList.get(fdIndex)); + expect(bytes).toEqual(expectedBytes); + done(); + }); + }); + + it('can call an asynchronously implemented remote method that returns a Unix FD', function (done) { + const expectedBytes = ByteArray.fromString('some bytes'); + proxy.fdOut2Remote(expectedBytes, ([fdIndex], exc, outFdList) => { + expect(exc).toBeNull(); + const bytes = readBytesFromFdSync(outFdList.get(fdIndex)); + expect(bytes).toEqual(expectedBytes); + done(); + }); + }); + + it('throws an exception when not passing a Gio.UnixFDList to a method that requires one', function () { + expect(() => proxy.fdInRemote(0, () => {})).toThrow(); + }); + + it('throws an exception when passing a handle out of range of a Gio.UnixFDList', function () { + const fdList = new Gio.UnixFDList(); + expect(() => proxy.fdInRemote(0, fdList, () => {})).toThrow(); + }); + + it('Has defined properties', function () { + expect(proxy.hasOwnProperty('PropReadWrite')).toBeTruthy(); + expect(proxy.hasOwnProperty('PropReadOnly')).toBeTruthy(); + expect(proxy.hasOwnProperty('PropWriteOnly')).toBeTruthy(); + }); + + it('reading readonly property works', function () { + expect(proxy.PropReadOnly).toEqual(PROP_READ_ONLY_INITIAL_VALUE); + }); + + it('reading readwrite property works', function () { + expect(proxy.PropReadWrite).toEqual( + GLib.Variant.new_string(PROP_READ_WRITE_INITIAL_VALUE.toString())); + }); + + it('reading writeonly throws an error', function () { + expect(() => proxy.PropWriteOnly).toThrowError('Property PropWriteOnly is not readable'); + }); + + it('Setting a readwrite property works', function () { + let testStr = 'GjsVariantValue'; + expect(() => { + proxy.PropReadWrite = GLib.Variant.new_string(testStr); + }).not.toThrow(); + + expect(proxy.PropReadWrite.deepUnpack()).toEqual(testStr); + + expect(waitForServerProperty('_propReadWrite', testStr)).toEqual(testStr); + }); + + it('Setting a writeonly property works', function () { + let testValue = Math.random().toString(); + expect(() => { + proxy.PropWriteOnly = testValue; + }).not.toThrow(); + + expect(() => proxy.PropWriteOnly).toThrow(); + expect(waitForServerProperty('_propWriteOnly', testValue)).toEqual(testValue); + }); + + it('Setting a readonly property throws an error', function () { + let testValue = Math.random().toString(); + expect(() => { + proxy.PropReadOnly = testValue; + }).toThrowError('Property PropReadOnly is not writable'); + + expect(proxy.PropReadOnly).toBe(PROP_READ_ONLY_INITIAL_VALUE); + }); }); diff --git a/installed-tests/js/testGIMarshalling.js b/installed-tests/js/testGIMarshalling.js index 713de01..cfd3062 100644 --- a/installed-tests/js/testGIMarshalling.js +++ b/installed-tests/js/testGIMarshalling.js @@ -1,3 +1,6 @@ +// Load overrides for GIMarshallingTests +imports.overrides.searchPath.unshift('resource:///org/gjs/jsunit/modules/overrides'); + const ByteArray = imports.byteArray; const GIMarshallingTests = imports.gi.GIMarshallingTests; @@ -6,17 +9,349 @@ const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; -describe('C array', function () { - function createStructArray() { +// Some helpers to cut down on repetitive marshalling tests. +// - options.omit: the test doesn't exist, don't create a test case +// - options.skip: the test does exist, but doesn't pass, either unsupported or +// a bug in GJS. Create the test case and mark it pending + +function testReturnValue(root, value, {omit, skip, funcName = `${root}_return`} = {}) { + if (omit) + return; + it('marshals as a return value', function () { + if (skip) + pending(skip); + expect(GIMarshallingTests[funcName]()).toEqual(value); + }); +} + +function testInParameter(root, value, {omit, skip, funcName = `${root}_in`} = {}) { + if (omit) + return; + it('marshals as an in parameter', function () { + if (skip) + pending(skip); + expect(() => GIMarshallingTests[funcName](value)).not.toThrow(); + }); +} + +function testOutParameter(root, value, {omit, skip, funcName = `${root}_out`} = {}) { + if (omit) + return; + it('marshals as an out parameter', function () { + if (skip) + pending(skip); + expect(GIMarshallingTests[funcName]()).toEqual(value); + }); +} + +function testInoutParameter(root, inValue, outValue, + {omit, skip, funcName = `${root}_inout`} = {}) { + if (omit) + return; + it('marshals as an inout parameter', function () { + if (skip) + pending(skip); + expect(GIMarshallingTests[funcName](inValue)).toEqual(outValue); + }); +} + +function testSimpleMarshalling(root, value, inoutValue, options = {}) { + testReturnValue(root, value, options.returnv); + testInParameter(root, value, options.in); + testOutParameter(root, value, options.out); + testInoutParameter(root, value, inoutValue, options.inout); +} + +function testTransferMarshalling(root, value, inoutValue, options = {}) { + describe('with transfer none', function () { + testSimpleMarshalling(`${root}_none`, value, inoutValue, options.none); + }); + describe('with transfer full', function () { + const fullOptions = { + in: { + omit: true, // this case is not in the test suite + }, + inout: { + skip: 'https://gitlab.gnome.org/GNOME/gobject-introspection/issues/192', + }, + }; + Object.assign(fullOptions, options.full); + testSimpleMarshalling(`${root}_full`, value, inoutValue, fullOptions); + }); +} + +function testContainerMarshalling(root, value, inoutValue, options = {}) { + testTransferMarshalling(root, value, inoutValue, options); + describe('with transfer container', function () { + const containerOptions = { + in: { + omit: true, // this case is not in the test suite + }, + inout: { + skip: 'https://gitlab.gnome.org/GNOME/gjs/issues/44', + }, + }; + Object.assign(containerOptions, options.container); + testSimpleMarshalling(`${root}_container`, value, inoutValue, containerOptions); + }); +} + +// Integer limits, defined without reference to GLib (because the GLib.MAXINT8 +// etc. constants are also subject to marshalling) +const Limits = { + int8: { + min: -(2 ** 7), + max: 2 ** 7 - 1, + umax: 2 ** 8 - 1, + }, + int16: { + min: -(2 ** 15), + max: 2 ** 15 - 1, + umax: 2 ** 16 - 1, + }, + int32: { + min: -(2 ** 31), + max: 2 ** 31 - 1, + umax: 2 ** 32 - 1, + }, + int64: { + min: -(2 ** 63), + max: 2 ** 63 - 1, + umax: 2 ** 64 - 1, + bit64: true, // note: unsafe, values will not be accurate! + }, + short: {}, + int: {}, + long: {}, + ssize: { + utype: 'size', + }, +}; +Object.assign(Limits.short, Limits.int16); +Object.assign(Limits.int, Limits.int32); +// Platform dependent sizes; expand definitions as needed +if (GLib.SIZEOF_LONG === 8) + Object.assign(Limits.long, Limits.int64); +else + Object.assign(Limits.long, Limits.int32); +if (GLib.SIZEOF_SSIZE_T === 8) + Object.assign(Limits.ssize, Limits.int64); +else + Object.assign(Limits.ssize, Limits.int32); + +// Functions for dealing with tests that require or return unsafe 64-bit ints, +// until we get BigInts. + +// Sometimes tests pass if we are comparing two inaccurate values in JS with +// each other. That's fine for now. Then we just have to suppress the warnings. +function warn64(is64bit, func, ...args) { + if (is64bit) { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + '*cannot be safely stored*'); + } + const retval = func(...args); + if (is64bit) { + GLib.test_assert_expected_messages_internal('Cjs', + 'testGIMarshalling.js', 0, 'Ignore message'); + } + return retval; +} + +// Other times we compare an inaccurate value marshalled from JS into C, with an +// accurate value in C. Those tests we have to skip. +function skip64(is64bit) { + if (is64bit) + pending('https://gitlab.gnome.org/GNOME/gjs/issues/271'); +} + +describe('Boolean', function () { + [true, false].forEach(bool => { + describe(`${bool}`, function () { + testSimpleMarshalling('boolean', bool, !bool, { + returnv: { + funcName: `boolean_return_${bool}`, + }, + in: { + funcName: `boolean_in_${bool}`, + }, + out: { + funcName: `boolean_out_${bool}`, + }, + inout: { + funcName: `boolean_inout_${bool}_${!bool}`, + }, + }); + }); + }); +}); + +describe('Integer', function () { + Object.entries(Limits).forEach(([type, {min, max, umax, bit64, utype = `u${type}`}]) => { + describe(`${type}-typed`, function () { + it('marshals signed value as a return value', function () { + expect(warn64(bit64, GIMarshallingTests[`${type}_return_max`])).toEqual(max); + expect(warn64(bit64, GIMarshallingTests[`${type}_return_min`])).toEqual(min); + }); + + it('marshals signed value as an in parameter', function () { + skip64(bit64); + expect(() => GIMarshallingTests[`${type}_in_max`](max)).not.toThrow(); + expect(() => GIMarshallingTests[`${type}_in_min`](min)).not.toThrow(); + }); + + it('marshals signed value as an out parameter', function () { + expect(warn64(bit64, GIMarshallingTests[`${type}_out_max`])).toEqual(max); + expect(warn64(bit64, GIMarshallingTests[`${type}_out_min`])).toEqual(min); + }); + + it('marshals as an inout parameter', function () { + skip64(bit64); + expect(GIMarshallingTests[`${type}_inout_max_min`](max)).toEqual(min); + expect(GIMarshallingTests[`${type}_inout_min_max`](min)).toEqual(max); + }); + + it('marshals unsigned value as a return value', function () { + expect(warn64(bit64, GIMarshallingTests[`${utype}_return`])).toEqual(umax); + }); + + it('marshals unsigned value as an in parameter', function () { + skip64(bit64); + expect(() => GIMarshallingTests[`${utype}_in`](umax)).not.toThrow(); + }); + + it('marshals unsigned value as an out parameter', function () { + expect(warn64(bit64, GIMarshallingTests[`${utype}_out`])).toEqual(umax); + }); + + it('marshals unsigned value as an inout parameter', function () { + skip64(bit64); + expect(GIMarshallingTests[`${utype}_inout`](umax)).toEqual(0); + }); + }); + }); +}); + +describe('Floating point', function () { + const FloatLimits = { + float: { + min: 2 ** -126, + max: (2 - 2 ** -23) * 2 ** 127, + }, + double: { + // GLib.MINDOUBLE is the minimum normal value, which is not the same + // as the minimum denormal value Number.MIN_VALUE + min: 2 ** -1022, + max: Number.MAX_VALUE, + }, + }; + + Object.entries(FloatLimits).forEach(([type, {min, max}]) => { + describe(`${type}-typed`, function () { + it('marshals value as a return value', function () { + expect(GIMarshallingTests[`${type}_return`]()).toBeCloseTo(max, 10); + }); + + testInParameter(type, max); + + it('marshals value as an out parameter', function () { + expect(GIMarshallingTests[`${type}_out`]()).toBeCloseTo(max, 10); + }); + + it('marshals value as an inout parameter', function () { + expect(GIMarshallingTests[`${type}_inout`](max)).toBeCloseTo(min, 10); + }); + }); + }); +}); + +describe('time_t', function () { + testSimpleMarshalling('time_t', 1234567890, 0); +}); + +describe('GType', function () { + describe('void', function () { + testSimpleMarshalling('gtype', GObject.TYPE_NONE, GObject.TYPE_INT); + }); + + describe('string', function () { + testSimpleMarshalling('gtype_string', GObject.TYPE_STRING, null, { + inout: {omit: true}, + }); + }); + + it('can be implicitly converted from a GObject type alias', function () { + expect(() => GIMarshallingTests.gtype_in(GObject.VoidType)).not.toThrow(); + }); + + it('can be implicitly converted from a JS type', function () { + expect(() => GIMarshallingTests.gtype_string_in(String)).not.toThrow(); + }); +}); + +describe('UTF-8 string', function () { + testTransferMarshalling('utf8', 'const ♥ utf8', ''); + + it('marshals value as a byte array', function () { + expect(() => GIMarshallingTests.utf8_as_uint8array_in('const ♥ utf8')).not.toThrow(); + }); + + it('makes a default out value for a broken C function', function () { + expect(GIMarshallingTests.utf8_dangling_out()).toBeNull(); + }); +}); + +describe('In-out array in the style of gtk_init()', function () { + it('marshals null', function () { + const [, newArray] = GIMarshallingTests.init_function(null); + expect(newArray).toEqual([]); + }); + + xit('marshals an inout empty array', function () { + const [, newArray] = GIMarshallingTests.init_function([]); + expect(newArray).toEqual([]); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/88'); + + xit('marshals an inout array', function () { + const [, newArray] = GIMarshallingTests.init_function(['--foo', '--bar']); + expect(newArray).toEqual(['--foo']); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/88'); +}); + +describe('Fixed-size C array', function () { + describe('of ints', function () { + testReturnValue('array_fixed_int', [-1, 0, 1, 2]); + testInParameter('array_fixed_int', [-1, 0, 1, 2]); + testInoutParameter('array_fixed', [-1, 0, 1, 2], [2, 1, 0, -1]); + }); + + describe('of shorts', function () { + testReturnValue('array_fixed_short', [-1, 0, 1, 2]); + testInParameter('array_fixed_short', [-1, 0, 1, 2]); + }); + + it('marshals a struct array as an out parameter', function () { + expect(GIMarshallingTests.array_fixed_out_struct()).toEqual([ + jasmine.objectContaining({long_: 7, int8: 6}), + jasmine.objectContaining({long_: 6, int8: 7}), + ]); + }); +}); + +describe('C array with length', function () { + function createStructArray(StructType = GIMarshallingTests.BoxedStruct) { return [1, 2, 3].map(num => { - let struct = new GIMarshallingTests.BoxedStruct(); + let struct = new StructType(); struct.long_ = num; return struct; }); } - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_in([-1, 0, 1, 2])).not.toThrow(); + testSimpleMarshalling('array', [-1, 0, 1, 2], [-2, -1, 0, 1, 2]); + + it('can be returned along with other arguments', function () { + let [array, sum] = GIMarshallingTests.array_return_etc(9, 5); + expect(sum).toEqual(14); + expect(array).toEqual([9, 0, 1, 5]); }); it('can be passed to a function with its length parameter before it', function () { @@ -29,45 +364,65 @@ describe('C array', function () { .not.toThrow(); }); - it('can be passed to a function in the style of gtk_init()', function () { - let [, newArray] = GIMarshallingTests.init_function(null); - expect(newArray).toEqual([]); + describe('of strings', function () { + testInParameter('array_string', ['foo', 'bar']); }); - it('can be returned with zero terminator', function () { - expect(GIMarshallingTests.array_zero_terminated_return()) - .toEqual(['0', '1', '2']); + it('marshals a byte array as an in parameter', function () { + expect(() => GIMarshallingTests.array_uint8_in('abcd')).not.toThrow(); + expect(() => GIMarshallingTests.array_uint8_in([97, 98, 99, 100])).not.toThrow(); + expect(() => GIMarshallingTests.array_uint8_in(ByteArray.fromString('abcd'))) + .not.toThrow(); }); - it('can be returned', function () { - expect(GIMarshallingTests.array_return()).toEqual([-1, 0, 1, 2]); + describe('of signed 64-bit ints', function () { + testInParameter('array_int64', [-1, 0, 1, 2]); }); - it('can be returned along with other arguments', function () { - let [array, sum] = GIMarshallingTests.array_return_etc(9, 5); - expect(sum).toEqual(14); - expect(array).toEqual([9, 0, 1, 5]); + describe('of unsigned 64-bit ints', function () { + testInParameter('array_uint64', [-1, 0, 1, 2]); }); - it('can be an out argument', function () { - expect(GIMarshallingTests.array_out()).toEqual([-1, 0, 1, 2]); + describe('of unichars', function () { + testInParameter('array_unichar', 'const ♥ utf8'); + testOutParameter('array_unichar', 'const ♥ utf8'); + + it('marshals from an array of codepoints', function () { + const codepoints = [...'const ♥ utf8'].map(c => c.codePointAt(0)); + expect(() => GIMarshallingTests.array_unichar_in(codepoints)).not.toThrow(); + }); }); - it('can be an out argument along with other arguments', function () { - let [array, sum] = GIMarshallingTests.array_out_etc(9, 5); - expect(sum).toEqual(14); - expect(array).toEqual([9, 0, 1, 5]); + describe('of booleans', function () { + testInParameter('array_bool', [true, false, true, true]); + testOutParameter('array_bool', [true, false, true, true]); + + it('marshals from an array of numbers', function () { + expect(() => GIMarshallingTests.array_bool_in([-1, 0, 1, 2])).not.toThrow(); + }); }); - it('can be an in-out argument', function () { - expect(GIMarshallingTests.array_inout([-1, 0, 1, 2])) - .toEqual([-2, -1, 0, 1, 2]); + describe('of boxed structs', function () { + testInParameter('array_struct', createStructArray()); + + describe('passed by value', function () { + testInParameter('array_struct_value', createStructArray(), { + skip: 'https://gitlab.gnome.org/GNOME/gjs/issues/44', + }); + }); }); - it('can be an in-out argument along with other arguments', function () { - let [array, sum] = GIMarshallingTests.array_inout_etc(9, [-1, 0, 1, 2], 5); - expect(sum).toEqual(14); - expect(array).toEqual([9, -1, 0, 1, 5]); + describe('of simple structs', function () { + testInParameter('array_simple_struct', + createStructArray(GIMarshallingTests.SimpleStruct), { + skip: 'https://gitlab.gnome.org/GNOME/gjs/issues/44', + }); + }); + + it('marshals two arrays with the same length parameter', function () { + const keys = ['one', 'two', 'three']; + const values = [1, 2, 3]; + expect(() => GIMarshallingTests.multi_array_key_value_in(keys, values)).not.toThrow(); }); // Run twice to ensure that copies are correctly made for (transfer full) @@ -79,161 +434,150 @@ describe('C array', function () { }).not.toThrow(); }); - describe('of structs', function () { - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_struct_in(createStructArray())) - .not.toThrow(); - }); - - it('can be returned with zero terminator', function () { - let structArray = GIMarshallingTests.array_zero_terminated_return_struct(); - expect(structArray.map(e => e.long_)).toEqual([42, 43, 44]); - }); + describe('of enums', function () { + testInParameter('array_enum', [ + GIMarshallingTests.Enum.VALUE1, + GIMarshallingTests.Enum.VALUE2, + GIMarshallingTests.Enum.VALUE3, + ]); }); - describe('of booleans', function () { - it('is coerced to true/false when passed to a function', function () { - expect(() => GIMarshallingTests.array_bool_in([-1, 0, 1, 2])) - .not.toThrow(); - }); - - it('can be an out argument', function () { - expect(GIMarshallingTests.array_bool_out()) - .toEqual([true, false, true, true]); - }); + it('marshals an array with a 64-bit length parameter', function () { + expect(() => GIMarshallingTests.array_in_guint64_len([-1, 0, 1, 2])).not.toThrow(); }); - describe('of unichars', function () { - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_unichar_in('const \u2665 utf8')) - .not.toThrow(); - }); + it('marshals an array with an 8-bit length parameter', function () { + expect(() => GIMarshallingTests.array_in_guint8_len([-1, 0, 1, 2])).not.toThrow(); + }); - it('can be an out argument', function () { - expect(GIMarshallingTests.array_unichar_out()) - .toEqual('const \u2665 utf8'); - }); + it('can be an out argument along with other arguments', function () { + let [array, sum] = GIMarshallingTests.array_out_etc(9, 5); + expect(sum).toEqual(14); + expect(array).toEqual([9, 0, 1, 5]); + }); - it('can be returned with zero terminator', function () { - expect(GIMarshallingTests.array_zero_terminated_return_unichar()) - .toEqual('const \u2665 utf8'); - }); + it('can be an in-out argument along with other arguments', function () { + let [array, sum] = GIMarshallingTests.array_inout_etc(9, [-1, 0, 1, 2], 5); + expect(sum).toEqual(14); + expect(array).toEqual([9, -1, 0, 1, 5]); + }); - it('can be implicitly converted from a number array', function () { - expect(() => GIMarshallingTests.array_unichar_in([0x63, 0x6f, 0x6e, 0x73, - 0x74, 0x20, 0x2665, 0x20, 0x75, 0x74, 0x66, 0x38])).not.toThrow(); - }); + it('does not interpret an unannotated integer as a length parameter', function () { + expect(() => GIMarshallingTests.array_in_nonzero_nonlen(42, 'abcd')).not.toThrow(); }); +}); +describe('Zero-terminated C array', function () { describe('of strings', function () { - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_string_in(['foo', 'bar'])) - .not.toThrow(); - }); + testSimpleMarshalling('array_zero_terminated', ['0', '1', '2'], + ['-1', '0', '1', '2']); }); - describe('of enums', function () { - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_enum_in([GIMarshallingTests.Enum.VALUE1, - GIMarshallingTests.Enum.VALUE2, GIMarshallingTests.Enum.VALUE3])).not.toThrow(); - }); + it('marshals null as a zero-terminated array return value', function () { + expect(GIMarshallingTests.array_zero_terminated_return_null()).toEqual(null); }); - describe('of bytes', function () { - it('can be an in argument with length', function () { - expect(() => GIMarshallingTests.array_in_guint8_len([-1, 0, 1, 2])) - .not.toThrow(); - }); + it('marshals an array of structs as a return value', function () { + let structArray = GIMarshallingTests.array_zero_terminated_return_struct(); + expect(structArray.map(e => e.long_)).toEqual([42, 43, 44]); + }); - it('can be implicitly converted from a string', function () { - expect(() => GIMarshallingTests.array_uint8_in('abcd')).not.toThrow(); - }); + it('marshals an array of unichars as a return value', function () { + expect(GIMarshallingTests.array_zero_terminated_return_unichar()) + .toEqual('const ♥ utf8'); }); - describe('of 64-bit ints', function () { - it('can be passed to a function', function () { - expect(() => GIMarshallingTests.array_int64_in([-1, 0, 1, 2])) - .not.toThrow(); - expect(() => GIMarshallingTests.array_uint64_in([-1, 0, 1, 2])) - .not.toThrow(); + describe('of GLib.Variants', function () { + let variantArray; + + beforeEach(function () { + variantArray = [ + new GLib.Variant('i', 27), + new GLib.Variant('s', 'Hello'), + ]; }); - it('can be an in argument with length', function () { - expect(() => GIMarshallingTests.array_in_guint64_len([-1, 0, 1, 2])) - .not.toThrow(); + ['none', 'container', 'full'].forEach(transfer => { + xit(`marshals as a transfer-${transfer} in and out parameter`, function () { + const returnedArray = + GIMarshallingTests[`array_gvariant_${transfer}_in`](variantArray); + expect(returnedArray.map(v => v.deepUnpack())).toEqual([27, 'Hello']); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/269'); }); }); }); describe('GArray', function () { - describe('of integers', function () { - it('can be passed in with transfer none', function () { - expect(() => GIMarshallingTests.garray_int_none_in([-1, 0, 1, 2])) - .not.toThrow(); - }); + describe('of ints with transfer none', function () { + testReturnValue('garray_int_none', [-1, 0, 1, 2]); + testInParameter('garray_int_none', [-1, 0, 1, 2]); + }); - it('can be returned with transfer none', function () { - expect(GIMarshallingTests.garray_int_none_return()) - .toEqual([-1, 0, 1, 2]); - }); + it('marshals int64s as a transfer-none return value', function () { + expect(warn64(true, GIMarshallingTests.garray_uint64_none_return)) + .toEqual([0, Limits.int64.umax]); }); describe('of strings', function () { - it('can be passed in with transfer none', function () { - expect(() => GIMarshallingTests.garray_utf8_none_in(['0', '1', '2'])) - .not.toThrow(); - }); + testContainerMarshalling('garray_utf8', ['0', '1', '2'], ['-2', '-1', '0', '1']); - ['return', 'out'].forEach(method => { - ['none', 'container', 'full'].forEach(transfer => { - it('can be passed as ' + method + ' with transfer ' + transfer, function () { - expect(GIMarshallingTests['garray_utf8_' + transfer + '_' + method]()) - .toEqual(['0', '1', '2']); - }); - }); - }); + it('marshals as a transfer-full caller-allocated out parameter', function () { + expect(GIMarshallingTests.garray_utf8_full_out_caller_allocated()) + .toEqual(['0', '1', '2']); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/106'); }); - describe('of booleans', function () { - it('can be passed in with transfer none', function () { - expect(() => GIMarshallingTests.garray_bool_none_in([-1, 0, 1, 2])) - .not.toThrow(); - }); + // it('marshals boxed structs as a transfer-full return value', function () { + // expect(GIMarshallingTests.garray_boxed_struct_full_return().map(e => e.long_)) + // .toEqual([42, 43, 44]); + // }); + + describe('of booleans with transfer none', function () { + testInParameter('garray_bool_none', [-1, 0, 1, 2]); }); describe('of unichars', function () { it('can be passed in with transfer none', function () { expect(() => GIMarshallingTests.garray_unichar_none_in('const \u2665 utf8')) .not.toThrow(); - }); - - it('can be implicitly converted from a number array', function () { expect(() => GIMarshallingTests.garray_unichar_none_in([0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20, 0x2665, 0x20, 0x75, 0x74, 0x66, 0x38])).not.toThrow(); }); }); }); +describe('GPtrArray', function () { + describe('of strings', function () { + testContainerMarshalling('garray_utf8', ['0', '1', '2'], ['-2', '-1', '0', '1']); + }); + + // describe('of structs', function () { + // it('can be returned with transfer full', function () { + // expect(GIMarshallingTests.gptrarray_boxed_struct_full_return().map(e => e.long_)) + // .toEqual([42, 43, 44]); + // }); + // }); +}); + describe('GByteArray', function () { - const refByteArray = ByteArray.fromArray([0, 49, 0xFF, 51]); + const refByteArray = Uint8Array.from([0, 49, 0xFF, 51]); + + testReturnValue('bytearray_full', refByteArray); it('can be passed in with transfer none', function () { expect(() => GIMarshallingTests.bytearray_none_in(refByteArray)) .not.toThrow(); - }); - - it('can be returned with transfer full', function () { - expect(GIMarshallingTests.bytearray_full_return()).toEqual(refByteArray); - }); - - it('can be implicitly converted from a normal array', function () { expect(() => GIMarshallingTests.bytearray_none_in([0, 49, 0xFF, 51])) .not.toThrow(); }); }); describe('GBytes', function () { - const refByteArray = ByteArray.fromArray([0, 49, 0xFF, 51]); + const refByteArray = Uint8Array.from([0, 49, 0xFF, 51]); + + it('marshals as a transfer-full return value', function () { + expect(GIMarshallingTests.gbytes_full_return().toArray()).toEqual(refByteArray); + }); it('can be created from an array and passed in', function () { let bytes = GLib.Bytes.new([0, 49, 0xFF, 51]); @@ -252,7 +596,7 @@ describe('GBytes', function () { }); it('can be created from a string and is encoded in UTF-8', function () { - let bytes = GLib.Bytes.new("const \u2665 utf8"); + let bytes = GLib.Bytes.new('const \u2665 utf8'); expect(() => GIMarshallingTests.utf8_as_uint8array_in(bytes.toArray())) .not.toThrow(); }); @@ -265,7 +609,7 @@ describe('GBytes', function () { expect(array[1]).toEqual(42); array[1] = 49; // Flip the value back // Now convert back to GBytes - expect(() => GIMarshallingTests.gbytes_none_in(array.toGBytes())) + expect(() => GIMarshallingTests.gbytes_none_in(ByteArray.toGBytes(array))) .not.toThrow(); }); @@ -276,141 +620,139 @@ describe('GBytes', function () { }); }); -describe('GPtrArray', function () { - const refArray = ['0', '1', '2']; +describe('GStrv', function () { + testSimpleMarshalling('gstrv', ['0', '1', '2'], ['-1', '0', '1', '2']); +}); - it('can be passed to a function with transfer none', function () { - expect(() => GIMarshallingTests.gptrarray_utf8_none_in(refArray)) - .not.toThrow(); - }); +['GList', 'GSList'].forEach(listKind => { + const list = listKind.toLowerCase(); - ['return', 'out'].forEach(method => { - ['none', 'container', 'full'].forEach(transfer => { - it('can be passed as ' + method + ' with transfer ' + transfer, function () { - expect(GIMarshallingTests['gptrarray_utf8_' + transfer + '_' + method]()) - .toEqual(refArray); + describe(listKind, function () { + describe('of ints with transfer none', function () { + testReturnValue(`${list}_int_none`, [-1, 0, 1, 2]); + testInParameter(`${list}_int_none`, [-1, 0, 1, 2]); + }); + + if (listKind === 'GList') { + describe('of unsigned 32-bit ints with transfer none', function () { + testReturnValue('glist_uint32_none', [0, Limits.int32.umax]); + testInParameter('glist_uint32_none', [0, Limits.int32.umax]); }); + } + + describe('of strings', function () { + testContainerMarshalling(`${list}_utf8`, ['0', '1', '2'], + ['-2', '-1', '0', '1']); }); }); }); describe('GHashTable', function () { - const INT_DICT = { - '-1': 1, - 0: 0, - 1: -1, - 2: -2, - }; - const STRING_DICT = { - '-1': '1', - 0: '0', - 1: '-1', - 2: '-2', - }; - const NUMBER_DICT = { + const numberDict = { '-1': -0.1, 0: 0, 1: 0.1, 2: 0.2, }; - const STRING_DICT_OUT = { - '-1': '1', - 0: '0', - 1: '1', - }; - it('can be passed in with integer value type', function () { - expect(() => GIMarshallingTests.ghashtable_int_none_in(INT_DICT)) - .not.toThrow(); + describe('with integer values', function () { + const intDict = { + '-1': 1, + 0: 0, + 1: -1, + 2: -2, + }; + testReturnValue('ghashtable_int_none', intDict); + testInParameter('ghashtable_int_none', intDict); }); - it('can be passed in with string value type', function () { - expect(() => GIMarshallingTests.ghashtable_utf8_none_in(STRING_DICT)) - .not.toThrow(); + describe('with string values', function () { + const stringDict = { + '-1': '1', + 0: '0', + 1: '-1', + 2: '-2', + }; + const stringDictOut = { + '-1': '1', + 0: '0', + 1: '1', + }; + testContainerMarshalling('ghashtable_utf8', stringDict, stringDictOut); }); - it('can be passed in with float value type', function () { - expect(() => GIMarshallingTests.ghashtable_float_in(NUMBER_DICT)) - .not.toThrow(); + describe('with double values', function () { + testInParameter('ghashtable_double', numberDict); }); - it('can be passed in with double value type', function () { - expect(() => GIMarshallingTests.ghashtable_double_in(NUMBER_DICT)) - .not.toThrow(); + describe('with float values', function () { + testInParameter('ghashtable_float', numberDict); }); - it('can be passed in with int64 value type', function () { + describe('with 64-bit int values', function () { const int64Dict = { '-1': -1, 0: 0, 1: 1, 2: 0x100000000, }; - expect(() => GIMarshallingTests.ghashtable_int64_in(int64Dict)) - .not.toThrow(); + testInParameter('ghashtable_int64', int64Dict); }); - it('can be passed in with uint64 value type', function () { + describe('with unsigned 64-bit int values', function () { const uint64Dict = { '-1': 0x100000000, 0: 0, 1: 1, 2: 2, }; - expect(() => GIMarshallingTests.ghashtable_uint64_in(uint64Dict)) - .not.toThrow(); - }); - - it('can be returned with integer value type', function () { - expect(GIMarshallingTests.ghashtable_int_none_return()).toEqual(INT_DICT); - }); - - ['return', 'out'].forEach(method => { - ['none', 'container', 'full'].forEach(transfer => { - it('can be passed as ' + method + ' with transfer ' + transfer, function () { - expect(GIMarshallingTests['ghashtable_utf8_' + transfer + '_' + method]()) - .toEqual(STRING_DICT); - }); - }); - }); - - it('can be passed as inout with transfer none', function () { - expect(GIMarshallingTests.ghashtable_utf8_none_inout(STRING_DICT)) - .toEqual(STRING_DICT_OUT); + testInParameter('ghashtable_uint64', uint64Dict); }); - - xit('can be passed as inout with transfer container', function () { - expect(GIMarshallingTests.ghashtable_utf8_container_inout(STRING_DICT)) - .toEqual(STRING_DICT_OUT); - }).pend('Container transfer for in parameters not supported'); - - xit('can be passed as inout with transfer full', function () { - expect(GIMarshallingTests.ghashtable_utf8_full_inout(STRING_DICT)) - .toEqual(STRING_DICT_OUT); - }).pend('https://bugzilla.gnome.org/show_bug.cgi?id=773763'); }); describe('GValue', function () { - it('can be passed into a function and packed', function () { - expect(() => GIMarshallingTests.gvalue_in(42)).not.toThrow(); + testSimpleMarshalling('gvalue', 42, '42', { + inout: { + skip: 'https://gitlab.gnome.org/GNOME/gobject-introspection/issues/192', + }, }); - it('array can be passed into a function and packed', function () { - expect(() => GIMarshallingTests.gvalue_flat_array([42, '42', true])) + xit('marshals as an int64 in parameter', function () { + expect(() => GIMarshallingTests.gvalue_int64_in(Limits.int64.max)).not.toThrow(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/271'); + + it('type objects can be converted from primitive-like types', function () { + expect(() => GIMarshallingTests.gvalue_in_with_type(42, GObject.Int)) + .not.toThrow(); + expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, GObject.Double)) + .not.toThrow(); + expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, Number)) .not.toThrow(); }); + it('can be passed into a function and modified', function () { + expect(() => GIMarshallingTests.gvalue_in_with_modification(42)).not.toThrow(); + // Let's assume this test doesn't expect that the modified GValue makes + // it back to the caller; I don't see how that could be achieved. + // See https://gitlab.gnome.org/GNOME/gjs/issues/80 + }); + xit('enum can be passed into a function and packed', function () { expect(() => GIMarshallingTests.gvalue_in_enum(GIMarshallingTests.Enum.VALUE3)) .not.toThrow(); }).pend("GJS doesn't support native enum types"); - it('can be returned and unpacked', function () { - expect(GIMarshallingTests.gvalue_return()).toEqual(42); + it('marshals as an int64 out parameter', function () { + expect(GIMarshallingTests.gvalue_int64_out()).toEqual(Limits.int64.max); + }); + + it('marshals as a caller-allocated out parameter', function () { + expect(GIMarshallingTests.gvalue_out_caller_allocates()).toEqual(42); }); - it('can be passed as an out argument and unpacked', function () { - expect(GIMarshallingTests.gvalue_out()).toEqual(42); + it('array can be passed into a function and packed', function () { + expect(() => GIMarshallingTests.gvalue_flat_array([42, '42', true])) + .not.toThrow(); }); it('array can be passed as an out argument and unpacked', function () { @@ -418,6 +760,11 @@ describe('GValue', function () { .toEqual([42, '42', true]); }); + xit('array can roundtrip with GValues intact', function () { + expect(GIMarshallingTests.gvalue_flat_array_round_trip(42, '42', true)) + .toEqual([42, '42', true]); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/272'); + it('can have its type inferred from primitive values', function () { expect(() => GIMarshallingTests.gvalue_in_with_type(42, GObject.TYPE_INT)) .not.toThrow(); @@ -429,14 +776,7 @@ describe('GValue', function () { .not.toThrow(); }); - it('type objects can be converted from primitive-like types', function () { - expect(() => GIMarshallingTests.gvalue_in_with_type(42, GObject.Int)) - .not.toThrow(); - expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, GObject.Double)) - .not.toThrow(); - expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, Number)) - .not.toThrow(); - }); + // supplementary tests for gvalue_in_with_type() it('can have its type inferred as a GObject type', function () { expect(() => GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction(), Gio.SimpleAction)) @@ -475,11 +815,11 @@ describe('GValue', function () { .not.toThrow(); }); - it('can have its type inferred as a union type', function () { - let union = GIMarshallingTests.union_returnv(); - expect(() => GIMarshallingTests.gvalue_in_with_type(union, GIMarshallingTests.Union)) - .not.toThrow(); - }); + // it('can have its type inferred as a union type', function () { + // let union = GIMarshallingTests.union_returnv(); + // expect(() => GIMarshallingTests.gvalue_in_with_type(union, GIMarshallingTests.Union)) + // .not.toThrow(); + // }); it('can have its type inferred as a GParamSpec', function () { let paramSpec = GObject.ParamSpec.string('my-param', '', '', @@ -488,57 +828,26 @@ describe('GValue', function () { .not.toThrow(); }); - xit('can have its type inferred as a foreign struct', function () { - let Cairo; - try { - Cairo = imports.cairo; - } catch(e) { - pending('Compiled without Cairo support'); - return; - } - - let surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 2, 2); - let cr = new Cairo.Context(surface); - expect(() => GIMarshallingTests.gvalue_in_with_type(cr, Cairo.Context)) - .not.toThrow(); - expect(() => GIMarshallingTests.gvalue_in_with_type(surface, Cairo.Surface)) - .not.toThrow(); - }).pend('Errors out with "not a subclass of GObject_Boxed"'); + // See testCairo.js for a test of GIMarshallingTests.gvalue_in_with_type() + // on Cairo foreign structs, since it will be skipped if compiling without + // Cairo support. }); -describe('GType', function () { - it('can be passed into a function', function () { - expect(() => GIMarshallingTests.gtype_in(GObject.TYPE_NONE)).not.toThrow(); - expect(() => GIMarshallingTests.gtype_in(GObject.VoidType)).not.toThrow(); - expect(() => GIMarshallingTests.gtype_string_in(GObject.TYPE_STRING)).not.toThrow(); - expect(() => GIMarshallingTests.gtype_string_in(GObject.String)).not.toThrow(); - }); +describe('Callback', function () { + describe('GClosure', function () { + testInParameter('gclosure', () => 42); - it('can be returned', function () { - expect(GIMarshallingTests.gtype_return()).toEqual(GObject.TYPE_NONE); - expect(GIMarshallingTests.gtype_string_return()) - .toEqual(GObject.TYPE_STRING); + xit('marshals a GClosure as a return value', function () { + // Currently a GObject.Closure instance is returned, upon which it's + // not possible to call invoke() because that method takes a bare + // pointer as an argument. + expect(GIMarshallingTests.gclosure_return()()).toEqual(42); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/80'); }); - it('can be passed as an out argument', function () { - expect(GIMarshallingTests.gtype_out()).toEqual(GObject.TYPE_NONE); - expect(GIMarshallingTests.gtype_string_out()).toEqual(GObject.TYPE_STRING); - }); - - it('can be passed as an inout argument', function () { - expect(GIMarshallingTests.gtype_inout(GObject.TYPE_NONE)) - .toEqual(GObject.TYPE_INT); - }); - - it('can be implicitly converted from a JS type', function () { - expect(() => GIMarshallingTests.gtype_string_in(String)).not.toThrow(); - }); -}); - -describe('Callback', function () { - it('marshals a return value', function () { - expect(GIMarshallingTests.callback_return_value_only(() => 42)) - .toEqual(42); + it('marshals a return value', function () { + expect(GIMarshallingTests.callback_return_value_only(() => 42)) + .toEqual(42); }); it('marshals one out parameter', function () { @@ -567,22 +876,239 @@ describe('Callback', function () { }).pend('Function not added to gobject-introspection test suite yet'); }); -const VFuncTester = GObject.registerClass(class VFuncTester extends GIMarshallingTests.Object { - vfunc_vfunc_return_value_only() { return 42; } - vfunc_vfunc_one_out_parameter() { return 43; } - vfunc_vfunc_multiple_out_parameters() { return [44, 45]; } - vfunc_vfunc_return_value_and_one_out_parameter() { return [46, 47]; } - vfunc_vfunc_return_value_and_multiple_out_parameters() { return [48, 49, 50]; } - vfunc_vfunc_array_out_parameter() { return [50, 51]; } +describe('Raw pointers', function () { + xit('can be roundtripped at least if the pointer is null', function () { + expect(GIMarshallingTests.pointer_in_return(null)).toBeNull(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/merge_requests/46'); +}); + +describe('Registered enum type', function () { + testSimpleMarshalling('genum', GIMarshallingTests.GEnum.VALUE3, + GIMarshallingTests.GEnum.VALUE1, { + returnv: { + funcName: 'genum_returnv', + }, + }); +}); + +describe('Bare enum type', function () { + testSimpleMarshalling('enum', GIMarshallingTests.Enum.VALUE3, + GIMarshallingTests.Enum.VALUE1, { + returnv: { + funcName: 'enum_returnv', + }, + }); +}); + +describe('Registered flags type', function () { + testSimpleMarshalling('flags', GIMarshallingTests.Flags.VALUE2, + GIMarshallingTests.Flags.VALUE1, { + returnv: { + funcName: 'flags_returnv', + }, + }); +}); + +describe('Bare flags type', function () { + testSimpleMarshalling('no_type_flags', GIMarshallingTests.NoTypeFlags.VALUE2, + GIMarshallingTests.NoTypeFlags.VALUE1, { + returnv: { + funcName: 'no_type_flags_returnv', + }, + }); +}); + +describe('Simple struct', function () { + it('marshals as a return value', function () { + expect(GIMarshallingTests.simple_struct_returnv()).toEqual(jasmine.objectContaining({ + long_: 6, + int8: 7, + })); + }); + + it('marshals as the this-argument of a method', function () { + const struct = new GIMarshallingTests.SimpleStruct({ + long_: 6, + int8: 7, + }); + expect(() => struct.inv()).not.toThrow(); // was this supposed to be static? + expect(() => struct.method()).not.toThrow(); + }); +}); + +describe('Pointer struct', function () { + it('marshals as a return value', function () { + expect(GIMarshallingTests.pointer_struct_returnv()).toEqual(jasmine.objectContaining({ + long_: 42, + })); + }); + + it('marshals as the this-argument of a method', function () { + const struct = new GIMarshallingTests.PointerStruct({ + long_: 42, + }); + expect(() => struct.inv()).not.toThrow(); + }); +}); + +describe('Boxed struct', function () { + it('marshals as a return value', function () { + expect(GIMarshallingTests.boxed_struct_returnv()).toEqual(jasmine.objectContaining({ + long_: 42, + string_: 'hello', + g_strv: ['0', '1', '2'], + })); + }); + + it('marshals as the this-argument of a method', function () { + const struct = new GIMarshallingTests.BoxedStruct({ + long_: 42, + }); + expect(() => struct.inv()).not.toThrow(); + }); + + it('marshals as an out parameter', function () { + expect(GIMarshallingTests.boxed_struct_out()).toEqual(jasmine.objectContaining({ + long_: 42, + })); + }); + + it('marshals as an inout parameter', function () { + const struct = new GIMarshallingTests.BoxedStruct({ + long_: 42, + }); + expect(GIMarshallingTests.boxed_struct_inout(struct)).toEqual(jasmine.objectContaining({ + long_: 0, + })); + }); +}); + +describe('Union', function () { + let union; + beforeEach(function () { + union = GIMarshallingTests.union_returnv(); + }); + + xit('marshals as a return value', function () { + expect(union.long_).toEqual(42); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/273'); + + // it('marshals as the this-argument of a method', function () { + // expect(() => union.inv()).not.toThrow(); // was this supposed to be static? + // expect(() => union.method()).not.toThrow(); + // }); +}); + +describe('GObject', function () { + it('has a static method that can be called', function () { + expect(() => GIMarshallingTests.Object.static_method()).not.toThrow(); + }); + + it('has a method that can be called', function () { + const o = new GIMarshallingTests.Object({int: 42}); + expect(() => o.method()).not.toThrow(); + }); + + it('has an overridden method that can be called', function () { + const o = new GIMarshallingTests.Object({int: 0}); + expect(() => o.overridden_method()).not.toThrow(); + }); + + it('can be created from a static constructor', function () { + const o = GIMarshallingTests.Object.new(42); + expect(o.int).toEqual(42); + }); + + it('can have a static constructor that fails', function () { + expect(() => GIMarshallingTests.Object.new_fail(42)).toThrow(); + }); + + describe('method', function () { + let o; + beforeEach(function () { + o = new GIMarshallingTests.Object(); + }); + + it('marshals an int array as an in parameter', function () { + expect(() => o.method_array_in([-1, 0, 1, 2])).not.toThrow(); + }); + + it('marshals an int array as an out parameter', function () { + expect(o.method_array_out()).toEqual([-1, 0, 1, 2]); + }); + + it('marshals an int array as an inout parameter', function () { + expect(o.method_array_inout([-1, 0, 1, 2])).toEqual([-2, -1, 0, 1, 2]); + }); + + it('marshals an int array as a return value', function () { + expect(o.method_array_return()).toEqual([-1, 0, 1, 2]); + }); + + it('with default implementation can be called', function () { + o = new GIMarshallingTests.Object({int: 42}); + o.method_with_default_implementation(43); + expect(o.int).toEqual(43); + }); + }); + + ['none', 'full'].forEach(transfer => { + ['return', 'out'].forEach(mode => { + it(`marshals as a ${mode} parameter with transfer ${transfer}`, function () { + expect(GIMarshallingTests.Object[`${transfer}_${mode}`]().int).toEqual(0); + }); + }); + + it(`marshals as an inout parameter with transfer ${transfer}`, function () { + const o = new GIMarshallingTests.Object({int: 42}); + expect(GIMarshallingTests.Object[`${transfer}_inout`](o).int).toEqual(0); + }); + }); + + it('marshals as a this value with transfer none', function () { + const o = new GIMarshallingTests.Object({int: 42}); + expect(() => o.none_in()).not.toThrow(); + }); +}); + +let VFuncTester = GObject.registerClass(class VFuncTester extends GIMarshallingTests.Object { + vfunc_vfunc_return_value_only() { + return 42; + } + + vfunc_vfunc_one_out_parameter() { + return 43; + } + + vfunc_vfunc_multiple_out_parameters() { + return [44, 45]; + } + + vfunc_vfunc_return_value_and_one_out_parameter() { + return [46, 47]; + } + + vfunc_vfunc_return_value_and_multiple_out_parameters() { + return [48, 49, 50]; + } + + vfunc_vfunc_array_out_parameter() { + return [50, 51]; + } + + vfunc_vfunc_caller_allocated_out_parameter() { + return 52; + } + vfunc_vfunc_meth_with_err(x) { switch (x) { case -1: return true; case 0: - undefined.throw_type_error(); + undefined.throwTypeError(); break; case 1: - void reference_error; // eslint-disable-line no-undef + void referenceError; // eslint-disable-line no-undef break; case 2: throw new Gio.IOErrorEnum({ @@ -596,8 +1122,64 @@ const VFuncTester = GObject.registerClass(class VFuncTester extends GIMarshallin }); } } + + vfunc_vfunc_return_enum() { + return GIMarshallingTests.Enum.VALUE2; + } + + vfunc_vfunc_out_enum() { + return GIMarshallingTests.Enum.VALUE3; + } + + vfunc_vfunc_return_object_transfer_none() { + if (!this._returnObject) + this._returnObject = new GIMarshallingTests.Object({int: 53}); + return this._returnObject; + } + + vfunc_vfunc_return_object_transfer_full() { + return new GIMarshallingTests.Object({int: 54}); + } + + vfunc_vfunc_out_object_transfer_none() { + if (!this._outObject) + this._outObject = new GIMarshallingTests.Object({int: 55}); + return this._outObject; + } + + vfunc_vfunc_out_object_transfer_full() { + return new GIMarshallingTests.Object({int: 56}); + } + + vfunc_vfunc_in_object_transfer_none(object) { + void object; + } + + vfunc_vfunc_in_object_transfer_full(object) { + this._inObject = object; + } }); +try { + VFuncTester = GObject.registerClass(class VFuncTesterInOut extends VFuncTester { + vfunc_vfunc_one_inout_parameter(input) { + return input * 5; + } + + vfunc_vfunc_multiple_inout_parameters(inputA, inputB) { + return [inputA * 5, inputB * -1]; + } + + vfunc_vfunc_return_value_and_one_inout_parameter(input) { + return [49, input * 5]; + } + + vfunc_vfunc_return_value_and_multiple_inout_parameters(inputA, inputB) { + return [49, inputA * 5, inputB * -1]; + } + }); +} catch {} + describe('Virtual function', function () { let tester; beforeEach(function () { @@ -626,10 +1208,40 @@ describe('Virtual function', function () { .toEqual([48, 49, 50]); }); + it('marshals one inout parameter', function () { + if (typeof VFuncTester.prototype.vfunc_one_inout_parameter === 'undefined') + pending('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/201'); + expect(tester.vfunc_one_inout_parameter(10)).toEqual(50); + }); + + it('marshals multiple inout parameters', function () { + if (typeof VFuncTester.prototype.vfunc_multiple_inout_parameters === 'undefined') + pending('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/201'); + expect(tester.vfunc_multiple_inout_parameters(10, 5)).toEqual([50, -5]); + }); + + it('marshals a return value and one inout parameter', function () { + if (typeof VFuncTester.prototype.vfunc_return_value_and_one_inout_parameter === 'undefined') + pending('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/201'); + expect(tester.vfunc_return_value_and_one_inout_parameter(10)) + .toEqual([49, 50]); + }); + + it('marshals a return value and multiple inout parameters', function () { + if (typeof VFuncTester.prototype.vfunc_return_value_and_multiple_inout_parameters === 'undefined') + pending('https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/201'); + expect(tester.vfunc_return_value_and_multiple_inout_parameters(10, -51)) + .toEqual([49, 50, 51]); + }); + it('marshals an array out parameter', function () { expect(tester.vfunc_array_out_parameter()).toEqual([50, 51]); }); + it('marshals a caller-allocated GValue out parameter', function () { + expect(tester.vfunc_caller_allocated_out_parameter()).toEqual(52); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/74'); + it('marshals an error out parameter when no error', function () { expect(tester.vfunc_meth_with_error(-1)).toBeTruthy(); }); @@ -656,6 +1268,188 @@ describe('Virtual function', function () { expect(e.message).toEqual('This test is Too Big to Fail'); } }); + + it('marshals an enum return value', function () { + expect(tester.vfunc_return_enum()).toEqual(GIMarshallingTests.Enum.VALUE2); + }); + + it('marshals an enum out parameter', function () { + expect(tester.vfunc_out_enum()).toEqual(GIMarshallingTests.Enum.VALUE3); + }); + + // These tests check what the refcount is of the returned objects; see + // comments in gimarshallingtests.c. + // Objects that are exposed in JS always have at least one reference (the + // toggle reference.) JS never owns more than one reference. There may be + // other references owned on the C side. + // In any case the refs should not be floating. We never have floating refs + // in JS. + function testVfuncRefcount(mode, transfer, expectedRefcount, options = {}, ...args) { + it(`marshals an object ${mode} parameter with transfer ${transfer}`, function () { + if (options.skip) + pending(options.skip); + const [refcount, floating] = + tester[`get_ref_info_for_vfunc_${mode}_object_transfer_${transfer}`](...args); + expect(floating).toBeFalsy(); + expect(refcount).toEqual(expectedRefcount); + }); + } + // 1 reference = the object is owned only by JS. + // 2 references = the object is owned by JS and the vfunc caller. + testVfuncRefcount('return', 'none', 1); + testVfuncRefcount('return', 'full', 2); + testVfuncRefcount('out', 'none', 1); + testVfuncRefcount('out', 'full', 2); + testVfuncRefcount('in', 'none', 2, {}, GIMarshallingTests.Object); + testVfuncRefcount('in', 'full', 1, { + skip: 'https://gitlab.gnome.org/GNOME/gjs/issues/275', + }, GIMarshallingTests.Object); +}); + +const WrongVFuncTester = GObject.registerClass(class WrongVFuncTester extends GIMarshallingTests.Object { + vfunc_vfunc_return_value_only() { + } + + vfunc_vfunc_one_out_parameter() { + } + + vfunc_vfunc_multiple_out_parameters() { + } + + vfunc_vfunc_return_value_and_one_out_parameter() { + } + + vfunc_vfunc_return_value_and_multiple_out_parameters() { + } + + vfunc_vfunc_array_out_parameter() { + } + + vfunc_vfunc_caller_allocated_out_parameter() { + } + + vfunc_vfunc_return_enum() { + } + + vfunc_vfunc_out_enum() { + } + + vfunc_vfunc_return_object_transfer_none() { + } + + vfunc_vfunc_return_object_transfer_full() { + } + + vfunc_vfunc_out_object_transfer_none() { + } + + vfunc_vfunc_out_object_transfer_full() { + } + + vfunc_vfunc_in_object_transfer_none() { + } +}); + +describe('Wrong virtual functions', function () { + let tester; + beforeEach(function () { + tester = new WrongVFuncTester(); + }); + + it('marshals a return value', function () { + expect(tester.vfunc_return_value_only()).toBeUndefined(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/311'); + + it('marshals one out parameter', function () { + expect(tester.vfunc_one_out_parameter()).toBeUndefined(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/311'); + + it('marshals multiple out parameters', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Function *vfunc_vfunc_multiple_out_parameters*Array*'); + + expect(tester.vfunc_multiple_out_parameters()).toEqual([0, 0]); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); + + it('marshals a return value and one out parameter', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Function *vfunc_return_value_and_one_out_parameter*Array*'); + + expect(tester.vfunc_return_value_and_one_out_parameter()).toEqual([0, 0]); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); + + it('marshals a return value and multiple out parameters', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Function *vfunc_return_value_and_multiple_out_parameters*Array*'); + + expect(tester.vfunc_return_value_and_multiple_out_parameters()).toEqual([0, 0, 0]); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); + + it('marshals an array out parameter', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Expected type gfloat for Argument*undefined*'); + + expect(tester.vfunc_array_out_parameter()).toEqual(null); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); + + it('marshals an enum return value', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Expected type enum for Return*undefined*'); + + expect(tester.vfunc_return_enum()).toEqual(0); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); + + it('marshals an enum out parameter', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'JS ERROR: Error: Expected type enum for Argument*undefined*'); + + expect(tester.vfunc_out_enum()).toEqual(0); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGIMarshalling.js', 0, + 'testVFuncReturnWrongValue'); + }); +}); + +describe('Inherited GObject', function () { + ['SubObject', 'SubSubObject'].forEach(klass => { + describe(klass, function () { + it('has a parent method that can be called', function () { + const o = new GIMarshallingTests.SubObject({int: 42}); + expect(() => o.method()).not.toThrow(); + }); + + it('has a method that can be called', function () { + const o = new GIMarshallingTests.SubObject({int: 0}); + expect(() => o.sub_method()).not.toThrow(); + }); + + it('has an overridden method that can be called', function () { + const o = new GIMarshallingTests.SubObject({int: 0}); + expect(() => o.overwritten_method()).not.toThrow(); + }); + + it('has a method with default implementation can be called', function () { + const o = new GIMarshallingTests.SubObject({int: 42}); + o.method_with_default_implementation(43); + expect(o.int).toEqual(43); + }); + }); + }); }); describe('Interface', function () { @@ -664,6 +1458,169 @@ describe('Interface', function () { let itself = ifaceImpl.get_as_interface(); expect(ifaceImpl).toEqual(itself); }); + + it('can call an interface vfunc in C', function () { + let ifaceImpl = new GIMarshallingTests.InterfaceImpl(); + expect(() => ifaceImpl.test_int8_in(42)).not.toThrow(); + expect(() => GIMarshallingTests.test_interface_test_int8_in(ifaceImpl, 42)) + .not.toThrow(); + }); + + it('can implement a C interface', function () { + const I2Impl = GObject.registerClass({ + Implements: [GIMarshallingTests.Interface2], + }, class I2Impl extends GObject.Object {}); + expect(() => new I2Impl()).not.toThrow(); + }); + + it('can implement a C interface with a vfunc', function () { + const I3Impl = GObject.registerClass({ + Implements: [GIMarshallingTests.Interface3], + }, class I3Impl extends GObject.Object { + vfunc_test_variant_array_in(variantArray) { + this.stuff = variantArray.map(v => v.deepUnpack()); + } + }); + const i3 = new I3Impl(); + i3.test_variant_array_in([ + new GLib.Variant('b', true), + new GLib.Variant('s', 'hello'), + new GLib.Variant('i', 42), + ]); + expect(i3.stuff).toEqual([true, 'hello', 42]); + }); +}); + +describe('Configurations of return values', function () { + it('can handle two out parameters', function () { + expect(GIMarshallingTests.int_out_out()).toEqual([6, 7]); + }); + + it('can handle three in and three out parameters', function () { + expect(GIMarshallingTests.int_three_in_three_out(1, 2, 3)).toEqual([1, 2, 3]); + }); + + it('can handle a return value and an out parameter', function () { + expect(GIMarshallingTests.int_return_out()).toEqual([6, 7]); + }); + + it('can handle four in parameters, two of which are nullable', function () { + expect(() => GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, '3', '4')) + .not.toThrow(); + expect(() => GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, '3', null)) + .not.toThrow(); + expect(() => GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, null, '4')) + .not.toThrow(); + expect(() => GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, null, null)) + .not.toThrow(); + }); + + it('can handle three in parameters, one of which is nullable and one not', function () { + expect(() => GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none(1, '2', '3')) + .not.toThrow(); + expect(() => GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none(1, null, '3')) + .not.toThrow(); + expect(() => GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none(1, '2', null)) + .toThrow(); + }); + + it('can handle an array in parameter and two nullable in parameters', function () { + expect(() => GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], '1', '2')) + .not.toThrow(); + expect(() => GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], '1', null)) + .not.toThrow(); + expect(() => GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], null, '2')) + .not.toThrow(); + expect(() => GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], null, null)) + .not.toThrow(); + }); + + it('can handle an array in parameter and two nullable in parameters, mixed with the array length', function () { + expect(() => + GIMarshallingTests.array_in_utf8_two_in_out_of_order('1', [-1, 0, 1, 2], '2')) + .not.toThrow(); + expect(() => + GIMarshallingTests.array_in_utf8_two_in_out_of_order('1', [-1, 0, 1, 2], null)) + .not.toThrow(); + expect(() => + GIMarshallingTests.array_in_utf8_two_in_out_of_order(null, [-1, 0, 1, 2], '2')) + .not.toThrow(); + expect(() => + GIMarshallingTests.array_in_utf8_two_in_out_of_order(null, [-1, 0, 1, 2], null)) + .not.toThrow(); + }); +}); + +describe('GError', function () { + it('marshals a GError** signature as an exception', function () { + expect(() => GIMarshallingTests.gerror()).toThrow(); + }); + + it('marshals a GError** at the end of the signature as an exception', function () { + expect(() => GIMarshallingTests.gerror_array_in([-1, 0, 1, 2])).toThrow(); + }); + + it('marshals a GError** elsewhere in the signature as an out parameter', function () { + expect(GIMarshallingTests.gerror_out()).toEqual([ + jasmine.any(GLib.Error), + 'we got an error, life is shit', + ]); + }); + + it('marshals a GError** elsewhere in the signature as an out parameter with transfer none', function () { + expect(GIMarshallingTests.gerror_out_transfer_none()).toEqual([ + jasmine.any(GLib.Error), + 'we got an error, life is shit', + ]); + }); + + it('marshals GError as a return value', function () { + expect(GIMarshallingTests.gerror_return()).toEqual(jasmine.any(GLib.Error)); + }); +}); + +describe('Overrides', function () { + it('can add constants', function () { + expect(GIMarshallingTests.OVERRIDES_CONSTANT).toEqual(7); + }); + + it('can override a struct method', function () { + const struct = new GIMarshallingTests.OverridesStruct(); + expect(struct.method()).toEqual(6); + }); + + it('can override an object constructor', function () { + const obj = new GIMarshallingTests.OverridesObject(42); + expect(obj.num).toEqual(42); + }); + + it('can override an object method', function () { + const obj = new GIMarshallingTests.OverridesObject(); + expect(obj.method()).toEqual(6); + }); +}); + +describe('Filename', function () { + testReturnValue('filename_list', []); +}); + +describe('GObject.ParamSpec', function () { + const pspec = GObject.ParamSpec.boolean('mybool', 'My Bool', 'My boolean property', + GObject.ParamFlags.READWRITE, true); + testInParameter('param_spec', pspec, { + funcName: 'param_spec_in_bool', + }); + + const expectedProps = { + name: 'test-param', + nick: 'test', + blurb: 'This is a test', + default_value: '42', + flags: GObject.ParamFlags.READABLE, + value_type: GObject.TYPE_STRING, + }; + testReturnValue('param_spec', jasmine.objectContaining(expectedProps)); + testOutParameter('param_spec', jasmine.objectContaining(expectedProps)); }); describe('GObject properties', function () { @@ -672,10 +1629,62 @@ describe('GObject properties', function () { obj = new GIMarshallingTests.PropertiesObject(); }); - it('can handle GValues', function () { - obj.some_gvalue = 42; - expect(obj.some_gvalue).toEqual(42); - obj.some_gvalue = 'foo'; - expect(obj.some_gvalue).toEqual('foo'); + function testPropertyGetSet(type, value1, value2, skip = false) { + it(`gets and sets a ${type} property`, function () { + if (skip) + pending(skip); + obj[`some_${type}`] = value1; + expect(obj[`some_${type}`]).toEqual(value1); + obj[`some_${type}`] = value2; + expect(obj[`some_${type}`]).toEqual(value2); + }); + } + testPropertyGetSet('boolean', true, false); + testPropertyGetSet('char', 42, 64); + testPropertyGetSet('uchar', 42, 64); + testPropertyGetSet('int', 42, 64); + testPropertyGetSet('uint', 42, 64); + testPropertyGetSet('long', 42, 64); + testPropertyGetSet('ulong', 42, 64); + testPropertyGetSet('int64', 42, 64); + testPropertyGetSet('uint64', 42, 64); + + it('gets and sets a float property', function () { + obj.some_float = Math.E; + expect(obj.some_float).toBeCloseTo(Math.E); + obj.some_float = Math.PI; + expect(obj.some_float).toBeCloseTo(Math.PI); + }); + + it('gets and sets a double property', function () { + obj.some_double = Math.E; + expect(obj.some_double).toBeCloseTo(Math.E); + obj.some_double = Math.PI; + expect(obj.some_double).toBeCloseTo(Math.PI); + }); + + testPropertyGetSet('strv', ['0', '1', '2'], []); + testPropertyGetSet('boxed_struct', new GIMarshallingTests.BoxedStruct(), + new GIMarshallingTests.BoxedStruct({long_: 42})); + testPropertyGetSet('boxed_glist', null, null); + testPropertyGetSet('gvalue', 42, 'foo'); + testPropertyGetSet('variant', new GLib.Variant('b', true), + new GLib.Variant('s', 'hello')); + testPropertyGetSet('object', new GObject.Object(), + new GIMarshallingTests.Object({int: 42})); + testPropertyGetSet('flags', GIMarshallingTests.Flags.VALUE2, + GIMarshallingTests.Flags.VALUE1 | GIMarshallingTests.Flags.VALUE2); + testPropertyGetSet('enum', GIMarshallingTests.GEnum.VALUE2, + GIMarshallingTests.GEnum.VALUE3); + testPropertyGetSet('byte_array', Uint8Array.of(1, 2, 3), + ByteArray.fromString('👾'), + 'https://gitlab.gnome.org/GNOME/gjs/issues/276'); + + it('gets a read-only property', function () { + expect(obj.some_readonly).toEqual(42); + }); + + it('throws when setting a read-only property', function () { + expect(() => (obj.some_readonly = 35)).toThrow(); }); }); diff --git a/installed-tests/js/testGLib.js b/installed-tests/js/testGLib.js index 4651671..1d5bd64 100644 --- a/installed-tests/js/testGLib.js +++ b/installed-tests/js/testGLib.js @@ -1,43 +1,232 @@ +const ByteArray = imports.byteArray; const GLib = imports.gi.GLib; describe('GVariant constructor', function () { it('constructs a string variant', function () { - let str_variant = new GLib.Variant('s', 'mystring'); - expect(str_variant.get_string()[0]).toEqual('mystring'); - expect(str_variant.deep_unpack()).toEqual('mystring'); + let strVariant = new GLib.Variant('s', 'mystring'); + expect(strVariant.get_string()[0]).toEqual('mystring'); + expect(strVariant.deepUnpack()).toEqual('mystring'); }); it('constructs a string variant (backwards compatible API)', function () { - let str_variant = new GLib.Variant('s', 'mystring'); - let str_variant_old = GLib.Variant.new('s', 'mystring'); - expect(str_variant.equal(str_variant_old)).toBeTruthy(); + let strVariant = new GLib.Variant('s', 'mystring'); + let strVariantOld = GLib.Variant.new('s', 'mystring'); + expect(strVariant.equal(strVariantOld)).toBeTruthy(); }); it('constructs a struct variant', function () { - let struct_variant = new GLib.Variant('(sogvau)', [ + let structVariant = new GLib.Variant('(sogvau)', [ 'a string', '/a/object/path', - 'asig', //nature + 'asig', // nature new GLib.Variant('s', 'variant'), - [ 7, 3 ] + [7, 3], ]); - expect(struct_variant.n_children()).toEqual(5); + expect(structVariant.n_children()).toEqual(5); - let unpacked = struct_variant.deep_unpack(); + let unpacked = structVariant.deepUnpack(); expect(unpacked[0]).toEqual('a string'); expect(unpacked[1]).toEqual('/a/object/path'); expect(unpacked[2]).toEqual('asig'); expect(unpacked[3] instanceof GLib.Variant).toBeTruthy(); - expect(unpacked[3].deep_unpack()).toEqual('variant'); + expect(unpacked[3].deepUnpack()).toEqual('variant'); expect(unpacked[4] instanceof Array).toBeTruthy(); expect(unpacked[4].length).toEqual(2); }); it('constructs a maybe variant', function () { - let maybe_variant = new GLib.Variant('ms', null); - expect(maybe_variant.deep_unpack()).toBeNull(); + let maybeVariant = new GLib.Variant('ms', null); + expect(maybeVariant.deepUnpack()).toBeNull(); - maybe_variant = new GLib.Variant('ms', 'string'); - expect(maybe_variant.deep_unpack()).toEqual('string'); + maybeVariant = new GLib.Variant('ms', 'string'); + expect(maybeVariant.deepUnpack()).toEqual('string'); + }); + + it('constructs a byte array variant', function () { + const byteArray = Uint8Array.from('pizza', c => c.charCodeAt(0)); + const byteArrayVariant = new GLib.Variant('ay', byteArray); + expect(ByteArray.toString(byteArrayVariant.deepUnpack())) + .toEqual('pizza'); + }); + + it('constructs a byte array variant from a string', function () { + const byteArrayVariant = new GLib.Variant('ay', 'pizza'); + expect(ByteArray.toString(byteArrayVariant.deepUnpack())) + .toEqual('pizza'); + }); + + it('0-terminates a byte array variant constructed from a string', function () { + const byteArrayVariant = new GLib.Variant('ay', 'pizza'); + const a = byteArrayVariant.deepUnpack(); + [112, 105, 122, 122, 97, 0].forEach((val, ix) => + expect(a[ix]).toEqual(val)); + }); + + it('does not 0-terminate a byte array variant constructed from a Uint8Array', function () { + const byteArray = Uint8Array.from('pizza', c => c.charCodeAt(0)); + const byteArrayVariant = new GLib.Variant('ay', byteArray); + const a = byteArrayVariant.deepUnpack(); + [112, 105, 122, 122, 97].forEach((val, ix) => + expect(a[ix]).toEqual(val)); + }); +}); + +describe('GVariant unpack', function () { + let v; + beforeEach(function () { + v = new GLib.Variant('a{sv}', {foo: new GLib.Variant('s', 'bar')}); + }); + + it('preserves type information if the unpacked object contains variants', function () { + expect(v.deepUnpack().foo instanceof GLib.Variant).toBeTruthy(); + expect(v.deep_unpack().foo instanceof GLib.Variant).toBeTruthy(); + }); + + it('recursive leaves no variants in the unpacked object', function () { + expect(v.recursiveUnpack().foo instanceof GLib.Variant).toBeFalsy(); + expect(v.recursiveUnpack().foo).toEqual('bar'); + }); +}); + +describe('GVariantDict lookup', function () { + let variantDict; + beforeEach(function () { + variantDict = new GLib.VariantDict(null); + variantDict.insert_value('foo', GLib.Variant.new_string('bar')); + }); + + it('returns the unpacked variant', function () { + expect(variantDict.lookup('foo')).toEqual('bar'); + expect(variantDict.lookup('foo', null)).toEqual('bar'); + expect(variantDict.lookup('foo', 's')).toEqual('bar'); + expect(variantDict.lookup('foo', new GLib.VariantType('s'))).toEqual('bar'); + }); + + it("returns null if the key isn't present", function () { + expect(variantDict.lookup('bar')).toBeNull(); + expect(variantDict.lookup('bar', null)).toBeNull(); + expect(variantDict.lookup('bar', 's')).toBeNull(); + expect(variantDict.lookup('bar', new GLib.VariantType('s'))).toBeNull(); + }); +}); + +describe('GLib string function overrides', function () { + let numExpectedWarnings; + + function expectWarnings(count) { + numExpectedWarnings = count; + for (let c = 0; c < count; c++) { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + '*not introspectable*'); + } + } + + function assertWarnings(testName) { + for (let c = 0; c < numExpectedWarnings; c++) { + GLib.test_assert_expected_messages_internal('Cjs', 'testGLib.js', 0, + `test GLib.${testName}`); + } + numExpectedWarnings = 0; + } + + beforeEach(function () { + numExpectedWarnings = 0; + }); + + it('GLib.stpcpy', function () { + expect(() => GLib.stpcpy('dest', 'src')).toThrowError(/not introspectable/); + }); + + it('GLib.strstr_len', function () { + expectWarnings(4); + expect(GLib.strstr_len('haystack', -1, 'needle')).toBeNull(); + expect(GLib.strstr_len('haystacks', -1, 'stack')).toEqual('stacks'); + expect(GLib.strstr_len('haystacks', 4, 'stack')).toBeNull(); + expect(GLib.strstr_len('haystack', 4, 'ays')).toEqual('aystack'); + assertWarnings('strstr_len'); + }); + + it('GLib.strrstr', function () { + expectWarnings(2); + expect(GLib.strrstr('haystack', 'needle')).toBeNull(); + expect(GLib.strrstr('hackstacks', 'ack')).toEqual('acks'); + assertWarnings('strrstr'); + }); + + it('GLib.strrstr_len', function () { + expectWarnings(3); + expect(GLib.strrstr_len('haystack', -1, 'needle')).toBeNull(); + expect(GLib.strrstr_len('hackstacks', -1, 'ack')).toEqual('acks'); + expect(GLib.strrstr_len('hackstacks', 4, 'ack')).toEqual('ackstacks'); + assertWarnings('strrstr_len'); + }); + + it('GLib.strup', function () { + expectWarnings(1); + expect(GLib.strup('string')).toEqual('STRING'); + assertWarnings('strup'); + }); + + it('GLib.strdown', function () { + expectWarnings(1); + expect(GLib.strdown('STRING')).toEqual('string'); + assertWarnings('strdown'); + }); + + it('GLib.strreverse', function () { + expectWarnings(1); + expect(GLib.strreverse('abcdef')).toEqual('fedcba'); + assertWarnings('strreverse'); + }); + + it('GLib.ascii_dtostr', function () { + expectWarnings(2); + expect(GLib.ascii_dtostr('', GLib.ASCII_DTOSTR_BUF_SIZE, Math.PI)) + .toEqual('3.141592653589793'); + expect(GLib.ascii_dtostr('', 4, Math.PI)).toEqual('3.14'); + assertWarnings('ascii_dtostr'); + }); + + it('GLib.ascii_formatd', function () { + expect(() => GLib.ascii_formatd('', 8, '%e', Math.PI)).toThrowError(/not introspectable/); + }); + + it('GLib.strchug', function () { + expectWarnings(2); + expect(GLib.strchug('text')).toEqual('text'); + expect(GLib.strchug(' text')).toEqual('text'); + assertWarnings('strchug'); + }); + + it('GLib.strchomp', function () { + expectWarnings(2); + expect(GLib.strchomp('text')).toEqual('text'); + expect(GLib.strchomp('text ')).toEqual('text'); + assertWarnings('strchomp'); + }); + + it('GLib.strstrip', function () { + expectWarnings(4); + expect(GLib.strstrip('text')).toEqual('text'); + expect(GLib.strstrip(' text')).toEqual('text'); + expect(GLib.strstrip('text ')).toEqual('text'); + expect(GLib.strstrip(' text ')).toEqual('text'); + assertWarnings('strstrip'); + }); + + it('GLib.strdelimit', function () { + expectWarnings(4); + expect(GLib.strdelimit('1a2b3c4', 'abc', '_'.charCodeAt())).toEqual('1_2_3_4'); + expect(GLib.strdelimit('1-2_3<4', null, '|'.charCodeAt())).toEqual('1|2|3|4'); + expect(GLib.strdelimit('1a2b3c4', 'abc', '_')).toEqual('1_2_3_4'); + expect(GLib.strdelimit('1-2_3<4', null, '|')).toEqual('1|2|3|4'); + assertWarnings('strdelimit'); + }); + + it('GLib.strcanon', function () { + expectWarnings(2); + expect(GLib.strcanon('1a2b3c4', 'abc', '?'.charCodeAt())).toEqual('?a?b?c?'); + expect(GLib.strcanon('1a2b3c4', 'abc', '?')).toEqual('?a?b?c?'); + assertWarnings('strcanon'); }); }); diff --git a/installed-tests/js/testGObject.js b/installed-tests/js/testGObject.js new file mode 100644 index 0000000..49bbba3 --- /dev/null +++ b/installed-tests/js/testGObject.js @@ -0,0 +1,50 @@ +// This is where overrides in modules/core/overrides/GObject.js are tested, +// except for the class machinery, interface machinery, and GObject.ParamSpec, +// which are big enough to get their own files. + +const {GLib, GObject} = imports.gi; + +describe('GObject overrides', function () { + const TestObj = GObject.registerClass({ + Properties: { + int: GObject.ParamSpec.int('int', '', '', GObject.ParamFlags.READWRITE, + 0, GLib.MAXINT32, 0), + string: GObject.ParamSpec.string('string', '', '', + GObject.ParamFlags.READWRITE, ''), + }, + Signals: { + test: {}, + }, + }, class TestObj extends GObject.Object {}); + + it('GObject.set()', function () { + const o = new TestObj(); + o.set({string: 'Answer', int: 42}); + expect(o.string).toBe('Answer'); + expect(o.int).toBe(42); + }); + + describe('Signal alternative syntax', function () { + let o, handler; + beforeEach(function () { + handler = jasmine.createSpy('handler'); + o = new TestObj(); + const handlerId = GObject.signal_connect(o, 'test', handler); + handler.and.callFake(() => + GObject.signal_handler_disconnect(o, handlerId)); + + GObject.signal_emit_by_name(o, 'test'); + }); + + it('handler is called with the right object', function () { + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(o); + }); + + it('disconnected handler is not called', function () { + handler.calls.reset(); + GObject.signal_emit_by_name(o, 'test'); + expect(handler).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js index 44f2bd3..1abe44b 100644 --- a/installed-tests/js/testGObjectClass.js +++ b/installed-tests/js/testGObjectClass.js @@ -2,6 +2,7 @@ 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; @@ -18,17 +19,17 @@ const MyObject = GObject.registerClass({ }, Signals: { 'empty': {}, - 'minimal': { param_types: [ GObject.TYPE_INT, GObject.TYPE_INT ] }, + 'minimal': {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]}, 'full': { flags: GObject.SignalFlags.RUN_LAST, accumulator: GObject.AccumulatorType.FIRST_WINS, return_type: GObject.TYPE_INT, param_types: [], }, - 'run-last': { flags: GObject.SignalFlags.RUN_LAST }, + 'run-last': {flags: GObject.SignalFlags.RUN_LAST}, 'detailed': { flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED, - param_types: [ GObject.TYPE_STRING ], + param_types: [GObject.TYPE_STRING], }, }, }, class MyObject extends GObject.Object { @@ -39,7 +40,7 @@ const MyObject = GObject.registerClass({ } set readwrite(val) { - if (val == 'ignore') + if (val === 'ignore') return; this._readwrite = val; @@ -72,30 +73,30 @@ const MyObject = GObject.registerClass({ this._constructCalled = true; } - notify_prop() { + notifyProp() { this._readonly = 'changed'; this.notify('readonly'); } - emit_empty() { + emitEmpty() { this.emit('empty'); } - emit_minimal(one, two) { + emitMinimal(one, two) { this.emit('minimal', one, two); } - emit_full() { + emitFull() { return this.emit('full'); } - emit_detailed() { + emitDetailed() { this.emit('detailed::one'); this.emit('detailed::two'); } - emit_run_last(callback) { + emitRunLast(callback) { this._run_last_callback = callback; this.emit('run-last'); } @@ -114,20 +115,25 @@ const MyObject = GObject.registerClass({ } }); +const MyAbstractObject = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, +}, class MyAbstractObject extends GObject.Object { +}); + const MyApplication = GObject.registerClass({ - Signals: { 'custom': { param_types: [ GObject.TYPE_INT ] } }, + Signals: {'custom': {param_types: [GObject.TYPE_INT]}}, }, class MyApplication extends Gio.Application { - emit_custom(n) { + emitCustom(n) { this.emit('custom', n); } }); const MyInitable = GObject.registerClass({ - Implements: [ Gio.Initable ], + Implements: [Gio.Initable], }, class MyInitable extends GObject.Object { vfunc_init(cancellable) { if (!(cancellable instanceof Gio.Cancellable)) - throw 'Bad argument'; + throw new Error('Bad argument'); this.inited = true; } @@ -135,10 +141,12 @@ const MyInitable = GObject.registerClass({ const Derived = GObject.registerClass(class Derived extends MyObject { _init() { - super._init({ readwrite: 'yes' }); + super._init({readwrite: 'yes'}); } }); +const Cla$$ = GObject.registerClass(class Cla$$ extends MyObject {}); + const MyCustomInit = GObject.registerClass(class MyCustomInit extends GObject.Object { _instance_init() { this.foo = true; @@ -153,7 +161,11 @@ describe('GObject class with decorator', function () { it('throws an error when not used with a GObject-derived class', function () { class Foo {} - expect (() => GObject.registerClass(class Bar extends Foo {})).toThrow(); + expect(() => GObject.registerClass(class Bar extends Foo {})).toThrow(); + }); + + it('throws an error when used with an abstract class', function () { + expect(() => new MyAbstractObject()).toThrow(); }); it('constructs with default values for properties', function () { @@ -163,12 +175,30 @@ describe('GObject class with decorator', function () { }); it('constructs with a hash of property values', function () { - let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' }); + let myInstance2 = new MyObject({readwrite: 'baz', construct: 'asdf'}); expect(myInstance2.readwrite).toEqual('baz'); expect(myInstance2.readonly).toEqual('bar'); expect(myInstance2.construct).toEqual('asdf'); }); + it('warns if more than one argument passed to the default constructor', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_MESSAGE, + '*Too many arguments*'); + + new MyObject({readwrite: 'baz'}, 'this is ignored', 123); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectClass.js', 0, + 'testGObjectClassTooManyArguments'); + }); + + it('throws an error if the first argument to the default constructor is not a property hash', function () { + expect(() => new MyObject('this is wrong')).toThrow(); + }); + + it('accepts a property hash that is not a plain object', function () { + expect(() => new MyObject(new GObject.Object())).not.toThrow(); + }); + const ui = ` baz @@ -196,8 +226,8 @@ describe('GObject class with decorator', function () { let notifySpy = jasmine.createSpy('notifySpy'); myInstance.connect('notify::readonly', notifySpy); - myInstance.notify_prop(); - myInstance.notify_prop(); + myInstance.notifyProp(); + myInstance.notifyProp(); expect(notifySpy).toHaveBeenCalledTimes(2); }); @@ -205,7 +235,7 @@ describe('GObject class with decorator', function () { it('can define its own signals', function () { let emptySpy = jasmine.createSpy('emptySpy'); myInstance.connect('empty', emptySpy); - myInstance.emit_empty(); + myInstance.emitEmpty(); expect(emptySpy).toHaveBeenCalled(); expect(myInstance.empty_called).toBeTruthy(); @@ -214,7 +244,7 @@ describe('GObject class with decorator', function () { it('passes emitted arguments to signal handlers', function () { let minimalSpy = jasmine.createSpy('minimalSpy'); myInstance.connect('minimal', minimalSpy); - myInstance.emit_minimal(7, 5); + myInstance.emitMinimal(7, 5); expect(minimalSpy).toHaveBeenCalledWith(myInstance, 7, 5); }); @@ -222,7 +252,7 @@ describe('GObject class with decorator', function () { it('can return values from signals', function () { let fullSpy = jasmine.createSpy('fullSpy').and.returnValue(42); myInstance.connect('full', fullSpy); - let result = myInstance.emit_full(); + let result = myInstance.emitFull(); expect(fullSpy).toHaveBeenCalled(); expect(result).toEqual(42); @@ -232,36 +262,40 @@ describe('GObject class with decorator', function () { let neverCalledSpy = jasmine.createSpy('neverCalledSpy'); myInstance.connect('full', () => 42); myInstance.connect('full', neverCalledSpy); - myInstance.emit_full(); + myInstance.emitFull(); expect(neverCalledSpy).not.toHaveBeenCalled(); expect(myInstance.full_default_handler_called).toBeFalsy(); }); it('gets the return value of the default handler', function () { - let result = myInstance.emit_full(); + let result = myInstance.emitFull(); expect(myInstance.full_default_handler_called).toBeTruthy(); expect(result).toEqual(79); }); it('calls run-last default handler last', function () { - let stack = [ ]; + let stack = []; let runLastSpy = jasmine.createSpy('runLastSpy') - .and.callFake(() => { stack.push(1); }); + .and.callFake(() => { + stack.push(1); + }); myInstance.connect('run-last', runLastSpy); - myInstance.emit_run_last(() => { stack.push(2); }); + myInstance.emitRunLast(() => { + stack.push(2); + }); expect(stack).toEqual([1, 2]); }); it("can inherit from something that's not GObject.Object", function () { // ...and still get all the goodies of GObject.Class - let instance = new MyApplication({ application_id: 'org.gjs.Application' }); + let instance = new MyApplication({application_id: 'org.gjs.Application'}); let customSpy = jasmine.createSpy('customSpy'); instance.connect('custom', customSpy); - instance.emit_custom(73); + instance.emitCustom(73); expect(customSpy).toHaveBeenCalledWith(instance, 73); }); @@ -292,6 +326,13 @@ describe('GObject class with decorator', function () { expect(derived.readwrite).toEqual('yes'); }); + it('can have any valid class name', function () { + let obj = new Cla$$(); + + expect(obj instanceof Cla$$).toBeTruthy(); + expect(obj instanceof MyObject).toBeTruthy(); + }); + it('calls its _instance_init() function while chaining up in constructor', function () { let instance = new MyCustomInit(); expect(instance.foo).toBeTruthy(); @@ -302,11 +343,11 @@ describe('GObject class with decorator', function () { Properties: { 'file': GObject.ParamSpec.object('file', 'File', 'File', GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - Gio.File.$gtype) + Gio.File.$gtype), }, }, class InterfacePropObject extends GObject.Object {}); let file = Gio.File.new_for_path('dummy'); - expect(() => new InterfacePropObject({ file: file })).not.toThrow(); + expect(() => new InterfacePropObject({file})).not.toThrow(); }); it('can override a property from the parent class', function () { @@ -318,8 +359,9 @@ describe('GObject class with decorator', function () { get readwrite() { return this._subclass_readwrite; } + set readwrite(val) { - this._subclass_readwrite = 'subclass' + val; + this._subclass_readwrite = `subclass${val}`; } }); let obj = new OverrideObject(); @@ -334,4 +376,513 @@ describe('GObject class with decorator', function () { }, }, class BadOverride extends GObject.Object {})).toThrow(); }); + + it('handles gracefully forgetting to override a C property', function () { + GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL, + "*Object class Gjs_ForgottenOverride doesn't implement property " + + "'anchors' from interface 'GTlsFileDatabase'*"); + + // This is a random interface in Gio with a read-write property + const ForgottenOverride = GObject.registerClass({ + Implements: [Gio.TlsFileDatabase], + }, class ForgottenOverride extends Gio.TlsDatabase {}); + const obj = new ForgottenOverride(); + expect(obj.anchors).not.toBeDefined(); + expect(() => (obj.anchors = 'foo')).not.toThrow(); + expect(obj.anchors).toEqual('foo'); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectClass.js', 0, + 'testGObjectClassForgottenOverride'); + }); + + it('handles gracefully overriding a C property but forgetting the accessors', function () { + // This is a random interface in Gio with a read-write property + const ForgottenAccessors = GObject.registerClass({ + Implements: [Gio.TlsFileDatabase], + Properties: { + 'anchors': GObject.ParamSpec.override('anchors', Gio.TlsFileDatabase), + }, + }, class ForgottenAccessors extends Gio.TlsDatabase {}); + const obj = new ForgottenAccessors(); + expect(obj.anchors).toBeNull(); // the property's default value + obj.anchors = 'foo'; + expect(obj.anchors).toEqual('foo'); + + const ForgottenAccessors2 = + GObject.registerClass(class ForgottenAccessors2 extends ForgottenAccessors {}); + const obj2 = new ForgottenAccessors2(); + expect(obj2.anchors).toBeNull(); + obj2.anchors = 'foo'; + expect(obj2.anchors).toEqual('foo'); + }); + + it('does not pollute the wrong prototype with GObject properties', function () { + const MyCustomCharset = GObject.registerClass(class MyCustomCharset extends Gio.CharsetConverter { + _init() { + super._init(); + void this.from_charset; + } + }); + + const MySecondCustomCharset = GObject.registerClass(class MySecondCustomCharset extends GObject.Object { + _init() { + super._init(); + this.from_charset = 'another value'; + } + }); + + expect(() => new MyCustomCharset() && new MySecondCustomCharset()).not.toThrow(); + }); + + it('resolves properties from interfaces', function () { + const mon = Gio.NetworkMonitor.get_default(); + expect(mon.network_available).toBeDefined(); + expect(mon.networkAvailable).toBeDefined(); + expect(mon['network-available']).toBeDefined(); + }); + + it('has a toString() defintion', function () { + expect(myInstance.toString()).toMatch( + /\[object instance wrapper GType:Gjs_MyObject jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + expect(new Derived().toString()).toMatch( + /\[object instance wrapper GType:Gjs_Derived jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + }); +}); + +describe('GObject virtual function', function () { + it('can have its property read', function () { + expect(GObject.Object.prototype.vfunc_constructed).toBeTruthy(); + }); + + it('can have its property overridden with an anonymous function', function () { + let callback; + + let key = 'vfunc_constructed'; + + class _SimpleTestClass1 extends GObject.Object {} + + if (GObject.Object.prototype.vfunc_constructed) { + let parentFunc = GObject.Object.prototype.vfunc_constructed; + _SimpleTestClass1.prototype[key] = function (...args) { + parentFunc.call(this, ...args); + callback('123'); + }; + } else { + _SimpleTestClass1.prototype[key] = function () { + callback('abc'); + }; + } + + callback = jasmine.createSpy('callback'); + + const SimpleTestClass1 = GObject.registerClass({GTypeName: 'SimpleTestClass1'}, _SimpleTestClass1); + new SimpleTestClass1(); + + expect(callback).toHaveBeenCalledWith('123'); + }); + + it('can access the parent prototype with super()', function () { + let callback; + + class _SimpleTestClass2 extends GObject.Object { + vfunc_constructed() { + super.vfunc_constructed(); + callback('vfunc_constructed'); + } + } + + callback = jasmine.createSpy('callback'); + + const SimpleTestClass2 = GObject.registerClass({GTypeName: 'SimpleTestClass2'}, _SimpleTestClass2); + new SimpleTestClass2(); + + expect(callback).toHaveBeenCalledWith('vfunc_constructed'); + }); + + it('handles non-existing properties', function () { + const _SimpleTestClass3 = class extends GObject.Object {}; + + _SimpleTestClass3.prototype.vfunc_doesnt_exist = function () {}; + + if (GObject.Object.prototype.vfunc_doesnt_exist) + fail('Virtual function should not exist'); + + + expect(() => GObject.registerClass({GTypeName: 'SimpleTestClass3'}, _SimpleTestClass3)).toThrow(); + }); + + it('gracefully bails out when overriding an unsupported vfunc type', function () { + expect(() => GObject.registerClass({ + Implements: [Gio.AsyncInitable], + }, class Foo extends GObject.Object { + vfunc_init_async() {} + })).toThrow(); + }); +}); + +describe('GObject creation using base classes without registered GType', function () { + it('fails when trying to instantiate a class that inherits from a GObject type', function () { + const BadInheritance = class extends GObject.Object {}; + const BadDerivedInheritance = class extends Derived {}; + + expect(() => new BadInheritance()).toThrowError(/Tried to construct an object without a GType/); + expect(() => new BadDerivedInheritance()).toThrowError(/Tried to construct an object without a GType/); + }); + + it('fails when trying to register a GObject class that inherits from a non-GObject type', function () { + const BadInheritance = class extends GObject.Object {}; + expect(() => GObject.registerClass(class BadInheritanceDerived extends BadInheritance {})) + .toThrowError(/Object 0x[a-f0-9]+ is not a subclass of GObject_Object, it's a Object/); + }); +}); + +describe('Register GType name', function () { + beforeAll(function () { + expect(GObject.gtypeNameBasedOnJSPath).toBeFalsy(); + }); + + afterEach(function () { + GObject.gtypeNameBasedOnJSPath = false; + }); + + it('uses the class name', function () { + const GTypeTestAutoName = GObject.registerClass( + class GTypeTestAutoName extends GObject.Object { }); + + expect(GTypeTestAutoName.$gtype.name).toEqual( + 'Gjs_GTypeTestAutoName'); + }); + + it('uses the sanitized class name', function () { + const GTypeTestAutoName = GObject.registerClass( + class GTypeTestAutoCla$$Name extends GObject.Object { }); + + expect(GTypeTestAutoName.$gtype.name).toEqual( + 'Gjs_GTypeTestAutoCla__Name'); + }); + + it('use the file path and class name', function () { + GObject.gtypeNameBasedOnJSPath = true; + const GTypeTestAutoName = GObject.registerClass( + class GTypeTestAutoName extends GObject.Object {}); + + /* Update this test if the file is moved */ + expect(GTypeTestAutoName.$gtype.name).toEqual( + 'Gjs_js_testGObjectClass_GTypeTestAutoName'); + }); + + it('use the file path and sanitized class name', function () { + GObject.gtypeNameBasedOnJSPath = true; + const GTypeTestAutoName = GObject.registerClass( + class GTypeTestAutoCla$$Name extends GObject.Object { }); + + /* Update this test if the file is moved */ + expect(GTypeTestAutoName.$gtype.name).toEqual( + 'Gjs_js_testGObjectClass_GTypeTestAutoCla__Name'); + }); + + it('use provided class name', function () { + const GtypeClass = GObject.registerClass({ + GTypeName: 'GTypeTestManualName', + }, class extends GObject.Object {}); + + expect(GtypeClass.$gtype.name).toEqual('GTypeTestManualName'); + }); + + it('sanitizes user provided class name', function () { + let gtypeName = 'GType$Test/WithLòt\'s of*bad§chars!'; + let expectedSanitized = 'GType_Test_WithL_t_s_of_bad_chars_'; + + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + `*RangeError: Provided GType name '${gtypeName}' is not valid; ` + + `automatically sanitized to '${expectedSanitized}'*`); + + const GtypeClass = GObject.registerClass({ + GTypeName: gtypeName, + }, class extends GObject.Object {}); + + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectClass.js', 0, + 'testGObjectRegisterClassSanitize'); + + expect(GtypeClass.$gtype.name).toEqual(expectedSanitized); + }); +}); + +describe('Signal handler matching', function () { + let o, handleEmpty, emptyId, handleDetailed, detailedId, handleDetailedOne, + detailedOneId, handleDetailedTwo, detailedTwoId, handleNotifyTwo, + notifyTwoId, handleMinimalOrFull, minimalId, fullId; + + beforeEach(function () { + o = new MyObject(); + handleEmpty = jasmine.createSpy('handleEmpty'); + emptyId = o.connect('empty', handleEmpty); + handleDetailed = jasmine.createSpy('handleDetailed'); + detailedId = o.connect('detailed', handleDetailed); + handleDetailedOne = jasmine.createSpy('handleDetailedOne'); + detailedOneId = o.connect('detailed::one', handleDetailedOne); + handleDetailedTwo = jasmine.createSpy('handleDetailedTwo'); + detailedTwoId = o.connect('detailed::two', handleDetailedTwo); + handleNotifyTwo = jasmine.createSpy('handleNotifyTwo'); + notifyTwoId = o.connect('notify::two', handleNotifyTwo); + handleMinimalOrFull = jasmine.createSpy('handleMinimalOrFull'); + minimalId = o.connect('minimal', handleMinimalOrFull); + fullId = o.connect('full', handleMinimalOrFull); + }); + + it('finds handlers by signal ID', function () { + expect(GObject.signal_handler_find(o, {signalId: 'empty'})).toEqual(emptyId); + // when more than one are connected, returns an arbitrary one + expect([detailedId, detailedOneId, detailedTwoId]) + .toContain(GObject.signal_handler_find(o, {signalId: 'detailed'})); + }); + + it('finds handlers by signal detail', function () { + expect(GObject.signal_handler_find(o, {detail: 'one'})).toEqual(detailedOneId); + // when more than one are connected, returns an arbitrary one + expect([detailedTwoId, notifyTwoId]) + .toContain(GObject.signal_handler_find(o, {detail: 'two'})); + }); + + it('finds handlers by callback', function () { + expect(GObject.signal_handler_find(o, {func: handleEmpty})).toEqual(emptyId); + expect(GObject.signal_handler_find(o, {func: handleDetailed})).toEqual(detailedId); + expect(GObject.signal_handler_find(o, {func: handleDetailedOne})).toEqual(detailedOneId); + expect(GObject.signal_handler_find(o, {func: handleDetailedTwo})).toEqual(detailedTwoId); + expect(GObject.signal_handler_find(o, {func: handleNotifyTwo})).toEqual(notifyTwoId); + // when more than one are connected, returns an arbitrary one + expect([minimalId, fullId]) + .toContain(GObject.signal_handler_find(o, {func: handleMinimalOrFull})); + }); + + it('finds handlers by a combination of parameters', function () { + expect(GObject.signal_handler_find(o, {signalId: 'detailed', detail: 'two'})) + .toEqual(detailedTwoId); + expect(GObject.signal_handler_find(o, {signalId: 'detailed', func: handleDetailed})) + .toEqual(detailedId); + }); + + it('blocks a handler by callback', function () { + expect(GObject.signal_handlers_block_matched(o, {func: handleEmpty})).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).not.toHaveBeenCalled(); + + expect(GObject.signal_handlers_unblock_matched(o, {func: handleEmpty})).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).toHaveBeenCalled(); + }); + + it('blocks multiple handlers by callback', function () { + expect(GObject.signal_handlers_block_matched(o, {func: handleMinimalOrFull})).toEqual(2); + o.emitMinimal(); + o.emitFull(); + expect(handleMinimalOrFull).not.toHaveBeenCalled(); + + expect(GObject.signal_handlers_unblock_matched(o, {func: handleMinimalOrFull})).toEqual(2); + o.emitMinimal(); + o.emitFull(); + expect(handleMinimalOrFull).toHaveBeenCalledTimes(2); + }); + + it('blocks handlers by a combination of parameters', function () { + expect(GObject.signal_handlers_block_matched(o, {signalId: 'detailed', func: handleDetailed})) + .toEqual(1); + o.emit('detailed', ''); + o.emit('detailed::one', ''); + expect(handleDetailed).not.toHaveBeenCalled(); + expect(handleDetailedOne).toHaveBeenCalled(); + + expect(GObject.signal_handlers_unblock_matched(o, {signalId: 'detailed', func: handleDetailed})) + .toEqual(1); + o.emit('detailed', ''); + o.emit('detailed::one', ''); + expect(handleDetailed).toHaveBeenCalled(); + }); + + it('disconnects a handler by callback', function () { + expect(GObject.signal_handlers_disconnect_matched(o, {func: handleEmpty})).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).not.toHaveBeenCalled(); + }); + + it('blocks multiple handlers by callback', function () { + expect(GObject.signal_handlers_disconnect_matched(o, {func: handleMinimalOrFull})).toEqual(2); + o.emitMinimal(); + o.emitFull(); + expect(handleMinimalOrFull).not.toHaveBeenCalled(); + }); + + it('blocks handlers by a combination of parameters', function () { + expect(GObject.signal_handlers_disconnect_matched(o, {signalId: 'detailed', func: handleDetailed})) + .toEqual(1); + o.emit('detailed', ''); + o.emit('detailed::one', ''); + expect(handleDetailed).not.toHaveBeenCalled(); + expect(handleDetailedOne).toHaveBeenCalled(); + }); + + it('blocks a handler by callback, convenience method', function () { + expect(GObject.signal_handlers_block_by_func(o, handleEmpty)).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).not.toHaveBeenCalled(); + + expect(GObject.signal_handlers_unblock_by_func(o, handleEmpty)).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).toHaveBeenCalled(); + }); + + it('disconnects a handler by callback, convenience method', function () { + expect(GObject.signal_handlers_disconnect_by_func(o, handleEmpty)).toEqual(1); + o.emitEmpty(); + expect(handleEmpty).not.toHaveBeenCalled(); + }); + + it('does not support disconnecting a handler by callback data', function () { + expect(() => GObject.signal_handlers_disconnect_by_data(o, null)).toThrow(); + }); +}); + +describe('Auto accessor generation', function () { + const AutoAccessors = GObject.registerClass({ + Properties: { + 'simple': GObject.ParamSpec.int('simple', 'Simple', 'Short-named property', + GObject.ParamFlags.READWRITE, 0, 100, 24), + 'long-long-name': GObject.ParamSpec.int('long-long-name', 'Long long name', + 'Long-named property', GObject.ParamFlags.READWRITE, 0, 100, 48), + 'construct': GObject.ParamSpec.int('construct', 'Construct', 'Construct', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, 0, 100, 96), + 'snake-name': GObject.ParamSpec.int('snake-name', 'Snake name', + 'Snake-cased property', GObject.ParamFlags.READWRITE, 0, 100, 36), + 'camel-name': GObject.ParamSpec.int('camel-name', 'Camel name', + 'Camel-cased property', GObject.ParamFlags.READWRITE, 0, 100, 72), + 'kebab-name': GObject.ParamSpec.int('kebab-name', 'Kebab name', + 'Kebab-cased property', GObject.ParamFlags.READWRITE, 0, 100, 12), + 'readonly': GObject.ParamSpec.int('readonly', 'Readonly', 'Readonly property', + GObject.ParamFlags.READABLE, 0, 100, 54), + 'writeonly': GObject.ParamSpec.int('writeonly', 'Writeonly', + 'Writeonly property', GObject.ParamFlags.WRITABLE, 0, 100, 60), + 'missing-getter': GObject.ParamSpec.int('missing-getter', 'Missing getter', + 'Missing a getter', GObject.ParamFlags.READWRITE, 0, 100, 18), + 'missing-setter': GObject.ParamSpec.int('missing-setter', 'Missing setter', + 'Missing a setter', GObject.ParamFlags.READWRITE, 0, 100, 42), + }, + }, class AutoAccessors extends GObject.Object { + _init(props = {}) { + super._init(props); + this._snakeNameGetterCalled = 0; + this._snakeNameSetterCalled = 0; + this._camelNameGetterCalled = 0; + this._camelNameSetterCalled = 0; + this._kebabNameGetterCalled = 0; + this._kebabNameSetterCalled = 0; + } + + get snake_name() { + this._snakeNameGetterCalled++; + return 42; + } + + set snake_name(value) { + this._snakeNameSetterCalled++; + } + + get camelName() { + this._camelNameGetterCalled++; + return 42; + } + + set camelName(value) { + this._camelNameSetterCalled++; + } + + get ['kebab-name']() { + this._kebabNameGetterCalled++; + return 42; + } + + set ['kebab-name'](value) { + this._kebabNameSetterCalled++; + } + + set missing_getter(value) { + this._missingGetter = value; + } + + get missing_setter() { + return 42; + } + }); + + let a; + beforeEach(function () { + a = new AutoAccessors(); + }); + + it('get and set the property', function () { + a.simple = 1; + expect(a.simple).toEqual(1); + a['long-long-name'] = 1; + expect(a['long-long-name']).toEqual(1); + a.construct = 1; + expect(a.construct).toEqual(1); + }); + + it("initial value is the param spec's default value", function () { + expect(a.simple).toEqual(24); + expect(a['long-long-name']).toEqual(48); + expect(a.construct).toEqual(96); + }); + + it('notify when the property changes', function () { + const notify = jasmine.createSpy('notify'); + a.connect('notify::simple', notify); + a.simple = 1; + expect(notify).toHaveBeenCalledTimes(1); + notify.calls.reset(); + a.simple = 1; + expect(notify).not.toHaveBeenCalled(); + }); + + it('copies accessors for camel and kebab if snake accessors given', function () { + a.snakeName = 42; + expect(a.snakeName).toEqual(42); + a['snake-name'] = 42; + expect(a['snake-name']).toEqual(42); + expect(a._snakeNameGetterCalled).toEqual(2); + expect(a._snakeNameSetterCalled).toEqual(2); + }); + + it('copies accessors for snake and kebab if camel accessors given', function () { + a.camel_name = 42; + expect(a.camel_name).toEqual(42); + a['camel-name'] = 42; + expect(a['camel-name']).toEqual(42); + expect(a._camelNameGetterCalled).toEqual(2); + expect(a._camelNameSetterCalled).toEqual(2); + }); + + it('copies accessors for snake and camel if kebab accessors given', function () { + a.kebabName = 42; + expect(a.kebabName).toEqual(42); + a.kebab_name = 42; + expect(a.kebab_name).toEqual(42); + expect(a._kebabNameGetterCalled).toEqual(2); + expect(a._kebabNameSetterCalled).toEqual(2); + }); + + it('readonly getter throws', function () { + expect(() => a.readonly).toThrowError(/getter/); + }); + + it('writeonly setter throws', function () { + expect(() => (a.writeonly = 1)).toThrowError(/setter/); + }); + + it('getter throws when setter defined', function () { + expect(() => a.missingGetter).toThrowError(/getter/); + }); + + it('setter throws when getter defined', function () { + expect(() => (a.missingSetter = 1)).toThrowError(/setter/); + }); }); diff --git a/installed-tests/js/testGObjectDestructionAccess.js b/installed-tests/js/testGObjectDestructionAccess.js index 6204156..328405f 100644 --- a/installed-tests/js/testGObjectDestructionAccess.js +++ b/installed-tests/js/testGObjectDestructionAccess.js @@ -4,102 +4,106 @@ imports.gi.versions.Gtk = '3.0'; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; -describe('Access to destroyed GObject', () => { +describe('Access to destroyed GObject', function () { let destroyedWindow; - beforeAll(() => { + beforeAll(function () { Gtk.init(null); }); - beforeEach(() => { + beforeEach(function () { destroyedWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); destroyedWindow.destroy(); }); - it('Get property', () => { + it('Get property', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); - let title = destroyedWindow.title; + void destroyedWindow.title; - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectPropertyGet'); }); - it('Set property', () => { + it('Set property', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); destroyedWindow.title = 'I am dead'; - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectPropertySet'); }); - it('Access to getter method', () => { + it('Access to getter method', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); + GLib.test_expect_message('Gtk', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*GTK_IS_WINDOW*'); - let title = destroyedWindow.get_title(); + void destroyedWindow.get_title(); - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectMethodGet'); }); - it('Access to setter method', () => { + it('Access to setter method', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); + GLib.test_expect_message('Gtk', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*GTK_IS_WINDOW*'); destroyedWindow.set_title('I am dead'); - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectMethodSet'); }); - it('Proto function connect', () => { + it('Proto function connect', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); destroyedWindow.connect('foo-signal', () => {}); - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectConnect'); }); - it('Proto function connect_after', () => { + it('Proto function connect_after', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); destroyedWindow.connect_after('foo-signal', () => {}); - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectConnectAfter'); }); - it('Proto function emit', () => { + it('Proto function emit', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); destroyedWindow.emit('foo-signal'); - GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + GLib.test_assert_expected_messages_internal('Cjs', 'testGObjectDestructionAccess.js', 0, 'testExceptionInDestroyedObjectEmit'); }); - it('Proto function toString', () => { + it('Proto function toString', function () { expect(destroyedWindow.toString()).toMatch( - /\[object \(FINALIZED\) instance proxy GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + /\[object \(FINALIZED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); }); - it('Porto function toString before/after', () => { + it('Proto function toString before/after', function () { var validWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); expect(validWindow.toString()).toMatch( - /\[object instance proxy GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + /\[object instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); validWindow.destroy(); expect(validWindow.toString()).toMatch( - /\[object \(FINALIZED\) instance proxy GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + /\[object \(FINALIZED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); }); }); diff --git a/installed-tests/js/testGObjectInterface.js b/installed-tests/js/testGObjectInterface.js index 9eab974..2574d93 100644 --- a/installed-tests/js/testGObjectInterface.js +++ b/installed-tests/js/testGObjectInterface.js @@ -1,19 +1,18 @@ const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; -const Mainloop = imports.mainloop; const AGObjectInterface = GObject.registerClass({ GTypeName: 'ArbitraryGTypeName', - Requires: [ GObject.Object ], + Requires: [GObject.Object], Properties: { 'interface-prop': GObject.ParamSpec.string('interface-prop', 'Interface property', 'Must be overridden in implementation', GObject.ParamFlags.READABLE, - 'foobar') + 'foobar'), }, Signals: { - 'interface-signal': {} + 'interface-signal': {}, }, }, class AGObjectInterface extends GObject.Interface { requiredG() { @@ -26,22 +25,22 @@ const AGObjectInterface = GObject.registerClass({ }); const InterfaceRequiringGObjectInterface = GObject.registerClass({ - Requires: [ AGObjectInterface ], + Requires: [AGObjectInterface], }, class InterfaceRequiringGObjectInterface extends GObject.Interface { optionalG() { - return 'InterfaceRequiringGObjectInterface.optionalG()\n' + - AGObjectInterface.optionalG(this); + return `InterfaceRequiringGObjectInterface.optionalG()\n${ + AGObjectInterface.optionalG(this)}`; } }); const GObjectImplementingGObjectInterface = GObject.registerClass({ - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', AGObjectInterface), 'class-prop': GObject.ParamSpec.string('class-prop', 'Class property', 'A property that is not on the interface', - GObject.ParamFlags.READABLE, 'meh') + GObject.ParamFlags.READABLE, 'meh'), }, Signals: { 'class-signal': {}, @@ -56,34 +55,52 @@ const GObjectImplementingGObjectInterface = GObject.registerClass({ } requiredG() {} + optionalG() { return AGObjectInterface.optionalG(this); } }); const MinimalImplementationOfAGObjectInterface = GObject.registerClass({ - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, }, class MinimalImplementationOfAGObjectInterface extends GObject.Object { requiredG() {} }); const ImplementationOfTwoInterfaces = GObject.registerClass({ - Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ], + Implements: [AGObjectInterface, InterfaceRequiringGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, }, class ImplementationOfTwoInterfaces extends GObject.Object { requiredG() {} + optionalG() { return InterfaceRequiringGObjectInterface.optionalG(this); } }); +const ImplementationOfIntrospectedInterface = GObject.registerClass({ + Implements: [Gio.Action], + Properties: { + 'enabled': GObject.ParamSpec.override('enabled', Gio.Action), + 'name': GObject.ParamSpec.override('name', Gio.Action), + 'state': GObject.ParamSpec.override('state', Gio.Action), + 'state-type': GObject.ParamSpec.override('state-type', Gio.Action), + 'parameter-type': GObject.ParamSpec.override('parameter-type', + Gio.Action), + }, +}, class ImplementationOfIntrospectedInterface extends GObject.Object { + get name() { + return 'inaction'; + } +}); + describe('GObject interface', function () { it('cannot be instantiated', function () { expect(() => new AGObjectInterface()).toThrow(); @@ -99,8 +116,9 @@ describe('GObject interface', function () { it('can be implemented by a GObject class', function () { let obj; - expect(() => { obj = new GObjectImplementingGObjectInterface(); }) - .not.toThrow(); + expect(() => { + obj = new GObjectImplementingGObjectInterface(); + }).not.toThrow(); expect(obj instanceof AGObjectInterface).toBeTruthy(); }); @@ -120,11 +138,11 @@ describe('GObject interface', function () { it('must have its required function implemented', function () { const BadObject = GObject.registerClass({ - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) - } + AGObjectInterface), + }, }, class BadObject extends GObject.Object {}); expect(() => new BadObject().requiredG()) .toThrowError(GObject.NotImplementedError); @@ -132,8 +150,9 @@ describe('GObject interface', function () { it("doesn't have to have its optional function implemented", function () { let obj; - expect(() => { obj = new MinimalImplementationOfAGObjectInterface(); }) - .not.toThrow(); + expect(() => { + obj = new MinimalImplementationOfAGObjectInterface(); + }).not.toThrow(); expect(obj instanceof AGObjectInterface).toBeTruthy(); }); @@ -149,7 +168,9 @@ describe('GObject interface', function () { it('can require another interface', function () { let obj; - expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow(); + expect(() => { + obj = new ImplementationOfTwoInterfaces(); + }).not.toThrow(); expect(obj instanceof AGObjectInterface).toBeTruthy(); expect(obj instanceof InterfaceRequiringGObjectInterface).toBeTruthy(); }); @@ -162,10 +183,10 @@ describe('GObject interface', function () { it("defers to the last interface's optional function", function () { const MinimalImplementationOfTwoInterfaces = GObject.registerClass({ - Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ], + Implements: [AGObjectInterface, InterfaceRequiringGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, }, class MinimalImplementationOfTwoInterfaces extends GObject.Object { requiredG() {} @@ -177,7 +198,7 @@ describe('GObject interface', function () { it('must be implemented by a class that implements all required interfaces', function () { expect(() => GObject.registerClass({ - Implements: [ InterfaceRequiringGObjectInterface ], + Implements: [InterfaceRequiringGObjectInterface], }, class BadObject { required() {} })).toThrow(); @@ -185,7 +206,7 @@ describe('GObject interface', function () { it('must be implemented by a class that implements required interfaces in correct order', function () { expect(() => GObject.registerClass({ - Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ], + Implements: [InterfaceRequiringGObjectInterface, AGObjectInterface], }, class BadObject { required() {} })).toThrow(); @@ -193,36 +214,41 @@ describe('GObject interface', function () { it('can require an interface from C', function () { const InitableInterface = GObject.registerClass({ - Requires: [ GObject.Object, Gio.Initable ] + Requires: [GObject.Object, Gio.Initable], }, class InitableInterface extends GObject.Interface {}); expect(() => GObject.registerClass({ - Implements: [ InitableInterface ], + Implements: [InitableInterface], }, class BadObject {})).toThrow(); }); - it('can define signals on the implementing class', function () { + it('can connect class signals on the implementing class', function (done) { function quitLoop() { - Mainloop.quit('signal'); + expect(classSignalSpy).toHaveBeenCalled(); + done(); } let obj = new GObjectImplementingGObjectInterface(); - let interfaceSignalSpy = jasmine.createSpy('interfaceSignalSpy') - .and.callFake(quitLoop); let classSignalSpy = jasmine.createSpy('classSignalSpy') .and.callFake(quitLoop); - obj.connect('interface-signal', interfaceSignalSpy); obj.connect('class-signal', classSignalSpy); GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { - obj.emit('interface-signal'); + obj.emit('class-signal'); return GLib.SOURCE_REMOVE; }); - Mainloop.run('signal'); + }); + + it('can connect interface signals on the implementing class', function (done) { + function quitLoop() { + expect(interfaceSignalSpy).toHaveBeenCalled(); + done(); + } + let obj = new GObjectImplementingGObjectInterface(); + let interfaceSignalSpy = jasmine.createSpy('interfaceSignalSpy') + .and.callFake(quitLoop); + obj.connect('interface-signal', interfaceSignalSpy); GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { - obj.emit('class-signal'); + obj.emit('interface-signal'); return GLib.SOURCE_REMOVE; }); - Mainloop.run('signal'); - expect(interfaceSignalSpy).toHaveBeenCalled(); - expect(classSignalSpy).toHaveBeenCalled(); }); it('can define properties on the implementing class', function () { @@ -238,7 +264,7 @@ describe('GObject interface', function () { "Object class * doesn't implement property 'interface-prop' from " + "interface 'ArbitraryGTypeName'"); GObject.registerClass({ - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], }, class MyNaughtyObject extends GObject.Object { requiredG() {} }); @@ -247,6 +273,11 @@ describe('GObject interface', function () { 253, 'testGObjectMustOverrideInterfaceProperties'); }); + it('can have introspected properties overriden', function () { + let obj = new ImplementationOfIntrospectedInterface(); + expect(obj.name).toEqual('inaction'); + }); + it('can be implemented by a class as well as its parent class', function () { const SubObject = GObject.registerClass( class SubObject extends GObjectImplementingGObjectInterface {}); @@ -257,10 +288,25 @@ describe('GObject interface', function () { it('can be reimplemented by a subclass of a class that already implements it', function () { const SubImplementer = GObject.registerClass({ - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], }, class SubImplementer extends GObjectImplementingGObjectInterface {}); let obj = new SubImplementer(); expect(obj instanceof AGObjectInterface).toBeTruthy(); expect(obj.interface_prop).toEqual('foobar'); // override not needed }); + + it('has a toString() defintion', function () { + expect(new GObjectImplementingGObjectInterface().toString()).toMatch( + /\[object instance wrapper GType:Gjs_GObjectImplementingGObjectInterface jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + }); +}); + +describe('Specific class and interface checks', function () { + it('Gio.AsyncInitable must implement vfunc_async_init', function () { + expect(() => GObject.registerClass({ + Implements: [Gio.Initable, Gio.AsyncInitable], + }, class BadAsyncInitable extends GObject.Object { + vfunc_init() {} + })).toThrow(); + }); }); diff --git a/installed-tests/js/testGTypeClass.js b/installed-tests/js/testGTypeClass.js index 919ac1e..3bfe2cd 100644 --- a/installed-tests/js/testGTypeClass.js +++ b/installed-tests/js/testGTypeClass.js @@ -5,9 +5,9 @@ const GObject = imports.gi.GObject; describe('Looking up param specs', function () { let p1, p2; beforeEach(function () { - let find_property = GObject.Object.find_property; - p1 = find_property.call(Gio.ThemedIcon, 'name'); - p2 = find_property.call(Gio.SimpleAction, 'enabled'); + let findProperty = GObject.Object.find_property; + p1 = findProperty.call(Gio.ThemedIcon, 'name'); + p2 = findProperty.call(Gio.SimpleAction, 'enabled'); }); it('works', function () { diff --git a/installed-tests/js/testGio.js b/installed-tests/js/testGio.js index 7feeb79..62df866 100644 --- a/installed-tests/js/testGio.js +++ b/installed-tests/js/testGio.js @@ -1,7 +1,11 @@ -const Gio = imports.gi.Gio; -const GObject = imports.gi.GObject; +const {GLib, Gio, GObject} = imports.gi; -const Foo = GObject.registerClass(class Foo extends GObject.Object { +const Foo = GObject.registerClass({ + Properties: { + boolval: GObject.ParamSpec.boolean('boolval', '', '', + GObject.ParamFlags.READWRITE, false), + }, +}, class Foo extends GObject.Object { _init(value) { super._init(); this.value = value; @@ -13,15 +17,133 @@ describe('ListStore iterator', function () { beforeEach(function () { list = new Gio.ListStore({item_type: Foo}); - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 100; i++) list.append(new Foo(i)); - } }); it('ListStore iterates', function () { let i = 0; - for (let f of list) { + for (let f of list) expect(f.value).toBe(i++); - } }); -}); \ No newline at end of file +}); + +describe('Gio.Settings overrides', function () { + it("doesn't crash when forgetting to specify a schema ID", function () { + expect(() => new Gio.Settings()).toThrowError(/schema/); + }); + + it("doesn't crash when specifying a schema ID that isn't installed", function () { + expect(() => new Gio.Settings({schema: 'com.example.ThisDoesntExist'})) + .toThrowError(/schema/); + }); + + it("doesn't crash when forgetting to specify a schema path", function () { + expect(() => new Gio.Settings({schema: 'org.cinnamon.CjsTest.Sub'})) + .toThrowError(/schema/); + }); + + it("doesn't crash when specifying conflicting schema paths", function () { + expect(() => new Gio.Settings({ + schema: 'org.cinnamon.CjsTest', + path: '/conflicting/path/', + })).toThrowError(/schema/); + }); + + describe('with existing schema', function () { + const KINDS = ['boolean', 'double', 'enum', 'flags', 'int', 'int64', + 'string', 'strv', 'uint', 'uint64', 'value']; + let settings; + + beforeEach(function () { + settings = new Gio.Settings({schema: 'org.cinnamon.CjsTest'}); + }); + + it("doesn't crash when resetting a nonexistent key", function () { + expect(() => settings.reset('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when checking a nonexistent key", function () { + KINDS.forEach(kind => { + expect(() => settings[`get_${kind}`]('foobar')).toThrowError(/key/); + }); + }); + + it("doesn't crash when setting a nonexistent key", function () { + KINDS.forEach(kind => { + expect(() => settings[`set_${kind}`]('foobar', null)).toThrowError(/key/); + }); + }); + + it("doesn't crash when checking writable for a nonexistent key", function () { + expect(() => settings.is_writable('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when getting the user value for a nonexistent key", function () { + expect(() => settings.get_user_value('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when getting the default value for a nonexistent key", function () { + expect(() => settings.get_default_value('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when binding a nonexistent key", function () { + const foo = new Foo(); + expect(() => settings.bind('foobar', foo, 'boolval', Gio.SettingsBindFlags.GET)) + .toThrowError(/key/); + expect(() => settings.bind_writable('foobar', foo, 'boolval', false)) + .toThrowError(/key/); + }); + + it("doesn't crash when creating actions for a nonexistent key", function () { + expect(() => settings.create_action('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when checking info about a nonexistent key", function () { + expect(() => settings.settings_schema.get_key('foobar')).toThrowError(/key/); + }); + + it("doesn't crash when getting a nonexistent sub-schema", function () { + expect(() => settings.get_child('foobar')).toThrowError(/foobar/); + }); + + it('still works with correct keys', function () { + const KEYS = ['window-size', 'maximized', 'fullscreen']; + + KEYS.forEach(key => expect(settings.is_writable(key)).toBeTruthy()); + + expect(() => { + settings.set_value('window-size', new GLib.Variant('(ii)', [100, 100])); + settings.set_boolean('maximized', true); + settings.set_boolean('fullscreen', true); + }).not.toThrow(); + + expect(settings.get_value('window-size').deepUnpack()).toEqual([100, 100]); + expect(settings.get_boolean('maximized')).toEqual(true); + expect(settings.get_boolean('fullscreen')).toEqual(true); + + expect(() => { + KEYS.forEach(key => settings.reset(key)); + }).not.toThrow(); + + KEYS.forEach(key => expect(settings.get_user_value(key)).toBeNull()); + expect(settings.get_default_value('window-size').deepUnpack()).toEqual([-1, -1]); + expect(settings.get_default_value('maximized').deepUnpack()).toEqual(false); + expect(settings.get_default_value('fullscreen').deepUnpack()).toEqual(false); + + const foo = new Foo({boolval: true}); + settings.bind('maximized', foo, 'boolval', Gio.SettingsBindFlags.GET); + expect(foo.boolval).toBeFalsy(); + Gio.Settings.unbind(foo, 'boolval'); + settings.bind_writable('maximized', foo, 'boolval', false); + expect(foo.boolval).toBeTruthy(); + + expect(settings.create_action('maximized')).not.toBeNull(); + + expect(settings.settings_schema.get_key('fullscreen')).not.toBeNull(); + + const sub = settings.get_child('sub'); + expect(sub.get_uint('marine')).toEqual(10); + }); + }); +}); diff --git a/installed-tests/js/testGtk.js b/installed-tests/js/testGtk3.js old mode 100755 new mode 100644 similarity index 52% rename from installed-tests/js/testGtk.js rename to installed-tests/js/testGtk3.js index 2d870b8..1481bb1 --- a/installed-tests/js/testGtk.js +++ b/installed-tests/js/testGtk3.js @@ -1,42 +1,45 @@ imports.gi.versions.Gtk = '3.0'; const ByteArray = imports.byteArray; -const {GLib, GObject, Gtk} = imports.gi; +const {GLib, Gio, GObject, Gtk} = imports.gi; const System = imports.system; // This is ugly here, but usually it would be in a resource -const template = ' \ - \ - \ -'; +function createTemplate(className) { + return ` + + +`; +} const MyComplexGtkSubclass = GObject.registerClass({ - Template: ByteArray.fromString(template), + Template: ByteArray.fromString(createTemplate('Gjs_MyComplexGtkSubclass')), Children: ['label-child', 'label-child2'], InternalChildren: ['internal-label-child'], CssName: 'complex-subclass', @@ -44,12 +47,16 @@ const MyComplexGtkSubclass = GObject.registerClass({ templateCallback(widget) { this.callbackEmittedBy = widget; } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } }); // Sadly, putting this in the body of the class will prevent calling // get_template_child, since MyComplexGtkSubclass will be bound to the ES6 // class name without the GObject goodies in it -MyComplexGtkSubclass.prototype.testChildrenExist = function() { +MyComplexGtkSubclass.prototype.testChildrenExist = function () { this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child'); expect(this._internalLabel).toEqual(jasmine.anything()); @@ -58,7 +65,7 @@ MyComplexGtkSubclass.prototype.testChildrenExist = function() { }; const MyComplexGtkSubclassFromResource = GObject.registerClass({ - Template: 'resource:///org/gjs/jsunit/complex.ui', + Template: 'resource:///org/gjs/jsunit/complex3.ui', Children: ['label-child', 'label-child2'], InternalChildren: ['internal-label-child'], }, class MyComplexGtkSubclassFromResource extends Gtk.Grid { @@ -71,19 +78,50 @@ const MyComplexGtkSubclassFromResource = GObject.registerClass({ templateCallback(widget) { this.callbackEmittedBy = widget; } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } +}); + +const [templateFile, stream] = Gio.File.new_tmp(null); +const baseStream = stream.get_output_stream(); +const out = new Gio.DataOutputStream({baseStream}); +out.put_string(createTemplate('Gjs_MyComplexGtkSubclassFromFile'), null); +out.close(null); + +const MyComplexGtkSubclassFromFile = GObject.registerClass({ + Template: templateFile.get_uri(), + Children: ['label-child', 'label-child2'], + InternalChildren: ['internal-label-child'], +}, class MyComplexGtkSubclassFromFile extends Gtk.Grid { + testChildrenExist() { + expect(this.label_child).toEqual(jasmine.anything()); + expect(this.label_child2).toEqual(jasmine.anything()); + expect(this._internal_label_child).toEqual(jasmine.anything()); + } + + templateCallback(widget) { + this.callbackEmittedBy = widget; + } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } }); const SubclassSubclass = GObject.registerClass( class SubclassSubclass extends MyComplexGtkSubclass {}); -function validateTemplate(description, ClassName, pending=false) { +function validateTemplate(description, ClassName, pending = false) { let suite = pending ? xdescribe : describe; suite(description, function () { let win, content; beforeEach(function () { - win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL }); + win = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); content = new ClassName(); content.label_child.emit('grab-focus'); + content.label_child2.emit('grab-focus'); win.add(content); }); @@ -105,6 +143,10 @@ function validateTemplate(description, ClassName, pending=false) { expect(content.callbackEmittedBy).toBe(content.label_child); }); + it('binds template callbacks to the correct object', function () { + expect(content.label_child2.callbackBoundTo).toBe(content.label_child); + }); + afterEach(function () { win.destroy(); }); @@ -116,33 +158,38 @@ describe('Gtk overrides', function () { Gtk.init(null); }); + afterAll(function () { + templateFile.delete(null); + }); + validateTemplate('UI template', MyComplexGtkSubclass); validateTemplate('UI template from resource', MyComplexGtkSubclassFromResource); + validateTemplate('UI template from file', MyComplexGtkSubclassFromFile); validateTemplate('Class inheriting from template class', SubclassSubclass, true); it('sets CSS names on classes', function () { expect(Gtk.Widget.get_css_name.call(MyComplexGtkSubclass)).toEqual('complex-subclass'); }); - it('avoid crashing when GTK vfuncs are called in garbage collection', function () { - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, - '*during garbage collection*'); - GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, - '*destroy*'); + // it('avoid crashing when GTK vfuncs are called in garbage collection', function () { + // GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + // '*during garbage collection*'); + // GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + // '*destroy*'); - let BadLabel = GObject.registerClass(class BadLabel extends Gtk.Label { - vfunc_destroy() {} - }); + // let BadLabel = GObject.registerClass(class BadLabel extends Gtk.Label { + // vfunc_destroy() {} + // }); - let w = new Gtk.Window(); - w.add(new BadLabel()); + // let w = new Gtk.Window(); + // w.add(new BadLabel()); - w.destroy(); - System.gc(); + // w.destroy(); + // System.gc(); - GLib.test_assert_expected_messages_internal('Gjs', 'testGtk.js', 0, - 'Gtk overrides avoid crashing and print a stack trace'); - }); + // GLib.test_assert_expected_messages_internal('Cjs', 'testGtk3.js', 0, + // 'Gtk overrides avoid crashing and print a stack trace'); + // }); it('accepts string in place of GdkAtom', function () { expect(() => Gtk.Clipboard.get(1)).toThrow(); @@ -175,4 +222,21 @@ describe('Gtk overrides', function () { */ expect(() => Gtk.Clipboard.get(null)).toThrowError(/null/); }); + + it('uses the correct GType for null child properties', function () { + let s = new Gtk.Stack(); + let p = new Gtk.Box(); + + s.add_named(p, 'foo'); + expect(s.get_child_by_name('foo')).toBe(p); + + s.child_set_property(p, 'name', null); + expect(s.get_child_by_name('foo')).toBeNull(); + }); + + it('can create a Gtk.TreeIter with accessible stamp field', function () { + const iter = new Gtk.TreeIter(); + iter.stamp = 42; + expect(iter.stamp).toEqual(42); + }); }); diff --git a/installed-tests/js/testGtk4.js b/installed-tests/js/testGtk4.js new file mode 100644 index 0000000..8aac724 --- /dev/null +++ b/installed-tests/js/testGtk4.js @@ -0,0 +1,174 @@ +imports.gi.versions.Gtk = '4.0'; + +const ByteArray = imports.byteArray; +const {Gio, GObject, Gtk} = imports.gi; + +// This is ugly here, but usually it would be in a resource +function createTemplate(className) { + return ` + + +`; +} + +const MyComplexGtkSubclass = GObject.registerClass({ + Template: ByteArray.fromString(createTemplate('Gjs_MyComplexGtkSubclass')), + Children: ['label-child', 'label-child2'], + InternalChildren: ['internal-label-child'], + CssName: 'complex-subclass', +}, class MyComplexGtkSubclass extends Gtk.Grid { + templateCallback(widget) { + this.callbackEmittedBy = widget; + } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } +}); + +// Sadly, putting this in the body of the class will prevent calling +// get_template_child, since MyComplexGtkSubclass will be bound to the ES6 +// class name without the GObject goodies in it +MyComplexGtkSubclass.prototype.testChildrenExist = function () { + this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child'); + expect(this._internalLabel).toEqual(jasmine.anything()); + + expect(this.label_child2).toEqual(jasmine.anything()); + expect(this._internal_label_child).toEqual(jasmine.anything()); +}; + +const MyComplexGtkSubclassFromResource = GObject.registerClass({ + Template: 'resource:///org/gjs/jsunit/complex4.ui', + Children: ['label-child', 'label-child2'], + InternalChildren: ['internal-label-child'], +}, class MyComplexGtkSubclassFromResource extends Gtk.Grid { + testChildrenExist() { + expect(this.label_child).toEqual(jasmine.anything()); + expect(this.label_child2).toEqual(jasmine.anything()); + expect(this._internal_label_child).toEqual(jasmine.anything()); + } + + templateCallback(widget) { + this.callbackEmittedBy = widget; + } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } +}); + +const [templateFile, stream] = Gio.File.new_tmp(null); +const baseStream = stream.get_output_stream(); +const out = new Gio.DataOutputStream({baseStream}); +out.put_string(createTemplate('Gjs_MyComplexGtkSubclassFromFile'), null); +out.close(null); + +const MyComplexGtkSubclassFromFile = GObject.registerClass({ + Template: templateFile.get_uri(), + Children: ['label-child', 'label-child2'], + InternalChildren: ['internal-label-child'], +}, class MyComplexGtkSubclassFromFile extends Gtk.Grid { + testChildrenExist() { + expect(this.label_child).toEqual(jasmine.anything()); + expect(this.label_child2).toEqual(jasmine.anything()); + expect(this._internal_label_child).toEqual(jasmine.anything()); + } + + templateCallback(widget) { + this.callbackEmittedBy = widget; + } + + boundCallback(widget) { + widget.callbackBoundTo = this; + } +}); + +const SubclassSubclass = GObject.registerClass( + class SubclassSubclass extends MyComplexGtkSubclass {}); + +function validateTemplate(description, ClassName, pending = false) { + let suite = pending ? xdescribe : describe; + suite(description, function () { + let win, content; + beforeEach(function () { + win = new Gtk.Window(); + content = new ClassName(); + content.label_child.emit('copy-clipboard'); + content.label_child2.emit('copy-clipboard'); + win.set_child(content); + }); + + it('sets up internal and public template children', function () { + content.testChildrenExist(); + }); + + it('sets up public template children with the correct widgets', function () { + expect(content.label_child.get_label()).toEqual('Complex!'); + expect(content.label_child2.get_label()).toEqual('Complex as well!'); + }); + + it('sets up internal template children with the correct widgets', function () { + expect(content._internal_label_child.get_label()) + .toEqual('Complex and internal!'); + }); + + it('connects template callbacks to the correct handler', function () { + expect(content.callbackEmittedBy).toBe(content.label_child); + }); + + it('binds template callbacks to the correct object', function () { + expect(content.label_child2.callbackBoundTo).toBe(content.label_child); + }); + + afterEach(function () { + win.destroy(); + }); + }); +} + +describe('Gtk overrides', function () { + beforeAll(function () { + Gtk.init(); + }); + + afterAll(function () { + templateFile.delete(null); + }); + + validateTemplate('UI template', MyComplexGtkSubclass); + validateTemplate('UI template from resource', MyComplexGtkSubclassFromResource); + validateTemplate('UI template from file', MyComplexGtkSubclassFromFile); + validateTemplate('Class inheriting from template class', SubclassSubclass, true); + + it('sets CSS names on classes', function () { + expect(Gtk.Widget.get_css_name.call(MyComplexGtkSubclass)).toEqual('complex-subclass'); + }); + + it('can create a Gtk.TreeIter with accessible stamp field', function () { + const iter = new Gtk.TreeIter(); + iter.stamp = 42; + expect(iter.stamp).toEqual(42); + }); +}); diff --git a/installed-tests/js/testImporter.js b/installed-tests/js/testImporter.js index 2a3cd37..4fdd4f6 100644 --- a/installed-tests/js/testImporter.js +++ b/installed-tests/js/testImporter.js @@ -6,11 +6,11 @@ describe('GI importer', function () { describe('on failure', function () { // For these tests, we provide special overrides files to sabotage the - // import, at the path resource:///org/gjs/jsunit/modules/overrides. + // import, at the path resource:///org/gjs/jsunit/modules/badOverrides. let oldSearchPath; beforeAll(function () { oldSearchPath = imports.overrides.searchPath.slice(); - imports.overrides.searchPath = ['resource:///org/gjs/jsunit/modules/overrides']; + imports.overrides.searchPath = ['resource:///org/gjs/jsunit/modules/badOverrides']; }); afterAll(function () { @@ -64,7 +64,7 @@ describe('Importer', function () { it('throws an import error when trying to import a nonexistent module', function () { expect(() => imports.nonexistentModuleName) - .toThrow(jasmine.objectContaining({ name: 'ImportError' })); + .toThrow(jasmine.objectContaining({name: 'ImportError'})); }); it('throws an error when evaluating the module file throws an error', function () { @@ -166,11 +166,11 @@ describe('Importer', function () { let LexicalScope; beforeAll(function () { - window.expectMe = true; + globalThis.expectMe = true; LexicalScope = imports.lexicalScope; }); - /*it('will log a compatibility warning when accessed', function () { + it('will log a compatibility warning when accessed', function () { const GLib = imports.gi.GLib; GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, "Some code accessed the property 'b' on the module " + @@ -183,9 +183,9 @@ describe('Importer', function () { void LexicalScope.c; // g_test_assert_expected_messages() is a macro, not introspectable - GLib.test_assert_expected_messages_internal('Gjs', + GLib.test_assert_expected_messages_internal('Cjs', 'testImporter.js', 179, ''); - });*/ + }); it('can be accessed', function () { expect(LexicalScope.a).toEqual(1); @@ -195,7 +195,7 @@ describe('Importer', function () { }); it('does not leak module properties into the global scope', function () { - expect(window.d).not.toBeDefined(); + expect(globalThis.d).not.toBeDefined(); }); }); diff --git a/installed-tests/js/testIntrospection.js b/installed-tests/js/testIntrospection.js new file mode 100644 index 0000000..a005a90 --- /dev/null +++ b/installed-tests/js/testIntrospection.js @@ -0,0 +1,154 @@ +// Various tests having to do with how introspection is implemented in GJS + +imports.gi.versions.Gdk = '3.0'; +imports.gi.versions.Gtk = '3.0'; +const {Gdk, Gio, GLib, GObject, Gtk} = imports.gi; +const System = imports.system; + +describe('GLib.DestroyNotify parameter', function () { + it('throws when encountering a GDestroyNotify not associated with a callback', function () { + // the 'destroy' argument applies to the data, which is not supported in + // gobject-introspection + expect(() => Gio.MemoryInputStream.new_from_data('foobar')) + .toThrowError(/destroy/); + }); +}); + +describe('Unsafe integer marshalling', function () { + it('warns when conversion is lossy', function () { + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + '*cannot be safely stored*'); + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + '*cannot be safely stored*'); + GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, + '*cannot be safely stored*'); + void GLib.MININT64; + void GLib.MAXINT64; + void GLib.MAXUINT64; + GLib.test_assert_expected_messages_internal('Cjs', + 'testEverythingBasic.js', 0, + 'Limits warns when conversion is lossy'); + }); +}); + +describe('Marshalling empty flat arrays of structs', function () { + let widget; + beforeAll(function () { + if (GLib.getenv('ENABLE_GTK') !== 'yes') { + pending('GTK disabled'); + return; + } + Gtk.init(null); + }); + + beforeEach(function () { + widget = new Gtk.Label(); + }); + + it('accepts null', function () { + widget.drag_dest_set(0, null, Gdk.DragAction.COPY); + }); + + it('accepts an empty array', function () { + widget.drag_dest_set(0, [], Gdk.DragAction.COPY); + }); +}); + +describe('Constructor', function () { + it('throws when constructor called without new', function () { + expect(() => Gio.AppLaunchContext()) + .toThrowError(/Constructor called as normal method/); + }); +}); + +describe('Enum classes', function () { + it('enum has a $gtype property', function () { + expect(Gio.BusType.$gtype).toBeDefined(); + }); + + it('enum $gtype property is enumerable', function () { + expect('$gtype' in Gio.BusType).toBeTruthy(); + }); +}); + +describe('GError domains', function () { + it('Number converts error to quark', function () { + expect(Gio.ResolverError.quark()).toEqual(Number(Gio.ResolverError)); + }); +}); + +describe('Object properties on GtkBuilder-constructed objects', function () { + let o1; + beforeAll(function () { + if (GLib.getenv('ENABLE_GTK') !== 'yes') { + pending('GTK disabled'); + return; + } + Gtk.init(null); + }); + + beforeEach(function () { + const ui = ` + + + Click me + + `; + + let builder = Gtk.Builder.new_from_string(ui, -1); + o1 = builder.get_object('button'); + }); + + it('are found on the GObject itself', function () { + expect(o1.label).toBe('Click me'); + }); + + it('are found on the GObject\'s parents', function () { + expect(o1.visible).toBeFalsy(); + }); + + it('are found on the GObject\'s interfaces', function () { + expect(o1.action_name).toBeNull(); + }); +}); + +describe('Garbage collection of introspected objects', function () { + // This tests a regression that would very rarely crash, but + // when run under valgrind this code would show use-after-free. + it('collects objects properly with signals connected', function (done) { + function orphanObject() { + let obj = new GObject.Object(); + obj.connect('notify', () => {}); + } + + orphanObject(); + System.gc(); + GLib.idle_add(GLib.PRIORITY_LOW, () => done()); + }); +}); + +describe('Gdk.Atom', function () { + it('is presented as string', function () { + expect(Gdk.Atom.intern('CLIPBOARD', false)).toBe('CLIPBOARD'); + expect(Gdk.Atom.intern('NONE', false)).toBe(null); + }); +}); + +describe('Complete enumeration (boxed types)', function () { + it('enumerates all properties', function () { + // Note: this test breaks down if other code access all the methods of Rectangle + const rect = new Gdk.Rectangle(); + const names = Object.getOwnPropertyNames(Object.getPrototypeOf(rect)); + const expectAtLeast = ['equal', 'intersect', 'union', 'x', 'y', 'width', 'height']; + expect(names).toEqual(jasmine.arrayContaining(expectAtLeast)); + }); +}); + +describe('Complete enumeration of GIRepositoryNamespace (new_enumerate)', function () { + it('enumerates all properties (sampled)', function () { + const names = Object.getOwnPropertyNames(Gdk); + // Note: properties which has been accessed are listed without new_enumerate hook + const expectAtLeast = ['KEY_ybelowdot', 'EventSequence', 'ByteOrder', 'Window']; + expect(names).toEqual(jasmine.arrayContaining(expectAtLeast)); + }); +}); diff --git a/installed-tests/js/testLang.js b/installed-tests/js/testLang.js index 13735e6..180e146 100644 --- a/installed-tests/js/testLang.js +++ b/installed-tests/js/testLang.js @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-properties */ // tests for imports.lang module // except for Lang.Class and Lang.Interface, which are tested in testLegacyClass @@ -5,12 +6,12 @@ const Lang = imports.lang; describe('Lang module', function () { it('counts properties with Lang.countProperties()', function () { - var foo = { 'a' : 10, 'b' : 11 }; + var foo = {'a': 10, 'b': 11}; expect(Lang.countProperties(foo)).toEqual(2); }); it('copies properties from one object to another with Lang.copyProperties()', function () { - var foo = { 'a' : 10, 'b' : 11 }; + var foo = {'a': 10, 'b': 11}; var bar = {}; Lang.copyProperties(foo, bar); @@ -18,23 +19,23 @@ describe('Lang module', function () { }); it('copies properties without an underscore with Lang.copyPublicProperties()', function () { - var foo = { 'a' : 10, 'b' : 11, '_c' : 12 }; + var foo = {'a': 10, 'b': 11, '_c': 12}; var bar = {}; Lang.copyPublicProperties(foo, bar); - expect(bar).toEqual({ 'a': 10, 'b': 11 }); + expect(bar).toEqual({'a': 10, 'b': 11}); }); it('copies property getters and setters', function () { var foo = { - 'a' : 10, - 'b' : 11, + 'a': 10, + 'b': 11, get c() { return this.a; }, set c(n) { this.a = n; - } + }, }; var bar = {}; @@ -59,7 +60,7 @@ describe('Lang module', function () { o = { callback() { return true; - } + }, }; spyOn(o, 'callback').and.callThrough(); }); diff --git a/installed-tests/js/testLegacyByteArray.js b/installed-tests/js/testLegacyByteArray.js new file mode 100644 index 0000000..3910273 --- /dev/null +++ b/installed-tests/js/testLegacyByteArray.js @@ -0,0 +1,110 @@ +const ByteArray = imports.byteArray; +const GIMarshallingTests = imports.gi.GIMarshallingTests; + +describe('Legacy byte array', function () { + it('has length 0 for empty array', function () { + let a = new ByteArray.ByteArray(); + expect(a.length).toEqual(0); + }); + + describe('initially sized to 10', function () { + let a; + beforeEach(function () { + a = new ByteArray.ByteArray(10); + }); + + it('has length 10', function () { + expect(a.length).toEqual(10); + }); + + it('is initialized to zeroes', function () { + for (let i = 0; i < a.length; ++i) + expect(a[i]).toEqual(0); + }); + }); + + it('assigns values correctly', function () { + let a = new ByteArray.ByteArray(256); + + for (let i = 0; i < a.length; ++i) + a[i] = 255 - i; + + for (let i = 0; i < a.length; ++i) + expect(a[i]).toEqual(255 - i); + }); + + describe('assignment past end', function () { + let a; + beforeEach(function () { + a = new ByteArray.ByteArray(); + a[2] = 5; + }); + + it('implicitly lengthens the array', function () { + expect(a.length).toEqual(3); + expect(a[2]).toEqual(5); + }); + + it('implicitly creates zero bytes', function () { + expect(a[0]).toEqual(0); + expect(a[1]).toEqual(0); + }); + }); + + it('changes the length when assigning to length property', function () { + let a = new ByteArray.ByteArray(20); + expect(a.length).toEqual(20); + a.length = 5; + expect(a.length).toEqual(5); + }); + + describe('conversions', function () { + let a; + beforeEach(function () { + a = new ByteArray.ByteArray(); + a[0] = 255; + }); + + it('gives a byte 5 when assigning 5', function () { + a[0] = 5; + expect(a[0]).toEqual(5); + }); + + it('gives a byte 0 when assigning null', function () { + a[0] = null; + expect(a[0]).toEqual(0); + }); + + it('gives a byte 0 when assigning undefined', function () { + a[0] = undefined; + expect(a[0]).toEqual(0); + }); + + it('rounds off when assigning a double', function () { + a[0] = 3.14; + expect(a[0]).toEqual(3); + }); + }); + + it('can be created from an array', function () { + let a = ByteArray.fromArray([1, 2, 3, 4]); + expect(a.length).toEqual(4); + [1, 2, 3, 4].forEach((val, ix) => expect(a[ix]).toEqual(val)); + }); + + it('can be converted to a string of ASCII characters', function () { + let a = new ByteArray.ByteArray(4); + a[0] = 97; + a[1] = 98; + a[2] = 99; + a[3] = 100; + let s = a.toString(); + expect(s.length).toEqual(4); + expect(s).toEqual('abcd'); + }); + + it('can be passed in with transfer none', function () { + const refByteArray = ByteArray.fromArray([0, 49, 0xFF, 51]); + expect(() => GIMarshallingTests.bytearray_none_in(refByteArray)).not.toThrow(); + }); +}); diff --git a/installed-tests/js/testLegacyClass.js b/installed-tests/js/testLegacyClass.js index e691abb..1599508 100644 --- a/installed-tests/js/testLegacyClass.js +++ b/installed-tests/js/testLegacyClass.js @@ -1,13 +1,14 @@ // -*- mode: js; indent-tabs-mode: nil -*- +/* eslint-disable no-restricted-properties */ const Lang = imports.lang; const NormalClass = new Lang.Class({ Name: 'NormalClass', - _init: function() { + _init() { this.one = 1; - } + }, }); let Subclassed = []; @@ -15,18 +16,18 @@ const MetaClass = new Lang.Class({ Name: 'MetaClass', Extends: Lang.Class, - _init: function(params) { + _init(params) { Subclassed.push(params.Name); this.parent(params); if (params.Extended) { - this.prototype.dynamic_method = this.wrapFunction('dynamic_method', function() { + this.prototype.dynamic_method = this.wrapFunction('dynamic_method', function () { return 73; }); this.DYNAMIC_CONSTANT = 2; } - } + }, }); const CustomMetaOne = new MetaClass({ @@ -34,11 +35,11 @@ const CustomMetaOne = new MetaClass({ Extends: NormalClass, Extended: false, - _init: function() { + _init() { this.parent(); this.two = 2; - } + }, }); const CustomMetaTwo = new MetaClass({ @@ -46,11 +47,11 @@ const CustomMetaTwo = new MetaClass({ Extends: NormalClass, Extended: true, - _init: function() { + _init() { this.parent(); this.two = 2; - } + }, }); // This should inherit CustomMeta, even though @@ -60,11 +61,11 @@ const CustomMetaSubclass = new Lang.Class({ Extends: CustomMetaOne, Extended: true, - _init: function() { + _init() { this.parent(); this.three = 3; - } + }, }); describe('A metaclass', function () { @@ -96,8 +97,8 @@ describe('A metaclass', function () { }); it('gets all the properties from its class and metaclass', function () { - expect(instanceOne).toEqual(jasmine.objectContaining({ one: 1, two: 2 })); - expect(instanceTwo).toEqual(jasmine.objectContaining({ one: 1, two: 2 })); + expect(instanceOne).toEqual(jasmine.objectContaining({one: 1, two: 2})); + expect(instanceTwo).toEqual(jasmine.objectContaining({one: 1, two: 2})); }); it('gets dynamically defined properties from metaclass', function () { @@ -111,7 +112,7 @@ describe('A metaclass', function () { expect(CustomMetaSubclass.DYNAMIC_CONSTANT).toEqual(2); let instance = new CustomMetaSubclass(); - expect(instance).toEqual(jasmine.objectContaining({ one: 1, two: 2, three: 3 })); + expect(instance).toEqual(jasmine.objectContaining({one: 1, two: 2, three: 3})); expect(instance.dynamic_method()).toEqual(73); }); @@ -125,19 +126,20 @@ describe('A metaclass', function () { const MagicBase = new Lang.Class({ Name: 'MagicBase', - _init: function(a, buffer) { - if (buffer) buffer.push(a); + _init(a, buffer) { + if (buffer) + buffer.push(a); this.a = a; }, - foo: function(a, buffer) { + foo(a, buffer) { buffer.push(a); return a * 3; }, - bar: function(a) { + bar(a) { return a * 5; - } + }, }); const Magic = new Lang.Class({ @@ -145,28 +147,29 @@ const Magic = new Lang.Class({ Extends: MagicBase, - _init: function(a, b, buffer) { + _init(a, b, buffer) { this.parent(a, buffer); - if (buffer) buffer.push(b); + if (buffer) + buffer.push(b); this.b = b; }, - foo: function(a, b, buffer) { + foo(a, b, buffer) { let val = this.parent(a, buffer); buffer.push(b); return val * 2; }, - bar: function(a, buffer) { - this.foo(a, 2*a, buffer); + bar(a, buffer) { + this.foo(a, 2 * a, buffer); return this.parent(a); - } + }, }); const Accessor = new Lang.Class({ Name: 'AccessorMagic', - _init: function(val) { + _init(val) { this._val = val; }, @@ -175,19 +178,19 @@ const Accessor = new Lang.Class({ }, set value(val) { - if (val != 42) + if (val !== 42) throw TypeError('Value is not a magic number'); this._val = val; - } + }, }); const AbstractBase = new Lang.Class({ Name: 'AbstractBase', Abstract: true, - _init: function() { + _init() { this.foo = 42; - } + }, }); describe('Class framework', function () { @@ -235,10 +238,10 @@ describe('Class framework', function () { const ToStringOverride = new Lang.Class({ Name: 'ToStringOverride', - toString: function() { + toString() { let oldToString = this.parent(); - return oldToString + '; hello'; - } + return `${oldToString}; hello`; + }, }); let override = new ToStringOverride(); @@ -256,7 +259,7 @@ describe('Class framework', function () { let newAccessor = new Accessor(11); expect(newAccessor.value).toEqual(11); - expect(() => newAccessor.value = 12).toThrow(); + expect(() => (newAccessor.value = 12)).toThrow(); newAccessor.value = 42; expect(newAccessor.value).toEqual(42); @@ -271,10 +274,10 @@ describe('Class framework', function () { Name: 'AbstractImpl', Extends: AbstractBase, - _init: function() { + _init() { this.parent(); this.bar = 42; - } + }, }); let newAbstract = new AbstractImpl(); @@ -292,8 +295,8 @@ describe('Class framework', function () { expect(newAbstract.foo).toEqual(42); }); - it('allows ES6 classes to inherit from abstract base classes', function() { - class AbstractImpl extends AbstractBase {}; + it('allows ES6 classes to inherit from abstract base classes', function () { + class AbstractImpl extends AbstractBase {} let newAbstract = new AbstractImpl(); expect(newAbstract.foo).toEqual(42); @@ -312,9 +315,9 @@ describe('Class framework', function () { const CustomConstruct = new Lang.Class({ Name: 'CustomConstruct', - _construct: function(one, two) { + _construct(one, two) { return [one, two]; - } + }, }); let instance = new CustomConstruct(1, 2); @@ -341,23 +344,23 @@ const AnInterface = new Lang.Interface({ required: Lang.Interface.UNIMPLEMENTED, - optional: function () { + optional() { return 'AnInterface.optional()'; }, - optionalGeneric: function () { + optionalGeneric() { return 'AnInterface.optionalGeneric()'; }, - argumentGeneric: function (arg) { - return 'AnInterface.argumentGeneric(' + arg + ')'; + argumentGeneric(arg) { + return `AnInterface.argumentGeneric(${arg})`; }, - usesThis: function () { + usesThis() { return this._interfacePrivateMethod(); }, - _interfacePrivateMethod: function () { + _interfacePrivateMethod() { return 'interface private method'; }, @@ -367,72 +370,72 @@ const AnInterface = new Lang.Interface({ set some_prop(value) { this.some_prop_setter_called = true; - } + }, }); const InterfaceRequiringOtherInterface = new Lang.Interface({ Name: 'InterfaceRequiringOtherInterface', - Requires: [ AnInterface ], + Requires: [AnInterface], - optional: function () { - return 'InterfaceRequiringOtherInterface.optional()\n' + - AnInterface.prototype.optional.apply(this, arguments); + optional(...args) { + return `InterfaceRequiringOtherInterface.optional()\n${ + AnInterface.prototype.optional.apply(this, args)}`; }, - optionalGeneric: function () { - return 'InterfaceRequiringOtherInterface.optionalGeneric()\n' + - AnInterface.optionalGeneric(this); - } + optionalGeneric() { + return `InterfaceRequiringOtherInterface.optionalGeneric()\n${ + AnInterface.optionalGeneric(this)}`; + }, }); const ObjectImplementingAnInterface = new Lang.Class({ Name: 'ObjectImplementingAnInterface', - Implements: [ AnInterface ], + Implements: [AnInterface], - _init: function () { + _init() { this.parent(); }, - required: function () {}, + required() {}, - optional: function () { - return AnInterface.prototype.optional.apply(this, arguments); + optional(...args) { + return AnInterface.prototype.optional.apply(this, args); }, - optionalGeneric: function () { + optionalGeneric() { return AnInterface.optionalGeneric(this); }, - argumentGeneric: function (arg) { - return AnInterface.argumentGeneric(this, arg + ' (hello from class)'); - } + argumentGeneric(arg) { + return AnInterface.argumentGeneric(this, `${arg} (hello from class)`); + }, }); const InterfaceRequiringClassAndInterface = new Lang.Interface({ Name: 'InterfaceRequiringClassAndInterface', - Requires: [ ObjectImplementingAnInterface, InterfaceRequiringOtherInterface ], + Requires: [ObjectImplementingAnInterface, InterfaceRequiringOtherInterface], }); const MinimalImplementationOfAnInterface = new Lang.Class({ Name: 'MinimalImplementationOfAnInterface', - Implements: [ AnInterface ], + Implements: [AnInterface], - required: function () {} + required() {}, }); const ImplementationOfTwoInterfaces = new Lang.Class({ Name: 'ImplementationOfTwoInterfaces', - Implements: [ AnInterface, InterfaceRequiringOtherInterface ], + Implements: [AnInterface, InterfaceRequiringOtherInterface], - required: function () {}, + required() {}, - optional: function () { - return InterfaceRequiringOtherInterface.prototype.optional.apply(this, arguments); + optional(...args) { + return InterfaceRequiringOtherInterface.prototype.optional.apply(this, args); }, - optionalGeneric: function () { + optionalGeneric() { return InterfaceRequiringOtherInterface.optionalGeneric(this); - } + }, }); describe('An interface', function () { @@ -451,14 +454,16 @@ describe('An interface', function () { it('can be implemented by a class', function () { let obj; - expect(() => { obj = new ObjectImplementingAnInterface(); }).not.toThrow(); + expect(() => { + obj = new ObjectImplementingAnInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); }); it("can be implemented by a class's superclass", function () { const ChildWhoseParentImplementsAnInterface = new Lang.Class({ Name: 'ChildWhoseParentImplementsAnInterface', - Extends: ObjectImplementingAnInterface + Extends: ObjectImplementingAnInterface, }); let obj = new ChildWhoseParentImplementsAnInterface(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); @@ -489,7 +494,7 @@ describe('An interface', function () { it('does not have to have its optional methods implemented', function () { let obj; - expect(() => obj = new MinimalImplementationOfAnInterface()).not.toThrow(); + expect(() => (obj = new MinimalImplementationOfAnInterface())).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); }); @@ -523,11 +528,11 @@ describe('An interface', function () { it('can have its property getter overridden', function () { const ObjectWithGetter = new Lang.Class({ Name: 'ObjectWithGetter', - Implements: [ AnInterface ], - required: function () {}, + Implements: [AnInterface], + required() {}, get some_prop() { return 'ObjectWithGetter.some_prop getter'; - } + }, }); let obj = new ObjectWithGetter(); expect(obj.some_prop).toEqual('ObjectWithGetter.some_prop getter'); @@ -536,11 +541,11 @@ describe('An interface', function () { it('can have its property setter overridden', function () { const ObjectWithSetter = new Lang.Class({ Name: 'ObjectWithSetter', - Implements: [ AnInterface ], - required: function () {}, + Implements: [AnInterface], + required() {}, set some_prop(value) { /* setter without getter */// jshint ignore:line this.overridden_some_prop_setter_called = true; - } + }, }); let obj = new ObjectWithSetter(); obj.some_prop = 'foobar'; @@ -550,7 +555,9 @@ describe('An interface', function () { it('can require another interface', function () { let obj; - expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow(); + expect(() => { + obj = new ImplementationOfTwoInterfaces(); + }).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); expect(obj.constructor.implements(InterfaceRequiringOtherInterface)).toBeTruthy(); }); @@ -558,7 +565,7 @@ describe('An interface', function () { it('can have empty requires', function () { expect(() => new Lang.Interface({ Name: 'InterfaceWithEmptyRequires', - Requires: [] + Requires: [], })).not.toThrow(); }); @@ -582,9 +589,9 @@ describe('An interface', function () { it('has its optional function defer to that of the last interface', function () { const MinimalImplementationOfTwoInterfaces = new Lang.Class({ Name: 'MinimalImplementationOfTwoInterfaces', - Implements: [ AnInterface, InterfaceRequiringOtherInterface ], + Implements: [AnInterface, InterfaceRequiringOtherInterface], - required: function () {} + required() {}, }); let obj = new MinimalImplementationOfTwoInterfaces(); expect(obj.optionalGeneric()) @@ -594,16 +601,16 @@ describe('An interface', function () { it('must have all its required interfaces implemented', function () { expect(() => new Lang.Class({ Name: 'ObjectWithNotEnoughInterfaces', - Implements: [ InterfaceRequiringOtherInterface ], - required: function () {} + Implements: [InterfaceRequiringOtherInterface], + required() {}, })).toThrow(); }); it('must have all its required interfaces implemented in the correct order', function () { expect(() => new Lang.Class({ Name: 'ObjectWithInterfacesInWrongOrder', - Implements: [ InterfaceRequiringOtherInterface, AnInterface ], - required: function () {} + Implements: [InterfaceRequiringOtherInterface, AnInterface], + required() {}, })).toThrow(); }); @@ -613,7 +620,7 @@ describe('An interface', function () { const ObjectInheritingFromInterfaceImplementation = new Lang.Class({ Name: 'ObjectInheritingFromInterfaceImplementation', Extends: ObjectImplementingAnInterface, - Implements: [ InterfaceRequiringOtherInterface ], + Implements: [InterfaceRequiringOtherInterface], }); obj = new ObjectInheritingFromInterfaceImplementation(); }).not.toThrow(); @@ -627,7 +634,7 @@ describe('An interface', function () { const ObjectImplementingInterfaceRequiringParentObject = new Lang.Class({ Name: 'ObjectImplementingInterfaceRequiringParentObject', Extends: ObjectImplementingAnInterface, - Implements: [ InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface ] + Implements: [InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface], }); obj = new ObjectImplementingInterfaceRequiringParentObject(); }).not.toThrow(); @@ -639,8 +646,8 @@ describe('An interface', function () { it('must be implemented by an object which subclasses the required class', function () { expect(() => new Lang.Class({ Name: 'ObjectWithoutRequiredParent', - Implements: [ AnInterface, InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface ], - required: function () {}, + Implements: [AnInterface, InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface], + required() {}, })).toThrow(); }); @@ -652,7 +659,7 @@ describe('An interface', function () { it('is implemented by a subclass of a class that implements it', function () { const SubObject = new Lang.Class({ Name: 'SubObject', - Extends: ObjectImplementingAnInterface + Extends: ObjectImplementingAnInterface, }); let obj = new SubObject(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); @@ -662,7 +669,7 @@ describe('An interface', function () { const SubImplementer = new Lang.Class({ Name: 'SubImplementer', Extends: ObjectImplementingAnInterface, - Implements: [ AnInterface ] + Implements: [AnInterface], }); let obj = new SubImplementer(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); @@ -688,8 +695,12 @@ describe('ES6 class inheriting from Lang.Class', function () { chainUpToMe() {}, overrideMe() {}, - get property() { return this._property + 1; }, - set property(value) { this._property = value - 2; }, + get property() { + return this._property + 1; + }, + set property(value) { + this._property = value - 2; + }, }); Legacy.staticMethod = function () {}; spyOn(Legacy, 'staticMethod'); @@ -697,12 +708,11 @@ describe('ES6 class inheriting from Lang.Class', function () { spyOn(Legacy.prototype, 'chainUpToMe'); spyOn(Legacy.prototype, 'overrideMe'); - Shiny = class Shiny extends Legacy { - constructor(someval) { - super(someval); + Shiny = class extends Legacy { + chainUpToMe() { + super.chainUpToMe(); } - chainUpToMe() { super.chainUpToMe(); } overrideMe() {} }; }); diff --git a/installed-tests/js/testLegacyGObject.js b/installed-tests/js/testLegacyGObject.js index 38cd6ea..562c7f1 100644 --- a/installed-tests/js/testLegacyGObject.js +++ b/installed-tests/js/testLegacyGObject.js @@ -1,4 +1,6 @@ // -*- mode: js; indent-tabs-mode: nil -*- +/* eslint-disable no-restricted-properties */ + imports.gi.versions.Gtk = '3.0'; const Gio = imports.gi.Gio; @@ -22,21 +24,21 @@ const MyObject = new GObject.Class({ }, Signals: { 'empty': { }, - 'minimal': { param_types: [ GObject.TYPE_INT, GObject.TYPE_INT ] }, + 'minimal': {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]}, 'full': { flags: GObject.SignalFlags.RUN_LAST, accumulator: GObject.AccumulatorType.FIRST_WINS, return_type: GObject.TYPE_INT, param_types: [], }, - 'run-last': { flags: GObject.SignalFlags.RUN_LAST }, + 'run-last': {flags: GObject.SignalFlags.RUN_LAST}, 'detailed': { flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED, - param_types: [ GObject.TYPE_STRING ], + param_types: [GObject.TYPE_STRING], }, }, - _init: function(props) { + _init(props) { // check that it's safe to set properties before // chaining up (priv is NULL at this point, remember) this._readwrite = 'foo'; @@ -52,7 +54,7 @@ const MyObject = new GObject.Class({ }, set readwrite(val) { - if (val == 'ignore') + if (val === 'ignore') return; this._readwrite = val; @@ -80,103 +82,108 @@ const MyObject = new GObject.Class({ this._constructCalled = true; }, - notify_prop: function() { + notify_prop() { this._readonly = 'changed'; this.notify('readonly'); }, - emit_empty: function() { + emit_empty() { this.emit('empty'); }, - emit_minimal: function(one, two) { + emit_minimal(one, two) { this.emit('minimal', one, two); }, - emit_full: function() { + emit_full() { return this.emit('full'); }, - emit_detailed: function() { + emit_detailed() { this.emit('detailed::one'); this.emit('detailed::two'); }, - emit_run_last: function(callback) { + emit_run_last(callback) { this._run_last_callback = callback; this.emit('run-last'); }, - on_run_last: function() { + on_run_last() { this._run_last_callback(); }, - on_empty: function() { + on_empty() { this.empty_called = true; }, - on_full: function() { + on_full() { this.full_default_handler_called = true; return 79; - } + }, }); const MyApplication = new Lang.Class({ Name: 'MyApplication', Extends: Gio.Application, - Signals: { 'custom': { param_types: [ GObject.TYPE_INT ] } }, + Signals: {'custom': {param_types: [GObject.TYPE_INT]}}, - _init: function(params) { + _init(params) { this.parent(params); }, - emit_custom: function(n) { + emit_custom(n) { this.emit('custom', n); - } + }, }); const MyInitable = new Lang.Class({ Name: 'MyInitable', Extends: GObject.Object, - Implements: [ Gio.Initable ], + Implements: [Gio.Initable], - _init: function(params) { + _init(params) { this.parent(params); this.inited = false; }, - vfunc_init: function(cancellable) { // error? + vfunc_init(cancellable) { // error? if (!(cancellable instanceof Gio.Cancellable)) - throw 'Bad argument'; + throw new Error('Bad argument'); this.inited = true; - } + }, }); const Derived = new Lang.Class({ Name: 'Derived', Extends: MyObject, - _init: function() { - this.parent({ readwrite: 'yes' }); - } + _init() { + this.parent({readwrite: 'yes'}); + }, +}); + +const OddlyNamed = new Lang.Class({ + Name: 'Legacy.OddlyNamed', + Extends: MyObject, }); const MyCustomInit = new Lang.Class({ Name: 'MyCustomInit', Extends: GObject.Object, - _init: function() { + _init() { this.foo = false; this.parent(); }, - _instance_init: function() { + _instance_init() { this.foo = true; - } + }, }); describe('GObject class', function () { @@ -192,7 +199,7 @@ describe('GObject class', function () { }); it('constructs with a hash of property values', function () { - let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' }); + let myInstance2 = new MyObject({readwrite: 'baz', construct: 'asdf'}); expect(myInstance2.readwrite).toEqual('baz'); expect(myInstance2.readonly).toEqual('bar'); expect(myInstance2.construct).toEqual('asdf'); @@ -275,18 +282,22 @@ describe('GObject class', function () { }); it('calls run-last default handler last', function () { - let stack = [ ]; + let stack = []; let runLastSpy = jasmine.createSpy('runLastSpy') - .and.callFake(() => { stack.push(1); }); + .and.callFake(() => { + stack.push(1); + }); myInstance.connect('run-last', runLastSpy); - myInstance.emit_run_last(() => { stack.push(2); }); + myInstance.emit_run_last(() => { + stack.push(2); + }); expect(stack).toEqual([1, 2]); }); it("can inherit from something that's not GObject.Object", function () { // ...and still get all the goodies of GObject.Class - let instance = new MyApplication({ application_id: 'org.gjs.Application' }); + let instance = new MyApplication({application_id: 'org.gjs.Application'}); let customSpy = jasmine.createSpy('customSpy'); instance.connect('custom', customSpy); @@ -316,6 +327,13 @@ describe('GObject class', function () { expect(derived.readwrite).toEqual('yes'); }); + it('can have any valid Lang.Class name', function () { + let obj = new OddlyNamed(); + + expect(obj instanceof OddlyNamed).toBeTruthy(); + expect(obj instanceof MyObject).toBeTruthy(); + }); + it('calls its _instance_init() function while chaining up in constructor', function () { let instance = new MyCustomInit(); expect(instance.foo).toBeTruthy(); @@ -328,11 +346,11 @@ describe('GObject class', function () { Properties: { 'file': GObject.ParamSpec.object('file', 'File', 'File', GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - Gio.File.$gtype) + Gio.File.$gtype), }, }); let file = Gio.File.new_for_path('dummy'); - expect(() => new InterfacePropObject({ file: file })).not.toThrow(); + expect(() => new InterfacePropObject({file})).not.toThrow(); }); it('can override a property from the parent class', function () { @@ -346,7 +364,7 @@ describe('GObject class', function () { return this._subclass_readwrite; }, set readwrite(val) { - this._subclass_readwrite = 'subclass' + val; + this._subclass_readwrite = `subclass${val}`; }, }); let obj = new OverrideObject(); @@ -363,6 +381,49 @@ describe('GObject class', function () { }, })).toThrow(); }); + + it('handles gracefully forgetting to override a C property', function () { + GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL, + "*Object class Gjs_ForgottenOverride doesn't implement property " + + "'anchors' from interface 'GTlsFileDatabase'*"); + + // This is a random interface in Gio with a read-write property + const ForgottenOverride = new Lang.Class({ + Name: 'ForgottenOverride', + Extends: Gio.TlsDatabase, + Implements: [Gio.TlsFileDatabase], + }); + const obj = new ForgottenOverride(); + expect(obj.anchors).not.toBeDefined(); + expect(() => (obj.anchors = 'foo')).not.toThrow(); + expect(obj.anchors).toEqual('foo'); + + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectClass.js', 0, + 'testGObjectClassForgottenOverride'); + }); + + it('handles gracefully overriding a C property but forgetting the accessors', function () { + // This is a random interface in Gio with a read-write property + const ForgottenAccessors = new Lang.Class({ + Name: 'ForgottenAccessors', + Extends: Gio.TlsDatabase, + Implements: [Gio.TlsFileDatabase], + Properties: { + 'anchors': GObject.ParamSpec.override('anchors', Gio.TlsFileDatabase), + }, + }); + const obj = new ForgottenAccessors(); + expect(obj.anchors).toBeNull(); + obj.anchors = 'foo'; + + const ForgottenAccessors2 = new Lang.Class({ + Name: 'ForgottenAccessors2', + Extends: ForgottenAccessors, + }); + const obj2 = new ForgottenAccessors2(); + expect(obj2.anchors).toBeNull(); + obj2.anchors = 'foo'; + }); }); const AnInterface = new Lang.Interface({ @@ -372,53 +433,49 @@ const AnInterface = new Lang.Interface({ const GObjectImplementingLangInterface = new Lang.Class({ Name: 'GObjectImplementingLangInterface', Extends: GObject.Object, - Implements: [ AnInterface ], - - _init: function (props={}) { - this.parent(props); - } + Implements: [AnInterface], }); const AGObjectInterface = new Lang.Interface({ Name: 'AGObjectInterface', GTypeName: 'ArbitraryGTypeName', - Requires: [ GObject.Object ], + Requires: [GObject.Object], Properties: { 'interface-prop': GObject.ParamSpec.string('interface-prop', 'Interface property', 'Must be overridden in implementation', GObject.ParamFlags.READABLE, - 'foobar') + 'foobar'), }, Signals: { - 'interface-signal': {} + 'interface-signal': {}, }, requiredG: Lang.Interface.UNIMPLEMENTED, - optionalG: function () { + optionalG() { return 'AGObjectInterface.optionalG()'; - } + }, }); const InterfaceRequiringGObjectInterface = new Lang.Interface({ Name: 'InterfaceRequiringGObjectInterface', - Requires: [ AGObjectInterface ], + Requires: [AGObjectInterface], - optionalG: function () { - return 'InterfaceRequiringGObjectInterface.optionalG()\n' + - AGObjectInterface.optionalG(this); - } + optionalG() { + return `InterfaceRequiringGObjectInterface.optionalG()\n${ + AGObjectInterface.optionalG(this)}`; + }, }); const GObjectImplementingGObjectInterface = new Lang.Class({ Name: 'GObjectImplementingGObjectInterface', Extends: GObject.Object, - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', AGObjectInterface), 'class-prop': GObject.ParamSpec.string('class-prop', 'Class property', 'A property that is not on the interface', - GObject.ParamFlags.READABLE, 'meh') + GObject.ParamFlags.READABLE, 'meh'), }, Signals: { 'class-signal': {}, @@ -432,53 +489,45 @@ const GObjectImplementingGObjectInterface = new Lang.Class({ return 'meh'; }, - _init: function (props={}) { - this.parent(props); - }, - requiredG: function () {}, - optionalG: function () { + requiredG() {}, + optionalG() { return AGObjectInterface.optionalG(this); - } + }, }); const MinimalImplementationOfAGObjectInterface = new Lang.Class({ Name: 'MinimalImplementationOfAGObjectInterface', Extends: GObject.Object, - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, - _init: function (props={}) { - this.parent(props); - }, - requiredG: function () {} + requiredG() {}, }); const ImplementationOfTwoInterfaces = new Lang.Class({ Name: 'ImplementationOfTwoInterfaces', Extends: GObject.Object, - Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ], + Implements: [AGObjectInterface, InterfaceRequiringGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, - _init: function (props={}) { - this.parent(props); - }, - requiredG: function () {}, - optionalG: function () { + requiredG() {}, + optionalG() { return InterfaceRequiringGObjectInterface.optionalG(this); - } + }, }); describe('GObject interface', function () { it('class can implement a Lang.Interface', function () { let obj; - expect(() => { obj = new GObjectImplementingLangInterface(); }) - .not.toThrow(); + expect(() => { + obj = new GObjectImplementingLangInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); }); @@ -486,7 +535,7 @@ describe('GObject interface', function () { expect(() => new Lang.Interface({ Name: 'GObjectInterfaceNotRequiringGObject', GTypeName: 'GTypeNameNotRequiringGObject', - Requires: [ Gio.Initable ] + Requires: [Gio.Initable], })).toThrow(); }); @@ -494,15 +543,12 @@ describe('GObject interface', function () { const ObjectImplementingLangInterfaceAndCInterface = new Lang.Class({ Name: 'ObjectImplementingLangInterfaceAndCInterface', Extends: GObject.Object, - Implements: [ AnInterface, Gio.Initable ], - - _init: function (props={}) { - this.parent(props); - } + Implements: [AnInterface, Gio.Initable], }); let obj; - expect(() => { obj = new ObjectImplementingLangInterfaceAndCInterface(); }) - .not.toThrow(); + expect(() => { + obj = new ObjectImplementingLangInterfaceAndCInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); expect(obj.constructor.implements(Gio.Initable)).toBeTruthy(); }); @@ -526,8 +572,9 @@ describe('GObject interface', function () { it('can be implemented by a GObject class', function () { let obj; - expect(() => { obj = new GObjectImplementingGObjectInterface(); }) - .not.toThrow(); + expect(() => { + obj = new GObjectImplementingGObjectInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); }); @@ -542,21 +589,19 @@ describe('GObject interface', function () { const GObjectImplementingBothKindsOfInterface = new Lang.Class({ Name: 'GObjectImplementingBothKindsOfInterface', Extends: GObject.Object, - Implements: [ AnInterface, AGObjectInterface ], + Implements: [AnInterface, AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, - _init: function (props={}) { - this.parent(props); - }, - required: function () {}, - requiredG: function () {} + required() {}, + requiredG() {}, }); let obj; - expect(() => { obj = new GObjectImplementingBothKindsOfInterface(); }) - .not.toThrow(); + expect(() => { + obj = new GObjectImplementingBothKindsOfInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AnInterface)).toBeTruthy(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); }); @@ -572,18 +617,19 @@ describe('GObject interface', function () { expect(() => new Lang.Class({ Name: 'BadObject', Extends: GObject.Object, - Implements: [ AGObjectInterface ], + Implements: [AGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) - } + AGObjectInterface), + }, })).toThrow(); }); it("doesn't have to have its optional function implemented", function () { let obj; - expect(() => { obj = new MinimalImplementationOfAGObjectInterface(); }) - .not.toThrow(); + expect(() => { + obj = new MinimalImplementationOfAGObjectInterface(); + }).not.toThrow(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); }); @@ -599,7 +645,9 @@ describe('GObject interface', function () { it('can require another interface', function () { let obj; - expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow(); + expect(() => { + obj = new ImplementationOfTwoInterfaces(); + }).not.toThrow(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); expect(obj.constructor.implements(InterfaceRequiringGObjectInterface)) .toBeTruthy(); @@ -615,16 +663,13 @@ describe('GObject interface', function () { const MinimalImplementationOfTwoInterfaces = new Lang.Class({ Name: 'MinimalImplementationOfTwoInterfaces', Extends: GObject.Object, - Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ], + Implements: [AGObjectInterface, InterfaceRequiringGObjectInterface], Properties: { 'interface-prop': GObject.ParamSpec.override('interface-prop', - AGObjectInterface) + AGObjectInterface), }, - _init: function (props={}) { - this.parent(props); - }, - requiredG: function () {} + requiredG() {}, }); let obj = new MinimalImplementationOfTwoInterfaces(); expect(obj.optionalG()) @@ -634,27 +679,27 @@ describe('GObject interface', function () { it('must be implemented by a class that implements all required interfaces', function () { expect(() => new Lang.Class({ Name: 'BadObject', - Implements: [ InterfaceRequiringGObjectInterface ], - required: function () {} + Implements: [InterfaceRequiringGObjectInterface], + required() {}, })).toThrow(); }); it('must be implemented by a class that implements required interfaces in correct order', function () { expect(() => new Lang.Class({ Name: 'BadObject', - Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ], - required: function () {} + Implements: [InterfaceRequiringGObjectInterface, AGObjectInterface], + required() {}, })).toThrow(); }); it('can require an interface from C', function () { const InitableInterface = new Lang.Interface({ Name: 'InitableInterface', - Requires: [ GObject.Object, Gio.Initable ] + Requires: [GObject.Object, Gio.Initable], }); expect(() => new Lang.Class({ Name: 'BadObject', - Implements: [ InitableInterface ] + Implements: [InitableInterface], })).toThrow(); }); @@ -698,11 +743,9 @@ describe('GObject interface', function () { new Lang.Class({ Name: 'MyNaughtyObject', Extends: GObject.Object, - Implements: [ AGObjectInterface ], - _init: function (props={}) { - this.parent(props); - }, - requiredG: function () {} + Implements: [AGObjectInterface], + + requiredG() {}, }); // g_test_assert_expected_messages() is a macro, not introspectable GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js', @@ -715,14 +758,14 @@ describe('GObject interface', function () { it('gets the correct type for its metaclass', function () { const MyMeta = new Lang.Class({ Name: 'MyMeta', - Extends: GObject.Class + Extends: GObject.Class, }); const MyMetaObject = new MyMeta({ - Name: 'MyMetaObject' + Name: 'MyMetaObject', }); const MyMetaInterface = new Lang.Interface({ Name: 'MyMetaInterface', - Requires: [ MyMetaObject ] + Requires: [MyMetaObject], }); expect(MyMetaInterface instanceof GObject.Interface).toBeTruthy(); }); @@ -730,7 +773,7 @@ describe('GObject interface', function () { it('can be implemented by a class as well as its parent class', function () { const SubObject = new Lang.Class({ Name: 'SubObject', - Extends: GObjectImplementingGObjectInterface + Extends: GObjectImplementingGObjectInterface, }); let obj = new SubObject(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); @@ -741,7 +784,7 @@ describe('GObject interface', function () { const SubImplementer = new Lang.Class({ Name: 'SubImplementer', Extends: GObjectImplementingGObjectInterface, - Implements: [ AGObjectInterface ] + Implements: [AGObjectInterface], }); let obj = new SubImplementer(); expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy(); @@ -752,13 +795,13 @@ describe('GObject interface', function () { const LegacyInterface1 = new Lang.Interface({ Name: 'LegacyInterface1', Requires: [GObject.Object], - Signals: { 'legacy-iface1-signal': {} }, + Signals: {'legacy-iface1-signal': {}}, }); const LegacyInterface2 = new Lang.Interface({ Name: 'LegacyInterface2', Requires: [GObject.Object], - Signals: { 'legacy-iface2-signal': {} }, + Signals: {'legacy-iface2-signal': {}}, }); const Legacy = new Lang.Class({ @@ -785,11 +828,19 @@ const Legacy = new Lang.Class({ chainUpToMe() {}, overrideMe() {}, - get property() { return this._property + 1; }, - set property(value) { this._property = value - 2; }, + get property() { + return this._property + 1; + }, + set property(value) { + this._property = value - 2; + }, - get override_property() { return this._override_property + 1; }, - set override_property(value) { this._override_property = value - 2; }, + get overrideProperty() { + return this._overrideProperty + 1; + }, + set overrideProperty(value) { + this._overrideProperty = value - 2; + }, }); Legacy.staticMethod = function () {}; @@ -806,8 +857,13 @@ const Shiny = GObject.registerClass({ overrideMe() {} - get override_property() { return this._override_property + 2; } - set override_property(value) { this._override_property = value - 1; } + get overrideProperty() { + return this._overrideProperty + 2; + } + + set overrideProperty(value) { + this._overrideProperty = value - 1; + } }); describe('ES6 GObject class inheriting from GObject.Class', function () { @@ -832,7 +888,7 @@ describe('ES6 GObject class inheriting from GObject.Class', function () { }); it("passes arguments to the parent class's constructor", function () { - let instance = new Shiny(42); + instance = new Shiny(42); expect(instance.constructorCalledWith).toEqual(42); }); @@ -852,8 +908,8 @@ describe('ES6 GObject class inheriting from GObject.Class', function () { }); it('overrides a property from the parent class', function () { - instance.override_property = 42; - expect(instance.override_property).toEqual(43); + instance.overrideProperty = 42; + expect(instance.overrideProperty).toEqual(43); }); it('inherits a signal from the parent class', function () { diff --git a/installed-tests/js/testLegacyGtk.js b/installed-tests/js/testLegacyGtk.js index b812d29..198d5c3 100644 --- a/installed-tests/js/testLegacyGtk.js +++ b/installed-tests/js/testLegacyGtk.js @@ -1,4 +1,6 @@ // -*- mode: js; indent-tabs-mode: nil -*- +/* eslint-disable no-restricted-properties */ + imports.gi.versions.Gtk = '3.0'; const ByteArray = imports.byteArray; @@ -42,34 +44,37 @@ const MyComplexGtkSubclass = new Lang.Class({ InternalChildren: ['internal-label-child'], CssName: 'complex-subclass', - testChildrenExist: function () { + testChildrenExist() { this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child'); expect(this._internalLabel).toEqual(jasmine.anything()); expect(this.label_child2).toEqual(jasmine.anything()); expect(this._internal_label_child).toEqual(jasmine.anything()); - } + }, }); const MyComplexGtkSubclassFromResource = new Lang.Class({ Name: 'MyComplexGtkSubclassFromResource', Extends: Gtk.Grid, - Template: 'resource:///org/gjs/jsunit/complex.ui', + Template: 'resource:///org/gjs/jsunit/complex3.ui', Children: ['label-child', 'label-child2'], InternalChildren: ['internal-label-child'], - testChildrenExist: function () { + testChildrenExist() { expect(this.label_child).toEqual(jasmine.anything()); expect(this.label_child2).toEqual(jasmine.anything()); expect(this._internal_label_child).toEqual(jasmine.anything()); - } + }, + + templateCallback() {}, + boundCallback() {}, }); function validateTemplate(description, ClassName) { describe(description, function () { let win, content; beforeEach(function () { - win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL }); + win = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); content = new ClassName(); win.add(content); }); diff --git a/installed-tests/js/testLocale.js b/installed-tests/js/testLocale.js deleted file mode 100644 index edd8a6e..0000000 --- a/installed-tests/js/testLocale.js +++ /dev/null @@ -1,41 +0,0 @@ -describe('JS_SetLocaleCallbacks', function () { - it('Intl API was compiled into SpiderMonkey', function () { - expect(Intl).toBeDefined(); - }); - - // Requesting the weekday name tests locale_to_unicode - it('toLocaleDateString() works', function () { - let date = new Date('12/15/1981'); - let datestr = date.toLocaleDateString('pt-BR', { weekday: 'long' }); - expect(datestr).toEqual('terça-feira'); - }); - - it('toLocaleLowerCase() works', function () { - expect('AAA'.toLocaleLowerCase()).toEqual('aaa'); - }); - - // String conversion is implemented internally to GLib, - // and is more-or-less independent of locale. (A few - // characters are handled specially for a few locales, - // like i in Turkish. But not A WITH ACUTE) - it('toLocaleLowerCase() works for Unicode', function () { - expect('\u00c1'.toLocaleLowerCase()).toEqual('\u00e1'); - }); - - it('toLocaleUpperCase() works', function () { - expect('aaa'.toLocaleUpperCase()).toEqual('AAA'); - }); - - it('toLocaleUpperCase() works for Unicode', function () { - expect('\u00e1'.toLocaleUpperCase()).toEqual('\u00c1'); - }); - - // GLib calls out to libc for collation, so we can't really - // assume anything - we could even be running in the - // C locale. The below is pretty safe. - it('localeCompare() works', function () { - expect('a'.localeCompare('b')).toBeLessThan(0); - expect('a'.localeCompare('a')).toEqual(0); - expect('b'.localeCompare('a')).toBeGreaterThan(0); - }); -}); diff --git a/installed-tests/js/testMainloop.js b/installed-tests/js/testMainloop.js index 0ffcf37..aff3d5a 100644 --- a/installed-tests/js/testMainloop.js +++ b/installed-tests/js/testMainloop.js @@ -1,7 +1,9 @@ +/* eslint-disable no-restricted-properties */ + const Mainloop = imports.mainloop; describe('Mainloop.timeout_add()', function () { - let runTenTimes, runOnlyOnce, neverRun; + let runTenTimes, runOnlyOnce, neverRun, neverRunSource; beforeAll(function (done) { let count = 0; runTenTimes = jasmine.createSpy('runTenTimes').and.callFake(() => { @@ -17,7 +19,7 @@ describe('Mainloop.timeout_add()', function () { Mainloop.timeout_add(10, runTenTimes); Mainloop.timeout_add(10, runOnlyOnce); - Mainloop.timeout_add(15000, neverRun); + neverRunSource = Mainloop.timeout_add(15000, neverRun); }); it('runs a timeout function', function () { @@ -31,6 +33,10 @@ describe('Mainloop.timeout_add()', function () { it('runs a timeout function after an initial timeout', function () { expect(neverRun).not.toHaveBeenCalled(); }); + + afterAll(function () { + Mainloop.source_remove(neverRunSource); + }); }); describe('Mainloop.idle_add()', function () { diff --git a/installed-tests/js/testPackage.js b/installed-tests/js/testPackage.js index 377ed8a..bcaf3db 100644 --- a/installed-tests/js/testPackage.js +++ b/installed-tests/js/testPackage.js @@ -62,7 +62,7 @@ describe('Package module', function () { }); it('doesn\'t find a non-existent interface method', function () { - expect(Pkg.checkSymbol('GIMarshallingTests', '1.0', 'Interface.test_int42_291in342')).toEqual(false); + expect(Pkg.checkSymbol('GIMarshallingTests', '1.0', 'Interface.test_int42_in')).toEqual(false); }); it('finds an enum value', function () { diff --git a/installed-tests/js/testParamSpec.js b/installed-tests/js/testParamSpec.js index 7ac0ad4..1382a45 100644 --- a/installed-tests/js/testParamSpec.js +++ b/installed-tests/js/testParamSpec.js @@ -7,11 +7,10 @@ let blurb = 'This is the foo property'; let flags = GObject.ParamFlags.READABLE; function testParamSpec(type, params, defaultValue) { - describe('GObject.ParamSpec.' + type, function () { + describe(`GObject.ParamSpec.${type}`, function () { let paramSpec; beforeEach(function () { - paramSpec = GObject.ParamSpec[type].apply(GObject.ParamSpec, - [name, nick, blurb, flags, ...params]); + paramSpec = GObject.ParamSpec[type](name, nick, blurb, flags, ...params); }); it('has the correct name strings', function () { diff --git a/installed-tests/js/testPrint.js b/installed-tests/js/testPrint.js new file mode 100644 index 0000000..c42b000 --- /dev/null +++ b/installed-tests/js/testPrint.js @@ -0,0 +1,31 @@ +describe('print', function () { + it('can be spied upon', function () { + spyOn(globalThis, 'print'); + print('foo'); + expect(print).toHaveBeenCalledWith('foo'); + }); +}); + +describe('printerr', function () { + it('can be spied upon', function () { + spyOn(globalThis, 'printerr'); + printerr('foo'); + expect(printerr).toHaveBeenCalledWith('foo'); + }); +}); + +describe('log', function () { + it('can be spied upon', function () { + spyOn(globalThis, 'log'); + log('foo'); + expect(log).toHaveBeenCalledWith('foo'); + }); +}); + +describe('logError', function () { + it('can be spied upon', function () { + spyOn(globalThis, 'logError'); + logError('foo', 'bar'); + expect(logError).toHaveBeenCalledWith('foo', 'bar'); + }); +}); diff --git a/installed-tests/js/testRegress.js b/installed-tests/js/testRegress.js new file mode 100644 index 0000000..8d6c9de --- /dev/null +++ b/installed-tests/js/testRegress.js @@ -0,0 +1,1609 @@ +const Regress = imports.gi.Regress; + +// We use Gio to have some objects that we know exist +imports.gi.versions.Gtk = '3.0'; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; + +describe('Life, the Universe and Everything', function () { + it('includes null return value', function () { + expect(Regress.test_return_allow_none()).toBeNull(); + expect(Regress.test_return_nullable()).toBeNull(); + }); + + it('includes booleans', function () { + expect(Regress.test_boolean(false)).toBe(false); + expect(Regress.test_boolean(true)).toBe(true); + expect(Regress.test_boolean_true(true)).toBe(true); + expect(Regress.test_boolean_false(false)).toBe(false); + }); + + [8, 16, 32, 64].forEach(bits => { + it(`includes ${bits}-bit integers`, function () { + const method = `test_int${bits}`; + expect(Regress[method](42)).toBe(42); + expect(Regress[method](-42)).toBe(-42); + }); + + it(`includes unsigned ${bits}-bit integers`, function () { + expect(Regress[`test_uint${bits}`](42)).toBe(42); + }); + }); + + ['short', 'int', 'long', 'ssize', 'float', 'double'].forEach(type => { + it(`includes ${type}s`, function () { + const method = `test_${type}`; + expect(Regress[method](42)).toBe(42); + expect(Regress[method](-42)).toBe(-42); + }); + }); + + ['ushort', 'uint', 'ulong', 'size'].forEach(type => { + it(`includes ${type}s`, function () { + expect(Regress[`test_${type}`](42)).toBe(42); + }); + }); + + describe('No implicit conversion to unsigned', function () { + ['uint8', 'uint16', 'uint32', 'uint64', 'uint', 'size'].forEach(type => { + it(`for ${type}`, function () { + expect(() => Regress[`test_${type}`](-42)).toThrow(); + }); + }); + }); + + it('includes wide characters', function () { + expect(Regress.test_unichar('c')).toBe('c'); + expect(Regress.test_unichar('')).toBe(''); + expect(Regress.test_unichar('\u2665')).toBe('\u2665'); + }); + + it('includes time_t', function () { + const now = Math.floor(new Date().getTime() / 1000); + const bounced = Math.floor(Regress.test_timet(now)); + expect(bounced).toEqual(now); + }); + + it('includes GTypes', function () { + expect(Regress.test_gtype(GObject.TYPE_NONE)).toBe(GObject.TYPE_NONE); + expect(Regress.test_gtype(String)).toBe(GObject.TYPE_STRING); + expect(Regress.test_gtype(GObject.Object)).toBe(GObject.Object.$gtype); + }); + + it('closures', function () { + const callback = jasmine.createSpy('callback').and.returnValue(42); + expect(Regress.test_closure(callback)).toEqual(42); + expect(callback).toHaveBeenCalledWith(); + }); + + it('closures with one argument', function () { + const callback = jasmine.createSpy('callback') + .and.callFake(someValue => someValue); + expect(Regress.test_closure_one_arg(callback, 42)).toEqual(42); + expect(callback).toHaveBeenCalledWith(42); + }); + + it('closure with GLib.Variant argument', function () { + const callback = jasmine.createSpy('callback') + .and.returnValue(new GLib.Variant('s', 'hello')); + const variant = new GLib.Variant('i', 42); + expect(Regress.test_closure_variant(callback, variant).deepUnpack()) + .toEqual('hello'); + expect(callback).toHaveBeenCalledWith(variant); + }); + + describe('GValue marshalling', function () { + it('integer in', function () { + expect(Regress.test_int_value_arg(42)).toEqual(42); + }); + + it('integer out', function () { + expect(Regress.test_value_return(42)).toEqual(42); + }); + }); + + // See testCairo.js for the following tests, since that will be skipped if + // we are building without Cairo support: + // Regress.test_cairo_context_full_return() + // Regress.test_cairo_context_none_in() + // Regress.test_cairo_surface_none_return() + // Regress.test_cairo_surface_full_return() + // Regress.test_cairo_surface_none_in() + // Regress.test_cairo_surface_full_out() + // Regress.TestObj.emit_sig_with_foreign_struct() + + it('integer GLib.Variant', function () { + const ivar = Regress.test_gvariant_i(); + expect(ivar.get_type_string()).toEqual('i'); + expect(ivar.unpack()).toEqual(1); + }); + + it('string GLib.Variant', function () { + const svar = Regress.test_gvariant_s(); + expect(String.fromCharCode(svar.classify())).toEqual('s'); + expect(svar.unpack()).toEqual('one'); + }); + + it('dictionary GLib.Variant', function () { + const asvvar = Regress.test_gvariant_asv(); + expect(asvvar.recursiveUnpack()).toEqual({name: 'foo', timeout: 10}); + }); + + it('variant GLib.Variant', function () { + const vvar = Regress.test_gvariant_v(); + expect(vvar.unpack()).toEqual(jasmine.any(GLib.Variant)); + expect(vvar.recursiveUnpack()).toEqual('contents'); + }); + + it('string array GLib.Variant', function () { + const asvar = Regress.test_gvariant_as(); + expect(asvar.deepUnpack()).toEqual(['one', 'two', 'three']); + }); + + describe('UTF-8 strings', function () { + const CONST_STR = 'const ♥ utf8'; + const NONCONST_STR = 'nonconst ♥ utf8'; + + it('as return types', function () { + expect(Regress.test_utf8_const_return()).toEqual(CONST_STR); + expect(Regress.test_utf8_nonconst_return()).toEqual(NONCONST_STR); + }); + + it('as in parameters', function () { + Regress.test_utf8_const_in(CONST_STR); + }); + + it('as out parameters', function () { + expect(Regress.test_utf8_out()).toEqual(NONCONST_STR); + }); + + xit('as in-out parameters', function () { + expect(Regress.test_utf8_inout(CONST_STR)).toEqual(NONCONST_STR); + }).pend('https://gitlab.gnome.org/GNOME/gobject-introspection/issues/192'); + }); + + it('return values in filename encoding', function () { + const filenames = Regress.test_filename_return(); + expect(filenames).toEqual(['\u00e5\u00e4\u00f6', '/etc/fstab']); + }); + + describe('Various configurations of arguments', function () { + it('in after out', function () { + const str = 'hello'; + const len = Regress.test_int_out_utf8(str); + expect(len).toEqual(str.length); + }); + + it('multiple number args', function () { + const [times2, times3] = Regress.test_multi_double_args(2.5); + expect(times2).toEqual(5); + expect(times3).toEqual(7.5); + }); + + it('multiple string out parameters', function () { + const [first, second] = Regress.test_utf8_out_out(); + expect(first).toEqual('first'); + expect(second).toEqual('second'); + }); + + it('strings as return value and output parameter', function () { + const [first, second] = Regress.test_utf8_out_nonconst_return(); + expect(first).toEqual('first'); + expect(second).toEqual('second'); + }); + + it('nullable string in parameter', function () { + expect(() => Regress.test_utf8_null_in(null)).not.toThrow(); + }); + + it('nullable string out parameter', function () { + expect(Regress.test_utf8_null_out()).toBeNull(); + }); + }); + + ['int', 'gint8', 'gint16', 'gint32', 'gint64'].forEach(inttype => { + it(`arrays of ${inttype} in`, function () { + expect(Regress[`test_array_${inttype}_in`]([1, 2, 3, 4])).toEqual(10); + }); + }); + + it('implicit conversions from strings to int arrays', function () { + expect(Regress.test_array_gint8_in('\x01\x02\x03\x04')).toEqual(10); + expect(Regress.test_array_gint16_in('\x01\x02\x03\x04')).toEqual(10); + expect(Regress.test_array_gint16_in('\u0100\u0200\u0300\u0400')).toEqual(2560); + }); + + it('out arrays of integers', function () { + expect(Regress.test_array_int_out()).toEqual([0, 1, 2, 3, 4]); + }); + + xit('inout arrays of integers', function () { + expect(Regress.test_array_int_inout([0, 1, 2, 3, 4])).toEqual([2, 3, 4, 5]); + }).pend('https://gitlab.gnome.org/GNOME/gobject-introspection/issues/192'); + + describe('String arrays', function () { + it('marshalling in', function () { + expect(Regress.test_strv_in(['1', '2', '3'])).toBeTruthy(); + expect(Regress.test_strv_in(['4', '5', '6'])).toBeFalsy(); + // Ensure that primitives throw without SEGFAULT + expect(() => Regress.test_strv_in(1)).toThrow(); + expect(() => Regress.test_strv_in('')).toThrow(); + expect(() => Regress.test_strv_in(false)).toThrow(); + // Second two are deliberately not strings + expect(() => Regress.test_strv_in(['1', 2, 3])).toThrow(); + }); + + it('marshalling out', function () { + expect(Regress.test_strv_out()) + .toEqual(['thanks', 'for', 'all', 'the', 'fish']); + }); + + it('marshalling return value with container transfer', function () { + expect(Regress.test_strv_out_container()).toEqual(['1', '2', '3']); + }); + + it('marshalling out parameter with container transfer', function () { + expect(Regress.test_strv_outarg()).toEqual(['1', '2', '3']); + }); + }); + + it('GType arrays', function () { + expect(Regress.test_array_gtype_in([Gio.SimpleAction, Gio.Icon, GObject.TYPE_BOXED])) + .toEqual('[GSimpleAction,GIcon,GBoxed,]'); + expect(() => Regress.test_array_gtype_in(42)).toThrow(); + expect(() => Regress.test_array_gtype_in([undefined])).toThrow(); + // 80 is G_TYPE_OBJECT, but we don't want it to work + expect(() => Regress.test_array_gtype_in([80])).toThrow(); + }); + + describe('Fixed arrays of integers', function () { + it('marshals as an in parameter', function () { + expect(Regress.test_array_fixed_size_int_in([1, 2, 3, 4])).toEqual(10); + }); + + it('marshals as an out parameter', function () { + expect(Regress.test_array_fixed_size_int_out()).toEqual([0, 1, 2, 3, 4]); + }); + + it('marshals as a return value', function () { + expect(Regress.test_array_fixed_size_int_return()).toEqual([0, 1, 2, 3, 4]); + }); + }); + + it("string array that's const in C", function () { + expect(Regress.test_strv_out_c()).toEqual(['thanks', 'for', 'all', 'the', 'fish']); + }); + + describe('arrays of integers with length parameter', function () { + it('marshals as a return value with transfer full', function () { + expect(Regress.test_array_int_full_out()).toEqual([0, 1, 2, 3, 4]); + }); + + it('marshals as a return value with transfer none', function () { + expect(Regress.test_array_int_none_out()).toEqual([1, 2, 3, 4, 5]); + }); + + it('marshalls as a nullable in parameter', function () { + expect(() => Regress.test_array_int_null_in(null)).not.toThrow(); + }); + + it('marshals as a nullable return value', function () { + expect(Regress.test_array_int_null_out()).toEqual([]); + }); + }); + + ['glist', 'gslist'].forEach(list => { + describe(`${list} types`, function () { + const STR_LIST = ['1', '2', '3']; + + it('return with transfer-none', function () { + expect(Regress[`test_${list}_nothing_return`]()).toEqual(STR_LIST); + expect(Regress[`test_${list}_nothing_return2`]()).toEqual(STR_LIST); + }); + + it('return with transfer-container', function () { + expect(Regress[`test_${list}_container_return`]()).toEqual(STR_LIST); + }); + + it('return with transfer-full', function () { + expect(Regress[`test_${list}_everything_return`]()).toEqual(STR_LIST); + }); + + it('in with transfer-none', function () { + Regress[`test_${list}_nothing_in`](STR_LIST); + Regress[`test_${list}_nothing_in2`](STR_LIST); + }); + + it('nullable in', function () { + expect(() => Regress[`test_${list}_null_in`]([])).not.toThrow(); + }); + + it('nullable out', function () { + expect(Regress[`test_${list}_null_out`]()).toEqual([]); + }); + + xit('in with transfer-container', function () { + Regress[`test_${list}_container_in`](STR_LIST); + }).pend('Function not added to gobject-introspection test suite yet'); + }); + }); + + it('GList of GTypes in with transfer container', function () { + expect(() => + Regress.test_glist_gtype_container_in([Regress.TestObj, Regress.TestSubObj])) + .not.toThrow(); + }); + + describe('GHash type', function () { + const EXPECTED_HASH = {baz: 'bat', foo: 'bar', qux: 'quux'}; + + it('null GHash out', function () { + expect(Regress.test_ghash_null_return()).toBeNull(); + }); + + it('out GHash', function () { + expect(Regress.test_ghash_nothing_return()).toEqual(EXPECTED_HASH); + expect(Regress.test_ghash_nothing_return2()).toEqual(EXPECTED_HASH); + }); + + const GVALUE_HASH_TABLE = { + 'integer': 12, + 'boolean': true, + 'string': 'some text', + 'strings': ['first', 'second', 'third'], + 'flags': Regress.TestFlags.FLAG1 | Regress.TestFlags.FLAG3, + 'enum': Regress.TestEnum.VALUE2, + }; + + it('with GValue value type out', function () { + expect(Regress.test_ghash_gvalue_return()).toEqual(GVALUE_HASH_TABLE); + }); + + xit('with GValue value type in', function () { + expect(() => Regress.test_ghash_gvalue_in(GVALUE_HASH_TABLE)).not.toThrow(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/272'); + + it('marshals as a return value with transfer container', function () { + expect(Regress.test_ghash_container_return()).toEqual(EXPECTED_HASH); + }); + + it('marshals as a return value with transfer full', function () { + expect(Regress.test_ghash_everything_return()).toEqual(EXPECTED_HASH); + }); + + it('null GHash in', function () { + Regress.test_ghash_null_in(null); + }); + + it('null GHashTable out', function () { + expect(Regress.test_ghash_null_out()).toBeNull(); + }); + + it('in GHash', function () { + Regress.test_ghash_nothing_in(EXPECTED_HASH); + Regress.test_ghash_nothing_in2(EXPECTED_HASH); + }); + + it('nested GHash', function () { + const EXPECTED_NESTED_HASH = {wibble: EXPECTED_HASH}; + + expect(Regress.test_ghash_nested_everything_return()) + .toEqual(EXPECTED_NESTED_HASH); + expect(Regress.test_ghash_nested_everything_return2()) + .toEqual(EXPECTED_NESTED_HASH); + }); + }); + + describe('GArray', function () { + it('marshals as a return value with transfer container', function () { + expect(Regress.test_garray_container_return()).toEqual(['regress']); + }); + + it('marshals as a return value with transfer full', function () { + expect(Regress.test_garray_full_return()).toEqual(['regress']); + }); + }); + + it('enum parameter', function () { + expect(Regress.test_enum_param(Regress.TestEnum.VALUE1)).toEqual('value1'); + expect(Regress.test_enum_param(Regress.TestEnum.VALUE3)).toEqual('value3'); + }); + + it('unsigned enum parameter', function () { + expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE1)) + .toEqual('value1'); + expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE2)) + .toEqual('value2'); + }); + + it('flags parameter', function () { + expect(Regress.global_get_flags_out()).toEqual(Regress.TestFlags.FLAG1 | + Regress.TestFlags.FLAG3); + }); + + describe('Simple introspected struct', function () { + let struct; + beforeEach(function () { + struct = new Regress.TestStructA(); + struct.some_int = 42; + struct.some_int8 = 43; + struct.some_double = 42.5; + struct.some_enum = Regress.TestEnum.VALUE3; + }); + + it('sets fields correctly', function () { + expect(struct.some_int).toEqual(42); + expect(struct.some_int8).toEqual(43); + expect(struct.some_double).toEqual(42.5); + expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + + it('can clone', function () { + const b = struct.clone(); + expect(b.some_int).toEqual(42); + expect(b.some_int8).toEqual(43); + expect(b.some_double).toEqual(42.5); + expect(b.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + + it('can be modified by a method', function () { + const c = Regress.TestStructA.parse('foobar'); + expect(c.some_int).toEqual(23); + }); + + describe('constructors', function () { + beforeEach(function () { + struct = new Regress.TestStructA({ + some_int: 42, + some_int8: 43, + some_double: 42.5, + some_enum: Regress.TestEnum.VALUE3, + }); + }); + + it('"copies" an object from a hash of field values', function () { + expect(struct.some_int).toEqual(42); + expect(struct.some_int8).toEqual(43); + expect(struct.some_double).toEqual(42.5); + expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + + it('catches bad field names', function () { + expect(() => new Regress.TestStructA({junk: 42})).toThrow(); + }); + + it('copies an object from another object of the same type', function () { + const copy = new Regress.TestStructA(struct); + expect(copy.some_int).toEqual(42); + expect(copy.some_int8).toEqual(43); + expect(copy.some_double).toEqual(42.5); + expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + }); + }); + + it('out arrays of structs', function () { + const array = Regress.test_array_struct_out(); + const ints = array.map(struct => struct.some_int); + expect(ints).toEqual([22, 33, 44]); + }); + + describe('Introspected nested struct', function () { + let struct; + beforeEach(function () { + struct = new Regress.TestStructB(); + struct.some_int8 = 43; + struct.nested_a.some_int8 = 66; + }); + + it('sets fields correctly', function () { + expect(struct.some_int8).toEqual(43); + expect(struct.nested_a.some_int8).toEqual(66); + }); + + it('can clone', function () { + const b = struct.clone(); + expect(b.some_int8).toEqual(43); + expect(b.nested_a.some_int8).toEqual(66); + }); + }); + + // Bare GObject pointer, not currently supported (and possibly not ever) + xdescribe('Struct with non-basic member', function () { + it('sets fields correctly', function () { + const struct = new Regress.TestStructC(); + struct.another_int = 43; + struct.obj = new GObject.Object(); + + expect(struct.another_int).toEqual(43); + expect(struct.obj).toEqual(jasmine.any(GObject.Object)); + }); + }); + + describe('Struct with annotated fields', function () { + xit('sets fields correctly', function () { + const testObjList = [new Regress.TestObj(), new Regress.TestObj()]; + const testStructList = [new Regress.TestStructA(), new Regress.TestStructA()]; + const struct = new Regress.TestStructD(); + struct.array1 = testStructList; + struct.array2 = testObjList; + struct.field = testObjList[0]; + struct.list = testObjList; + struct.garray = testObjList; + + expect(struct.array1).toEqual(testStructList); + expect(struct.array2).toEqual(testObjList); + expect(struct.field).toEqual(testObjList[0]); + expect(struct.list).toEqual(testObjList); + expect(struct.garray).toEqual(testObjList); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/83'); + }); + + describe('Struct with array of anonymous unions', function () { + xit('sets fields correctly', function () { + const struct = new Regress.TestStructE(); + struct.some_type = GObject.Object.$gtype; + for (let ix = 0; ix < 1; ix++) { + struct.some_union[ix].v_int = 42; + struct.some_union[ix].v_uint = 43; + struct.some_union[ix].v_long = 44; + struct.some_union[ix].v_ulong = 45; + struct.some_union[ix].v_int64 = 46; + struct.some_union[ix].v_uint64 = 47; + struct.some_union[ix].v_float = 48.5; + struct.some_union[ix].v_double = 49.5; + struct.some_union[ix].v_pointer = null; + } + + expect(struct.some_type).toEqual(GObject.Object.$gtype); + for (let ix = 0; ix < 1; ix++) { + expect(struct.some_union[ix].v_int).toEqual(42); + expect(struct.some_union[ix].v_uint).toEqual(43); + expect(struct.some_union[ix].v_long).toEqual(44); + expect(struct.some_union[ix].v_ulong).toEqual(45); + expect(struct.some_union[ix].v_int64).toEqual(46); + expect(struct.some_union[ix].v_uint64).toEqual(47); + expect(struct.some_union[ix].v_float).toEqual(48.5); + expect(struct.some_union[ix].v_double).toEqual(49.5); + expect(struct.some_union[ix].v_pointer).toBeNull(); + } + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/273'); + }); + + // Bare int pointers, not currently supported (and possibly not ever) + xdescribe('Struct with const/volatile members', function () { + it('sets fields correctly', function () { + const struct = new Regress.TestStructF(); + struct.ref_count = 1; + struct.data1 = null; + struct.data2 = null; + struct.data3 = null; + struct.data4 = null; + struct.data5 = null; + struct.data6 = null; + struct.data7 = 42; + + expect(struct.ref_count).toEqual(1); + expect(struct.data1).toBeNull(); + expect(struct.data2).toBeNull(); + expect(struct.data3).toBeNull(); + expect(struct.data4).toBeNull(); + expect(struct.data5).toBeNull(); + expect(struct.data6).toBeNull(); + expect(struct.data7).toEqual(42); + }); + }); + + describe('Introspected simple boxed struct', function () { + let struct; + beforeEach(function () { + struct = new Regress.TestSimpleBoxedA(); + struct.some_int = 42; + struct.some_int8 = 43; + struct.some_double = 42.5; + struct.some_enum = Regress.TestEnum.VALUE3; + }); + + it('sets fields correctly', function () { + expect(struct.some_int).toEqual(42); + expect(struct.some_int8).toEqual(43); + expect(struct.some_double).toEqual(42.5); + expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + + it('can be passed to a method', function () { + const other = new Regress.TestSimpleBoxedA({ + some_int: 42, + some_int8: 43, + some_double: 42.5, + }); + expect(other.equals(struct)).toBeTruthy(); + }); + + it('can be returned from a method', function () { + const other = Regress.TestSimpleBoxedA.const_return(); + expect(other.some_int).toEqual(5); + expect(other.some_int8).toEqual(6); + expect(other.some_double).toEqual(7); + }); + + describe('constructors', function () { + beforeEach(function () { + struct = new Regress.TestSimpleBoxedA({ + some_int: 42, + some_int8: 43, + some_double: 42.5, + some_enum: Regress.TestEnum.VALUE3, + }); + }); + + it('"copies" an object from a hash of field values', function () { + expect(struct.some_int).toEqual(42); + expect(struct.some_int8).toEqual(43); + expect(struct.some_double).toEqual(42.5); + expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + + it('catches bad field names', function () { + expect(() => new Regress.TestSimpleBoxedA({junk: 42})).toThrow(); + }); + + it('copies an object from another object of the same type', function () { + const copy = new Regress.TestSimpleBoxedA(struct); + expect(copy).toEqual(jasmine.any(Regress.TestSimpleBoxedA)); + expect(copy.some_int).toEqual(42); + expect(copy.some_int8).toEqual(43); + expect(copy.some_double).toEqual(42.5); + expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3); + }); + }); + }); + + describe('Introspected boxed nested struct', function () { + let struct; + beforeEach(function () { + struct = new Regress.TestSimpleBoxedB(); + struct.some_int8 = 42; + struct.nested_a.some_int = 43; + }); + + it('reads fields and nested fields', function () { + expect(struct.some_int8).toEqual(42); + expect(struct.nested_a.some_int).toEqual(43); + }); + + it('assigns nested struct field from an instance', function () { + struct.nested_a = new Regress.TestSimpleBoxedA({some_int: 53}); + expect(struct.nested_a.some_int).toEqual(53); + }); + + it('assigns nested struct field directly from a hash of field values', function () { + struct.nested_a = {some_int: 63}; + expect(struct.nested_a.some_int).toEqual(63); + }); + + describe('constructors', function () { + it('constructs with a nested hash of field values', function () { + const simple2 = new Regress.TestSimpleBoxedB({ + some_int8: 42, + nested_a: { + some_int: 43, + some_int8: 44, + some_double: 43.5, + }, + }); + expect(simple2.some_int8).toEqual(42); + expect(simple2.nested_a.some_int).toEqual(43); + expect(simple2.nested_a.some_int8).toEqual(44); + expect(simple2.nested_a.some_double).toEqual(43.5); + }); + + it('copies an object from another object of the same type', function () { + const copy = new Regress.TestSimpleBoxedB(struct); + expect(copy.some_int8).toEqual(42); + expect(copy.nested_a.some_int).toEqual(43); + }); + }); + }); + + describe('Introspected boxed types', function () { + describe('Opaque', function () { + it('constructs from a default constructor', function () { + const boxed = new Regress.TestBoxed(); + expect(boxed).toEqual(jasmine.any(Regress.TestBoxed)); + }); + + it('sets fields correctly', function () { + const boxed = new Regress.TestBoxed(); + boxed.some_int8 = 42; + expect(boxed.some_int8).toEqual(42); + }); + + it('constructs from a static constructor', function () { + const boxed = Regress.TestBoxed.new_alternative_constructor1(42); + expect(boxed.some_int8).toEqual(42); + }); + + it('constructs from a static constructor with different args', function () { + const boxed = Regress.TestBoxed.new_alternative_constructor2(40, 2); + expect(boxed.some_int8).toEqual(42); + }); + + it('constructs from a static constructor with differently typed args', function () { + const boxed = Regress.TestBoxed.new_alternative_constructor3('42'); + expect(boxed.some_int8).toEqual(42); + }); + + it('constructs from a another object of the same type', function () { + const boxed = new Regress.TestBoxed({some_int8: 42}); + const copy = new Regress.TestBoxed(boxed); + expect(copy.some_int8).toEqual(42); + expect(copy.equals(boxed)).toBeTruthy(); + }); + + it('ensures methods are named correctly', function () { + const boxed = new Regress.TestBoxed(); + expect(boxed.s_not_a_method).not.toBeDefined(); + expect(boxed.not_a_method).not.toBeDefined(); + expect(() => Regress.test_boxeds_not_a_method(boxed)).not.toThrow(); + }); + + it('ensures static methods are named correctly', function () { + expect(Regress.TestBoxed.s_not_a_static).not.toBeDefined(); + expect(Regress.TestBoxed.not_a_static).not.toBeDefined(); + expect(Regress.test_boxeds_not_a_static).not.toThrow(); + }); + }); + + describe('Simple', function () { + it('sets fields correctly', function () { + const boxed = new Regress.TestBoxedB(); + boxed.some_int8 = 7; + boxed.some_long = 5; + expect(boxed.some_int8).toEqual(7); + expect(boxed.some_long).toEqual(5); + }); + + it('constructs from a static constructor', function () { + const boxed = Regress.TestBoxedB.new(7, 5); + expect(boxed.some_int8).toEqual(7); + expect(boxed.some_long).toEqual(5); + }); + + it('constructs from another object of the same type', function () { + const boxed = Regress.TestBoxedB.new(7, 5); + const copy = new Regress.TestBoxedB(boxed); + expect(copy.some_int8).toEqual(7); + expect(copy.some_long).toEqual(5); + }); + + // Regress.TestBoxedB has a constructor that takes multiple arguments, + // but since it is directly allocatable, we keep the old style of + // passing an hash of fields. The two real world structs that have this + // behavior are Clutter.Color and Clutter.ActorBox. + it('constructs in backwards compatibility mode', function () { + const boxed = new Regress.TestBoxedB({some_int8: 7, some_long: 5}); + expect(boxed.some_int8).toEqual(7); + expect(boxed.some_long).toEqual(5); + }); + }); + + describe('Refcounted', function () { + it('constructs from a default constructor', function () { + const boxed = new Regress.TestBoxedC(); + expect(boxed.another_thing).toEqual(42); + }); + + it('constructs from another object of the same type', function () { + const boxed = new Regress.TestBoxedC({another_thing: 43}); + const copy = new Regress.TestBoxedC(boxed); + expect(copy.another_thing).toEqual(43); + }); + }); + + describe('Private', function () { + it('constructs using a custom constructor', function () { + const boxed = new Regress.TestBoxedD('abcd', 8); + expect(boxed.get_magic()).toEqual(12); + }); + + it('constructs from another object of the same type', function () { + const boxed = new Regress.TestBoxedD('abcd', 8); + const copy = new Regress.TestBoxedD(boxed); + expect(copy.get_magic()).toEqual(12); + }); + + it('does not construct with a default constructor', function () { + expect(() => new Regress.TestBoxedD()).toThrow(); + }); + }); + }); + + describe('wrong type for GBoxed', function () { + let simpleBoxed, wrongObject, wrongBoxed; + beforeEach(function () { + simpleBoxed = new Regress.TestSimpleBoxedA(); + wrongObject = new Gio.SimpleAction(); + wrongBoxed = new GLib.KeyFile(); + }); + + // simpleBoxed.equals expects a Everything.TestSimpleBoxedA + it('function does not accept a GObject of the wrong type', function () { + expect(() => simpleBoxed.equals(wrongObject)).toThrow(); + }); + + it('function does not accept a GBoxed of the wrong type', function () { + expect(() => simpleBoxed.equals(wrongBoxed)).toThrow(); + }); + + it('function does accept a GBoxed of the correct type', function () { + expect(simpleBoxed.equals(simpleBoxed)).toBeTruthy(); + }); + + it('method cannot be called on a GObject', function () { + expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(wrongObject)) + .toThrow(); + }); + + it('method cannot be called on a GBoxed of the wrong type', function () { + expect(() => Regress.TestSimpleBoxedA.protoype.copy.call(wrongBoxed)) + .toThrow(); + }); + + it('method can be called on correct GBoxed type', function () { + expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(simpleBoxed)) + .not.toThrow(); + }); + }); + + describe('Introspected GObject', function () { + let o; + beforeEach(function () { + o = new Regress.TestObj({ + // These properties have backing public fields with different names + int: 42, + float: 3.1416, + double: 2.71828, + }); + }); + + it('can access fields with simple types', function () { + // Compare the values gotten through the GObject property getters to the + // values of the backing fields + expect(o.some_int8).toEqual(o.int); + expect(o.some_float).toEqual(o.float); + expect(o.some_double).toEqual(o.double); + }); + + it('cannot access fields with complex types (GI limitation)', function () { + expect(() => o.parent_instance).toThrow(); + expect(() => o.function_ptr).toThrow(); + }); + + it('throws when setting a read-only field', function () { + expect(() => (o.some_int8 = 41)).toThrow(); + }); + + it('has normal Object methods', function () { + o.ownprop = 'foo'; + // eslint-disable-next-line no-prototype-builtins + expect(o.hasOwnProperty('ownprop')).toBeTruthy(); + }); + + // it('sets write-only properties', function () { + // expect(o.int).not.toEqual(0); + // o.write_only = true; + // expect(o.int).toEqual(0); + // }); + + // it('gives undefined for write-only properties', function () { + // expect(o.write_only).not.toBeDefined(); + // }); + + it('constructs from constructors annotated with (constructor)', function () { + expect(Regress.TestObj.new(o)).toEqual(jasmine.any(Regress.TestObj)); + expect(Regress.TestObj.constructor()).toEqual(jasmine.any(Regress.TestObj)); + }); + + it('static methods', function () { + const v = Regress.TestObj.new_from_file('/enoent'); + expect(v).toEqual(jasmine.any(Regress.TestObj)); + }); + + describe('Object-valued GProperty', function () { + let o1, t1, t2; + beforeEach(function () { + o1 = new GObject.Object(); + t1 = new Regress.TestObj({bare: o1}); + t2 = new Regress.TestSubObj(); + t2.bare = o1; + }); + + it('marshals correctly in the getter', function () { + expect(t1.bare).toBe(o1); + }); + + it('marshals correctly when inherited', function () { + expect(t2.bare).toBe(o1); + }); + + it('marshals into setter function', function () { + const o2 = new GObject.Object(); + t2.set_bare(o2); + expect(t2.bare).toBe(o2); + }); + + it('marshals null', function () { + t2.unset_bare(); + expect(t2.bare).toBeNull(); + }); + }); + + describe('Signal connection', function () { + it('calls correct handlers with correct arguments', function () { + const handler = jasmine.createSpy('handler'); + const handlerId = o.connect('test', handler); + handler.and.callFake(() => o.disconnect(handlerId)); + + o.emit('test'); + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(o); + + handler.calls.reset(); + o.emit('test'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('throws errors for invalid signals', function () { + expect(() => o.connect('invalid-signal', () => {})).toThrow(); + expect(() => o.emit('invalid-signal')).toThrow(); + }); + + it('signal handler with static scope arg gets arg passed by reference', function () { + const b = new Regress.TestSimpleBoxedA({ + some_int: 42, + some_int8: 43, + some_double: 42.5, + some_enum: Regress.TestEnum.VALUE3, + }); + o.connect('test-with-static-scope-arg', (signalObject, signalArg) => { + signalArg.some_int = 44; + }); + o.emit('test-with-static-scope-arg', b); + expect(b.some_int).toEqual(44); + }); + + it('signal with object gets correct arguments', function (done) { + o.connect('sig-with-obj', (self, objectParam) => { + expect(objectParam.int).toEqual(3); + done(); + }); + o.emit_sig_with_obj(); + }); + + // See testCairo.js for a test of + // Regress.TestObj::sig-with-foreign-struct. + + xit('signal with int64 gets correct value', function (done) { + o.connect('sig-with-int64-prop', (self, number) => { + expect(number).toEqual(GLib.MAXINT64); + done(); + return GLib.MAXINT64; + }); + o.emit_sig_with_int64(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/271'); + + xit('signal with uint64 gets correct value', function (done) { + o.connect('sig-with-uint64-prop', (self, number) => { + expect(number).toEqual(GLib.MAXUINT64); + done(); + return GLib.MAXUINT64; + }); + o.emit_sig_with_uint64(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/271'); + + it('signal with array len parameter is not passed correct array and no length arg', function (done) { + o.connect('sig-with-array-len-prop', (signalObj, signalArray, shouldBeUndefined) => { + expect(shouldBeUndefined).not.toBeDefined(); + expect(signalArray).toEqual([0, 1, 2, 3, 4]); + done(); + }); + o.emit_sig_with_array_len_prop(); + }); + + xit('can pass parameter to signal with array len parameter via emit', function (done) { + o.connect('sig-with-array-len-prop', (signalObj, signalArray) => { + expect(signalArray).toEqual([0, 1, 2, 3, 4]); + done(); + }); + o.emit('sig-with-array-len-prop', [0, 1, 2, 3, 4]); + }).pend('Not yet implemented'); + + xit('can pass null to signal with array len parameter', function () { + const handler = jasmine.createSpy('handler'); + o.connect('sig-with-array-len-prop', handler); + o.emit('sig-with-array-len-prop', null); + expect(handler).toHaveBeenCalledWith([jasmine.any(Object), null]); + }).pend('Not yet implemented'); + + xit('signal with int in-out parameter', function () { + const handler = jasmine.createSpy('handler').and.callFake(() => 43); + o.connect('sig-with-inout-int', handler); + o.emit_sig_with_inout_int(); + expect(handler.toHaveBeenCalledWith([jasmine.any(Object), 42])); + }).pend('Not yet implemented'); + + // it('GError signal with GError set', function (done) { + // o.connect('sig-with-gerror', (obj, e) => { + // expect(e).toEqual(jasmine.any(Gio.IOErrorEnum)); + // expect(e.domain).toEqual(Gio.io_error_quark()); + // expect(e.code).toEqual(Gio.IOErrorEnum.FAILED); + // done(); + // }); + // o.emit_sig_with_error(); + // }); + + // it('GError signal with no GError set', function (done) { + // o.connect('sig-with-gerror', (obj, e) => { + // expect(e).toBeNull(); + // done(); + // }); + // o.emit_sig_with_null_error(); + // }); + }); + + it('can call an instance method', function () { + expect(o.instance_method()).toEqual(-1); + }); + + it('can call a transfer-full instance method', function () { + expect(() => o.instance_method_full()).not.toThrow(); + }); + + it('can call a static method', function () { + expect(Regress.TestObj.static_method(5)).toEqual(5); + }); + + it('can call a method annotated with (method)', function () { + expect(() => o.forced_method()).not.toThrow(); + }); + + describe('Object torture signature', function () { + it('0', function () { + const [y, z, q] = o.torture_signature_0(42, 'foo', 7); + expect(Math.floor(y)).toEqual(42); + expect(z).toEqual(84); + expect(q).toEqual(10); + }); + + it('1 fail', function () { + expect(() => o.torture_signature_1(42, 'foo', 7)).toThrow(); + }); + + it('1 success', function () { + const [, y, z, q] = o.torture_signature_1(11, 'barbaz', 8); + expect(Math.floor(y)).toEqual(11); + expect(z).toEqual(22); + expect(q).toEqual(14); + }); + }); + + describe('Introspected function length', function () { + it('skips over instance parameters of methods', function () { + expect(o.set_bare.length).toEqual(1); + }); + + it('skips over out and GError parameters', function () { + expect(o.torture_signature_1.length).toEqual(3); + }); + + it('does not skip over inout parameters', function () { + expect(o.skip_return_val.length).toEqual(5); + }); + + xit('skips over return value annotated with skip', function () { + const [b, d, sum] = o.skip_return_val(1, 2, 3, 4, 5); + expect(b).toEqual(2); + expect(d).toEqual(4); + expect(sum).toEqual(54); + + const retval = o.skip_return_val_no_out(1); + expect(retval).not.toBeDefined(); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/59'); + + xit('skips over parameters annotated with skip', function () { + expect(o.skip_param.length).toEqual(4); + + const [success, b, d, sum] = o.skip_param(1, 2, 3, 4); + expect(success).toBeTruthy(); + expect(b).toEqual(2); + expect(d).toEqual(3); + expect(sum).toEqual(43); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/59'); + + xit('skips over out parameters annotated with skip', function () { + const [success, d, sum] = o.skip_out_param(1, 2, 3, 4, 5); + expect(success).toBeTruthy(); + expect(d).toEqual(4); + expect(sum).toEqual(54); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/59'); + + xit('skips over inout parameters annotated with skip', function () { + expect(o.skip_inout_param.length).toEqual(4); + + const [success, b, sum] = o.skip_inout_param(1, 2, 3, 4); + expect(success).toBeTruthy(); + expect(b).toEqual(2); + expect(sum).toEqual(43); + }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/59'); + + it('gives number of arguments for static methods', function () { + expect(Regress.TestObj.new_from_file.length).toEqual(1); + }); + + it('skips over destroy-notify and user-data parameters', function () { + expect(Regress.TestObj.new_callback.length).toEqual(1); + }); + }); + + it('virtual function', function () { + expect(o.do_matrix('meaningless string')).toEqual(42); + }); + + describe('wrong type for GObject', function () { + let wrongObject, wrongBoxed, subclassObject; + beforeEach(function () { + wrongObject = new Gio.SimpleAction(); + wrongBoxed = new GLib.KeyFile(); + subclassObject = new Regress.TestSubObj(); + }); + + // Regress.func_obj_null_in expects a Regress.TestObj + it('function does not accept a GObject of the wrong type', function () { + expect(() => Regress.func_obj_null_in(wrongObject)).toThrow(); + }); + + it('function does not accept a GBoxed instead of GObject', function () { + expect(() => Regress.func_obj_null_in(wrongBoxed)).toThrow(); + }); + + it('function does not accept returned GObject of the wrong type', function () { + const wrongReturnedObject = Gio.File.new_for_path('/'); + expect(() => Regress.func_obj_null_in(wrongReturnedObject)).toThrow(); + }); + + it('function accepts GObject of subclass of expected type', function () { + expect(() => Regress.func_obj_null_in(subclassObject)).not.toThrow(); + }); + + it('method cannot be called on a GObject of the wrong type', function () { + expect(() => Regress.TestObj.prototype.instance_method.call(wrongObject)) + .toThrow(); + }); + + it('method cannot be called on a GBoxed', function () { + expect(() => Regress.TestObj.prototype.instance_method.call(wrongBoxed)) + .toThrow(); + }); + + it('method can be called on a GObject of subclass of expected type', function () { + expect(() => Regress.TestObj.prototype.instance_method.call(subclassObject)) + .not.toThrow(); + }); + }); + + it('marshals a null object in', function () { + expect(() => Regress.func_obj_null_in(null)).not.toThrow(); + expect(() => Regress.func_obj_nullable_in(null)).not.toThrow(); + }); + + it('marshals a null object out', function () { + expect(Regress.TestObj.null_out()).toBeNull(); + }); + + it('marshals a gpointer with a type annotation in', function () { + const o2 = new GObject.Object(); + expect(() => o.not_nullable_typed_gpointer_in(o2)).not.toThrow(); + }); + + it('marshals a gpointer with an element-type annotation in', function () { + expect(() => o.not_nullable_element_typed_gpointer_in([1, 2])).not.toThrow(); + }); + + // This test is not meant to be normative; a GObject behaving like this is + // doing something unsupported. However, we have been handling this so far + // in a certain way, and we don't want to break user code because of badly + // behaved libraries. This test ensures that any change to the behaviour + // must be intentional. + it('resolves properties when they are shadowed by methods', function () { + expect(o.name_conflict).toEqual(42); + expect(o.name_conflict).not.toEqual(jasmine.any(Function)); + }); + }); + + it('marshals a fixed-size array of objects out', function () { + expect(Regress.test_array_fixed_out_objects()).toEqual([ + jasmine.any(Regress.TestObj), + jasmine.any(Regress.TestObj), + ]); + }); + + describe('Inherited GObject', function () { + let subobj; + beforeEach(function () { + subobj = new Regress.TestSubObj({ + int: 42, + float: Math.PI, + double: Math.E, + }); + }); + + it('can read fields from a parent class', function () { + // see "can access fields with simple types" above + expect(subobj.some_int8).toEqual(subobj.int); + expect(subobj.some_float).toEqual(subobj.float); + expect(subobj.some_double).toEqual(subobj.double); + }); + + it('can be constructed from a static constructor', function () { + expect(Regress.TestSubObj.new).not.toThrow(); + }); + + it('can call an instance method that overrides the parent class', function () { + expect(subobj.instance_method()).toEqual(0); + }); + }); + + // describe('Overridden properties on interfaces', function () { + // it('set and get properly', function () { + // const o = new Regress.TestSubObj(); + // o.number = 4; + // expect(o.number).toEqual(4); + // }); + + // it('default properly', function () { + // const o = new Regress.TestSubObj(); + // expect(o.number).toBeDefined(); + // expect(o.number).toEqual(0); + // }); + + // it('construct properly', function () { + // const o = new Regress.TestSubObj({number: 4}); + // expect(o.number).toEqual(4); + // }); + // }); + + describe('Fundamental type', function () { + it('constructs a subtype of a fundamental type', function () { + expect(() => new Regress.TestFundamentalSubObject('plop')).not.toThrow(); + }); + + it('constructs a subtype of a hidden (no introspection data) fundamental type', function () { + expect(() => Regress.test_create_fundamental_hidden_class_instance()).not.toThrow(); + }); + }); + + it('callbacks', function () { + const callback = jasmine.createSpy('callback').and.returnValue(42); + expect(Regress.test_callback(callback)).toEqual(42); + }); + + it('null / undefined callback', function () { + expect(Regress.test_callback(null)).toEqual(0); + expect(() => Regress.test_callback(undefined)).toThrow(); + }); + + it('callback called more than once', function () { + const callback = jasmine.createSpy('callback').and.returnValue(21); + expect(Regress.test_multi_callback(callback)).toEqual(42); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('null callback called more than once', function () { + expect(Regress.test_multi_callback(null)).toEqual(0); + }); + + it('array callbacks', function () { + const callback = jasmine.createSpy('callback').and.returnValue(7); + expect(Regress.test_array_callback(callback)).toEqual(14); + expect(callback).toHaveBeenCalledWith([-1, 0, 1, 2], ['one', 'two', 'three']); + }); + + it('null array callback', function () { + expect(() => Regress.test_array_callback(null)).toThrow(); + }); + + xit('callback with inout array', function () { + const callback = jasmine.createSpy('callback').and.callFake(arr => arr.slice(1)); + expect(Regress.test_array_inout_callback(callback)).toEqual(3); + expect(callback).toHaveBeenCalledWith([-2, -1, 0, 1, 2], [-1, 0, 1, 2]); + }); // assertion failed, "Use gjs_value_from_explicit_array() for arrays with length param"" + + ['simple', 'noptr'].forEach(type => { + it(`${type} callback`, function () { + const callback = jasmine.createSpy('callback'); + Regress[`test_${type}_callback`](callback); + expect(callback).toHaveBeenCalled(); + }); + + it('null simple callback', function () { + expect(() => Regress[`test_${type}_callback`](null)).not.toThrow(); + }); + }); + + it('callback with user data', function () { + const callback = jasmine.createSpy('callback').and.returnValue(7); + expect(Regress.test_callback_user_data(callback)).toEqual(7); + expect(callback).toHaveBeenCalled(); + }); + + it('callback with transfer-full return value', function () { + const callback = jasmine.createSpy('callback') + .and.returnValue(Regress.TestObj.new_from_file('/enoent')); + Regress.test_callback_return_full(callback); + expect(callback).toHaveBeenCalled(); + }); + + it('callback with destroy-notify', function () { + const callback1 = jasmine.createSpy('callback').and.returnValue(42); + const callback2 = jasmine.createSpy('callback').and.returnValue(58); + expect(Regress.test_callback_destroy_notify(callback1)).toEqual(42); + expect(callback1).toHaveBeenCalledTimes(1); + expect(Regress.test_callback_destroy_notify(callback2)).toEqual(58); + expect(callback2).toHaveBeenCalledTimes(1); + expect(Regress.test_callback_thaw_notifications()).toEqual(100); + expect(callback1).toHaveBeenCalledTimes(2); + expect(callback2).toHaveBeenCalledTimes(2); + }); + + xit('callback with destroy-notify and no user data', function () { + const callback1 = jasmine.createSpy('callback').and.returnValue(42); + const callback2 = jasmine.createSpy('callback').and.returnValue(58); + expect(Regress.test_callback_destroy_notify_no_user_data(callback1)).toEqual(42); + expect(callback1).toHaveBeenCalledTimes(1); + expect(Regress.test_callback_destroy_notify_no_user_data(callback2)).toEqual(58); + expect(callback2).toHaveBeenCalledTimes(1); + expect(Regress.test_callback_thaw_notifications()).toEqual(100); + expect(callback1).toHaveBeenCalledTimes(2); + expect(callback2).toHaveBeenCalledTimes(2); + }).pend('Callback with destroy-notify and no user data not currently supported'); + + it('async callback', function () { + Regress.test_callback_async(() => 44); + expect(Regress.test_callback_thaw_async()).toEqual(44); + }); + + it('Gio.AsyncReadyCallback', function (done) { + Regress.test_async_ready_callback((obj, res) => { + expect(obj).toBeNull(); + expect(res).toEqual(jasmine.any(Gio.SimpleAsyncResult)); + done(); + }); + }); + + it('instance method taking a callback', function () { + const o = new Regress.TestObj(); + const callback = jasmine.createSpy('callback'); + o.instance_method_callback(callback); + expect(callback).toHaveBeenCalled(); + }); + + it('constructor taking a callback', function () { + const callback = jasmine.createSpy('callback').and.returnValue(42); + void Regress.TestObj.new_callback(callback); + expect(callback).toHaveBeenCalled(); + expect(Regress.test_callback_thaw_notifications()).toEqual(42); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('hash table passed to callback', function () { + const hashtable = { + a: 1, + b: 2, + c: 3, + }; + const callback = jasmine.createSpy('callback'); + Regress.test_hash_table_callback(hashtable, callback); + expect(callback).toHaveBeenCalledWith(hashtable); + }); + + it('GError callback', function (done) { + Regress.test_gerror_callback(e => { + expect(e).toEqual(jasmine.any(Gio.IOErrorEnum)); + expect(e.domain).toEqual(Gio.io_error_quark()); + expect(e.code).toEqual(Gio.IOErrorEnum.NOT_SUPPORTED); + done(); + }); + }); + + it('null GError callback', function () { + const callback = jasmine.createSpy('callback'); + Regress.test_null_gerror_callback(callback); + expect(callback).toHaveBeenCalledWith(null); + }); + + it('owned GError callback', function (done) { + Regress.test_owned_gerror_callback(e => { + expect(e).toEqual(jasmine.any(Gio.IOErrorEnum)); + expect(e.domain).toEqual(Gio.io_error_quark()); + expect(e.code).toEqual(Gio.IOErrorEnum.PERMISSION_DENIED); + done(); + }); + }); + + // describe('Introspected interface', function () { + // const Implementor = GObject.registerClass({ + // Implements: [Regress.TestInterface], + // Properties: { + // number: GObject.ParamSpec.override('number', Regress.TestInterface), + // }, + // }, class Implementor extends GObject.Object { + // get number() { + // return 5; + // } + // }); + + // it('correctly emits interface signals', function () { + // const obj = new Implementor(); + // const handler = jasmine.createSpy('handler').and.callFake(() => {}); + // obj.connect('interface-signal', handler); + // obj.emit_signal(); + // expect(handler).toHaveBeenCalled(); + // }); + // }); + + describe('GObject with nonstandard prefix', function () { + let o; + beforeEach(function () { + o = new Regress.TestWi8021x(); + }); + + it('sets and gets properties', function () { + expect(o.testbool).toBeTruthy(); + o.testbool = false; + expect(o.testbool).toBeFalsy(); + }); + + it('constructs via a static constructor', function () { + expect(Regress.TestWi8021x.new()).toEqual(jasmine.any(Regress.TestWi8021x)); + }); + + it('calls methods', function () { + expect(o.get_testbool()).toBeTruthy(); + o.set_testbool(false); + expect(o.get_testbool()).toBeFalsy(); + }); + + it('calls a static method', function () { + expect(Regress.TestWi8021x.static_method(21)).toEqual(42); + }); + }); + + describe('GObject.InitiallyUnowned', function () { + it('constructs', function () { + expect(new Regress.TestFloating()).toEqual(jasmine.any(Regress.TestFloating)); + }); + + it('constructs via a static constructor', function () { + expect(Regress.TestFloating.new()).toEqual(jasmine.any(Regress.TestFloating)); + }); + }); + + it('torture signature 0', function () { + const [y, z, q] = Regress.test_torture_signature_0(42, 'foo', 7); + expect(Math.floor(y)).toEqual(42); + expect(z).toEqual(84); + expect(q).toEqual(10); + }); + + it('torture signature 1 fail', function () { + expect(() => Regress.test_torture_signature_1(42, 'foo', 7)).toThrow(); + }); + + it('torture signature 1 success', function () { + const [, y, z, q] = Regress.test_torture_signature_1(11, 'barbaz', 8); + expect(Math.floor(y)).toEqual(11); + expect(z).toEqual(22); + expect(q).toEqual(14); + }); + + it('torture signature 2', function () { + const [y, z, q] = Regress.test_torture_signature_2(42, () => 0, 'foo', 7); + expect(Math.floor(y)).toEqual(42); + expect(z).toEqual(84); + expect(q).toEqual(10); + }); + + describe('GValue boxing and unboxing', function () { + it('date in', function () { + const date = Regress.test_date_in_gvalue(); + expect(date.get_year()).toEqual(1984); + expect(date.get_month()).toEqual(GLib.DateMonth.DECEMBER); + expect(date.get_day()).toEqual(5); + }); + + it('strv in', function () { + expect(Regress.test_strv_in_gvalue()).toEqual(['one', 'two', 'three']); + }); + + it('correctly converts a NULL strv in a GValue to an empty array', function () { + expect(Regress.test_null_strv_in_gvalue()).toEqual([]); + }); + }); + + it("code coverage for documentation tests that don't do anything", function () { + expect(() => { + Regress.test_multiline_doc_comments(); + Regress.test_nested_parameter(5); + Regress.test_versioning(); + }).not.toThrow(); + }); + + it('marshals an aliased type', function () { + // GLib.PtrArray is not introspectable, so neither is an alias of it + // Regress.introspectable_via_alias(new GLib.PtrArray()); + expect(Regress.aliased_caller_alloc()).toEqual(jasmine.any(Regress.TestBoxed)); + }); + + it('deals with a fixed-size array in a struct', function () { + const struct = new Regress.TestStructFixedArray(); + struct.frob(); + expect(struct.just_int).toEqual(7); + expect(struct.array).toEqual([42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); + }); + + it('marshals a fixed-size int array as a gpointer', function () { + expect(() => Regress.has_parameter_named_attrs(0, Array(32).fill(42))).not.toThrow(); + }); + + it('deals with a fixed-size and also zero-terminated array in a struct', function () { + const x = new Regress.LikeXklConfigItem(); + x.set_name('foo'); + expect(x.name).toEqual([...'foo'].map(c => c.codePointAt()).concat(Array(29).fill(0))); + x.set_name('*'.repeat(33)); + expect(x.name).toEqual(Array(31).fill('*'.codePointAt()).concat([0])); + }); + + it('marshals a transfer-floating GLib.Variant', function () { + expect(Regress.get_variant().unpack()).toEqual(42); + }); + + // describe('Flat array of structs', function () { + // it('out parameter with transfer none', function () { + // const expected = [111, 222, 333].map(some_int => + // jasmine.objectContaining({some_int})); + // expect(Regress.test_array_struct_out_none()).toEqual(expected); + // }); + + // it('out parameter with transfer container', function () { + // const expected = [11, 13, 17, 19, 23].map(some_int => + // jasmine.objectContaining({some_int})); + // expect(Regress.test_array_struct_out_container()).toEqual(expected); + // }); + + // it('out parameter with transfer full', function () { + // const expected = [2, 3, 5, 7].map(some_int => + // jasmine.objectContaining({some_int})); + // expect(Regress.test_array_struct_out_full_fixed()).toEqual(expected); + // }); + + // xit('caller-allocated out parameter', function () { + // // With caller-allocated array in, there's no way to supply the + // // length. This happens in GLib.MainContext.query() + // expect(Regress.test_array_struct_out_caller_alloc()).toEqual([]); + // }).pend('Not supported'); + + // it('transfer-full in parameter', function () { + // const array = [201, 202].map(some_int => + // new Regress.TestStructA({some_int})); + // expect(() => Regress.test_array_struct_in_full(array)).not.toThrow(); + // }); + + // it('transfer-none in parameter', function () { + // const array = [301, 302, 303].map(some_int => + // new Regress.TestStructA({some_int})); + // expect(() => Regress.test_array_struct_in_none(array)).not.toThrow(); + // }); + // }); +}); diff --git a/installed-tests/js/testSignals.js b/installed-tests/js/testSignals.js index c87a20d..1c55f77 100644 --- a/installed-tests/js/testSignals.js +++ b/installed-tests/js/testSignals.js @@ -1,3 +1,5 @@ +/* eslint-disable no-restricted-properties */ + const GLib = imports.gi.GLib; const Lang = imports.lang; const Signals = imports.signals; @@ -5,7 +7,7 @@ const Signals = imports.signals; const Foo = new Lang.Class({ Name: 'Foo', Implements: [Signals.WithSignals], - _init: function () {}, + _init() {}, }); describe('Legacy object with signals', function () { @@ -28,17 +30,17 @@ function testSignals(klass) { it('calls a signal handler when a signal is emitted', function () { foo.connect('bar', bar); - foo.emit('bar', "This is a", "This is b"); + foo.emit('bar', 'This is a', 'This is b'); expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b'); }); it('does not call a signal handler after the signal is disconnected', function () { let id = foo.connect('bar', bar); - foo.emit('bar', "This is a", "This is b"); + foo.emit('bar', 'This is a', 'This is b'); bar.calls.reset(); foo.disconnect(id); // this emission should do nothing - foo.emit('bar', "Another a", "Another b"); + foo.emit('bar', 'Another a', 'Another b'); expect(bar).not.toHaveBeenCalled(); }); @@ -92,6 +94,13 @@ function testSignals(klass) { expect(bonk).not.toHaveBeenCalled(); }); + it('determines if a signal is connected on a JS object', function () { + let id = foo.connect('bar', bar); + expect(foo.signalHandlerIsConnected(id)).toEqual(true); + foo.disconnect(id); + expect(foo.signalHandlerIsConnected(id)).toEqual(false); + }); + describe('with exception in signal handler', function () { let bar2; beforeEach(function () { @@ -100,7 +109,7 @@ function testSignals(klass) { foo.connect('bar', bar); foo.connect('bar', bar2); GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: Exception in callback for signal: *'); + 'JS ERROR: Exception in callback for signal: *'); foo.emit('bar'); }); @@ -111,7 +120,7 @@ function testSignals(klass) { it('does not disconnect the callback', function () { GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING, - 'JS ERROR: Exception in callback for signal: *'); + 'JS ERROR: Exception in callback for signal: *'); foo.emit('bar'); expect(bar).toHaveBeenCalledTimes(2); expect(bar2).toHaveBeenCalledTimes(2); diff --git a/installed-tests/js/testSystem.js b/installed-tests/js/testSystem.js index 35e342c..a4c098c 100644 --- a/installed-tests/js/testSystem.js +++ b/installed-tests/js/testSystem.js @@ -23,8 +23,28 @@ describe('System.refcount()', function () { }); }); +describe('System.addressOfGObject()', function () { + it('gives different results for different objects', function () { + let a = new GObject.Object({}); + let b = new GObject.Object({}); + expect(System.addressOfGObject(a)).toEqual(System.addressOfGObject(a)); + expect(System.addressOfGObject(a)).not.toEqual(System.addressOfGObject(b)); + }); + + it('throws for non GObject objects', function () { + expect(() => System.addressOfGObject({})) + .toThrowError(/Object 0x[a-f0-9]+ is not a GObject/); + }); +}); + describe('System.gc()', function () { it('does not crash the application', function () { expect(System.gc).not.toThrow(); }); }); + +describe('System.dumpHeap()', function () { + it('throws but does not crash when given a nonexistent path', function () { + expect(() => System.dumpHeap('/does/not/exist')).toThrow(); + }); +}); diff --git a/installed-tests/js/testTweener.js b/installed-tests/js/testTweener.js index 9e891ae..1f6efe5 100644 --- a/installed-tests/js/testTweener.js +++ b/installed-tests/js/testTweener.js @@ -6,10 +6,10 @@ function installFrameTicker() { let ticker = { FRAME_RATE: 50, - _init : function() { + _init() { }, - start : function() { + start() { this._currentTime = 0; this._timeoutID = setInterval(() => { @@ -18,7 +18,7 @@ function installFrameTicker() { }, Math.floor(1000 / this.FRAME_RATE)); }, - stop : function() { + stop() { if ('_timeoutID' in this) { clearInterval(this._timeoutID); delete this._timeoutID; @@ -27,9 +27,9 @@ function installFrameTicker() { this._currentTime = 0; }, - getTime : function() { + getTime() { return this._currentTime; - } + }, }; imports.signals.addSignalMethods(ticker); @@ -57,16 +57,16 @@ describe('Tweener', function () { it('runs a simple tween', function () { var objectA = { x: 0, - y: 0 + y: 0, }; var objectB = { x: 0, - y: 0 + y: 0, }; - Tweener.addTween(objectA, { x: 10, y: 10, time: 1, transition: "linear" }); - Tweener.addTween(objectB, { x: 10, y: 10, time: 1, delay: 0.5, transition: "linear" }); + Tweener.addTween(objectA, {x: 10, y: 10, time: 1, transition: 'linear'}); + Tweener.addTween(objectB, {x: 10, y: 10, time: 1, delay: 0.5, transition: 'linear'}); jasmine.clock().tick(1001); @@ -92,27 +92,27 @@ describe('Tweener', function () { it('can pause tweens', function () { var objectA = { - foo: 0 + foo: 0, }; var objectB = { - bar: 0 + bar: 0, }; var objectC = { - baaz: 0 + baaz: 0, }; - Tweener.addTween(objectA, { foo: 100, time: 0.1 }); - Tweener.addTween(objectC, { baaz: 100, time: 0.1 }); - Tweener.addTween(objectB, { bar: 100, time: 0.1 }); + Tweener.addTween(objectA, {foo: 100, time: 0.1}); + Tweener.addTween(objectC, {baaz: 100, time: 0.1}); + Tweener.addTween(objectB, {bar: 100, time: 0.1}); Tweener.pauseTweens(objectA); // This should do nothing expect(Tweener.pauseTweens(objectB, 'quux')).toBeFalsy(); /* Pause and resume should be equal to doing nothing */ - Tweener.pauseTweens(objectC, "baaz"); - Tweener.resumeTweens(objectC, "baaz"); + Tweener.pauseTweens(objectC, 'baaz'); + Tweener.resumeTweens(objectC, 'baaz'); jasmine.clock().tick(101); @@ -125,15 +125,15 @@ describe('Tweener', function () { var object = { foo: 0, bar: 0, - baaz: 0 + baaz: 0, }; - Tweener.addTween(object, { foo: 50, time: 0.1 }); - Tweener.addTween(object, { bar: 50, time: 0.1 }); - Tweener.addTween(object, { baaz: 50, time: 0.1}); + Tweener.addTween(object, {foo: 50, time: 0.1}); + Tweener.addTween(object, {bar: 50, time: 0.1}); + Tweener.addTween(object, {baaz: 50, time: 0.1}); /* The Tween on property foo should still be run after removing the other two */ - Tweener.removeTweens(object, "bar", "baaz"); + Tweener.removeTweens(object, 'bar', 'baaz'); jasmine.clock().tick(101); @@ -144,11 +144,11 @@ describe('Tweener', function () { it('overrides a tween with another one acting on the same object and property at the same time', function () { var objectA = { - foo: 0 + foo: 0, }; - Tweener.addTween(objectA, { foo: 100, time: 0.1 }); - Tweener.addTween(objectA, { foo: 0, time: 0.1 }); + Tweener.addTween(objectA, {foo: 100, time: 0.1}); + Tweener.addTween(objectA, {foo: 0, time: 0.1}); jasmine.clock().tick(101); @@ -157,14 +157,14 @@ describe('Tweener', function () { it('does not override a tween with another one acting not at the same time', function () { var objectB = { - bar: 0 + bar: 0, }; /* In this case both tweens should be executed, as they don't * act on the object at the same time (the second one has a * delay equal to the running time of the first one) */ - Tweener.addTween(objectB, { bar: 100, time: 0.1 }); - Tweener.addTween(objectB, { bar: 150, time: 0.1, delay: 0.1 }); + Tweener.addTween(objectB, {bar: 100, time: 0.1}); + Tweener.addTween(objectB, {bar: 150, time: 0.1, delay: 0.1}); jasmine.clock(0).tick(201); @@ -173,14 +173,14 @@ describe('Tweener', function () { it('can pause and resume all tweens', function () { var objectA = { - foo: 0 + foo: 0, }; var objectB = { - bar: 0 + bar: 0, }; - Tweener.addTween(objectA, { foo: 100, time: 0.1 }); - Tweener.addTween(objectB, { bar: 100, time: 0.1 }); + Tweener.addTween(objectA, {foo: 100, time: 0.1}); + Tweener.addTween(objectB, {bar: 100, time: 0.1}); Tweener.pauseAllTweens(); @@ -196,14 +196,14 @@ describe('Tweener', function () { it('can remove all tweens', function () { var objectA = { - foo: 0 + foo: 0, }; var objectB = { - bar: 0 + bar: 0, }; - Tweener.addTween(objectA, { foo: 100, time: 0.1 }); - Tweener.addTween(objectB, { bar: 100, time: 0.1 }); + Tweener.addTween(objectA, {foo: 100, time: 0.1}); + Tweener.addTween(objectB, {bar: 100, time: 0.1}); Tweener.removeAllTweens(); @@ -215,11 +215,13 @@ describe('Tweener', function () { it('runs a tween with a time of 0 immediately', function () { var object = { - foo: 100 + foo: 100, }; - Tweener.addTween(object, { foo: 50, time: 0, delay: 0 }); - Tweener.addTween(object, { foo: 200, time: 0.1, + Tweener.addTween(object, {foo: 50, time: 0, delay: 0}); + Tweener.addTween(object, { + foo: 200, + time: 0.1, onStart: () => { /* The immediate tween should set it to 50 before we run */ expect(object.foo).toEqual(50); @@ -233,11 +235,13 @@ describe('Tweener', function () { it('can call a callback a certain number of times', function () { var object = { - foo: 0 + foo: 0, }; Tweener.addCaller(object, { - onUpdate: () => { object.foo += 1; }, + onUpdate: () => { + object.foo += 1; + }, count: 10, time: 0.1, }); @@ -252,18 +256,18 @@ describe('Tweener', function () { foo: 0, bar: 0, baaz: 0, - quux: 0 + quux: 0, }; expect(Tweener.getTweenCount(object)).toEqual(0); - Tweener.addTween(object, { foo: 100, time: 0.1 }); + Tweener.addTween(object, {foo: 100, time: 0.1}); expect(Tweener.getTweenCount(object)).toEqual(1); - Tweener.addTween(object, { bar: 100, time: 0.1 }); + Tweener.addTween(object, {bar: 100, time: 0.1}); expect(Tweener.getTweenCount(object)).toEqual(2); - Tweener.addTween(object, { baaz: 100, time: 0.1 }); + Tweener.addTween(object, {baaz: 100, time: 0.1}); expect(Tweener.getTweenCount(object)).toEqual(3); - Tweener.addTween(object, { quux: 100, time: 0.1 }); + Tweener.addTween(object, {quux: 100, time: 0.1}); expect(Tweener.getTweenCount(object)).toEqual(4); Tweener.removeTweens(object, 'bar', 'baaz'); @@ -273,16 +277,20 @@ describe('Tweener', function () { it('can register special properties', function () { Tweener.registerSpecialProperty( 'negative_x', - function(obj) { return -obj.x; }, - function(obj, val) { obj.x = -val; } + function (obj) { + return -obj.x; + }, + function (obj, val) { + obj.x = -val; + } ); var objectA = { x: 0, - y: 0 + y: 0, }; - Tweener.addTween(objectA, { negative_x: 10, y: 10, time: 1, transition: "linear" }); + Tweener.addTween(objectA, {negative_x: 10, y: 10, time: 1, transition: 'linear'}); jasmine.clock().tick(1001); @@ -291,13 +299,14 @@ describe('Tweener', function () { }); it('can register special modifiers for properties', function () { - Tweener.registerSpecialPropertyModifier('discrete', - discrete_modifier, - discrete_get); - function discrete_modifier(props) { - return props.map(function (prop) { return { name: prop, parameters: null }; }); + Tweener.registerSpecialPropertyModifier('discrete', discreteModifier, + discreteGet); + function discreteModifier(props) { + return props.map(function (prop) { + return {name: prop, parameters: null}; + }); } - function discrete_get(begin, end, time, params) { + function discreteGet(begin, end, time) { return Math.floor(begin + time * (end - begin)); } @@ -305,16 +314,17 @@ describe('Tweener', function () { x: 0, y: 0, xFraction: false, - yFraction: false + yFraction: false, }; - Tweener.addTween(objectA, { x: 10, y: 10, time: 1, - discrete: ["x"], - transition: "linear", - onUpdate: function() { - if (objectA.x != Math.floor(objectA.x)) + Tweener.addTween(objectA, { + x: 10, y: 10, time: 1, + discrete: ['x'], + transition: 'linear', + onUpdate() { + if (objectA.x !== Math.floor(objectA.x)) objectA.xFraction = true; - if (objectA.y != Math.floor(objectA.y)) + if (objectA.y !== Math.floor(objectA.y)) objectA.yFraction = true; }, }); @@ -330,16 +340,18 @@ describe('Tweener', function () { it('can split properties into more than one special property', function () { Tweener.registerSpecialPropertySplitter( 'xnegy', - function(val) { return [ { name: "x", value: val }, - { name: "y", value: -val } ]; } + function (val) { + return [{name: 'x', value: val}, + {name: 'y', value: -val}]; + } ); var objectA = { x: 0, - y: 0 + y: 0, }; - Tweener.addTween(objectA, { xnegy: 10, time: 1, transition: "linear" }); + Tweener.addTween(objectA, {xnegy: 10, time: 1, transition: 'linear'}); jasmine.clock().tick(1001); @@ -352,15 +364,17 @@ describe('Tweener', function () { a: 0, b: 0, c: 0, - d: 0 + d: 0, }; - var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1, + var tweenA = { + a: 10, b: 10, c: 10, d: 10, time: 0.1, onStart: start, onOverwrite: overwrite, onComplete: complete, }; - var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1, + var tweenB = { + a: 20, b: 20, c: 20, d: 20, time: 0.1, onStart: start, onOverwrite: overwrite, onComplete: complete, @@ -381,10 +395,11 @@ describe('Tweener', function () { a: 0, b: 0, c: 0, - d: 0 + d: 0, }; - var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1, + var tweenA = { + a: 10, b: 10, c: 10, d: 10, time: 0.1, onStart: () => { start(); Tweener.addTween(object, tweenB); @@ -392,7 +407,8 @@ describe('Tweener', function () { onOverwrite: overwrite, onComplete: complete, }; - var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1, + var tweenB = { + a: 20, b: 20, c: 20, d: 20, time: 0.1, onStart: start, onOverwrite: overwrite, onComplete: complete, @@ -410,16 +426,16 @@ describe('Tweener', function () { it('stays within min and max values', function () { var objectA = { x: 0, - y: 0 + y: 0, }; var objectB = { x: 0, - y: 0 + y: 0, }; - Tweener.addTween(objectA, { x: 300, y: 300, time: 1, max: 255, transition: "linear" }); - Tweener.addTween(objectB, { x: -200, y: -200, time: 1, delay: 0.5, min: 0, transition: "linear" }); + Tweener.addTween(objectA, {x: 300, y: 300, time: 1, max: 255, transition: 'linear'}); + Tweener.addTween(objectB, {x: -200, y: -200, time: 1, delay: 0.5, min: 0, transition: 'linear'}); jasmine.clock().tick(1001); diff --git a/installed-tests/js/testWarnLib.js b/installed-tests/js/testWarnLib.js new file mode 100644 index 0000000..4097186 --- /dev/null +++ b/installed-tests/js/testWarnLib.js @@ -0,0 +1,38 @@ +// File with tests from the WarnLib-1.0.gir test suite from GI + +const {Gio, GObject, WarnLib} = imports.gi; + +describe('WarnLib', function () { + // Calling matches() on an unpaired error used to JSUnit.assert: + // https://bugzilla.gnome.org/show_bug.cgi?id=689482 + it('bug 689482', function () { + try { + WarnLib.throw_unpaired(); + fail(); + } catch (e) { + expect(e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)).toBeFalsy(); + } + }); + + const WhateverImpl = GObject.registerClass({ + Implements: [WarnLib.Whatever], + }, class WhateverImpl extends GObject.Object { + vfunc_do_moo(x) { + expect(x).toEqual(5); + this.mooCalled = true; + } + + vfunc_do_boo(x) { + expect(x).toEqual(6); + this.booCalled = true; + } + }); + + it('calls vfuncs with unnamed parameters', function () { + const o = new WhateverImpl(); + o.do_moo(5, null); + o.do_boo(6, null); + expect(o.mooCalled).toBeTruthy(); // spies don't work on vfuncs + expect(o.booCalled).toBeTruthy(); + }); +}); diff --git a/installed-tests/js/testself.js b/installed-tests/js/testself.js index 5899ab5..6f748a4 100644 --- a/installed-tests/js/testself.js +++ b/installed-tests/js/testself.js @@ -3,8 +3,8 @@ describe('Test harness internal consistency', function () { var someUndefined; var someNumber = 1; var someOtherNumber = 42; - var someString = "hello"; - var someOtherString = "world"; + var someString = 'hello'; + var someOtherString = 'world'; expect(true).toBeTruthy(); expect(false).toBeFalsy(); @@ -22,9 +22,17 @@ describe('Test harness internal consistency', function () { expect(0 / 0).toBeNaN(); expect(someNumber).not.toBeNaN(); - expect(() => { throw {}; }).toThrow(); + expect(() => { + throw new Error(); + }).toThrow(); expect(() => expect(true).toThrow()).toThrow(); expect(() => true).not.toThrow(); }); }); + +describe('SpiderMonkey features check', function () { + it('Intl API was compiled into SpiderMonkey', function () { + expect(Intl).toBeDefined(); + }); +}); diff --git a/installed-tests/meson.build b/installed-tests/meson.build new file mode 100644 index 0000000..04c7910 --- /dev/null +++ b/installed-tests/meson.build @@ -0,0 +1,93 @@ +### Installed tests ############################################################ + +installed_tests_execdir = get_option('prefix') / get_option('libexecdir') / 'installed-tests' / meson.project_name() +installed_tests_metadir = abs_datadir / 'installed-tests' / meson.project_name() + +# Simple shell script tests # + +simple_tests = [] + +# The test scripts need to be ported from shell scripts +# for clang-cl builds, which do not use BASH-style shells +if cxx.get_argument_syntax() != 'msvc' + simple_tests += [ + 'CommandLine', + 'Warnings', + ] +endif + +foreach test : simple_tests + test_file = files('scripts' / 'test@0@.sh'.format(test)) + + test(test, test_file, env: tests_environment, protocol: 'tap', + suite: 'Scripts') + + test_description_subst = { + 'name': 'test@0@.sh'.format(test), + 'installed_tests_execdir': installed_tests_execdir, + } + test_description = configure_file(configuration: test_description_subst, + input: 'script.test.in', output: 'test@0@.sh.test'.format(test), + install: get_option('installed_tests'), + install_dir: installed_tests_metadir) + + if get_option('installed_tests') + install_data(test_file, install_dir: installed_tests_execdir / 'scripts') + endif +endforeach + +# Jasmine tests # + +subdir('js') + +# Debugger script tests # + +debugger_tests = [ + 'backtrace', + 'breakpoint', + 'continue', + 'delete', + 'detach', + 'down-up', + 'finish', + 'frame', + 'keys', + 'next', + 'print', + 'quit', + 'return', + 'set', + 'step', + 'throw', + 'until', +] + +debugger_test_driver = find_program(files('debugger-test.sh')) +if get_option('installed_tests') + install_data('debugger-test.sh', install_dir: installed_tests_execdir) +endif + +foreach test : debugger_tests + test_file = files('debugger' / '@0@.debugger'.format(test)) + + test('@0@ command'.format(test), debugger_test_driver, + args: test_file, env: tests_environment, protocol: 'tap', + suite: 'Debugger') + + test_description_subst = { + 'name': '@0@.debugger'.format(test), + 'installed_tests_execdir': installed_tests_execdir, + } + test_description = configure_file(configuration: test_description_subst, + input: 'debugger.test.in', + output: '@0@.test'.format(test), + install: get_option('installed_tests'), + install_dir: installed_tests_metadir) + + if get_option('installed_tests') + install_data(test_file, install_dir: installed_tests_execdir / 'debugger') + install_data('debugger' / '@0@.debugger.js'.format(test), + 'debugger' / '@0@.debugger.output'.format(test), + install_dir: installed_tests_execdir / 'debugger') + endif +endforeach diff --git a/installed-tests/minijasmine.cpp b/installed-tests/minijasmine.cpp index dfeefc7..974d5ab 100644 --- a/installed-tests/minijasmine.cpp +++ b/installed-tests/minijasmine.cpp @@ -21,23 +21,17 @@ * IN THE SOFTWARE. */ -#include "config.h" +#include // for setlocale, LC_ALL +#include // for exit -#include -#include - -#include -#include #include +#include +#include +#include -#include "cjs/gjs.h" -#include "cjs/mem.h" +#include -G_GNUC_NORETURN -static void -bail_out(GjsContext *gjs_context, - const char *msg) -{ +[[noreturn]] static void bail_out(GjsContext* gjs_context, const char* msg) { g_object_unref(gjs_context); g_print("Bail out! %s\n", msg); exit(1); @@ -51,8 +45,6 @@ main(int argc, char **argv) /* The fact that this isn't the default is kind of lame... */ g_setenv("GJS_DEBUG_OUTPUT", "stderr", false); - /* Jasmine library has some code style nits that trip this */ - g_setenv("GJS_DISABLE_EXTRA_WARNINGS", "1", false); setlocale(LC_ALL, ""); @@ -60,13 +52,16 @@ main(int argc, char **argv) g_irepository_prepend_search_path(g_getenv("TOP_BUILDDIR")); } else { g_irepository_prepend_search_path(INSTTESTDIR); - g_irepository_prepend_library_path(PKGLIBDIR); + g_irepository_prepend_library_path(INSTTESTDIR); } const char *coverage_prefix = g_getenv("GJS_UNIT_COVERAGE_PREFIX"); const char *coverage_output_path = g_getenv("GJS_UNIT_COVERAGE_OUTPUT"); const char *search_path[] = { "resource:///org/gjs/jsunit", NULL }; + if (coverage_prefix) + gjs_coverage_enable(); + GjsContext *cx = gjs_context_new_with_search_path((char **)search_path); GjsCoverage *coverage = NULL; diff --git a/installed-tests/minijasmine.test.in b/installed-tests/minijasmine.test.in index 0069da3..f3b8d5b 100644 --- a/installed-tests/minijasmine.test.in +++ b/installed-tests/minijasmine.test.in @@ -1,4 +1,4 @@ [Test] Type=session -Exec=@pkglibexecdir@/installed-tests/minijasmine @pkglibexecdir@/installed-tests/js/@name@ +Exec=@installed_tests_execdir@/minijasmine @installed_tests_execdir@/js/@name@ Output=TAP diff --git a/installed-tests/script.test.in b/installed-tests/script.test.in index 6b0d403..03c1b05 100644 --- a/installed-tests/script.test.in +++ b/installed-tests/script.test.in @@ -1,4 +1,4 @@ [Test] Type=session -Exec=sh @pkglibexecdir@/installed-tests/scripts/@name@ +Exec=sh @installed_tests_execdir@/scripts/@name@ Output=TAP diff --git a/installed-tests/scripts/common.sh b/installed-tests/scripts/common.sh new file mode 100755 index 0000000..ed7993e --- /dev/null +++ b/installed-tests/scripts/common.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then + gjs="$TOP_BUILDDIR/cjs-console" +else + gjs="cjs-console" +fi + +# Avoid interference in the profiler tests from stray environment variable +unset GJS_ENABLE_PROFILER + +total=0 + +report () { + exit_code=$? + total=$((total + 1)) + if test $exit_code -eq 0; then + echo "ok $total - $1" + else + echo "not ok $total - $1 [EXIT CODE: $exit_code]" + fi +} + +report_timeout () { + exit_code=$? + total=$((total + 1)) + if test $exit_code -eq 0 -o $exit_code -eq 124; then + echo "ok $total - $1" + else + echo "not ok $total - $1 [EXIT CODE: $exit_code]" + fi +} + +report_xfail () { + exit_code=$? + total=$((total + 1)) + if test $exit_code -ne 0; then + echo "ok $total - $1" + else + echo "not ok $total - $1" + fi +} + +skip () { + total=$((total + 1)) + echo "ok $total - $1 # SKIP $2" +} diff --git a/installed-tests/scripts/testCommandLine.sh b/installed-tests/scripts/testCommandLine.sh index 51ef868..b52e0b4 100755 --- a/installed-tests/scripts/testCommandLine.sh +++ b/installed-tests/scripts/testCommandLine.sh @@ -1,14 +1,17 @@ #!/bin/sh if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then - gjs="$LOG_COMPILER $LOG_FLAGS $TOP_BUILDDIR/cjs-console" + gjs="$TOP_BUILDDIR/cjs-console" else - gjs="$LOG_COMPILER $LOG_FLAGS cjs-console" + gjs="cjs-console" fi # Avoid interference in the profiler tests from stray environment variable unset GJS_ENABLE_PROFILER +# Avoid interference in the warning tests from G_DEBUG=fatal-warnings/criticals +OLD_G_DEBUG="$G_DEBUG" + # This JS script should exit immediately with code 42. If that is not working, # then it will exit after 3 seconds as a fallback, with code 0. cat <exit.js @@ -68,8 +71,10 @@ report () { report_xfail () { exit_code=$? total=$((total + 1)) - if test $exit_code -ne 0; then - echo "ok $total - $1" + if test $exit_code -eq 23; then + echo "not ok $total - $1 (leaked memory)" + elif test $exit_code -ne 0; then + echo "ok $total - $1 (exit code $exit_code)" else echo "not ok $total - $1" fi @@ -80,6 +85,11 @@ skip () { echo "ok $total - $1 # SKIP $2" } +$gjs --invalid-option >/dev/null 2>/dev/null +report_xfail "Invalid option should exit with failure" +$gjs --invalid-option 2>&1 | grep -q invalid-option +report "Invalid option should print a relevant message" + # Test that System.exit() works in gjs-console $gjs -c 'imports.system.exit(0)' report "System.exit(0) should exit successfully" @@ -88,10 +98,16 @@ test $? -eq 42 report "System.exit(42) should exit with the correct exit code" # FIXME: should check -eq 42 specifically, but in debug mode we will be -# hitting an assertion -$gjs exit.js -test $? -ne 0 -report "System.exit() should still exit across an FFI boundary" +# hitting an assertion. For this reason, skip when running under valgrind +# since nothing will be freed. Also suppress LSan for the same reason. +echo "# VALGRIND = $VALGRIND" +if test -z $VALGRIND; then + ASAN_OPTIONS=detect_leaks=0 $gjs exit.js + test $? -ne 0 + report "System.exit() should still exit across an FFI boundary" +else + skip "System.exit() should still exit across an FFI boundary" "running under valgrind" +fi # ensure the encoding of argv is being properly handled $gjs -c 'imports.system.exit((ARGV[0] !== "Valentín") ? 1 : 0)' "Valentín" @@ -127,14 +143,14 @@ report "--help should print something before -c" # --help after a script file name is passed to the script $gjs -I sentinel help.js --help report "--help after script file should be passed to script" -test -z "$("$gjs" -I sentinel help.js --help)" +test -z "$($gjs -I sentinel help.js --help)" report "--help after script file should not print anything" # --help after a -c argument is passed to the script script='if(ARGV[0] !== "--help") imports.system.exit(1)' $gjs -c "$script" --help report "--help after -c should be passed to script" -test -z "$("$gjs" -c "$script" --help)" +test -z "$($gjs -c "$script" --help)" report "--help after -c should not print anything" # -I after a program is not consumed by GJS @@ -142,6 +158,7 @@ report "--help after -c should not print anything" # "$gjs" help.js --help -I sentinel # report_xfail "-I after script file should not be added to search path" # fi +G_DEBUG=$(echo "$G_DEBUG" | sed -e 's/fatal-warnings,\{0,1\}//') $gjs help.js --help -I sentinel 2>&1 | grep -q 'Cjs-WARNING.*--include-path' report "-I after script should succeed but give a warning" $gjs -c 'imports.system.exit(0)' --coverage-prefix=foo --coverage-output=foo 2>&1 | grep -q 'Cjs-WARNING.*--coverage-prefix' @@ -149,9 +166,7 @@ report "--coverage-prefix after script should succeed but give a warning" $gjs -c 'imports.system.exit(0)' --coverage-prefix=foo --coverage-output=foo 2>&1 | grep -q 'Cjs-WARNING.*--coverage-output' report "--coverage-output after script should succeed but give a warning" rm -f foo/coverage.lcov -$gjs -c 'imports.system.exit(0)' --profile=foo 2>&1 | grep -q 'Cjs-WARNING.*--profile' -report "--profile after script should succeed but give a warning" -rm -f foo +G_DEBUG="$OLD_G_DEBUG" for version_arg in --version --jsversion; do # --version and --jsversion work @@ -170,23 +185,24 @@ done # --profile rm -f gjs-*.syscap foo.syscap -$gjs -c 'imports.system.exit(0)' && ! stat gjs-*.syscap &> /dev/null +$gjs -c 'imports.system.exit(0)' && ! stat gjs-*.syscap > /dev/null 2>&1 report "no profiling data should be dumped without --profile" # Skip some tests if built without profiler support -if gjs --profile -c 1 2>&1 | grep -q 'Cjs-Message.*Profiler is disabled'; then +if $gjs --profile -c 1 2>&1 | grep -q 'Cjs-Message.*Profiler is disabled'; then reason="profiler is disabled" skip "--profile should dump profiling data to the default file name" "$reason" skip "--profile with argument should dump profiling data to the named file" "$reason" skip "GJS_ENABLE_PROFILER=1 should enable the profiler" "$reason" else rm -f gjs-*.syscap - $gjs --profile -c 'imports.system.exit(0)' && stat gjs-*.syscap &> /dev/null + $gjs --profile -c 'imports.system.exit(0)' && stat gjs-*.syscap > /dev/null 2>&1 report "--profile should dump profiling data to the default file name" + rm -f gjs-*.syscap $gjs --profile=foo.syscap -c 'imports.system.exit(0)' && test -f foo.syscap report "--profile with argument should dump profiling data to the named file" - rm -f gjs-*.syscap foo.syscap - GJS_ENABLE_PROFILER=1 $gjs -c 'imports.system.exit(0)' && test -f gjs-*.syscap + rm -f foo.syscap && rm -f gjs-*.syscap + GJS_ENABLE_PROFILER=1 $gjs -c 'imports.system.exit(0)' && stat gjs-*.syscap > /dev/null 2>&1 report "GJS_ENABLE_PROFILER=1 should enable the profiler" rm -f gjs-*.syscap fi @@ -202,16 +218,41 @@ report "interpreter should stop running jobs when one calls System.exit()" $gjs -c "Promise.resolve().then(() => { throw new Error(); });" 2>&1 | grep -q 'Cjs-WARNING.*Unhandled promise rejection.*[sS]tack trace' report "unhandled promise rejection should be reported" -test -z $($gjs awaitcatch.js) +test -z "$($gjs awaitcatch.js)" report "catching an await expression should not cause unhandled rejection" # https://gitlab.gnome.org/GNOME/gjs/issues/18 -$gjs -c "(async () => await true)(); void foobar;" 2>&1 | grep -q 'Script .* threw an exception' +G_DEBUG=$(echo "$G_DEBUG" | sed -e 's/fatal-warnings,\{0,1\}//') +$gjs -c "(async () => await true)(); void foobar;" 2>&1 | grep -q 'ReferenceError: foobar is not defined' report "main program exceptions are not swallowed by queued promise jobs" +G_DEBUG="$OLD_G_DEBUG" # https://gitlab.gnome.org/GNOME/gjs/issues/26 $gjs -c 'new imports.gi.Gio.Subprocess({argv: ["true"]}).init(null);' report "object unref from other thread after shutdown should not race" +# https://gitlab.gnome.org/GNOME/gjs/issues/212 +if test -n "$ENABLE_GTK"; then + G_DEBUG=$(echo "$G_DEBUG" | sed -e 's/fatal-warnings,\{0,1\}//' -e 's/fatal-criticals,\{0,1\}//') + $gjs -c 'imports.gi.versions.Gtk = "3.0"; + const Gtk = imports.gi.Gtk; + const GObject = imports.gi.GObject; + Gtk.init(null); + let BadWidget = GObject.registerClass(class BadWidget extends Gtk.Widget { + vfunc_destroy() {}; + }); + let w = new BadWidget ();' + report "avoid crashing when GTK vfuncs are called on context destroy" + G_DEBUG="$OLD_G_DEBUG" +else + skip "avoid crashing when GTK vfuncs are called on context destroy" "GTK disabled" +fi + +# https://gitlab.gnome.org/GNOME/gjs/-/issues/322 +$gjs --coverage-prefix=$(pwd) --coverage-output=$(pwd) awaitcatch.js +grep -q TN: coverage.lcov +report "coverage prefix is treated as an absolute path" +rm -f coverage.lcov + rm -f exit.js help.js promise.js awaitcatch.js echo "1..$total" diff --git a/installed-tests/scripts/testExamples.sh b/installed-tests/scripts/testExamples.sh new file mode 100755 index 0000000..2552907 --- /dev/null +++ b/installed-tests/scripts/testExamples.sh @@ -0,0 +1,29 @@ +#!/bin/bash +DIR="$( cd "$( dirname "${0}" )" && pwd )" +source "${DIR}"/common.sh + +# Run the examples +$gjs examples/gio-cat.js meson.build +report "run the gio-cat.js example" + +if [[ -n "${ENABLE_GTK}" ]]; then + export graphical_gjs="xvfb-run -a dbus-run-session -- $gjs" + + eval timeout 5s $graphical_gjs examples/calc.js + report_timeout "run the calc.js example" + + eval timeout 5s $graphical_gjs examples/gtk.js + report_timeout "run the gtk.js example" + + eval timeout 5s $graphical_gjs examples/gtk-application.js + report_timeout "run the gtk-application.js example" + + eval timeout 5s $graphical_gjs examples/gettext.js + report_timeout "run the gettext.js example" +else + skip "run the calc.js example" "running without GTK" + skip "run the gtk.js example" "running without GTK" + skip "run the gtk-application.js example" "running without GTK" + skip "run the gettext.js example" "running without GTK" +fi +echo "1..$total" diff --git a/installed-tests/scripts/testWarnings.sh b/installed-tests/scripts/testWarnings.sh index e1e60e6..fc60a5b 100755 --- a/installed-tests/scripts/testWarnings.sh +++ b/installed-tests/scripts/testWarnings.sh @@ -1,9 +1,9 @@ #!/bin/sh if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then - gjs="$LOG_COMPILER $LOG_FLAGS $TOP_BUILDDIR/cjs-console" + gjs="$TOP_BUILDDIR/cjs-console" else - gjs="$LOG_COMPILER $LOG_FLAGS cjs-console" + gjs="cjs-console" fi total=0 diff --git a/js.gresource.xml b/js.gresource.xml new file mode 100644 index 0000000..f0c3b93 --- /dev/null +++ b/js.gresource.xml @@ -0,0 +1,37 @@ + + + + + modules/script/_bootstrap/debugger.js + modules/script/_bootstrap/default.js + modules/script/_bootstrap/coverage.js + + modules/script/tweener/equations.js + modules/script/tweener/tweener.js + modules/script/tweener/tweenList.js + + modules/script/byteArray.js + modules/script/cairo.js + modules/script/gettext.js + modules/script/lang.js + modules/script/_legacy.js + modules/script/mainloop.js + modules/script/jsUnit.js + modules/script/signals.js + modules/script/format.js + modules/script/package.js + + + modules/core/overrides/cairo.js + modules/core/overrides/GLib.js + modules/core/overrides/Gio.js + modules/core/overrides/GObject.js + modules/core/overrides/Gtk.js + + modules/core/_cairo.js + modules/core/_common.js + modules/core/_format.js + modules/core/_gettext.js + modules/core/_signals.js + + diff --git a/libgjs-private/gjs-gdbus-wrapper.cpp b/libgjs-private/gjs-gdbus-wrapper.c similarity index 56% rename from libgjs-private/gjs-gdbus-wrapper.cpp rename to libgjs-private/gjs-gdbus-wrapper.c index e43d98c..eb0516f 100644 --- a/libgjs-private/gjs-gdbus-wrapper.cpp +++ b/libgjs-private/gjs-gdbus-wrapper.c @@ -1,10 +1,13 @@ /* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ /* Copyright 2011 Giovanni Campagna. All Rights Reserved. */ -#include -#include +#include // for strcmp -#include "gjs-gdbus-wrapper.h" +#include +#include +#include + +#include "libgjs-private/gjs-gdbus-wrapper.h" enum { PROP_0, @@ -30,45 +33,87 @@ struct _GjsDBusImplementationPrivate { guint idle_id; }; -/* 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_WITH_PRIVATE(GjsDBusImplementation, gjs_dbus_implementation, G_TYPE_DBUS_INTERFACE_SKELETON); -#if __GNUC__ >= 8 -_Pragma("GCC diagnostic pop") -#endif -static void -gjs_dbus_implementation_method_call(GDBusConnection *connection, - const char *sender, - const char *object_path, - const char *interface_name, - const char *method_name, - GVariant *parameters, - GDBusMethodInvocation *invocation, - gpointer user_data) -{ +static gboolean gjs_dbus_implementation_check_interface( + GjsDBusImplementation* self, GDBusConnection* connection, + const char* object_path, const char* interface_name, GError** error) { + const char* exported_object_path; + + if (!g_dbus_interface_skeleton_has_connection( + G_DBUS_INTERFACE_SKELETON(self), connection)) { + g_set_error_literal(error, G_DBUS_ERROR, G_DBUS_ERROR_DISCONNECTED, + "Wrong connection"); + return FALSE; + } + exported_object_path = g_dbus_interface_skeleton_get_object_path( + G_DBUS_INTERFACE_SKELETON(self)); + if (!exported_object_path || strcmp(object_path, exported_object_path)) { + g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT, + "Wrong object path %s for %s", object_path, + exported_object_path); + return FALSE; + } + if (strcmp(interface_name, self->priv->ifaceinfo->name) != 0) { + g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE, + "Unknown interface %s on %s", interface_name, + self->priv->ifaceinfo->name); + return FALSE; + } + return TRUE; +} + +static gboolean gjs_dbus_implementation_check_property( + GjsDBusImplementation* self, const char* interface_name, + const char* property_name, GError** error) { + if (!g_dbus_interface_info_lookup_property(self->priv->ifaceinfo, + property_name)) { + g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown property %s on %s", property_name, interface_name); + return FALSE; + } + return TRUE; +} + +static void gjs_dbus_implementation_method_call( + GDBusConnection* connection, const char* sender G_GNUC_UNUSED, + const char* object_path, const char* interface_name, + const char* method_name, GVariant* parameters, + GDBusMethodInvocation* invocation, void* user_data) { GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION (user_data); + GError* error = NULL; + + if (!gjs_dbus_implementation_check_interface(self, connection, object_path, + interface_name, &error)) { + g_dbus_method_invocation_take_error(invocation, error); + return; + } + if (!g_dbus_interface_info_lookup_method(self->priv->ifaceinfo, + method_name)) { + g_dbus_method_invocation_return_error( + invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method %s on %s", method_name, interface_name); + return; + } g_signal_emit(self, signals[SIGNAL_HANDLE_METHOD], 0, method_name, parameters, invocation); g_object_unref (invocation); } -static GVariant * -gjs_dbus_implementation_property_get(GDBusConnection *connection, - const char *sender, - const char *object_path, - const char *interface_name, - const char *property_name, - GError **error, - gpointer user_data) -{ +static GVariant* gjs_dbus_implementation_property_get( + GDBusConnection* connection, const char* sender G_GNUC_UNUSED, + const char* object_path, const char* interface_name, + const char* property_name, GError** error, void* user_data) { GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION (user_data); GVariant *value; + if (!gjs_dbus_implementation_check_interface(self, connection, object_path, + interface_name, error) || + !gjs_dbus_implementation_check_property(self, interface_name, + property_name, error)) + return NULL; + g_signal_emit(self, signals[SIGNAL_HANDLE_PROPERTY_GET], 0, property_name, &value); /* Marshaling GErrors is not supported, so this is the best we can do @@ -79,26 +124,28 @@ gjs_dbus_implementation_property_get(GDBusConnection *connection, return value; } -static gboolean -gjs_dbus_implementation_property_set(GDBusConnection *connection, - const char *sender, - const char *object_path, - const char *interface_name, - const char *property_name, - GVariant *value, - GError **error, - gpointer user_data) -{ +static gboolean gjs_dbus_implementation_property_set( + GDBusConnection* connection, const char* sender G_GNUC_UNUSED, + const char* object_path, const char* interface_name, + const char* property_name, GVariant* value, GError** error, + void* user_data) { GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION (user_data); + if (!gjs_dbus_implementation_check_interface(self, connection, object_path, + interface_name, error) || + !gjs_dbus_implementation_check_property(self, interface_name, + property_name, error)) + return FALSE; + g_signal_emit(self, signals[SIGNAL_HANDLE_PROPERTY_SET], 0, property_name, value); - return true; + return TRUE; } static void gjs_dbus_implementation_init(GjsDBusImplementation *self) { - GjsDBusImplementationPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GJS_TYPE_DBUS_IMPLEMENTATION, GjsDBusImplementationPrivate); + GjsDBusImplementationPrivate* priv = + gjs_dbus_implementation_get_instance_private(self); self->priv = priv; @@ -109,12 +156,20 @@ gjs_dbus_implementation_init(GjsDBusImplementation *self) { priv->outstanding_properties = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_variant_unref); } +static void gjs_dbus_implementation_dispose(GObject* object) { + GjsDBusImplementation* self = GJS_DBUS_IMPLEMENTATION(object); + + g_clear_handle_id(&self->priv->idle_id, g_source_remove); + + G_OBJECT_CLASS(gjs_dbus_implementation_parent_class)->dispose(object); +} + static void gjs_dbus_implementation_finalize(GObject *object) { GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION (object); g_dbus_interface_info_unref (self->priv->ifaceinfo); - g_hash_table_unref (self->priv->outstanding_properties); + g_hash_table_destroy(self->priv->outstanding_properties); G_OBJECT_CLASS(gjs_dbus_implementation_parent_class)->finalize(object); } @@ -176,7 +231,7 @@ gjs_dbus_implementation_get_properties (GDBusInterfaceSkeleton *skeleton) { static void gjs_dbus_implementation_flush (GDBusInterfaceSkeleton *skeleton) { - GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION (skeleton); + GjsDBusImplementation *self = GJS_DBUS_IMPLEMENTATION(skeleton); GVariantBuilder changed_props; GVariantBuilder invalidated_props; @@ -195,22 +250,30 @@ gjs_dbus_implementation_flush (GDBusInterfaceSkeleton *skeleton) { g_variant_builder_add(&invalidated_props, "s", prop_name); } - g_dbus_connection_emit_signal(g_dbus_interface_skeleton_get_connection(skeleton), - NULL, /* bus name */ - g_dbus_interface_skeleton_get_object_path(skeleton), - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - g_variant_new("(s@a{sv}@as)", - self->priv->ifaceinfo->name, - g_variant_builder_end(&changed_props), - g_variant_builder_end(&invalidated_props)), - NULL /* error */); + GList *connections = g_dbus_interface_skeleton_get_connections(skeleton); + const char *object_path = g_dbus_interface_skeleton_get_object_path(skeleton); + GVariant *properties = g_variant_new("(s@a{sv}@as)", + self->priv->ifaceinfo->name, + g_variant_builder_end(&changed_props), + g_variant_builder_end(&invalidated_props)); + g_variant_ref_sink(properties); + + for (const GList *iter = connections; iter; iter = iter->next) { + g_dbus_connection_emit_signal(G_DBUS_CONNECTION(iter->data), + NULL, /* bus name */ + object_path, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + properties, + NULL /* error */); + + g_object_unref(iter->data); + } + g_variant_unref(properties); + g_list_free(connections); g_hash_table_remove_all(self->priv->outstanding_properties); - if (self->priv->idle_id) { - g_source_remove(self->priv->idle_id); - self->priv->idle_id = 0; - } + g_clear_handle_id(&self->priv->idle_id, g_source_remove); } void @@ -218,6 +281,7 @@ gjs_dbus_implementation_class_init(GjsDBusImplementationClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); GDBusInterfaceSkeletonClass *skeleton_class = G_DBUS_INTERFACE_SKELETON_CLASS(klass); + gobject_class->dispose = gjs_dbus_implementation_dispose; gobject_class->finalize = gjs_dbus_implementation_finalize; gobject_class->set_property = gjs_dbus_implementation_set_property; @@ -313,13 +377,71 @@ gjs_dbus_implementation_emit_signal (GjsDBusImplementation *self, gchar *signal_name, GVariant *parameters) { - GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON (self); - - g_dbus_connection_emit_signal(g_dbus_interface_skeleton_get_connection(skeleton), - NULL, - g_dbus_interface_skeleton_get_object_path(skeleton), - self->priv->ifaceinfo->name, - signal_name, - parameters, - NULL); + GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON(self); + GList *connections = g_dbus_interface_skeleton_get_connections(skeleton); + const char *object_path = g_dbus_interface_skeleton_get_object_path(skeleton); + + if (parameters != NULL) + g_variant_ref_sink(parameters); + + for (const GList *iter = connections; iter; iter = iter->next) { + g_dbus_connection_emit_signal(G_DBUS_CONNECTION(iter->data), + NULL, + object_path, + self->priv->ifaceinfo->name, + signal_name, + parameters, + NULL); + + g_object_unref(iter->data); + } + if (parameters != NULL) + g_variant_unref(parameters); + + g_list_free(connections); +} + +/** + * gjs_dbus_implementation_unexport: + * @self: a #GjsDBusImplementation + * + * Stops exporting @self on all connections it is exported on. + * + * To unexport @self from only a single connection, use + * gjs_dbus_implementation_skeleton_unexport_from_connection() + */ +void +gjs_dbus_implementation_unexport(GjsDBusImplementation *self) { + GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON(self); + + g_hash_table_remove_all(self->priv->outstanding_properties); + g_clear_handle_id(&self->priv->idle_id, g_source_remove); + + g_dbus_interface_skeleton_unexport(skeleton); +} + +/** + * gjs_dbus_implementation_unexport_from_connection: + * @self: a #GjsDBusImplementation + * @connection: a #GDBusConnection + * + * Stops exporting @self on @connection. + * + * To stop exporting on all connections the interface is exported on, + * use gjs_dbus_implementation_unexport(). + */ +void +gjs_dbus_implementation_unexport_from_connection(GjsDBusImplementation *self, + GDBusConnection *connection) { + GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON(self); + GList *connections = g_dbus_interface_skeleton_get_connections(skeleton); + + if (g_list_length(connections) <= 1) { + g_hash_table_remove_all(self->priv->outstanding_properties); + g_clear_handle_id(&self->priv->idle_id, g_source_remove); + } + + g_list_free_full(connections, g_object_unref); + + g_dbus_interface_skeleton_unexport_from_connection(skeleton, connection); } diff --git a/libgjs-private/gjs-gdbus-wrapper.h b/libgjs-private/gjs-gdbus-wrapper.h index 2b4a8fc..386a575 100644 --- a/libgjs-private/gjs-gdbus-wrapper.h +++ b/libgjs-private/gjs-gdbus-wrapper.h @@ -20,19 +20,17 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_UTIL_DBUS_H__ -#define __GJS_UTIL_DBUS_H__ +#ifndef LIBGJS_PRIVATE_GJS_GDBUS_WRAPPER_H_ +#define LIBGJS_PRIVATE_GJS_GDBUS_WRAPPER_H_ -#include -#include #include +#include +#include -#include +#include "cjs/macros.h" G_BEGIN_DECLS -typedef struct _GjsDBusImplementation GjsDBusImplementation; -typedef struct _GjsDBusImplementationClass GjsDBusImplementationClass; typedef struct _GjsDBusImplementationPrivate GjsDBusImplementationPrivate; #define GJS_TYPE_DBUS_IMPLEMENTATION (gjs_dbus_implementation_get_type ()) @@ -47,17 +45,21 @@ struct _GjsDBusImplementation { GjsDBusImplementationPrivate *priv; }; +typedef struct _GjsDBusImplementation GjsDBusImplementation; struct _GjsDBusImplementationClass { GDBusInterfaceSkeletonClass parent_class; }; +typedef struct _GjsDBusImplementationClass GjsDBusImplementationClass; GJS_EXPORT GType gjs_dbus_implementation_get_type (void); +GJS_EXPORT void gjs_dbus_implementation_emit_property_changed (GjsDBusImplementation *self, gchar *property, GVariant *newvalue); +GJS_EXPORT void gjs_dbus_implementation_emit_signal (GjsDBusImplementation *self, gchar *signal_name, GVariant *parameters); G_END_DECLS -#endif /* __GJS_UTIL_DBUS_H__ */ +#endif /* LIBGJS_PRIVATE_GJS_GDBUS_WRAPPER_H_ */ diff --git a/libgjs-private/gjs-gtk-util.c b/libgjs-private/gjs-gtk-util.c deleted file mode 100644 index f15bfd5..0000000 --- a/libgjs-private/gjs-gtk-util.c +++ /dev/null @@ -1,61 +0,0 @@ -/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* Copyright 2014 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. - */ - -#include -#include - -#include "gjs-gtk-util.h" - -void -gjs_gtk_container_child_set_property (GtkContainer *container, - GtkWidget *child, - const gchar *property, - const GValue *value) -{ - GParamSpec *pspec; - - pspec = gtk_container_class_find_child_property (G_OBJECT_GET_CLASS (container), - property); - if (pspec == NULL) { - g_warning ("%s does not have a property called %s", - g_type_name (G_OBJECT_TYPE (container)), property); - return; - } - - if ((G_VALUE_TYPE (value) == G_TYPE_POINTER) && - (g_value_get_pointer (value) == NULL) && - !g_value_type_transformable (G_VALUE_TYPE (value), pspec->value_type)) { - /* Set an empty value. This will happen when we set a NULL value from JS. - * Since GJS doesn't know the GParamSpec for this property, it - * will just put NULL into a G_TYPE_POINTER GValue, which will later - * fail when trying to transform it to the GParamSpec's GType. - */ - GValue null_value = G_VALUE_INIT; - g_value_init (&null_value, pspec->value_type); - gtk_container_child_set_property (container, child, - property, &null_value); - g_value_unset (&null_value); - } else { - gtk_container_child_set_property (container, child, - property, value); - } -} diff --git a/libgjs-private/gjs-util.c b/libgjs-private/gjs-util.c new file mode 100644 index 0000000..d4f7470 --- /dev/null +++ b/libgjs-private/gjs-util.c @@ -0,0 +1,311 @@ +/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* Copyright 2012 Giovanni Campagna + * + * 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 /* for setlocale */ +#include /* for size_t */ +#include /* for strcmp */ + +#include +#include +#include +#include +#include /* for bindtextdomain, bind_textdomain_codeset, textdomain */ + +#ifdef G_OS_UNIX +# include +# include /* for FD_CLOEXEC */ +# include +# include /* for close, write */ + +# include /* for g_unix_open_pipe */ +#endif + +#include "libgjs-private/gjs-util.h" + +char * +gjs_format_int_alternative_output(int n) +{ +#ifdef HAVE_PRINTF_ALTERNATIVE_INT + return g_strdup_printf("%Id", n); +#else + return g_strdup_printf("%d", n); +#endif +} + +GType +gjs_locale_category_get_type(void) +{ + static volatile size_t g_define_type_id__volatile = 0; + if (g_once_init_enter(&g_define_type_id__volatile)) { + static const GEnumValue v[] = { + { GJS_LOCALE_CATEGORY_ALL, "GJS_LOCALE_CATEGORY_ALL", "all" }, + { GJS_LOCALE_CATEGORY_COLLATE, "GJS_LOCALE_CATEGORY_COLLATE", "collate" }, + { GJS_LOCALE_CATEGORY_CTYPE, "GJS_LOCALE_CATEGORY_CTYPE", "ctype" }, + { GJS_LOCALE_CATEGORY_MESSAGES, "GJS_LOCALE_CATEGORY_MESSAGES", "messages" }, + { GJS_LOCALE_CATEGORY_MONETARY, "GJS_LOCALE_CATEGORY_MONETARY", "monetary" }, + { GJS_LOCALE_CATEGORY_NUMERIC, "GJS_LOCALE_CATEGORY_NUMERIC", "numeric" }, + { GJS_LOCALE_CATEGORY_TIME, "GJS_LOCALE_CATEGORY_TIME", "time" }, + { 0, NULL, NULL } + }; + GType g_define_type_id = + g_enum_register_static(g_intern_static_string("GjsLocaleCategory"), v); + + g_once_init_leave(&g_define_type_id__volatile, g_define_type_id); + } + return g_define_type_id__volatile; +} + +/** + * gjs_setlocale: + * @category: + * @locale: (allow-none): + * + * Returns: + */ +const char * +gjs_setlocale(GjsLocaleCategory category, const char *locale) +{ + /* According to man setlocale(3), the return value may be allocated in + * static storage. */ + return (const char *) setlocale(category, locale); +} + +void +gjs_textdomain(const char *domain) +{ + textdomain(domain); +} + +void +gjs_bindtextdomain(const char *domain, + const char *location) +{ + bindtextdomain(domain, location); + /* Always use UTF-8; we assume it internally here */ + bind_textdomain_codeset(domain, "UTF-8"); +} + +GParamFlags +gjs_param_spec_get_flags(GParamSpec *pspec) +{ + return pspec->flags; +} + +GType +gjs_param_spec_get_value_type(GParamSpec *pspec) +{ + return pspec->value_type; +} + +GType +gjs_param_spec_get_owner_type(GParamSpec *pspec) +{ + return pspec->owner_type; +} + +#ifdef G_OS_UNIX + +// Adapted from glnx_throw_errno_prefix() +G_GNUC_PRINTF(2, 3) +static gboolean throw_errno_prefix(GError** error, const char* fmt, ...) { + int errsv = errno; + char* old_msg; + GString* buf; + + va_list args; + + if (!error) + return FALSE; + + va_start(args, fmt); + + g_set_error_literal(error, G_IO_ERROR, g_io_error_from_errno(errsv), + g_strerror(errsv)); + + old_msg = g_steal_pointer(&(*error)->message); + buf = g_string_new(""); + g_string_append_vprintf(buf, fmt, args); + g_string_append(buf, ": "); + g_string_append(buf, old_msg); + g_free(old_msg); + (*error)->message = g_string_free(g_steal_pointer(&buf), FALSE); + + va_end(args); + + errno = errsv; + return FALSE; +} + +#endif /* G_OS_UNIX */ + +/** + * gjs_open_bytes: + * @bytes: bytes to send to the pipe + * @error: Return location for a #GError, or %NULL + * + * Creates a pipe and sends @bytes to it, such that it is suitable for passing + * to g_subprocess_launcher_take_fd(). + * + * Returns: file descriptor, or -1 on error + */ +int gjs_open_bytes(GBytes* bytes, GError** error) { + int pipefd[2], result; + size_t count; + const void* buf; + ssize_t bytes_written; + + g_return_val_if_fail(bytes, -1); + g_return_val_if_fail(error == NULL || *error == NULL, -1); + +#ifdef G_OS_UNIX + if (!g_unix_open_pipe(pipefd, FD_CLOEXEC, error)) + return -1; + + buf = g_bytes_get_data(bytes, &count); + + bytes_written = write(pipefd[1], buf, count); + if (bytes_written < 0) { + throw_errno_prefix(error, "write"); + return -1; + } + + if ((size_t)bytes_written != count) + g_warning("%s: %zd bytes sent, only %zu bytes written", __func__, count, + bytes_written); + + result = close(pipefd[1]); + if (result == -1) { + throw_errno_prefix(error, "close"); + return -1; + } + + return pipefd[0]; +#else + g_error("%s is currently supported on UNIX only", __func__); +#endif +} + +static GIBaseInfo* find_method_fallback(GIStructInfo* class_info, + const char* method_name) { + GIBaseInfo* method; + guint n_methods, i; + + n_methods = g_struct_info_get_n_methods(class_info); + + for (i = 0; i < n_methods; i++) { + method = g_struct_info_get_method(class_info, i); + + if (strcmp(g_base_info_get_name(method), method_name) == 0) + return method; + g_base_info_unref(method); + } + + return NULL; +} + +static GParamSpec* gjs_gtk_container_class_find_child_property( + GIObjectInfo* container_info, GObject* container, const char* property) { + GIBaseInfo* class_info = NULL; + GIBaseInfo* find_child_property_fun = NULL; + + GIArgument ret; + GIArgument find_child_property_args[2]; + + class_info = g_object_info_get_class_struct(container_info); + find_child_property_fun = + g_struct_info_find_method(class_info, "find_child_property"); + + /* Workaround for + https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/171 + */ + if (find_child_property_fun == NULL) + find_child_property_fun = + find_method_fallback(class_info, "find_child_property"); + + find_child_property_args[0].v_pointer = G_OBJECT_GET_CLASS(container); + find_child_property_args[1].v_string = (char*)property; + + g_function_info_invoke(find_child_property_fun, find_child_property_args, 2, + NULL, 0, &ret, NULL); + + g_clear_pointer(&class_info, g_base_info_unref); + g_clear_pointer(&find_child_property_fun, g_base_info_unref); + + return (GParamSpec*)ret.v_pointer; +} + +void gjs_gtk_container_child_set_property(GObject* container, GObject* child, + const char* property, + const GValue* value) { + GParamSpec* pspec = NULL; + GIBaseInfo* base_info = NULL; + GIBaseInfo* child_set_property_fun = NULL; + GIObjectInfo* container_info; + GValue value_arg = G_VALUE_INIT; + GIArgument ret; + + GIArgument child_set_property_args[4]; + + base_info = g_irepository_find_by_name(NULL, "Gtk", "Container"); + container_info = (GIObjectInfo*)base_info; + + pspec = gjs_gtk_container_class_find_child_property(container_info, + container, property); + if (pspec == NULL) { + g_warning("%s does not have a property called %s", + g_type_name(G_OBJECT_TYPE(container)), property); + goto out; + } + + if ((G_VALUE_TYPE(value) == G_TYPE_POINTER) && + (g_value_get_pointer(value) == NULL) && + !g_value_type_transformable(G_VALUE_TYPE(value), pspec->value_type)) { + /* Set an empty value. This will happen when we set a NULL value from + * JS. Since GJS doesn't know the GParamSpec for this property, it will + * just put NULL into a G_TYPE_POINTER GValue, which will later fail + * when trying to transform it to the GParamSpec's GType. + */ + g_value_init(&value_arg, pspec->value_type); + } else { + g_value_init(&value_arg, G_VALUE_TYPE(value)); + g_value_copy(value, &value_arg); + } + + child_set_property_fun = + g_object_info_find_method(container_info, "child_set_property"); + + child_set_property_args[0].v_pointer = container; + child_set_property_args[1].v_pointer = child; + child_set_property_args[2].v_string = (char*)property; + child_set_property_args[3].v_pointer = &value_arg; + + g_function_info_invoke(child_set_property_fun, child_set_property_args, 4, + NULL, 0, &ret, NULL); + + g_value_unset(&value_arg); + +out: + g_clear_pointer(&base_info, g_base_info_unref); + g_clear_pointer(&child_set_property_fun, g_base_info_unref); +} diff --git a/libgjs-private/gjs-util.cpp b/libgjs-private/gjs-util.cpp deleted file mode 100644 index f0e941f..0000000 --- a/libgjs-private/gjs-util.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ -/* Copyright 2012 Giovanni Campagna - * - * 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 "gjs-util.h" - -char * -gjs_format_int_alternative_output(int n) -{ -#ifdef HAVE_PRINTF_ALTERNATIVE_INT - return g_strdup_printf("%Id", n); -#else - return g_strdup_printf("%d", n); -#endif -} - -GType -gjs_locale_category_get_type(void) -{ - static volatile size_t g_define_type_id__volatile = 0; - if (g_once_init_enter(&g_define_type_id__volatile)) { - static const GEnumValue v[] = { - { GJS_LOCALE_CATEGORY_ALL, "GJS_LOCALE_CATEGORY_ALL", "all" }, - { GJS_LOCALE_CATEGORY_COLLATE, "GJS_LOCALE_CATEGORY_COLLATE", "collate" }, - { GJS_LOCALE_CATEGORY_CTYPE, "GJS_LOCALE_CATEGORY_CTYPE", "ctype" }, - { GJS_LOCALE_CATEGORY_MESSAGES, "GJS_LOCALE_CATEGORY_MESSAGES", "messages" }, - { GJS_LOCALE_CATEGORY_MONETARY, "GJS_LOCALE_CATEGORY_MONETARY", "monetary" }, - { GJS_LOCALE_CATEGORY_NUMERIC, "GJS_LOCALE_CATEGORY_NUMERIC", "numeric" }, - { GJS_LOCALE_CATEGORY_TIME, "GJS_LOCALE_CATEGORY_TIME", "time" }, - { 0, NULL, NULL } - }; - GType g_define_type_id = - g_enum_register_static(g_intern_static_string("GjsLocaleCategory"), v); - - g_once_init_leave(&g_define_type_id__volatile, g_define_type_id); - } - return g_define_type_id__volatile; -} - -/** - * gjs_setlocale: - * @category: - * @locale: (allow-none): - * - * Returns: - */ -const char * -gjs_setlocale(GjsLocaleCategory category, const char *locale) -{ - /* According to man setlocale(3), the return value may be allocated in - * static storage. */ - return (const char *) setlocale(category, locale); -} - -void -gjs_textdomain(const char *domain) -{ - textdomain(domain); -} - -void -gjs_bindtextdomain(const char *domain, - const char *location) -{ - bindtextdomain(domain, location); - /* Always use UTF-8; we assume it internally here */ - bind_textdomain_codeset(domain, "UTF-8"); -} - -GParamFlags -gjs_param_spec_get_flags(GParamSpec *pspec) -{ - return pspec->flags; -} - -GType -gjs_param_spec_get_value_type(GParamSpec *pspec) -{ - return pspec->value_type; -} - -GType -gjs_param_spec_get_owner_type(GParamSpec *pspec) -{ - return pspec->owner_type; -} diff --git a/libgjs-private/gjs-util.h b/libgjs-private/gjs-util.h index a17fd03..91276ff 100644 --- a/libgjs-private/gjs-util.h +++ b/libgjs-private/gjs-util.h @@ -20,18 +20,20 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_PRIVATE_UTIL_H__ -#define __GJS_PRIVATE_UTIL_H__ +#ifndef LIBGJS_PRIVATE_GJS_UTIL_H_ +#define LIBGJS_PRIVATE_GJS_UTIL_H_ #include -#include + #include +#include -#include +#include "cjs/macros.h" G_BEGIN_DECLS /* For imports.format */ +GJS_EXPORT char * gjs_format_int_alternative_output (int n); /* For imports.gettext */ @@ -46,19 +48,35 @@ typedef enum GJS_LOCALE_CATEGORY_TIME = LC_TIME } GjsLocaleCategory; +GJS_EXPORT const char *gjs_setlocale (GjsLocaleCategory category, const char *locale); +GJS_EXPORT void gjs_textdomain (const char *domain); +GJS_EXPORT void gjs_bindtextdomain (const char *domain, const char *location); GJS_EXPORT GType gjs_locale_category_get_type (void) G_GNUC_CONST; /* For imports.overrides.GObject */ +GJS_EXPORT GParamFlags gjs_param_spec_get_flags (GParamSpec *pspec); +GJS_EXPORT GType gjs_param_spec_get_value_type (GParamSpec *pspec); +GJS_EXPORT GType gjs_param_spec_get_owner_type (GParamSpec *pspec); +/* For imports.overrides.Gtk */ +GJS_EXPORT +void gjs_gtk_container_child_set_property(GObject* container, GObject* child, + const char* property, + const GValue* value); + +/* For tests */ +GJS_EXPORT +int gjs_open_bytes(GBytes* bytes, GError** error); + G_END_DECLS -#endif +#endif /* LIBGJS_PRIVATE_GJS_UTIL_H_ */ diff --git a/libgjs.map b/libgjs.map new file mode 100644 index 0000000..cef1fea --- /dev/null +++ b/libgjs.map @@ -0,0 +1,6 @@ +{ +global: + gjs_*; +local: + *; +}; diff --git a/libgjs.symbols b/libgjs.symbols new file mode 100644 index 0000000..a78269e --- /dev/null +++ b/libgjs.symbols @@ -0,0 +1,7 @@ +# Workaround for https://github.com/mesonbuild/meson/issues/3047 +# Linker scripts are not understood by the macOS linker, we need to use a +# symbol export file instead. +# With autotools, this was all done transparently by -export-symbols-regex. +_gjs_* +__Z*[0-9]gjs_* +__ZN*GjsContextPrivate*from_object* diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4 deleted file mode 100644 index 6484f03..0000000 --- a/m4/ax_code_coverage.m4 +++ /dev/null @@ -1,264 +0,0 @@ -# =========================================================================== -# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_CODE_COVERAGE() -# -# DESCRIPTION -# -# Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS, -# CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included -# in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every -# build target (program or library) which should be built with code -# coverage support. Also defines CODE_COVERAGE_RULES which should be -# substituted in your Makefile; and $enable_code_coverage which can be -# used in subsequent configure output. CODE_COVERAGE_ENABLED is defined -# and substituted, and corresponds to the value of the -# --enable-code-coverage option, which defaults to being disabled. -# -# Test also for gcov program and create GCOV variable that could be -# substituted. -# -# Note that all optimization flags in CFLAGS must be disabled when code -# coverage is enabled. -# -# Usage example: -# -# configure.ac: -# -# AX_CODE_COVERAGE -# -# Makefile.am: -# -# @CODE_COVERAGE_RULES@ -# my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ... -# my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ... -# my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ... -# my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ... -# -# This results in a "check-code-coverage" rule being added to any -# Makefile.am which includes "@CODE_COVERAGE_RULES@" (assuming the module -# has been configured with --enable-code-coverage). Running `make -# check-code-coverage` in that directory will run the module's test suite -# (`make check`) and build a code coverage report detailing the code which -# was touched, then print the URI for the report. -# -# In earlier versions of this macro, CODE_COVERAGE_LDFLAGS was defined -# instead of CODE_COVERAGE_LIBS. They are both still defined, but use of -# CODE_COVERAGE_LIBS is preferred for clarity; CODE_COVERAGE_LDFLAGS is -# deprecated. They have the same value. -# -# This code was derived from Makefile.decl in GLib, originally licenced -# under LGPLv2.1+. -# -# LICENSE -# -# Copyright (c) 2012, 2016 Philip Withnall -# Copyright (c) 2012 Xan Lopez -# Copyright (c) 2012 Christian Persch -# Copyright (c) 2012 Paolo Borelli -# Copyright (c) 2012 Dan Winship -# Copyright (c) 2015 Bastien ROUCARIES -# -# This library is free software; you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 2.1 of the License, or (at -# your option) any later version. -# -# This library is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser -# General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - -#serial 25 - -AC_DEFUN([AX_CODE_COVERAGE],[ - dnl Check for --enable-code-coverage - AC_REQUIRE([AC_PROG_SED]) - - # allow to override gcov location - AC_ARG_WITH([gcov], - [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])], - [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov], - [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov]) - - AC_MSG_CHECKING([whether to build with code coverage support]) - AC_ARG_ENABLE([code-coverage], - AS_HELP_STRING([--enable-code-coverage], - [Whether to enable code coverage support]),, - enable_code_coverage=no) - - AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test x$enable_code_coverage = xyes]) - AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage]) - AC_MSG_RESULT($enable_code_coverage) - - AS_IF([ test "$enable_code_coverage" = "yes" ], [ - # check for gcov - AC_CHECK_TOOL([GCOV], - [$_AX_CODE_COVERAGE_GCOV_PROG_WITH], - [:]) - AS_IF([test "X$GCOV" = "X:"], - [AC_MSG_ERROR([gcov is needed to do coverage])]) - AC_SUBST([GCOV]) - - dnl Check if gcc is being used - AS_IF([ test "$GCC" = "no" ], [ - AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage]) - ]) - - AC_CHECK_PROG([LCOV], [lcov], [lcov]) - AC_CHECK_PROG([GENHTML], [genhtml], [genhtml]) - - AS_IF([ test -z "$LCOV" ], [ - AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed]) - ]) - - AS_IF([ test -z "$GENHTML" ], [ - AC_MSG_ERROR([Could not find genhtml from the lcov package]) - ]) - - dnl Build the code coverage flags - dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility - CODE_COVERAGE_CPPFLAGS="-DNDEBUG" - CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" - CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" - CODE_COVERAGE_LIBS="-lgcov" - CODE_COVERAGE_LDFLAGS="$CODE_COVERAGE_LIBS" - - AC_SUBST([CODE_COVERAGE_CPPFLAGS]) - AC_SUBST([CODE_COVERAGE_CFLAGS]) - AC_SUBST([CODE_COVERAGE_CXXFLAGS]) - AC_SUBST([CODE_COVERAGE_LIBS]) - AC_SUBST([CODE_COVERAGE_LDFLAGS]) - - [CODE_COVERAGE_RULES_CHECK=' - -$(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k check - $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) code-coverage-capture -'] - [CODE_COVERAGE_RULES_CAPTURE=' - $(code_coverage_v_lcov_cap)$(LCOV) $(code_coverage_quiet) $(addprefix --directory ,$(CODE_COVERAGE_DIRECTORY)) --capture --output-file "$(CODE_COVERAGE_OUTPUT_FILE).tmp" --test-name "$(call code_coverage_sanitize,$(PACKAGE_NAME)-$(PACKAGE_VERSION))" --no-checksum --compat-libtool $(CODE_COVERAGE_LCOV_SHOPTS) $(CODE_COVERAGE_LCOV_OPTIONS) - $(code_coverage_v_lcov_ign)$(LCOV) $(code_coverage_quiet) $(addprefix --directory ,$(CODE_COVERAGE_DIRECTORY)) --remove "$(CODE_COVERAGE_OUTPUT_FILE).tmp" "/tmp/*" $(CODE_COVERAGE_IGNORE_PATTERN) --output-file "$(CODE_COVERAGE_OUTPUT_FILE)" $(CODE_COVERAGE_LCOV_SHOPTS) $(CODE_COVERAGE_LCOV_RMOPTS) - -@rm -f $(CODE_COVERAGE_OUTPUT_FILE).tmp - $(code_coverage_v_genhtml)LANG=C $(GENHTML) $(code_coverage_quiet) $(addprefix --prefix ,$(CODE_COVERAGE_DIRECTORY)) --output-directory "$(CODE_COVERAGE_OUTPUT_DIRECTORY)" --title "$(PACKAGE_NAME)-$(PACKAGE_VERSION) Code Coverage" --legend --show-details "$(CODE_COVERAGE_OUTPUT_FILE)" $(CODE_COVERAGE_GENHTML_OPTIONS) - @echo "file://$(abs_builddir)/$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html" -'] - [CODE_COVERAGE_RULES_CLEAN=' -clean: code-coverage-clean -distclean: code-coverage-clean -code-coverage-clean: - -$(LCOV) --directory $(top_builddir) -z - -rm -rf $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_FILE).tmp $(CODE_COVERAGE_OUTPUT_DIRECTORY) - -find . \( -name "*.gcda" -o -name "*.gcno" -o -name "*.gcov" \) -delete -'] - ], [ - [CODE_COVERAGE_RULES_CHECK=' - @echo "Need to reconfigure with --enable-code-coverage" -'] - CODE_COVERAGE_RULES_CAPTURE="$CODE_COVERAGE_RULES_CHECK" - CODE_COVERAGE_RULES_CLEAN='' - ]) - -[CODE_COVERAGE_RULES=' -# Code coverage -# -# Optional: -# - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting. -# Multiple directories may be specified, separated by whitespace. -# (Default: $(top_builddir)) -# - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated -# by lcov for code coverage. (Default: -# $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage.info) -# - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage -# reports to be created. (Default: -# $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage) -# - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage, -# set to 0 to disable it and leave empty to stay with the default. -# (Default: empty) -# - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov -# instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) -# - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov -# instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) -# - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov -# - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the -# collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) -# - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov -# instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) -# - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering -# lcov instance. (Default: empty) -# - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov -# instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) -# - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the -# genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) -# - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml -# instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) -# - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore -# -# The generated report will be titled using the $(PACKAGE_NAME) and -# $(PACKAGE_VERSION). In order to add the current git hash to the title, -# use the git-version-gen script, available online. - -# Optional variables -CODE_COVERAGE_DIRECTORY ?= $(top_builddir) -CODE_COVERAGE_OUTPUT_FILE ?= $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage.info -CODE_COVERAGE_OUTPUT_DIRECTORY ?= $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage -CODE_COVERAGE_BRANCH_COVERAGE ?= -CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= $(if $(CODE_COVERAGE_BRANCH_COVERAGE),\ ---rc lcov_branch_coverage=$(CODE_COVERAGE_BRANCH_COVERAGE)) -CODE_COVERAGE_LCOV_SHOPTS ?= $(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) -CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool "$(GCOV)" -CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= $(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) -CODE_COVERAGE_LCOV_OPTIONS ?= $(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) -CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?= -CODE_COVERAGE_LCOV_RMOPTS ?= $(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) -CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\ -$(if $(CODE_COVERAGE_BRANCH_COVERAGE),\ ---rc genhtml_branch_coverage=$(CODE_COVERAGE_BRANCH_COVERAGE)) -CODE_COVERAGE_GENHTML_OPTIONS ?= $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) -CODE_COVERAGE_IGNORE_PATTERN ?= - -GITIGNOREFILES ?= -GITIGNOREFILES += $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_DIRECTORY) - -code_coverage_v_lcov_cap = $(code_coverage_v_lcov_cap_$(V)) -code_coverage_v_lcov_cap_ = $(code_coverage_v_lcov_cap_$(AM_DEFAULT_VERBOSITY)) -code_coverage_v_lcov_cap_0 = @echo " LCOV --capture"\ - $(CODE_COVERAGE_OUTPUT_FILE); -code_coverage_v_lcov_ign = $(code_coverage_v_lcov_ign_$(V)) -code_coverage_v_lcov_ign_ = $(code_coverage_v_lcov_ign_$(AM_DEFAULT_VERBOSITY)) -code_coverage_v_lcov_ign_0 = @echo " LCOV --remove /tmp/*"\ - $(CODE_COVERAGE_IGNORE_PATTERN); -code_coverage_v_genhtml = $(code_coverage_v_genhtml_$(V)) -code_coverage_v_genhtml_ = $(code_coverage_v_genhtml_$(AM_DEFAULT_VERBOSITY)) -code_coverage_v_genhtml_0 = @echo " GEN " $(CODE_COVERAGE_OUTPUT_DIRECTORY); -code_coverage_quiet = $(code_coverage_quiet_$(V)) -code_coverage_quiet_ = $(code_coverage_quiet_$(AM_DEFAULT_VERBOSITY)) -code_coverage_quiet_0 = --quiet - -# sanitizes the test-name: replaces with underscores: dashes and dots -code_coverage_sanitize = $(subst -,_,$(subst .,_,$(1))) - -# Use recursive makes in order to ignore errors during check -check-code-coverage:'"$CODE_COVERAGE_RULES_CHECK"' - -# Capture code coverage data -code-coverage-capture: code-coverage-capture-hook'"$CODE_COVERAGE_RULES_CAPTURE"' - -# Hook rule executed before code-coverage-capture, overridable by the user -code-coverage-capture-hook: - -'"$CODE_COVERAGE_RULES_CLEAN"' - -A''M_DISTCHECK_CONFIGURE_FLAGS ?= -A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-code-coverage - -.PHONY: check-code-coverage code-coverage-capture code-coverage-capture-hook code-coverage-clean -'] - - AC_SUBST([CODE_COVERAGE_RULES]) - m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([CODE_COVERAGE_RULES])]) -]) diff --git a/m4/extensions.m4 b/m4/extensions.m4 deleted file mode 100644 index d1b2321..0000000 --- a/m4/extensions.m4 +++ /dev/null @@ -1,183 +0,0 @@ -# serial 17 -*- Autoconf -*- -# Enable extensions on systems that normally disable them. - -# Copyright (C) 2003, 2006-2018 Free Software Foundation, Inc. -# This file is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This definition of AC_USE_SYSTEM_EXTENSIONS is stolen from git -# Autoconf. Perhaps we can remove this once we can assume Autoconf -# 2.70 or later everywhere, but since Autoconf mutates rapidly -# enough in this area it's likely we'll need to redefine -# AC_USE_SYSTEM_EXTENSIONS for quite some time. - -# If autoconf reports a warning -# warning: AC_COMPILE_IFELSE was called before AC_USE_SYSTEM_EXTENSIONS -# or warning: AC_RUN_IFELSE was called before AC_USE_SYSTEM_EXTENSIONS -# the fix is -# 1) to ensure that AC_USE_SYSTEM_EXTENSIONS is never directly invoked -# but always AC_REQUIREd, -# 2) to ensure that for each occurrence of -# AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS]) -# or -# AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS]) -# the corresponding gnulib module description has 'extensions' among -# its dependencies. This will ensure that the gl_USE_SYSTEM_EXTENSIONS -# invocation occurs in gl_EARLY, not in gl_INIT. - -# AC_USE_SYSTEM_EXTENSIONS -# ------------------------ -# Enable extensions on systems that normally disable them, -# typically due to standards-conformance issues. -# -# Remember that #undef in AH_VERBATIM gets replaced with #define by -# AC_DEFINE. The goal here is to define all known feature-enabling -# macros, then, if reports of conflicts are made, disable macros that -# cause problems on some platforms (such as __EXTENSIONS__). -AC_DEFUN_ONCE([AC_USE_SYSTEM_EXTENSIONS], -[AC_BEFORE([$0], [AC_COMPILE_IFELSE])dnl -AC_BEFORE([$0], [AC_RUN_IFELSE])dnl - - AC_CHECK_HEADER([minix/config.h], [MINIX=yes], [MINIX=]) - if test "$MINIX" = yes; then - AC_DEFINE([_POSIX_SOURCE], [1], - [Define to 1 if you need to in order for 'stat' and other - things to work.]) - AC_DEFINE([_POSIX_1_SOURCE], [2], - [Define to 2 if the system does not provide POSIX.1 features - except with this defined.]) - AC_DEFINE([_MINIX], [1], - [Define to 1 if on MINIX.]) - AC_DEFINE([_NETBSD_SOURCE], [1], - [Define to 1 to make NetBSD features available. MINIX 3 needs this.]) - fi - -dnl Use a different key than __EXTENSIONS__, as that name broke existing -dnl configure.ac when using autoheader 2.62. - AH_VERBATIM([USE_SYSTEM_EXTENSIONS], -[/* Enable extensions on AIX 3, Interix. */ -#ifndef _ALL_SOURCE -# undef _ALL_SOURCE -#endif -/* Enable general extensions on macOS. */ -#ifndef _DARWIN_C_SOURCE -# undef _DARWIN_C_SOURCE -#endif -/* Enable GNU extensions on systems that have them. */ -#ifndef _GNU_SOURCE -# undef _GNU_SOURCE -#endif -/* Enable NetBSD extensions on NetBSD. */ -#ifndef _NETBSD_SOURCE -# undef _NETBSD_SOURCE -#endif -/* Enable OpenBSD extensions on NetBSD. */ -#ifndef _OPENBSD_SOURCE -# undef _OPENBSD_SOURCE -#endif -/* Enable threading extensions on Solaris. */ -#ifndef _POSIX_PTHREAD_SEMANTICS -# undef _POSIX_PTHREAD_SEMANTICS -#endif -/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */ -#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -# undef __STDC_WANT_IEC_60559_ATTRIBS_EXT__ -#endif -/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */ -#ifndef __STDC_WANT_IEC_60559_BFP_EXT__ -# undef __STDC_WANT_IEC_60559_BFP_EXT__ -#endif -/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */ -#ifndef __STDC_WANT_IEC_60559_DFP_EXT__ -# undef __STDC_WANT_IEC_60559_DFP_EXT__ -#endif -/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */ -#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__ -# undef __STDC_WANT_IEC_60559_FUNCS_EXT__ -#endif -/* Enable extensions specified by ISO/IEC TS 18661-3:2015. */ -#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__ -# undef __STDC_WANT_IEC_60559_TYPES_EXT__ -#endif -/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */ -#ifndef __STDC_WANT_LIB_EXT2__ -# undef __STDC_WANT_LIB_EXT2__ -#endif -/* Enable extensions specified by ISO/IEC 24747:2009. */ -#ifndef __STDC_WANT_MATH_SPEC_FUNCS__ -# undef __STDC_WANT_MATH_SPEC_FUNCS__ -#endif -/* Enable extensions on HP NonStop. */ -#ifndef _TANDEM_SOURCE -# undef _TANDEM_SOURCE -#endif -/* Enable X/Open extensions if necessary. HP-UX 11.11 defines - mbstate_t only if _XOPEN_SOURCE is defined to 500, regardless of - whether compiling with -Ae or -D_HPUX_SOURCE=1. */ -#ifndef _XOPEN_SOURCE -# undef _XOPEN_SOURCE -#endif -/* Enable general extensions on Solaris. */ -#ifndef __EXTENSIONS__ -# undef __EXTENSIONS__ -#endif -]) - AC_CACHE_CHECK([whether it is safe to define __EXTENSIONS__], - [ac_cv_safe_to_define___extensions__], - [AC_COMPILE_IFELSE( - [AC_LANG_PROGRAM([[ -# define __EXTENSIONS__ 1 - ]AC_INCLUDES_DEFAULT])], - [ac_cv_safe_to_define___extensions__=yes], - [ac_cv_safe_to_define___extensions__=no])]) - test $ac_cv_safe_to_define___extensions__ = yes && - AC_DEFINE([__EXTENSIONS__]) - AC_DEFINE([_ALL_SOURCE]) - AC_DEFINE([_DARWIN_C_SOURCE]) - AC_DEFINE([_GNU_SOURCE]) - AC_DEFINE([_NETBSD_SOURCE]) - AC_DEFINE([_OPENBSD_SOURCE]) - AC_DEFINE([_POSIX_PTHREAD_SEMANTICS]) - AC_DEFINE([__STDC_WANT_IEC_60559_ATTRIBS_EXT__]) - AC_DEFINE([__STDC_WANT_IEC_60559_BFP_EXT__]) - AC_DEFINE([__STDC_WANT_IEC_60559_DFP_EXT__]) - AC_DEFINE([__STDC_WANT_IEC_60559_FUNCS_EXT__]) - AC_DEFINE([__STDC_WANT_IEC_60559_TYPES_EXT__]) - AC_DEFINE([__STDC_WANT_LIB_EXT2__]) - AC_DEFINE([__STDC_WANT_MATH_SPEC_FUNCS__]) - AC_DEFINE([_TANDEM_SOURCE]) - AC_CACHE_CHECK([whether _XOPEN_SOURCE should be defined], - [ac_cv_should_define__xopen_source], - [ac_cv_should_define__xopen_source=no - AC_COMPILE_IFELSE( - [AC_LANG_PROGRAM([[ - #include - mbstate_t x;]])], - [], - [AC_COMPILE_IFELSE( - [AC_LANG_PROGRAM([[ - #define _XOPEN_SOURCE 500 - #include - mbstate_t x;]])], - [ac_cv_should_define__xopen_source=yes])])]) - test $ac_cv_should_define__xopen_source = yes && - AC_DEFINE([_XOPEN_SOURCE], [500]) -])# AC_USE_SYSTEM_EXTENSIONS - -# gl_USE_SYSTEM_EXTENSIONS -# ------------------------ -# Enable extensions on systems that normally disable them, -# typically due to standards-conformance issues. -AC_DEFUN_ONCE([gl_USE_SYSTEM_EXTENSIONS], -[ - dnl Require this macro before AC_USE_SYSTEM_EXTENSIONS. - dnl gnulib does not need it. But if it gets required by third-party macros - dnl after AC_USE_SYSTEM_EXTENSIONS is required, autoconf 2.62..2.63 emit a - dnl warning: "AC_COMPILE_IFELSE was called before AC_USE_SYSTEM_EXTENSIONS". - dnl Note: We can do this only for one of the macros AC_AIX, AC_GNU_SOURCE, - dnl AC_MINIX. If people still use AC_AIX or AC_MINIX, they are out of luck. - AC_REQUIRE([AC_GNU_SOURCE]) - - AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS]) -]) diff --git a/m4/timer_time.m4 b/m4/timer_time.m4 deleted file mode 100644 index d5c35c5..0000000 --- a/m4/timer_time.m4 +++ /dev/null @@ -1,44 +0,0 @@ -# timer_time.m4 serial 3 -dnl Copyright (C) 2011-2018 Free Software Foundation, Inc. -dnl This file is free software; the Free Software Foundation -dnl gives unlimited permission to copy and/or distribute it, -dnl with or without modifications, as long as this notice is preserved. - -# Check for timer_settime, and set LIB_TIMER_TIME. - -AC_DEFUN([gl_TIMER_TIME], -[ - dnl Based on clock_time.m4. See details there. - - AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS]) - - dnl Test whether the gnulib module 'threadlib' is in use. - dnl Some packages like Emacs use --avoid=threadlib. - dnl Write the symbol in such a way that it does not cause 'aclocal' to pick - dnl the threadlib.m4 file that is installed in $PREFIX/share/aclocal/. - m4_ifdef([gl_][THREADLIB], [AC_REQUIRE([gl_][THREADLIB])]) - - LIB_TIMER_TIME= - AC_SUBST([LIB_TIMER_TIME]) - gl_saved_libs=$LIBS - AC_SEARCH_LIBS([timer_settime], [rt posix4], - [test "$ac_cv_search_timer_settime" = "none required" || - LIB_TIMER_TIME=$ac_cv_search_timer_settime]) - m4_ifdef([gl_][THREADLIB], - [dnl GLIBC uses threads to emulate posix timers when kernel support - dnl is not available (like Linux < 2.6 or when used with kFreeBSD) - dnl Now the pthread lib is linked automatically in the normal case, - dnl but when linking statically, it needs to be explicitly specified. - AC_EGREP_CPP([Thread], - [#include - #ifdef __GNU_LIBRARY__ - #if ((__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2) || (__GLIBC__ > 2)) \ - && !(__UCLIBC__ && __HAS_NO_THREADS__) - Thread emulation available - #endif - #endif - ], - [LIB_TIMER_TIME="$LIB_TIMER_TIME $LIBMULTITHREAD"])]) - AC_CHECK_FUNCS([timer_settime]) - LIBS=$gl_saved_libs -]) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..ed41000 --- /dev/null +++ b/meson.build @@ -0,0 +1,708 @@ +project('cjs', 'cpp', 'c', version: '4.7.0', license: ['MIT', 'LGPL2+'], + meson_version: '>= 0.49.2', + default_options: ['cpp_std=c++17', 'c_std=c99', 'warning_level=2']) + +api_version = '1.0' +api_name = '@0@-@1@'.format(meson.project_name(), api_version) + +gnome = import('gnome') +pkg = import('pkgconfig') + +top_include = include_directories('.') + +### Check for conflicting build options ######################################## + +if get_option('systemtap') and not get_option('dtrace') + error('-Ddtrace=true is required for -Dsystemtap=true') +endif + +if get_option('buildtype').startswith('release') and get_option('verbose_logs') + error('-Dverbose_logs=true is not allowed with --buildtype=release') +endif + +### Check for compiler args #################################################### + +cxx = meson.get_compiler('cpp') +cc = meson.get_compiler('c') + +if cc.get_id() == 'msvc' + add_project_arguments(cxx.get_supported_arguments([ + '-utf-8', # Use UTF-8 mode + '/Zc:externConstexpr', # Required for 'extern constexpr' on MSVC + + # Ignore spurious compiler warnings for things that GLib and SpiderMonkey + # header files commonly do + '-FImsvc_recommended_pragmas.h', + '-EHsc', + '-D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS', # Don't worry about the C++17 deprecations + '-wd4099', + '-wd4251', + '-wd4291', + '-wd4800', + ]), language: ['cpp', 'c']) +else + # Ignore spurious compiler warnings for things that GLib and SpiderMonkey + # header files commonly do + add_project_arguments(cxx.get_supported_arguments([ + '-fno-strict-aliasing', + '-Wno-variadic-macros', # GLib uses these in header files + '-Wno-missing-field-initializers', # SpiderMonkey JSClass, among others + ]), language: 'cpp') + + add_project_arguments(cc.get_supported_arguments([ + '-Wno-typedef-redefinition', # GLib does this in header files + ]), language: 'c') +endif + +if cc.get_argument_syntax() == 'msvc' + add_project_arguments(cxx.get_supported_arguments([ + '-Dssize_t=gssize', # Windows SDK/MSVC headers do not come with ssize_t + '-DNOMINMAX', # We don't want 'min' or 'max' to interfere + ]), language: ['cpp', 'c']) +else + if get_option('bsymbolic_functions') + if not cxx.has_link_argument('-Bsymbolic-functions') + error('''-Bsymbolic-functions not supported, configure with +-Dbsymbolic_functions=false''') + endif + add_project_link_arguments('-Bsymbolic-functions', language: ['cpp', 'c']) + if cc.has_argument('-fno-semantic-interposition') + add_project_arguments('-fno-semantic-interposition', language: 'c') + endif + if cxx.has_argument('-fno-semantic-interposition') + add_project_arguments('-fno-semantic-interposition', language: 'cpp') + endif + endif +endif + +# SpiderMonkey can be compiled with or without runtime type information, we must +# match that option because we need to derive from SpiderMonkey classes +if get_option('spidermonkey_rtti') + if cxx.has_argument('-GR-') + add_project_arguments('-GR-', language: 'cpp') # MSVC/clang-cl option + endif +else + if cxx.has_argument('-fno-rtti') + add_project_arguments('-fno-rtti', language: 'cpp') + # -fno-rtti is not compatible with the vptr sanitizer (part of ubsan) + if get_option('b_sanitize') != 'none' and cxx.has_argument('-fno-sanitize=vptr') + add_project_arguments('-fno-sanitize=vptr', language: 'cpp') + endif + endif +endif + +if get_option('verbose_logs') + add_project_arguments([ + '-DGJS_VERBOSE_ENABLE_PROPS=1', + '-DGJS_VERBOSE_ENABLE_MARSHAL=1', + '-DGJS_VERBOSE_ENABLE_LIFECYCLE=1', + '-DGJS_VERBOSE_ENABLE_GI_USAGE=1', + '-DGJS_VERBOSE_ENABLE_GCLOSURE=1', + '-DGJS_VERBOSE_ENABLE_GSIGNAL=1', + ], language: 'cpp') +endif + +if get_option('buildtype').startswith('release') + add_project_arguments('-DG_DISABLE_CAST_CHECKS', language: ['c', 'cpp']) +endif + +### Check for required libraries ############################################### + +glib_required_version = '>= 2.58.0' +glib = dependency('glib-2.0', version: glib_required_version, + fallback: ['glib', 'libglib_dep']) +gthread = dependency('gthread-2.0', version: glib_required_version, + fallback: ['glib', 'libgthread_dep']) +gobject = dependency('gobject-2.0', version: glib_required_version, + fallback: ['glib', 'libgobject_dep']) +gio = dependency('gio-2.0', version: glib_required_version, + fallback: ['glib', 'libgio_dep']) +ffi = dependency('libffi', fallback: ['libffi', 'ffi_dep']) +gi = dependency('gobject-introspection-1.0', version: '>= 1.58.3', + fallback: ['gobject-introspection', 'girepo_dep']) +spidermonkey = dependency('mozjs-78') + +# We might need to look for the headers and lib's for Cairo +# manually on MSVC/clang-cl builds... +cairo = dependency('cairo', required: get_option('cairo').enabled() and cxx.get_argument_syntax() != 'msvc') +cairo_gobject = dependency('cairo-gobject', required: cairo.found() and cxx.get_argument_syntax() != 'msvc') +cairo_xlib = dependency('cairo-xlib', required: false) + +if cxx.get_argument_syntax() == 'msvc' + if not cairo.found() + cairo = cc.find_library('cairo', has_headers: ['cairo.h'], required: get_option('cairo').enabled()) + endif + if not cairo_gobject.found() + cairo_gobject = cc.find_library('cairo-gobject', has_headers: ['cairo-gobject.h'], required: cairo.found()) + endif +endif + +sysprof_capture = dependency('sysprof-capture-4', + required: get_option('profiler'), include_type: 'system', + fallback: ['sysprof', 'libsysprof_capture_dep'], + default_options: [ + 'enable_examples=false', + 'enable_gtk=false', + 'enable_tests=false', + 'enable_tools=false', + 'libsysprof=false', + 'with_sysprofd=none', + 'help=false', + ]) + +readline = cxx.find_library('readline', required: get_option('readline')) +# On some systems we need to link readline to a termcap compatible library +readline_code = ''' +#include +#include +int main(void) { + readline("foo"); + return 0; +}''' +readline_deps = [readline] +if readline.found() and not cxx.links(readline_code, dependencies: readline) + extra_readline_libs = ['ncursesw', 'ncurses', 'curses', 'termcap'] + found = false + foreach lib : extra_readline_libs + termcap = cxx.find_library(lib, required: false) + if cxx.links(readline_code, dependencies: [readline, termcap]) + found = true + readline_deps += termcap + break + endif + endforeach + if not found + error('''Couldn't figure out how to link +readline library. Configure with -Dreadline=disabled to skip the readline +features.''') + endif +endif + +build_profiler = sysprof_capture.found() and not get_option('profiler').disabled() +profiler_deps = [sysprof_capture] +if build_profiler and not cxx.has_function('timer_settime') + extra_timer_libs = ['rt', 'posix4'] + found = false + foreach lib : extra_timer_libs + timer_lib = cxx.find_library(lib, required: false) + if cxx.has_function('timer_settime', dependencies: timer_lib) + found = true + profiler_deps += timer_lib + break + endif + endforeach + if not found or not cxx.has_header_symbol('signal.h', 'SIGEV_THREAD_ID') + if get_option('profiler').enabled() + error('''The profiler is currently only +supported on Linux. The standard library must support timer_settime() and +SIGEV_THREAD_ID. Configure with -Dprofiler=auto or -Dprofiler=disabled to skip +it on other platforms.''') + endif + build_profiler = false + endif +endif + +build_cairo = cairo.found() and not get_option('cairo').disabled() +build_readline = readline.found() and not get_option('readline').disabled() + +### Check for library features ################################################# + +# Check if SpiderMonkey was compiled with --enable-debug. If this is the case, +# you must compile all your sources with -DDEBUG=1 +# See https://bugzilla.mozilla.org/show_bug.cgi?id=1261161 +debug_arg = [] +nondebug_spidermonkey = cxx.compiles(''' +#include +#ifdef JS_DEBUG +#error debug yes, if we did not already error out due to DEBUG not being defined +#endif +''', + dependencies: spidermonkey, + name: 'SpiderMonkey is a non-debug build') + +if not nondebug_spidermonkey + debug_arg = ['-DDEBUG'] # for compile tests +endif + +if get_option('buildtype').startswith('release') and not nondebug_spidermonkey + error('''You are trying to make a release +build with a debug-enabled copy of SpiderMonkey. This is probably not what you +want, since it will have bad performance and is not binary-compatible with +release builds of SpiderMonkey. Try configuring SpiderMonkey with +--disable-debug.''') +endif + +# Check if a minimal SpiderMonkey program compiles, links, and runs. If not, +# it's most likely the case that SpiderMonkey was configured incorrectly, for +# example by building mozglue as a shared library. +minimal_program = cxx.run(''' +#include +int main(void) { + if (!JS_Init()) return 1; + JS_ShutDown(); + return 0; +} +''', + args: debug_arg, dependencies: spidermonkey, + name: 'SpiderMonkey sanity check') + +if not minimal_program.compiled() or minimal_program.returncode() != 0 + error('''A minimal SpiderMonkey program +could not be compiled, linked, or run. Most likely you should build it with a +different configuration. Check the recommended configuration: +https://github.com/spidermonkey-embedders/spidermonkey-embedding-examples/blob/esr78/docs/Building%20SpiderMonkey.md''') +endif + +have_printf_alternative_int = cc.compiles(''' +#include +int main(void) { + printf("%Id", (int)0); + return 0; +} +''', + args: ['-Werror', '-Wformat'], + name: 'printf() supports %I alternative int syntax') + +### Check for external programs ################################################ + +dtrace = find_program('dtrace', required: get_option('dtrace')) +dbus_run_session = find_program('dbus-run-session', + required: not get_option('skip_dbus_tests')) +glib_compile_schemas = find_program('glib-compile-schemas') + +### Generate config.h ########################################################## + +header_conf = configuration_data() + +versions = meson.project_version().split('.') +major_version = versions[0].to_int() +minor_version = versions[1].to_int() +micro_version = versions[2].to_int() +int_version = (major_version * 100 + minor_version) * 100 + micro_version +header_conf.set_quoted('VERSION', meson.project_version()) +header_conf.set('GJS_VERSION', int_version, + description: 'The GJS version as an integer') +header_conf.set_quoted('PACKAGE_STRING', '@0@ @1@'.format(meson.project_name(), + meson.project_version())) + +header_conf.set('ENABLE_CAIRO', build_cairo, + description: 'Build with Cairo support') +header_conf.set('ENABLE_PROFILER', build_profiler, + description: 'Build the profiler') +# COMPAT: SpiderMonkey headers in some places use DEBUG instead of JS_DEBUG +# https://bugzilla.mozilla.org/show_bug.cgi?id=1261161 */ +header_conf.set('DEBUG', not nondebug_spidermonkey, + description: 'SpiderMonkey was compiled with --enable-debug') +header_conf.set('HAVE_DTRACE', get_option('dtrace'), + description: 'Using dtrace probes') +header_conf.set('HAVE_PRINTF_ALTERNATIVE_INT', have_printf_alternative_int, + description: 'printf() accepts "%Id" for alternative integer output') +if build_readline + header_conf.set('HAVE_READLINE_READLINE_H', + cxx.check_header('readline/readline.h', prefix: '#include ', + required: readline.found())) +endif +header_conf.set('HAVE_SYS_SYSCALL_H', cxx.check_header('sys/syscall.h')) +header_conf.set('HAVE_UNISTD_H', cxx.check_header('unistd.h')) +header_conf.set('HAVE_SIGNAL_H', cxx.check_header('signal.h', + required: build_profiler)) + +# enable GNU extensions on systems that have them +header_conf.set('_GNU_SOURCE', 1) + +configure_file(output: 'config.h', configuration: header_conf) + +### Check for environment ###################################################### + +gjsjsdir = get_option('datadir') / api_name +abs_datadir = get_option('prefix') / get_option('datadir') +pkglibdir = get_option('libdir') / meson.project_name() + +### Build dtrace probes ######################################################## + +if get_option('dtrace') + probes_header_gen = generator(dtrace, output: '@BASENAME@.h', + arguments: ['-C', '-h', '-s', '@INPUT@', '-o', '@OUTPUT@']) + probes_objfile_gen = generator(dtrace, output: '@BASENAME@.o', + arguments: ['-G', '-s', '@INPUT@', '-o', '@OUTPUT@']) + probes_header = probes_header_gen.process('gi/gjs_gi_probes.d') + probes_objfile = probes_objfile_gen.process('gi/gjs_gi_probes.d') +else + probes_header = [] + probes_objfile = [] +endif + +tapset_subst = configuration_data({ + 'EXPANDED_LIBDIR': get_option('libdir'), +}) +tapset = configure_file(input: 'cjs/cjs.stp.in', output: 'cjs.stp', + configuration: tapset_subst) +if get_option('systemtap') + install_data(tapset, + install_dir: get_option('datadir') / 'systemtap/tapset') +endif + +### Build library ############################################################## + +directory_defines = [ + '-DGJS_JS_DIR="@0@"'.format(get_option('prefix') / gjsjsdir), + '-DPKGLIBDIR="@0@"'.format(get_option('prefix') / pkglibdir), +] + +gjs_public_headers = [ + 'cjs/context.h', + 'cjs/coverage.h', + 'cjs/error-types.h', + 'cjs/gjs.h', + 'cjs/macros.h', + 'cjs/mem.h', + 'cjs/profiler.h', +] + +# For historical reasons, some files live in gi/ +# Some headers in the following list were formerly public + +libgjs_sources = [ + 'gi/arg.cpp', 'gi/arg.h', 'gi/arg-inl.h', + 'gi/arg-cache.cpp', 'gi/arg-cache.h', + 'gi/boxed.cpp', 'gi/boxed.h', + 'gi/closure.cpp', 'gi/closure.h', + 'gi/enumeration.cpp', 'gi/enumeration.h', + 'gi/foreign.cpp', 'gi/foreign.h', + 'gi/fundamental.cpp', 'gi/fundamental.h', + 'gi/function.cpp', 'gi/function.h', + 'gi/gerror.cpp', 'gi/gerror.h', + 'gi/gjs_gi_trace.h', + 'gi/gobject.cpp', 'gi/gobject.h', + 'gi/gtype.cpp', 'gi/gtype.h', + 'gi/interface.cpp', 'gi/interface.h', + 'gi/ns.cpp', 'gi/ns.h', + 'gi/object.cpp', 'gi/object.h', + 'gi/param.cpp', 'gi/param.h', + 'gi/private.cpp', 'gi/private.h', + 'gi/repo.cpp', 'gi/repo.h', + 'gi/toggle.cpp', 'gi/toggle.h', + 'gi/union.cpp', 'gi/union.h', + 'gi/utils-inl.h', + 'gi/value.cpp', 'gi/value.h', + 'gi/wrapperutils.cpp', 'gi/wrapperutils.h', + 'cjs/atoms.cpp', 'cjs/atoms.h', + 'cjs/byteArray.cpp', 'cjs/byteArray.h', + 'cjs/context.cpp', 'cjs/context-private.h', + 'cjs/coverage.cpp', + 'cjs/debugger.cpp', + 'cjs/deprecation.cpp', 'cjs/deprecation.h', + 'cjs/engine.cpp', 'cjs/engine.h', + 'cjs/error-types.cpp', + 'cjs/global.cpp', 'cjs/global.h', + 'cjs/importer.cpp', 'cjs/importer.h', + 'cjs/mem.cpp', 'cjs/mem-private.h', + 'cjs/module.cpp', 'cjs/module.h', + 'cjs/native.cpp', 'cjs/native.h', + 'cjs/profiler.cpp', 'cjs/profiler-private.h', + 'cjs/stack.cpp', + 'modules/console.cpp', 'modules/console.h', + 'modules/modules.cpp', 'modules/modules.h', + 'modules/print.cpp', 'modules/print.h', + 'modules/system.cpp', 'modules/system.h', +] + +# GjsPrivate introspection sources +libgjs_private_sources = [ + 'libgjs-private/gjs-gdbus-wrapper.c', 'libgjs-private/gjs-gdbus-wrapper.h', + 'libgjs-private/gjs-util.c', 'libgjs-private/gjs-util.h', +] + +libgjs_jsapi_sources = [ + 'cjs/jsapi-class.h', + 'cjs/jsapi-dynamic-class.cpp', + 'cjs/jsapi-util-args.h', + 'cjs/jsapi-util-error.cpp', + 'cjs/jsapi-util-root.h', + 'cjs/jsapi-util-string.cpp', + 'cjs/jsapi-util.cpp', 'cjs/jsapi-util.h', + 'util/log.cpp', 'util/log.h', + 'util/misc.cpp', 'util/misc.h', +] + +module_cairo_srcs = [ + 'modules/cairo-private.h', + 'modules/cairo-module.h', + 'modules/cairo-region.cpp', + 'modules/cairo-context.cpp', + 'modules/cairo-path.cpp', + 'modules/cairo-surface.cpp', + 'modules/cairo-image-surface.cpp', + 'modules/cairo-ps-surface.cpp', + 'modules/cairo-pdf-surface.cpp', + 'modules/cairo-svg-surface.cpp', + 'modules/cairo-pattern.cpp', + 'modules/cairo-gradient.cpp', + 'modules/cairo-linear-gradient.cpp', + 'modules/cairo-radial-gradient.cpp', + 'modules/cairo-surface-pattern.cpp', + 'modules/cairo-solid-pattern.cpp', + 'modules/cairo.cpp', +] + +module_resource_srcs = gnome.compile_resources('js-resources', + 'js.gresource.xml', + c_name: 'js_resources') + +libgjs_dependencies = [glib, gobject, gthread, gio, gi, ffi, spidermonkey, + readline] +pkg_dependencies = [glib, gobject, gthread, gio, gi, ffi, spidermonkey] +libraries_private = [] + +if build_cairo + libgjs_sources += module_cairo_srcs + libgjs_dependencies += [cairo, cairo_gobject] + if cairo.type_name() == 'pkgconfig' + pkg_dependencies += [cairo] + elif cairo.type_name() == 'library' + libraries_private += cairo + endif + if cairo_gobject.type_name() == 'pkgconfig' + pkg_dependencies += [cairo_gobject] + elif cairo_gobject.type_name() == 'library' + libraries_private += cairo_gobject + endif + if cairo_xlib.found() + libgjs_dependencies += cairo_xlib + pkg_dependencies += cairo_xlib + endif +endif + +if build_profiler + libgjs_dependencies += profiler_deps +endif + +link_args = [] +if cxx.has_link_argument('-Wl,-no-undefined') + link_args += '-Wl,-no-undefined' +else + # -undefined error is the equivalent of -no-undefined for the macOS linker, + # but -undefined would also be understood as a valid argument for GNU ld! + link_args += cxx.get_supported_link_arguments('-Wl,-undefined,error') +endif + +libgjs_cpp_args = ['-DGJS_COMPILATION'] + directory_defines + +# Check G-I and/or Meson on this one. +libgjs_cpp_args += ['-DG_LOG_DOMAIN="Cjs"'] + +if host_machine.system() == 'windows' + # We need these defines to build properly for all Windows builds + libgjs_cpp_args += ['-DWIN32', '-DXP_WIN'] +endif + +libgjs_jsapi = static_library(meson.project_name() + '-jsapi', + libgjs_jsapi_sources, probes_header, probes_objfile, + cpp_args: libgjs_cpp_args, + link_args: link_args, + dependencies: libgjs_dependencies, + install: false) + +symbol_map = files('libgjs.map') +symbol_list = files('libgjs.symbols') +link_args += cxx.get_supported_link_arguments([ + '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), + symbol_map[0]), + '-Wl,-exported_symbols_list,@0@/@1@'.format(meson.current_source_dir(), + symbol_list[0]), # macOS linker +]) + +libgjs = shared_library(meson.project_name(), + libgjs_sources, libgjs_private_sources, module_resource_srcs, + probes_header, probes_objfile, + cpp_args: libgjs_cpp_args, + link_args: link_args, link_depends: [symbol_map, symbol_list], + link_with: libgjs_jsapi, + dependencies: libgjs_dependencies, + version: '0.0.0', soversion: '0', + gnu_symbol_visibility: 'hidden', + install: true) + +install_headers(gjs_public_headers, subdir: api_name / 'cjs') + +# Allow using libgjs as a subproject +libgjs_dep = declare_dependency(link_with: [libgjs, libgjs_jsapi], + dependencies: libgjs_dependencies, include_directories: top_include) + +### Build GjsPrivate introspection library ##################################### + +gjs_private_gir = gnome.generate_gir(libgjs, + includes: ['GObject-2.0', 'Gio-2.0'], sources: libgjs_private_sources, + namespace: 'CjsPrivate', nsversion: '1.0', identifier_prefix: 'Gjs', + symbol_prefix: 'gjs_', extra_args: '--warn-error', install: true, + install_dir_gir: false, install_dir_typelib: pkglibdir / 'girepository-1.0') +gjs_private_typelib = gjs_private_gir[1] + +### Build gjs-console interpreter ############################################## + +gjs_console_srcs = ['cjs/console.cpp'] + +gjs_console = executable('cjs-console', gjs_console_srcs, + cpp_args: libgjs_cpp_args, + dependencies: libgjs_dep, install: true) + +meson.add_install_script('build/symlink-gjs.py', get_option('bindir')) + +### Install data files ######################################################### + +install_data('installed-tests/extra/gjs.supp', + install_dir: get_option('datadir') / api_name / 'valgrind') +install_data('installed-tests/extra/lsan.supp', + install_dir: get_option('datadir') / api_name / 'lsan') + +if get_option('installed_tests') + schemadir = abs_datadir / 'glib-2.0' / 'schemas' + install_data('installed-tests/js/org.cinnamon.CjsTest.gschema.xml', install_dir: schemadir) + meson.add_install_script('build/compile-gschemas.py', schemadir) +endif + +### Generate pkg-config file ################################################### + +pkg.generate(libgjs, name: api_name, description: 'JS bindings for GObjects', + requires: [glib, gobject, gio], requires_private: pkg_dependencies, + libraries_private: libraries_private, + subdirs: api_name, + variables: [ + 'exec_prefix=${prefix}', + 'bindir=${exec_prefix}' / get_option('bindir'), + 'datarootdir=${prefix}' / get_option('datadir'), + 'datadir=${datarootdir}', + 'gjs_console=${bindir}/cjs-console', + ]) + +### Test environment ########################################################### + +tests_environment = environment() +js_tests_builddir = meson.current_build_dir() / 'installed-tests' / 'js' +# GJS_PATH is empty here since we want to force the use of our own +# resources. G_FILENAME_ENCODING ensures filenames are not UTF-8 +tests_environment.set('TOP_BUILDDIR', meson.build_root()) +tests_environment.set('GJS_USE_UNINSTALLED_FILES', '1') +tests_environment.set('GJS_PATH', '') +tests_environment.prepend('GI_TYPELIB_PATH', meson.current_build_dir(), + js_tests_builddir) +tests_environment.prepend('LD_LIBRARY_PATH', meson.current_build_dir(), + js_tests_builddir) +tests_environment.set('G_FILENAME_ENCODING', 'latin1') +tests_environment.set('LSAN_OPTIONS', 'exitcode=23,suppressions=@0@'.format( + meson.current_source_dir() / 'installed-tests' / 'extra' / 'lsan.supp')) +tests_environment.set('NO_AT_BRIDGE', '1') +tests_environment.set('GSETTINGS_SCHEMA_DIR', js_tests_builddir) +tests_environment.set('GSETTINGS_BACKEND', 'memory') +tests_environment.set('G_DEBUG', 'fatal-warnings,fatal-criticals') + +tests_locale = 'N/A' +if cxx.get_argument_syntax() != 'msvc' + result = run_command('build/choose-tests-locale.sh') + if result.returncode() == 0 + tests_locale = result.stdout().strip() + tests_environment.set('LC_ALL', tests_locale) + endif +endif + +if not get_option('skip_gtk_tests') + tests_environment.set('ENABLE_GTK', 'yes') +endif + +if get_option('b_coverage') + tests_environment.set('GJS_UNIT_COVERAGE_OUTPUT', 'lcov') + tests_environment.set('GJS_UNIT_COVERAGE_PREFIX', + 'resource:///org/gnome/gjs') +endif + +### Tests and test setups ###################################################### + +# Note: The test program in test/ needs to be ported +# to Windows before we can build it on Windows. +if host_machine.system() != 'windows' + subdir('test') +endif + +if not get_option('skip_gtk_tests') + have_gtk4 = dependency('gtk4', required: false).found() +endif + +subdir('installed-tests') + +valgrind_environment = environment() +valgrind_environment.set('G_SLICE', 'always-malloc,debug-blocks') +valgrind_environment.set('G_DEBUG', + 'fatal-warnings,fatal-criticals,gc-friendly') +valgrind_environment.set('VALGRIND', 'valgrind') + +glib_suppresssions = (glib.get_pkgconfig_variable('prefix') / 'share' / + 'glib-2.0' / 'valgrind' / 'glib.supp') +gjs_suppressions = (meson.current_source_dir() / 'installed-tests' / 'extra' / + 'cjs.supp') +valgrind_args = [ + '--suppressions=@0@'.format(glib_suppresssions), + '--suppressions=@0@'.format(gjs_suppressions), + '--leak-check=full', + '--num-callers=15', + '--trace-children=yes', + '--trace-children-skip=*basename,*cat,*diff,*echo,*grep,*rm,*sed,*stat,*true', + '--error-exitcode=1' +] + +add_test_setup('valgrind', timeout_multiplier: 40, env: valgrind_environment, + exe_wrapper: ['valgrind'] + valgrind_args) + +zeal2_environment = environment() +zeal2_environment.set('JS_GC_ZEAL', '2,10') +add_test_setup('extra_gc', timeout_multiplier: 40, env: zeal2_environment) + +zeal4_environment = environment() +zeal4_environment.set('JS_GC_ZEAL', '4') +add_test_setup('pre_verify', timeout_multiplier: 40, env: zeal4_environment) + +zeal11_environment = environment() +zeal11_environment.set('JS_GC_ZEAL', '11') +add_test_setup('post_verify', timeout_multiplier: 2, env: zeal11_environment) + +### Warn about conditions that may affect runtime ############################## + +if gi.version().version_compare('<1.61.2') + warning('''You do not have a new enough version of +gobject-introspection to run the tests. You can still build GJS, but some +tests will fail.''') +endif + +if tests_locale == 'C' or tests_locale == 'N/A' + warning('''Your libc does not have the C.UTF-8 locale and no other +suitable UTF-8 fallback locale could be found. You can still build GJS, but +some tests will fail.''') +endif + +if get_option('buildtype').startswith('debug') and nondebug_spidermonkey + warning('''Your copy of SpiderMonkey is not debug-enabled, but you are +building a debug or debugoptimized build. This will make development more +difficult. Consider reconfiguring SpiderMonkey with --enable-debug.''') +endif + +if not build_cairo + warning('Building without Cairo support, not all tests will be run.') +endif + +if get_option('skip_gtk_tests') + warning('Not using GTK, not all tests will be run.') +endif + +if get_option('skip_dbus_tests') + warning('Not using DBus, not all tests will be run.') +endif + +### Summarize options ########################################################## + +message('\n'.join([ + 'Optional features', + '==========================', + 'Build Cairo module: @0@'.format(build_cairo), + 'Use readline for input in interactive shell and debugger: @0@'.format( + build_readline), + 'Build profiler (Linux only): @0@'.format(build_profiler), +])) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..825ba77 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,27 @@ +# Features + +option('cairo', type: 'feature', value: 'auto', + description: 'Build cairo module') +option('readline', type: 'feature', value: 'auto', + description: 'Use readline for input in interactive shell and debugger') +option('profiler', type: 'feature', value: 'auto', + description: 'Build profiler (Linux only)') + +# Flags + +option('installed_tests', type: 'boolean', value: true, + description: 'Install test programs') +option('dtrace', type: 'boolean', value: false, + description: 'Include dtrace trace support') +option('systemtap', type: 'boolean', value: false, + description: 'Include systemtap trace support (requires -Ddtrace=true)') +option('bsymbolic_functions', type: 'boolean', value: true, + description: 'Link with -Bsymbolic-functions linker flag used to avoid intra-library PLT jumps, if supported; not used for Visual Studio and clang-cl builds') +option('spidermonkey_rtti', type: 'boolean', value: false, + description: 'Needs to match SpiderMonkey\'s config option') +option('skip_dbus_tests', type: 'boolean', value: false, + description: 'Skip tests that use a DBus session bus') +option('skip_gtk_tests', type: 'boolean', value: false, + description: 'Skip tests that need a display connection') +option('verbose_logs', type: 'boolean', value: false, + description: 'Enable extra log messages that may decrease performance (not allowed in release builds)') diff --git a/modules/_bootstrap/default.js b/modules/_bootstrap/default.js deleted file mode 100644 index 29dbdb1..0000000 --- a/modules/_bootstrap/default.js +++ /dev/null @@ -1,7 +0,0 @@ -(function(exports) { - 'use strict'; - - // Do early initialization here. - void exports; - -})(window); diff --git a/modules/cairo-context.cpp b/modules/cairo-context.cpp index 95bf44e..29614cb 100644 --- a/modules/cairo-context.cpp +++ b/modules/cairo-context.cpp @@ -24,23 +24,39 @@ #include +#include +#include +#include +#include + +#include // for JS::NewArrayObject +#include +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for UniqueChars +#include +#include // for JS_SetElement + +#include "gi/arg-inl.h" +#include "gi/arg.h" #include "gi/foreign.h" #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" - -#include -#include -#include "cairo-private.h" - -#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(mname) \ -static bool \ -mname##_func(JSContext *context, \ - unsigned argc, \ - JS::Value *vp) \ -{ \ - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); \ - cairo_t *cr = priv ? priv->cr : NULL; +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" + +#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(mname) \ + GJS_JSAPI_RETURN_CONVENTION \ + static bool mname##_func(JSContext* context, unsigned argc, \ + JS::Value* vp) { \ + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); \ + if (!cr) \ + return true; #define _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END \ return gjs_cairo_check_status(context, cairo_status(cr), "context"); \ @@ -74,65 +90,76 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ argv.rval().setBoolean(ret); \ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END -#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC2FFAFF(method, cfunc, n1, n2) \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ - double arg1, arg2; \ - if (!gjs_parse_call_args(context, #method, argv, "ff", \ - #n1, &arg1, #n2, &arg2)) \ - return false; \ - cfunc(cr, &arg1, &arg2); \ - if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { \ - JS::RootedObject array(context, \ - JS_NewArrayObject(context, JS::HandleValueArray::empty())); \ - if (!array) \ - return false; \ - JS::RootedValue r(context, JS::NumberValue(arg1)); \ - if (!JS_SetElement(context, array, 0, r)) return false; \ - r.setNumber(arg2); \ - if (!JS_SetElement(context, array, 1, r)) return false; \ - argv.rval().setObject(*array); \ - } \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_END - -#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC0AFF(method, cfunc) \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ - double arg1, arg2; \ - _GJS_CAIRO_CONTEXT_CHECK_NO_ARGS(method) \ - cfunc(cr, &arg1, &arg2); \ - if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { \ - JS::RootedObject array(context, \ - JS_NewArrayObject(context, JS::HandleValueArray::empty())); \ - if (!array) \ - return false; \ - JS::RootedValue r(context, JS::NumberValue(arg1)); \ - if (!JS_SetElement(context, array, 0, r)) return false; \ - r.setNumber(arg2); \ - if (!JS_SetElement(context, array, 1, r)) return false; \ - argv.rval().setObject(*array); \ - } \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_END - -#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC0AFFFF(method, cfunc) \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ - double arg1, arg2, arg3, arg4; \ - _GJS_CAIRO_CONTEXT_CHECK_NO_ARGS(method) \ - cfunc(cr, &arg1, &arg2, &arg3, &arg4); \ - { \ - JS::RootedObject array(context, \ - JS_NewArrayObject(context, JS::HandleValueArray::empty())); \ - if (!array) \ - return false; \ - JS::RootedValue r(context, JS::NumberValue(arg1)); \ - if (!JS_SetElement(context, array, 0, r)) return false; \ - r.setNumber(arg2); \ - if (!JS_SetElement(context, array, 1, r)) return false; \ - r.setNumber(arg3); \ - if (!JS_SetElement(context, array, 2, r)) return false; \ - r.setNumber(arg4); \ - if (!JS_SetElement(context, array, 3, r)) return false; \ - argv.rval().setObject(*array); \ - } \ -_GJS_CAIRO_CONTEXT_DEFINE_FUNC_END +#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC2FFAFF(method, cfunc, n1, n2) \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ + double arg1, arg2; \ + if (!gjs_parse_call_args(context, #method, argv, "ff", #n1, &arg1, #n2, \ + &arg2)) \ + return false; \ + cfunc(cr, &arg1, &arg2); \ + if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { \ + JS::RootedObject array( \ + context, \ + JS::NewArrayObject(context, JS::HandleValueArray::empty())); \ + if (!array) \ + return false; \ + JS::RootedValue r(context, JS::NumberValue(arg1)); \ + if (!JS_SetElement(context, array, 0, r)) \ + return false; \ + r.setNumber(arg2); \ + if (!JS_SetElement(context, array, 1, r)) \ + return false; \ + argv.rval().setObject(*array); \ + } \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END + +#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC0AFF(method, cfunc) \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ + double arg1, arg2; \ + _GJS_CAIRO_CONTEXT_CHECK_NO_ARGS(method) \ + cfunc(cr, &arg1, &arg2); \ + if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) { \ + JS::RootedObject array( \ + context, \ + JS::NewArrayObject(context, JS::HandleValueArray::empty())); \ + if (!array) \ + return false; \ + JS::RootedValue r(context, JS::NumberValue(arg1)); \ + if (!JS_SetElement(context, array, 0, r)) \ + return false; \ + r.setNumber(arg2); \ + if (!JS_SetElement(context, array, 1, r)) \ + return false; \ + argv.rval().setObject(*array); \ + } \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END + +#define _GJS_CAIRO_CONTEXT_DEFINE_FUNC0AFFFF(method, cfunc) \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ + double arg1, arg2, arg3, arg4; \ + _GJS_CAIRO_CONTEXT_CHECK_NO_ARGS(method) \ + cfunc(cr, &arg1, &arg2, &arg3, &arg4); \ + { \ + JS::RootedObject array( \ + context, \ + JS::NewArrayObject(context, JS::HandleValueArray::empty())); \ + if (!array) \ + return false; \ + JS::RootedValue r(context, JS::NumberValue(arg1)); \ + if (!JS_SetElement(context, array, 0, r)) \ + return false; \ + r.setNumber(arg2); \ + if (!JS_SetElement(context, array, 1, r)) \ + return false; \ + r.setNumber(arg3); \ + if (!JS_SetElement(context, array, 2, r)) \ + return false; \ + r.setNumber(arg4); \ + if (!JS_SetElement(context, array, 3, r)) \ + return false; \ + argv.rval().setObject(*array); \ + } \ + _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END #define _GJS_CAIRO_CONTEXT_DEFINE_FUNC0F(method, cfunc) \ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ @@ -231,41 +258,21 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_BEGIN(method) \ argv.rval().setUndefined(); \ _GJS_CAIRO_CONTEXT_DEFINE_FUNC_END -typedef struct { - void *dummy; - JSContext *context; - JSObject *object; - cairo_t * cr; -} GjsCairoContext; - -static JSObject *gjs_cairo_context_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_context_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_GTYPE("Context", cairo_context, CAIRO_GOBJECT_TYPE_CONTEXT, JSCLASS_BACKGROUND_FINALIZE) -GJS_DEFINE_PRIV_FROM_JS(GjsCairoContext, gjs_cairo_context_class); +GJS_DEFINE_PRIV_FROM_JS(cairo_t, gjs_cairo_context_class); -static void -_gjs_cairo_context_construct_internal(JSContext *context, - JS::HandleObject obj, - cairo_t *cr) -{ - GjsCairoContext *priv; - - priv = g_slice_new0(GjsCairoContext); - - g_assert(priv_from_js(context, obj) == NULL); - JS_SetPrivate(obj, priv); - - priv->context = context; - priv->object = obj; - priv->cr = cairo_reference(cr); +static void _gjs_cairo_context_construct_internal(JSObject* obj, cairo_t* cr) { + g_assert(!JS_GetPrivate(obj)); + JS_SetPrivate(obj, cairo_reference(cr)); } GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_context) { GJS_NATIVE_CONSTRUCTOR_VARIABLES(cairo_context) - cairo_surface_t *surface; cairo_t *cr; GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_context); @@ -275,18 +282,17 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_context) "surface", &surface_wrapper)) return false; - surface = gjs_cairo_surface_get_surface(context, surface_wrapper); - if (!surface) { - gjs_throw(context, "first argument to Context() should be a surface"); + cairo_surface_t* surface = + gjs_cairo_surface_get_surface(context, surface_wrapper); + if (!surface) return false; - } cr = cairo_create(surface); if (!gjs_cairo_check_status(context, cairo_status(cr), "context")) return false; - _gjs_cairo_context_construct_internal(context, object, cr); + _gjs_cairo_context_construct_internal(object, cr); cairo_destroy(cr); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_context); @@ -294,25 +300,18 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_context) return true; } -static void -gjs_cairo_context_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GjsCairoContext *priv; - priv = (GjsCairoContext*) JS_GetPrivate(obj); - if (priv == NULL) - return; - - if (priv->cr != NULL) - cairo_destroy(priv->cr); - - g_slice_free(GjsCairoContext, priv); +static void gjs_cairo_context_finalize(JSFreeOp*, JSObject* obj) { + using AutoCairoContext = GjsAutoPointer; + AutoCairoContext cr = static_cast(JS_GetPrivate(obj)); + JS_SetPrivate(obj, nullptr); } /* Properties */ +// clang-format off JSPropertySpec gjs_cairo_context_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Context", JSPROP_READONLY), + JS_PS_END}; +// clang-format on /* Methods */ @@ -393,55 +392,59 @@ _GJS_CAIRO_CONTEXT_DEFINE_FUNC2(translate, cairo_translate, "ff", double, tx, do _GJS_CAIRO_CONTEXT_DEFINE_FUNC2FFAFF(userToDevice, cairo_user_to_device, "x", "y") _GJS_CAIRO_CONTEXT_DEFINE_FUNC2FFAFF(userToDeviceDistance, cairo_user_to_device_distance, "x", "y") - +GJS_JSAPI_RETURN_CONVENTION static bool dispose_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, rec, obj, cairo_t, cr); + if (!cr) + return true; + + cairo_destroy(cr); + JS_SetPrivate(obj, nullptr); - if (priv->cr != NULL) { - cairo_destroy(priv->cr); - priv->cr = NULL; - } rec.rval().setUndefined(); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool appendPath_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + JS::RootedObject path_wrapper(context); - cairo_path_t *path; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "path", argv, "o", "path", &path_wrapper)) return false; - path = gjs_cairo_path_get_path(context, path_wrapper); - if (!path) { - gjs_throw(context, "first argument to appendPath() should be a path"); + cairo_path_t* path = gjs_cairo_path_get_path(context, path_wrapper); + if (!path) return false; - } cairo_append_path(cr, path); argv.rval().setUndefined(); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool copyPath_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + cairo_path_t *path; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "", argv, "")) return false; @@ -451,14 +454,17 @@ copyPath_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool copyPathFlat_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + cairo_path_t *path; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "", argv, "")) return false; @@ -468,25 +474,26 @@ copyPathFlat_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool mask_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + JS::RootedObject pattern_wrapper(context); - cairo_pattern_t *pattern; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "mask", argv, "o", "pattern", &pattern_wrapper)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, pattern_wrapper); - if (!pattern) { - gjs_throw(context, "first argument to mask() should be a pattern"); + cairo_pattern_t* pattern = + gjs_cairo_pattern_get_pattern(context, pattern_wrapper); + if (!pattern) return false; - } cairo_mask(cr, pattern); @@ -497,16 +504,18 @@ mask_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool maskSurface_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + JS::RootedObject surface_wrapper(context); double x, y; - cairo_surface_t *surface; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "maskSurface", argv, "off", "surface", &surface_wrapper, @@ -514,11 +523,10 @@ maskSurface_func(JSContext *context, "y", &y)) return false; - surface = gjs_cairo_surface_get_surface(context, surface_wrapper); - if (!surface) { - gjs_throw(context, "first argument to maskSurface() should be a surface"); + cairo_surface_t* surface = + gjs_cairo_surface_get_surface(context, surface_wrapper); + if (!surface) return false; - } cairo_mask_surface(cr, surface, x, y); @@ -529,14 +537,17 @@ maskSurface_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool setDash_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + guint i; - cairo_t *cr = priv ? priv->cr : NULL; JS::RootedObject dashes(context); double offset; guint len; @@ -547,14 +558,14 @@ setDash_func(JSContext *context, "offset", &offset)) return false; - if (!JS_IsArrayObject(context, dashes, &is_array)) + if (!JS::IsArrayObject(context, dashes, &is_array)) return false; if (!is_array) { gjs_throw(context, "dashes must be an array"); return false; } - if (!JS_GetArrayLength(context, dashes, &len)) { + if (!JS::GetArrayLength(context, dashes, &len)) { gjs_throw(context, "Can't get length of dashes"); return false; } @@ -582,30 +593,31 @@ setDash_func(JSContext *context, dashes_c.push_back(b); } - cairo_set_dash(cr, &dashes_c[0], dashes_c.size(), offset); + cairo_set_dash(cr, dashes_c.data(), dashes_c.size(), offset); argv.rval().setUndefined(); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool setSource_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + JS::RootedObject pattern_wrapper(context); - cairo_pattern_t *pattern; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "setSource", argv, "o", "pattern", &pattern_wrapper)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, pattern_wrapper); - if (!pattern) { - gjs_throw(context, "first argument to setSource() should be a pattern"); + cairo_pattern_t* pattern = + gjs_cairo_pattern_get_pattern(context, pattern_wrapper); + if (!pattern) return false; - } cairo_set_source(cr, pattern); @@ -617,16 +629,18 @@ setSource_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool setSourceSurface_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + JS::RootedObject surface_wrapper(context); double x, y; - cairo_surface_t *surface; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "setSourceSurface", argv, "off", "surface", &surface_wrapper, @@ -634,11 +648,10 @@ setSourceSurface_func(JSContext *context, "y", &y)) return false; - surface = gjs_cairo_surface_get_surface(context, surface_wrapper); - if (!surface) { - gjs_throw(context, "first argument to setSourceSurface() should be a surface"); + cairo_surface_t* surface = + gjs_cairo_surface_get_surface(context, surface_wrapper); + if (!surface) return false; - } cairo_set_source_surface(cr, surface, x, y); @@ -650,20 +663,23 @@ setSourceSurface_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool showText_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); - GjsAutoJSChar utf8; - cairo_t *cr = priv ? priv->cr : NULL; + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + + JS::UniqueChars utf8; if (!gjs_parse_call_args(context, "showText", argv, "s", "utf8", &utf8)) return false; - cairo_show_text(cr, utf8); + cairo_show_text(cr, utf8.get()); if (!gjs_cairo_check_status(context, cairo_status(cr), "context")) return false; @@ -673,16 +689,19 @@ showText_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool selectFontFace_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoContext, priv); - GjsAutoJSChar family; + GJS_GET_PRIV(context, argc, vp, argv, obj, cairo_t, cr); + if (!cr) + return true; + + JS::UniqueChars family; cairo_font_slant_t slant; cairo_font_weight_t weight; - cairo_t *cr = priv ? priv->cr : NULL; if (!gjs_parse_call_args(context, "selectFontFace", argv, "sii", "family", &family, @@ -690,7 +709,7 @@ selectFontFace_func(JSContext *context, "weight", &weight)) return false; - cairo_select_font_face(cr, family, slant, weight); + cairo_select_font_face(cr, family.get(), slant, weight); if (!gjs_cairo_check_status(context, cairo_status(cr), "context")) return false; @@ -699,13 +718,16 @@ selectFontFace_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool popGroup_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, GjsCairoContext, priv); - cairo_t *cr = priv ? priv->cr : NULL; + GJS_GET_PRIV(context, argc, vp, rec, obj, cairo_t, cr); + if (!cr) + return true; + cairo_pattern_t *pattern; JSObject *pattern_wrapper; @@ -720,7 +742,6 @@ popGroup_func(JSContext *context, pattern_wrapper = gjs_cairo_pattern_from_pattern(context, pattern); cairo_pattern_destroy(pattern); - if (!pattern_wrapper) { gjs_throw(context, "failed to create pattern"); return false; @@ -730,13 +751,16 @@ popGroup_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getSource_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, GjsCairoContext, priv); - cairo_t *cr = priv ? priv->cr : NULL; + GJS_GET_PRIV(context, argc, vp, rec, obj, cairo_t, cr); + if (!cr) + return true; + cairo_pattern_t *pattern; JSObject *pattern_wrapper; @@ -761,13 +785,16 @@ getSource_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getTarget_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, GjsCairoContext, priv); - cairo_t *cr = priv ? priv->cr : NULL; + GJS_GET_PRIV(context, argc, vp, rec, obj, cairo_t, cr); + if (!cr) + return true; + cairo_surface_t *surface; JSObject *surface_wrapper; @@ -792,13 +819,16 @@ getTarget_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getGroupTarget_func(JSContext *context, unsigned argc, JS::Value *vp) { - GJS_GET_PRIV(context, argc, vp, rec, obj, GjsCairoContext, priv); - cairo_t *cr = priv ? priv->cr : NULL; + GJS_GET_PRIV(context, argc, vp, rec, obj, cairo_t, cr); + if (!cr) + return true; + cairo_surface_t *surface; JSObject *surface_wrapper; @@ -824,106 +854,105 @@ getGroupTarget_func(JSContext *context, } JSFunctionSpec gjs_cairo_context_proto_funcs[] = { - JS_FS("$dispose", dispose_func, 0, 0), - JS_FS("appendPath", appendPath_func, 0, 0), - JS_FS("arc", arc_func, 0, 0), - JS_FS("arcNegative", arcNegative_func, 0, 0), - JS_FS("clip", clip_func, 0, 0), - JS_FS("clipExtents", clipExtents_func, 0, 0), - JS_FS("clipPreserve", clipPreserve_func, 0, 0), - JS_FS("closePath", closePath_func, 0, 0), - JS_FS("copyPage", copyPage_func, 0, 0), - JS_FS("copyPath", copyPath_func, 0, 0), - JS_FS("copyPathFlat", copyPathFlat_func, 0, 0), - JS_FS("curveTo", curveTo_func, 0, 0), - JS_FS("deviceToUser", deviceToUser_func, 0, 0), - JS_FS("deviceToUserDistance", deviceToUserDistance_func, 0, 0), - JS_FS("fill", fill_func, 0, 0), - JS_FS("fillPreserve", fillPreserve_func, 0, 0), - JS_FS("fillExtents", fillExtents_func, 0, 0), + JS_FN("$dispose", dispose_func, 0, 0), + JS_FN("appendPath", appendPath_func, 0, 0), + JS_FN("arc", arc_func, 0, 0), + JS_FN("arcNegative", arcNegative_func, 0, 0), + JS_FN("clip", clip_func, 0, 0), + JS_FN("clipExtents", clipExtents_func, 0, 0), + JS_FN("clipPreserve", clipPreserve_func, 0, 0), + JS_FN("closePath", closePath_func, 0, 0), + JS_FN("copyPage", copyPage_func, 0, 0), + JS_FN("copyPath", copyPath_func, 0, 0), + JS_FN("copyPathFlat", copyPathFlat_func, 0, 0), + JS_FN("curveTo", curveTo_func, 0, 0), + JS_FN("deviceToUser", deviceToUser_func, 0, 0), + JS_FN("deviceToUserDistance", deviceToUserDistance_func, 0, 0), + JS_FN("fill", fill_func, 0, 0), + JS_FN("fillPreserve", fillPreserve_func, 0, 0), + JS_FN("fillExtents", fillExtents_func, 0, 0), // fontExtents - JS_FS("getAntialias", getAntialias_func, 0, 0), - JS_FS("getCurrentPoint", getCurrentPoint_func, 0, 0), + JS_FN("getAntialias", getAntialias_func, 0, 0), + JS_FN("getCurrentPoint", getCurrentPoint_func, 0, 0), // getDash - JS_FS("getDashCount", getDashCount_func, 0, 0), - JS_FS("getFillRule", getFillRule_func, 0, 0), + JS_FN("getDashCount", getDashCount_func, 0, 0), + JS_FN("getFillRule", getFillRule_func, 0, 0), // getFontFace // getFontMatrix // getFontOptions - JS_FS("getGroupTarget", getGroupTarget_func, 0, 0), - JS_FS("getLineCap", getLineCap_func, 0, 0), - JS_FS("getLineJoin", getLineJoin_func, 0, 0), - JS_FS("getLineWidth", getLineWidth_func, 0, 0), + JS_FN("getGroupTarget", getGroupTarget_func, 0, 0), + JS_FN("getLineCap", getLineCap_func, 0, 0), + JS_FN("getLineJoin", getLineJoin_func, 0, 0), + JS_FN("getLineWidth", getLineWidth_func, 0, 0), // getMatrix - JS_FS("getMiterLimit", getMiterLimit_func, 0, 0), - JS_FS("getOperator", getOperator_func, 0, 0), + JS_FN("getMiterLimit", getMiterLimit_func, 0, 0), + JS_FN("getOperator", getOperator_func, 0, 0), // getScaledFont - JS_FS("getSource", getSource_func, 0, 0), - JS_FS("getTarget", getTarget_func, 0, 0), - JS_FS("getTolerance", getTolerance_func, 0, 0), + JS_FN("getSource", getSource_func, 0, 0), + JS_FN("getTarget", getTarget_func, 0, 0), + JS_FN("getTolerance", getTolerance_func, 0, 0), // glyphPath // glyphExtents - JS_FS("hasCurrentPoint", hasCurrentPoint_func, 0, 0), - JS_FS("identityMatrix", identityMatrix_func, 0, 0), - JS_FS("inFill", inFill_func, 0, 0), - JS_FS("inStroke", inStroke_func, 0, 0), - JS_FS("lineTo", lineTo_func, 0, 0), - JS_FS("mask", mask_func, 0, 0), - JS_FS("maskSurface", maskSurface_func, 0, 0), - JS_FS("moveTo", moveTo_func, 0, 0), - JS_FS("newPath", newPath_func, 0, 0), - JS_FS("newSubPath", newSubPath_func, 0, 0), - JS_FS("paint", paint_func, 0, 0), - JS_FS("paintWithAlpha", paintWithAlpha_func, 0, 0), - JS_FS("pathExtents", pathExtents_func, 0, 0), - JS_FS("popGroup", popGroup_func, 0, 0), - JS_FS("popGroupToSource", popGroupToSource_func, 0, 0), - JS_FS("pushGroup", pushGroup_func, 0, 0), - JS_FS("pushGroupWithContent", pushGroupWithContent_func, 0, 0), - JS_FS("rectangle", rectangle_func, 0, 0), - JS_FS("relCurveTo", relCurveTo_func, 0, 0), - JS_FS("relLineTo", relLineTo_func, 0, 0), - JS_FS("relMoveTo", relMoveTo_func, 0, 0), - JS_FS("resetClip", resetClip_func, 0, 0), - JS_FS("restore", restore_func, 0, 0), - JS_FS("rotate", rotate_func, 0, 0), - JS_FS("save", save_func, 0, 0), - JS_FS("scale", scale_func, 0, 0), - JS_FS("selectFontFace", selectFontFace_func, 0, 0), - JS_FS("setAntialias", setAntialias_func, 0, 0), - JS_FS("setDash", setDash_func, 0, 0), + JS_FN("hasCurrentPoint", hasCurrentPoint_func, 0, 0), + JS_FN("identityMatrix", identityMatrix_func, 0, 0), + JS_FN("inFill", inFill_func, 0, 0), + JS_FN("inStroke", inStroke_func, 0, 0), + JS_FN("lineTo", lineTo_func, 0, 0), + JS_FN("mask", mask_func, 0, 0), + JS_FN("maskSurface", maskSurface_func, 0, 0), + JS_FN("moveTo", moveTo_func, 0, 0), + JS_FN("newPath", newPath_func, 0, 0), + JS_FN("newSubPath", newSubPath_func, 0, 0), + JS_FN("paint", paint_func, 0, 0), + JS_FN("paintWithAlpha", paintWithAlpha_func, 0, 0), + JS_FN("pathExtents", pathExtents_func, 0, 0), + JS_FN("popGroup", popGroup_func, 0, 0), + JS_FN("popGroupToSource", popGroupToSource_func, 0, 0), + JS_FN("pushGroup", pushGroup_func, 0, 0), + JS_FN("pushGroupWithContent", pushGroupWithContent_func, 0, 0), + JS_FN("rectangle", rectangle_func, 0, 0), + JS_FN("relCurveTo", relCurveTo_func, 0, 0), + JS_FN("relLineTo", relLineTo_func, 0, 0), + JS_FN("relMoveTo", relMoveTo_func, 0, 0), + JS_FN("resetClip", resetClip_func, 0, 0), + JS_FN("restore", restore_func, 0, 0), + JS_FN("rotate", rotate_func, 0, 0), + JS_FN("save", save_func, 0, 0), + JS_FN("scale", scale_func, 0, 0), + JS_FN("selectFontFace", selectFontFace_func, 0, 0), + JS_FN("setAntialias", setAntialias_func, 0, 0), + JS_FN("setDash", setDash_func, 0, 0), // setFontFace // setFontMatrix // setFontOptions - JS_FS("setFontSize", setFontSize_func, 0, 0), - JS_FS("setFillRule", setFillRule_func, 0, 0), - JS_FS("setLineCap", setLineCap_func, 0, 0), - JS_FS("setLineJoin", setLineJoin_func, 0, 0), - JS_FS("setLineWidth", setLineWidth_func, 0, 0), + JS_FN("setFontSize", setFontSize_func, 0, 0), + JS_FN("setFillRule", setFillRule_func, 0, 0), + JS_FN("setLineCap", setLineCap_func, 0, 0), + JS_FN("setLineJoin", setLineJoin_func, 0, 0), + JS_FN("setLineWidth", setLineWidth_func, 0, 0), // setMatrix - JS_FS("setMiterLimit", setMiterLimit_func, 0, 0), - JS_FS("setOperator", setOperator_func, 0, 0), + JS_FN("setMiterLimit", setMiterLimit_func, 0, 0), + JS_FN("setOperator", setOperator_func, 0, 0), // setScaledFont - JS_FS("setSource", setSource_func, 0, 0), - JS_FS("setSourceRGB", setSourceRGB_func, 0, 0), - JS_FS("setSourceRGBA", setSourceRGBA_func, 0, 0), - JS_FS("setSourceSurface", setSourceSurface_func, 0, 0), - JS_FS("setTolerance", setTolerance_func, 0, 0), + JS_FN("setSource", setSource_func, 0, 0), + JS_FN("setSourceRGB", setSourceRGB_func, 0, 0), + JS_FN("setSourceRGBA", setSourceRGBA_func, 0, 0), + JS_FN("setSourceSurface", setSourceSurface_func, 0, 0), + JS_FN("setTolerance", setTolerance_func, 0, 0), // showGlyphs - JS_FS("showPage", showPage_func, 0, 0), - JS_FS("showText", showText_func, 0, 0), + JS_FN("showPage", showPage_func, 0, 0), + JS_FN("showText", showText_func, 0, 0), // showTextGlyphs - JS_FS("stroke", stroke_func, 0, 0), - JS_FS("strokeExtents", strokeExtents_func, 0, 0), - JS_FS("strokePreserve", strokePreserve_func, 0, 0), + JS_FN("stroke", stroke_func, 0, 0), + JS_FN("strokeExtents", strokeExtents_func, 0, 0), + JS_FN("strokePreserve", strokePreserve_func, 0, 0), // textPath // textExtends // transform - JS_FS("translate", translate_func, 0, 0), - JS_FS("userToDevice", userToDevice_func, 0, 0), - JS_FS("userToDeviceDistance", userToDeviceDistance_func, 0, 0), - JS_FS_END -}; + JS_FN("translate", translate_func, 0, 0), + JS_FN("userToDevice", userToDevice_func, 0, 0), + JS_FN("userToDeviceDistance", userToDeviceDistance_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_cairo_context_static_funcs[] = { JS_FS_END }; @@ -935,9 +964,9 @@ gjs_cairo_context_from_context(JSContext *context, JS::RootedObject object(context, JS_NewObjectWithGivenProto(context, &gjs_cairo_context_class, proto)); if (!object) - return NULL; + return nullptr; - _gjs_cairo_context_construct_internal(context, object, cr); + _gjs_cairo_context_construct_internal(object, cr); return object; } @@ -946,24 +975,26 @@ cairo_t * gjs_cairo_context_get_context(JSContext *context, JS::HandleObject object) { - GjsCairoContext *priv; - priv = priv_from_js(context, object); - if (priv == NULL) - return NULL; - - return priv->cr; + return priv_from_js(context, object); } -static bool -context_to_g_argument(JSContext *context, - JS::Value value, - const char *arg_name, - GjsArgumentType argument_type, - GITransfer transfer, - bool may_be_null, - GArgument *arg) -{ - JS::RootedObject obj(context, value.toObjectOrNull()); +[[nodiscard]] static bool context_to_g_argument( + JSContext* context, JS::Value value, const char* arg_name, + GjsArgumentType argument_type, GITransfer transfer, bool may_be_null, + GIArgument* arg) { + if (value.isNull()) { + if (!may_be_null) { + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, argument_type); + gjs_throw(context, "%s may not be null", display_name.get()); + return false; + } + + gjs_arg_unset(arg); + return true; + } + + JS::RootedObject obj(context, &value.toObject()); cairo_t *cr; cr = gjs_cairo_context_get_context(context, obj); @@ -972,18 +1003,18 @@ context_to_g_argument(JSContext *context, if (transfer == GI_TRANSFER_EVERYTHING) cairo_reference(cr); - arg->v_pointer = cr; + gjs_arg_set(arg, cr); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool context_from_g_argument(JSContext *context, JS::MutableHandleValue value_p, GIArgument *arg) { - JSObject *obj; - - obj = gjs_cairo_context_from_context(context, (cairo_t*)arg->v_pointer); + JSObject* obj = + gjs_cairo_context_from_context(context, gjs_arg_get(arg)); if (!obj) { gjs_throw(context, "Could not create Cairo context"); return false; @@ -993,12 +1024,10 @@ context_from_g_argument(JSContext *context, return true; } -static bool -context_release_argument(JSContext *context, - GITransfer transfer, - GArgument *arg) -{ - cairo_destroy((cairo_t*)arg->v_pointer); +static bool context_release_argument(JSContext*, GITransfer transfer, + GIArgument* arg) { + if (transfer != GI_TRANSFER_NOTHING) + cairo_destroy(gjs_arg_get(arg)); return true; } @@ -1008,8 +1037,6 @@ static GjsForeignInfo foreign_info = { context_release_argument }; -void -gjs_cairo_context_init(JSContext *context) -{ +void gjs_cairo_context_init(void) { gjs_struct_foreign_register("cairo", "Context", &foreign_info); } diff --git a/modules/cairo-gradient.cpp b/modules/cairo-gradient.cpp index 22e5bbe..c96f518 100644 --- a/modules/cairo-gradient.cpp +++ b/modules/cairo-gradient.cpp @@ -22,11 +22,20 @@ #include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include + #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" GJS_DEFINE_PROTO_ABSTRACT_WITH_PARENT("Gradient", cairo_gradient, cairo_pattern, @@ -40,12 +49,15 @@ gjs_cairo_gradient_finalize(JSFreeOp *fop, } /* Properties */ +// clang-format off JSPropertySpec gjs_cairo_gradient_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Gradient", JSPROP_READONLY), + JS_PS_END}; +// clang-format on /* Methods */ +GJS_JSAPI_RETURN_CONVENTION static bool addColorStopRGB_func(JSContext *context, unsigned argc, @@ -53,7 +65,6 @@ addColorStopRGB_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, argv, obj); double offset, red, green, blue; - cairo_pattern_t *pattern; if (!gjs_parse_call_args(context, "addColorStopRGB", argv, "ffff", "offset", &offset, @@ -62,7 +73,9 @@ addColorStopRGB_func(JSContext *context, "blue", &blue)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; cairo_pattern_add_color_stop_rgb(pattern, offset, red, green, blue); @@ -73,6 +86,7 @@ addColorStopRGB_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool addColorStopRGBA_func(JSContext *context, unsigned argc, @@ -80,7 +94,6 @@ addColorStopRGBA_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, argv, obj); double offset, red, green, blue, alpha; - cairo_pattern_t *pattern; if (!gjs_parse_call_args(context, "addColorStopRGBA", argv, "fffff", "offset", &offset, @@ -90,7 +103,10 @@ addColorStopRGBA_func(JSContext *context, "alpha", &alpha)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + cairo_pattern_add_color_stop_rgba(pattern, offset, red, green, blue, alpha); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -101,11 +117,10 @@ addColorStopRGBA_func(JSContext *context, } JSFunctionSpec gjs_cairo_gradient_proto_funcs[] = { - JS_FS("addColorStopRGB", addColorStopRGB_func, 0, 0), - JS_FS("addColorStopRGBA", addColorStopRGBA_func, 0, 0), + JS_FN("addColorStopRGB", addColorStopRGB_func, 0, 0), + JS_FN("addColorStopRGBA", addColorStopRGBA_func, 0, 0), // getColorStopRGB // getColorStopRGBA - JS_FS_END -}; + JS_FS_END}; JSFunctionSpec gjs_cairo_gradient_static_funcs[] = { JS_FS_END }; diff --git a/modules/cairo-image-surface.cpp b/modules/cairo-image-surface.cpp index 8140e30..3fb95a2 100644 --- a/modules/cairo-image-surface.cpp +++ b/modules/cairo-image-surface.cpp @@ -22,14 +22,24 @@ #include +#include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_NewObjectWithGivenProto + #include "cjs/jsapi-class.h" -#include "cjs/jsapi-util.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_image_surface_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_image_surface_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("ImageSurface", cairo_image_surface, cairo_surface, JSCLASS_BACKGROUND_FINALIZE) @@ -54,7 +64,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_image_surface) if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) return false; - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); cairo_surface_destroy(surface); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_image_surface); @@ -69,10 +79,13 @@ gjs_cairo_image_surface_finalize(JSFreeOp *fop, gjs_cairo_surface_finalize_surface(fop, obj); } +// clang-format off JSPropertySpec gjs_cairo_image_surface_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "ImageSurface", JSPROP_READONLY), + JS_PS_END}; +// clang-format on +GJS_JSAPI_RETURN_CONVENTION static bool createFromPNG_func(JSContext *context, unsigned argc, @@ -99,20 +112,20 @@ createFromPNG_func(JSContext *context, gjs_throw(context, "failed to create surface"); return false; } - gjs_cairo_surface_construct(context, surface_wrapper, surface); + gjs_cairo_surface_construct(surface_wrapper, surface); cairo_surface_destroy(surface); argv.rval().setObject(*surface_wrapper); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getFormat_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_surface_t *surface; cairo_format_t format; if (argc > 1) { @@ -120,7 +133,10 @@ getFormat_func(JSContext *context, return false; } - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); + if (!surface) + return false; + format = cairo_image_surface_get_format(surface); if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) @@ -130,13 +146,13 @@ getFormat_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getWidth_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_surface_t *surface; int width; if (argc > 1) { @@ -144,7 +160,10 @@ getWidth_func(JSContext *context, return false; } - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); + if (!surface) + return false; + width = cairo_image_surface_get_width(surface); if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) @@ -154,13 +173,13 @@ getWidth_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getHeight_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_surface_t *surface; int height; if (argc > 1) { @@ -168,7 +187,10 @@ getHeight_func(JSContext *context, return false; } - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); + if (!surface) + return false; + height = cairo_image_surface_get_height(surface); if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) @@ -178,13 +200,13 @@ getHeight_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getStride_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_surface_t *surface; int stride; if (argc > 1) { @@ -192,7 +214,10 @@ getStride_func(JSContext *context, return false; } - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); + if (!surface) + return false; + stride = cairo_image_surface_get_stride(surface); if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) @@ -203,27 +228,26 @@ getStride_func(JSContext *context, } JSFunctionSpec gjs_cairo_image_surface_proto_funcs[] = { - JS_FS("createFromPNG", createFromPNG_func, 0, 0), + JS_FN("createFromPNG", createFromPNG_func, 0, 0), // getData - JS_FS("getFormat", getFormat_func, 0, 0), - JS_FS("getWidth", getWidth_func, 0, 0), - JS_FS("getHeight", getHeight_func, 0, 0), - JS_FS("getStride", getStride_func, 0, 0), - JS_FS_END -}; + JS_FN("getFormat", getFormat_func, 0, 0), + JS_FN("getWidth", getWidth_func, 0, 0), + JS_FN("getHeight", getHeight_func, 0, 0), + JS_FN("getStride", getStride_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_cairo_image_surface_static_funcs[] = { - JS_FS("createFromPNG", createFromPNG_func, 1, GJS_MODULE_PROP_FLAGS), - JS_FS_END -}; + JS_FN("createFromPNG", createFromPNG_func, 1, GJS_MODULE_PROP_FLAGS), + JS_FS_END}; JSObject * gjs_cairo_image_surface_from_surface(JSContext *context, cairo_surface_t *surface) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(surface != NULL, NULL); - g_return_val_if_fail(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(surface, nullptr); + g_return_val_if_fail( + cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE, nullptr); JS::RootedObject proto(context, gjs_cairo_image_surface_get_proto(context)); JS::RootedObject object(context, @@ -231,10 +255,10 @@ gjs_cairo_image_surface_from_surface(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create image surface"); - return NULL; + return nullptr; } - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); return object; } diff --git a/modules/cairo-linear-gradient.cpp b/modules/cairo-linear-gradient.cpp index 3dab027..4577a1b 100644 --- a/modules/cairo-linear-gradient.cpp +++ b/modules/cairo-linear-gradient.cpp @@ -22,13 +22,23 @@ #include +#include +#include + +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_NewObjectWithGivenProto + #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_linear_gradient_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_linear_gradient_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("LinearGradient", cairo_linear_gradient, cairo_gradient, JSCLASS_BACKGROUND_FINALIZE) @@ -53,7 +63,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_linear_gradient) if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) return false; - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); cairo_pattern_destroy(pattern); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_linear_gradient); @@ -69,8 +79,8 @@ gjs_cairo_linear_gradient_finalize(JSFreeOp *fop, } JSPropertySpec gjs_cairo_linear_gradient_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "LinearGradient", JSPROP_READONLY), + JS_PS_END}; JSFunctionSpec gjs_cairo_linear_gradient_proto_funcs[] = { // getLinearPoints @@ -83,9 +93,10 @@ JSObject * gjs_cairo_linear_gradient_from_pattern(JSContext *context, cairo_pattern_t *pattern) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(pattern != NULL, NULL); - g_return_val_if_fail(cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_LINEAR, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(pattern, nullptr); + g_return_val_if_fail( + cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_LINEAR, nullptr); JS::RootedObject proto(context, gjs_cairo_linear_gradient_get_proto(context)); @@ -94,10 +105,10 @@ gjs_cairo_linear_gradient_from_pattern(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create linear gradient pattern"); - return NULL; + return nullptr; } - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); return object; } diff --git a/modules/cairo-module.h b/modules/cairo-module.h index a839cce..91049df 100644 --- a/modules/cairo-module.h +++ b/modules/cairo-module.h @@ -20,10 +20,17 @@ * IN THE SOFTWARE. */ -#ifndef __CAIRO_MODULE_H__ -#define __CAIRO_MODULE_H__ +#ifndef MODULES_CAIRO_MODULE_H_ +#define MODULES_CAIRO_MODULE_H_ +#include + +#include + +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION bool gjs_js_define_cairo_stuff(JSContext *context, JS::MutableHandleObject module); -#endif /* __CAIRO_MODULE_H__ */ +#endif // MODULES_CAIRO_MODULE_H_ diff --git a/modules/cairo-path.cpp b/modules/cairo-path.cpp index 85763de..00f83b2 100644 --- a/modules/cairo-path.cpp +++ b/modules/cairo-path.cpp @@ -22,39 +22,36 @@ #include -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-util.h" -#include "cjs/jsapi-wrapper.h" #include -#include "cairo-private.h" +#include -typedef struct { - JSContext *context; - JSObject *object; - cairo_path_t *path; -} GjsCairoPath; +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_GetClass, JS_GetInstancePrivate + +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" -static JSObject *gjs_cairo_path_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_path_get_proto(JSContext*); GJS_DEFINE_PROTO_ABSTRACT("Path", cairo_path, JSCLASS_BACKGROUND_FINALIZE) -GJS_DEFINE_PRIV_FROM_JS(GjsCairoPath, gjs_cairo_path_class) -static void -gjs_cairo_path_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GjsCairoPath *priv; - priv = (GjsCairoPath*) JS_GetPrivate(obj); - if (priv == NULL) - return; - cairo_path_destroy(priv->path); - g_slice_free(GjsCairoPath, priv); +static void gjs_cairo_path_finalize(JSFreeOp*, JSObject* obj) { + using AutoCairoPath = + GjsAutoPointer; + AutoCairoPath path = static_cast(JS_GetPrivate(obj)); + JS_SetPrivate(obj, nullptr); } /* Properties */ +// clang-format off JSPropertySpec gjs_cairo_path_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Path", JSPROP_READONLY), + JS_PS_END}; +// clang-format on JSFunctionSpec gjs_cairo_path_proto_funcs[] = { JS_FS_END @@ -74,51 +71,42 @@ JSObject * gjs_cairo_path_from_path(JSContext *context, cairo_path_t *path) { - GjsCairoPath *priv; - - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(path != NULL, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(path, nullptr); JS::RootedObject proto(context, gjs_cairo_path_get_proto(context)); JS::RootedObject object(context, JS_NewObjectWithGivenProto(context, &gjs_cairo_path_class, proto)); if (!object) { gjs_throw(context, "failed to create path"); - return NULL; + return nullptr; } - priv = g_slice_new0(GjsCairoPath); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - priv->context = context; - priv->object = object; - priv->path = path; + g_assert(!JS_GetPrivate(object)); + JS_SetPrivate(object, path); return object; } - /** * gjs_cairo_path_get_path: - * @context: the context - * @object: path wrapper + * @cx: the context + * @path_wrapper: path wrapper * * Returns: the path attached to the wrapper. - * */ -cairo_path_t * -gjs_cairo_path_get_path(JSContext *context, - JSObject *object) -{ - GjsCairoPath *priv; - - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(object != NULL, NULL); +cairo_path_t* gjs_cairo_path_get_path(JSContext* cx, + JS::HandleObject path_wrapper) { + g_return_val_if_fail(cx, nullptr); + g_return_val_if_fail(path_wrapper, nullptr); + + auto* path = static_cast(JS_GetInstancePrivate( + cx, path_wrapper, &gjs_cairo_path_class, nullptr)); + if (!path) { + gjs_throw(cx, "Expected Cairo.Path but got %s", + JS_GetClass(path_wrapper)->name); + return nullptr; + } - priv = (GjsCairoPath*) JS_GetPrivate(object); - if (priv == NULL) - return NULL; - return priv->path; + return path; } diff --git a/modules/cairo-pattern.cpp b/modules/cairo-pattern.cpp index f24e11d..a0c0f3e 100644 --- a/modules/cairo-pattern.cpp +++ b/modules/cairo-pattern.cpp @@ -22,51 +22,47 @@ #include -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-util.h" -#include "cjs/jsapi-wrapper.h" -#include #include -#include "cairo-private.h" +#include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_GetPrivate, JS_GetClass, ... -typedef struct { - void *dummy; - JSContext *context; - JSObject *object; - cairo_pattern_t *pattern; -} GjsCairoPattern; +#include "cjs/jsapi-class.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" GJS_DEFINE_PROTO_ABSTRACT_WITH_GTYPE("Pattern", cairo_pattern, CAIRO_GOBJECT_TYPE_PATTERN, JSCLASS_BACKGROUND_FINALIZE) -GJS_DEFINE_PRIV_FROM_JS(GjsCairoPattern, gjs_cairo_pattern_class) -static void -gjs_cairo_pattern_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GjsCairoPattern *priv; - priv = (GjsCairoPattern*) JS_GetPrivate(obj); - if (priv == NULL) - return; - cairo_pattern_destroy(priv->pattern); - g_slice_free(GjsCairoPattern, priv); +static void gjs_cairo_pattern_finalize(JSFreeOp*, JSObject* obj) { + using AutoPattern = + GjsAutoPointer; + AutoPattern pattern = static_cast(JS_GetPrivate(obj)); + JS_SetPrivate(obj, nullptr); } /* Properties */ JSPropertySpec gjs_cairo_pattern_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Pattern", JSPROP_READONLY), JS_PS_END}; /* Methods */ +GJS_JSAPI_RETURN_CONVENTION static bool getType_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_pattern_t *pattern; cairo_pattern_type_t type; if (argc > 1) { @@ -74,7 +70,10 @@ getType_func(JSContext *context, return false; } - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + type = cairo_pattern_get_type(pattern); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -86,10 +85,9 @@ getType_func(JSContext *context, JSFunctionSpec gjs_cairo_pattern_proto_funcs[] = { // getMatrix - JS_FS("getType", getType_func, 0, 0), + JS_FN("getType", getType_func, 0, 0), // setMatrix - JS_FS_END -}; + JS_FS_END}; JSFunctionSpec gjs_cairo_pattern_static_funcs[] = { JS_FS_END }; @@ -97,7 +95,6 @@ JSFunctionSpec gjs_cairo_pattern_static_funcs[] = { JS_FS_END }; /** * gjs_cairo_pattern_construct: - * @context: the context * @object: object to construct * @pattern: cairo_pattern to attach to the object * @@ -106,25 +103,12 @@ JSFunctionSpec gjs_cairo_pattern_static_funcs[] = { JS_FS_END }; * * This is mainly used for subclasses where object is already created. */ -void -gjs_cairo_pattern_construct(JSContext *context, - JS::HandleObject object, - cairo_pattern_t *pattern) -{ - GjsCairoPattern *priv; - - g_return_if_fail(context != NULL); - g_return_if_fail(object != nullptr); - g_return_if_fail(pattern != NULL); - - priv = g_slice_new0(GjsCairoPattern); +void gjs_cairo_pattern_construct(JSObject* object, cairo_pattern_t* pattern) { + g_return_if_fail(object); + g_return_if_fail(pattern); - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); - - priv->context = context; - priv->object = object; - priv->pattern = cairo_pattern_reference(pattern); + g_assert(!JS_GetPrivate(object)); + JS_SetPrivate(object, cairo_pattern_reference(pattern)); } /** @@ -141,8 +125,8 @@ void gjs_cairo_pattern_finalize_pattern(JSFreeOp *fop, JSObject *object) { - g_return_if_fail(fop != NULL); - g_return_if_fail(object != NULL); + g_return_if_fail(fop); + g_return_if_fail(object); gjs_cairo_pattern_finalize(fop, object); } @@ -160,8 +144,8 @@ JSObject * gjs_cairo_pattern_from_pattern(JSContext *context, cairo_pattern_t *pattern) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(pattern != NULL, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(pattern, nullptr); switch (cairo_pattern_get_type(pattern)) { case CAIRO_PATTERN_TYPE_SOLID: @@ -178,31 +162,33 @@ gjs_cairo_pattern_from_pattern(JSContext *context, gjs_throw(context, "failed to create pattern, unsupported pattern type %d", cairo_pattern_get_type(pattern)); - return NULL; + return nullptr; } } /** * gjs_cairo_pattern_get_pattern: - * @context: the context - * @object: pattern wrapper - * - * Returns: the pattern attaches to the wrapper. + * @cx: the context + * @pattern_wrapper: pattern wrapper * + * Returns: the pattern attached to the wrapper. */ -cairo_pattern_t * -gjs_cairo_pattern_get_pattern(JSContext *context, - JSObject *object) -{ - GjsCairoPattern *priv; - - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(object != NULL, NULL); - - priv = (GjsCairoPattern*) JS_GetPrivate(object); - if (priv == NULL) - return NULL; +cairo_pattern_t* gjs_cairo_pattern_get_pattern( + JSContext* cx, JS::HandleObject pattern_wrapper) { + g_return_val_if_fail(cx, nullptr); + g_return_val_if_fail(pattern_wrapper, nullptr); + + JS::RootedObject proto(cx, gjs_cairo_pattern_get_proto(cx)); + + bool is_pattern_subclass = false; + if (!gjs_object_in_prototype_chain(cx, proto, pattern_wrapper, + &is_pattern_subclass)) + return nullptr; + if (!is_pattern_subclass) { + gjs_throw(cx, "Expected Cairo.Pattern but got %s", + JS_GetClass(pattern_wrapper)->name); + return nullptr; + } - return priv->pattern; + return static_cast(JS_GetPrivate(pattern_wrapper)); } - diff --git a/modules/cairo-pdf-surface.cpp b/modules/cairo-pdf-surface.cpp index d4f8fb6..d5ced6d 100644 --- a/modules/cairo-pdf-surface.cpp +++ b/modules/cairo-pdf-surface.cpp @@ -22,16 +22,29 @@ #include -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" +#include // for CAIRO_HAS_PDF_SURFACE #include -#include "cairo-private.h" + +#include + +#include "cjs/jsapi-util.h" #if CAIRO_HAS_PDF_SURFACE -#include +# include +# include + +# include +# include // for JSPROP_READONLY +# include +# include +# include // for JS_NewObjectWithGivenProto -static JSObject *gjs_cairo_pdf_surface_get_proto(JSContext *); +# include "cjs/jsapi-class.h" +# include "cjs/jsapi-util-args.h" +# include "cjs/macros.h" +# include "modules/cairo-private.h" + +[[nodiscard]] static JSObject* gjs_cairo_pdf_surface_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("PDFSurface", cairo_pdf_surface, cairo_surface, JSCLASS_BACKGROUND_FINALIZE) @@ -57,7 +70,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_pdf_surface) "surface")) return false; - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); cairo_surface_destroy(surface); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_pdf_surface); @@ -72,9 +85,11 @@ gjs_cairo_pdf_surface_finalize(JSFreeOp *fop, gjs_cairo_surface_finalize_surface(fop, obj); } +// clang-format off JSPropertySpec gjs_cairo_pdf_surface_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "PDFSurface", JSPROP_READONLY), + JS_PS_END}; +// clang-format on JSFunctionSpec gjs_cairo_pdf_surface_proto_funcs[] = { JS_FS_END @@ -86,9 +101,10 @@ JSObject * gjs_cairo_pdf_surface_from_surface(JSContext *context, cairo_surface_t *surface) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(surface != NULL, NULL); - g_return_val_if_fail(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_PDF, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(surface, nullptr); + g_return_val_if_fail( + cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_PDF, nullptr); JS::RootedObject proto(context, gjs_cairo_pdf_surface_get_proto(context)); JS::RootedObject object(context, @@ -96,10 +112,10 @@ gjs_cairo_pdf_surface_from_surface(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create pdf surface"); - return NULL; + return nullptr; } - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); return object; } @@ -111,6 +127,6 @@ gjs_cairo_pdf_surface_from_surface(JSContext *context, gjs_throw(context, "could not create PDF surface, recompile cairo and gjs with " "PDF support."); - return NULL; + return nullptr; } #endif /* CAIRO_HAS_PDF_SURFACE */ diff --git a/modules/cairo-private.h b/modules/cairo-private.h index caef140..37cbba5 100644 --- a/modules/cairo-private.h +++ b/modules/cairo-private.h @@ -20,61 +20,76 @@ * IN THE SOFTWARE. */ -#ifndef __CAIRO_PRIVATE_H__ -#define __CAIRO_PRIVATE_H__ +#ifndef MODULES_CAIRO_PRIVATE_H_ +#define MODULES_CAIRO_PRIVATE_H_ -#include "cairo-module.h" +#include + +#include // for CAIRO_HAS_PDF_SURFACE, CAIRO_HAS_PS_SURFACE #include +#include + +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_check_status (JSContext *context, cairo_status_t status, const char *name); +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_region_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); -void gjs_cairo_region_init (JSContext *context); +void gjs_cairo_region_init(void); +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_context_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); -cairo_t * gjs_cairo_context_get_context (JSContext *context, - JS::HandleObject object); +[[nodiscard]] cairo_t* gjs_cairo_context_get_context(JSContext* cx, + JS::HandleObject object); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_context_from_context (JSContext *context, cairo_t *cr); -void gjs_cairo_context_init (JSContext *context); -void gjs_cairo_surface_init (JSContext *context); +void gjs_cairo_context_init(void); +void gjs_cairo_surface_init(void); /* path */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_path_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_path_from_path (JSContext *context, cairo_path_t *path); -cairo_path_t * gjs_cairo_path_get_path (JSContext *context, - JSObject *path_wrapper); +GJS_JSAPI_RETURN_CONVENTION +cairo_path_t* gjs_cairo_path_get_path(JSContext* cx, + JS::HandleObject path_wrapper); /* surface */ -JSObject *gjs_cairo_surface_get_proto(JSContext *cx); +[[nodiscard]] JSObject* gjs_cairo_surface_get_proto(JSContext* cx); +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_surface_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); -void gjs_cairo_surface_construct (JSContext *context, - JS::HandleObject object, - cairo_surface_t *surface); +void gjs_cairo_surface_construct(JSObject* object, cairo_surface_t* surface); void gjs_cairo_surface_finalize_surface (JSFreeOp *fop, JSObject *object); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_surface_from_surface (JSContext *context, cairo_surface_t *surface); -cairo_surface_t* gjs_cairo_surface_get_surface (JSContext *context, - JSObject *object); +GJS_JSAPI_RETURN_CONVENTION +cairo_surface_t* gjs_cairo_surface_get_surface( + JSContext* cx, JS::HandleObject surface_wrapper); /* image surface */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_image_surface_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); @@ -82,91 +97,107 @@ bool gjs_cairo_image_surface_define_proto(JSContext *cx, void gjs_cairo_image_surface_init (JSContext *context, JS::HandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_image_surface_from_surface (JSContext *context, cairo_surface_t *surface); /* postscript surface */ #ifdef CAIRO_HAS_PS_SURFACE +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_ps_surface_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); #endif +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_ps_surface_from_surface (JSContext *context, cairo_surface_t *surface); /* pdf surface */ #ifdef CAIRO_HAS_PDF_SURFACE +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_pdf_surface_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); #endif +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_pdf_surface_from_surface (JSContext *context, cairo_surface_t *surface); /* svg surface */ #ifdef CAIRO_HAS_SVG_SURFACE +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_svg_surface_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); #endif +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_svg_surface_from_surface (JSContext *context, cairo_surface_t *surface); /* pattern */ -JSObject *gjs_cairo_pattern_get_proto(JSContext *cx); +[[nodiscard]] JSObject* gjs_cairo_pattern_get_proto(JSContext* cx); +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_pattern_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); -void gjs_cairo_pattern_construct (JSContext *context, - JS::HandleObject object, - cairo_pattern_t *pattern); +void gjs_cairo_pattern_construct(JSObject* object, cairo_pattern_t* pattern); void gjs_cairo_pattern_finalize_pattern (JSFreeOp *fop, JSObject *object); +GJS_JSAPI_RETURN_CONVENTION JSObject* gjs_cairo_pattern_from_pattern (JSContext *context, cairo_pattern_t *pattern); -cairo_pattern_t* gjs_cairo_pattern_get_pattern (JSContext *context, - JSObject *object); +GJS_JSAPI_RETURN_CONVENTION +cairo_pattern_t* gjs_cairo_pattern_get_pattern( + JSContext* cx, JS::HandleObject pattern_wrapper); /* gradient */ -JSObject *gjs_cairo_gradient_get_proto(JSContext *cx); +[[nodiscard]] JSObject* gjs_cairo_gradient_get_proto(JSContext* cx); +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_gradient_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); /* linear gradient */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_linear_gradient_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_linear_gradient_from_pattern (JSContext *context, cairo_pattern_t *pattern); /* radial gradient */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_radial_gradient_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_radial_gradient_from_pattern (JSContext *context, cairo_pattern_t *pattern); /* surface pattern */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_surface_pattern_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_surface_pattern_from_pattern (JSContext *context, cairo_pattern_t *pattern); /* solid pattern */ +GJS_JSAPI_RETURN_CONVENTION bool gjs_cairo_solid_pattern_define_proto(JSContext *cx, JS::HandleObject module, JS::MutableHandleObject proto); +GJS_JSAPI_RETURN_CONVENTION JSObject * gjs_cairo_solid_pattern_from_pattern (JSContext *context, cairo_pattern_t *pattern); -#endif /* __CAIRO_PRIVATE_H__ */ - +#endif // MODULES_CAIRO_PRIVATE_H_ diff --git a/modules/cairo-ps-surface.cpp b/modules/cairo-ps-surface.cpp index 7818e5e..2806087 100644 --- a/modules/cairo-ps-surface.cpp +++ b/modules/cairo-ps-surface.cpp @@ -22,16 +22,29 @@ #include -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" +#include // for CAIRO_HAS_PS_SURFACE #include -#include "cairo-private.h" + +#include + +#include "cjs/jsapi-util.h" #if CAIRO_HAS_PS_SURFACE -#include +# include +# include + +# include +# include // for JSPROP_READONLY +# include +# include +# include // for JS_NewObjectWithGivenProto -static JSObject *gjs_cairo_ps_surface_get_proto(JSContext *); +# include "cjs/jsapi-class.h" +# include "cjs/jsapi-util-args.h" +# include "cjs/macros.h" +# include "modules/cairo-private.h" + +[[nodiscard]] static JSObject* gjs_cairo_ps_surface_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("PSSurface", cairo_ps_surface, cairo_surface, JSCLASS_BACKGROUND_FINALIZE) @@ -57,7 +70,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_ps_surface) "surface")) return false; - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); cairo_surface_destroy(surface); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_ps_surface); @@ -72,9 +85,11 @@ gjs_cairo_ps_surface_finalize(JSFreeOp *fop, gjs_cairo_surface_finalize_surface(fop, obj); } +// clang-format off JSPropertySpec gjs_cairo_ps_surface_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "PSSurface", JSPROP_READONLY), + JS_PS_END}; +// clang-format on JSFunctionSpec gjs_cairo_ps_surface_proto_funcs[] = { // restrictToLevel @@ -95,19 +110,20 @@ JSObject * gjs_cairo_ps_surface_from_surface(JSContext *context, cairo_surface_t *surface) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(surface != NULL, NULL); - g_return_val_if_fail(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_PS, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(surface, nullptr); + g_return_val_if_fail( + cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_PS, nullptr); JS::RootedObject proto(context, gjs_cairo_ps_surface_get_proto(context)); JS::RootedObject object(context, JS_NewObjectWithGivenProto(context, &gjs_cairo_ps_surface_class, proto)); if (!object) { gjs_throw(context, "failed to create ps surface"); - return NULL; + return nullptr; } - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); return object; } @@ -119,6 +135,6 @@ gjs_cairo_ps_surface_from_surface(JSContext *context, gjs_throw(context, "could not create PS surface, recompile cairo and gjs with " "PS support."); - return NULL; + return nullptr; } #endif /* CAIRO_HAS_PS_SURFACE */ diff --git a/modules/cairo-radial-gradient.cpp b/modules/cairo-radial-gradient.cpp index b9dbeb7..3c7bbb2 100644 --- a/modules/cairo-radial-gradient.cpp +++ b/modules/cairo-radial-gradient.cpp @@ -22,13 +22,23 @@ #include +#include +#include + +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_NewObjectWithGivenProto + #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_radial_gradient_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_radial_gradient_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("RadialGradient", cairo_radial_gradient, cairo_gradient, JSCLASS_BACKGROUND_FINALIZE) @@ -55,7 +65,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_radial_gradient) if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) return false; - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); cairo_pattern_destroy(pattern); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_radial_gradient); @@ -71,8 +81,8 @@ gjs_cairo_radial_gradient_finalize(JSFreeOp *fop, } JSPropertySpec gjs_cairo_radial_gradient_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "RadialGradient", JSPROP_READONLY), + JS_PS_END}; JSFunctionSpec gjs_cairo_radial_gradient_proto_funcs[] = { // getRadialCircles @@ -85,9 +95,10 @@ JSObject * gjs_cairo_radial_gradient_from_pattern(JSContext *context, cairo_pattern_t *pattern) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(pattern != NULL, NULL); - g_return_val_if_fail(cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_RADIAL, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(pattern, nullptr); + g_return_val_if_fail( + cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_RADIAL, nullptr); JS::RootedObject proto(context, gjs_cairo_radial_gradient_get_proto(context)); @@ -96,10 +107,10 @@ gjs_cairo_radial_gradient_from_pattern(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create radial gradient pattern"); - return NULL; + return nullptr; } - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); return object; } diff --git a/modules/cairo-region.cpp b/modules/cairo-region.cpp index 4628efe..1124ebf 100644 --- a/modules/cairo-region.cpp +++ b/modules/cairo-region.cpp @@ -22,90 +22,94 @@ #include +#include +#include +#include +#include + +#include +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include +#include // for JS_GetPropertyById, JS_SetPropert... + +#include "gi/arg-inl.h" +#include "gi/arg.h" #include "gi/foreign.h" +#include "cjs/atoms.h" +#include "cjs/context-private.h" #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" - -#include -#include -#include "cairo-private.h" - -typedef struct { - JSContext *context; - JSObject *object; - cairo_region_t *region; -} GjsCairoRegion; +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_region_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_region_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_GTYPE("Region", cairo_region, CAIRO_GOBJECT_TYPE_REGION, JSCLASS_BACKGROUND_FINALIZE) -GJS_DEFINE_PRIV_FROM_JS(GjsCairoRegion, gjs_cairo_region_class); static cairo_region_t * get_region(JSContext *context, JS::HandleObject obj) { - GjsCairoRegion *priv = priv_from_js(context, obj); - if (priv == NULL) - return NULL; - else - return priv->region; + return static_cast( + JS_GetInstancePrivate(context, obj, &gjs_cairo_region_class, nullptr)); } +GJS_JSAPI_RETURN_CONVENTION static bool fill_rectangle(JSContext *context, JS::HandleObject obj, cairo_rectangle_int_t *rect); -#define PRELUDE \ - GJS_GET_PRIV(context, argc, vp, argv, obj, GjsCairoRegion, priv); \ - cairo_region_t *this_region = priv ? priv->region : NULL; +#define PRELUDE \ + GJS_GET_THIS(context, argc, vp, argv, obj); \ + auto* this_region = static_cast(JS_GetInstancePrivate( \ + context, obj, &gjs_cairo_region_class, nullptr)); #define RETURN_STATUS \ return gjs_cairo_check_status(context, cairo_region_status(this_region), "region"); -#define REGION_DEFINE_REGION_FUNC(method) \ - static bool \ - method##_func(JSContext *context, \ - unsigned argc, \ - JS::Value *vp) \ - { \ - PRELUDE; \ - JS::RootedObject other_obj(context); \ - cairo_region_t *other_region; \ - if (!gjs_parse_call_args(context, #method, argv, "o", \ - "other_region", &other_obj)) \ - return false; \ - \ - other_region = get_region(context, other_obj); \ - \ - cairo_region_##method(this_region, other_region); \ - argv.rval().setUndefined(); \ - RETURN_STATUS; \ +#define REGION_DEFINE_REGION_FUNC(method) \ + GJS_JSAPI_RETURN_CONVENTION \ + static bool method##_func(JSContext* context, unsigned argc, \ + JS::Value* vp) { \ + PRELUDE; \ + JS::RootedObject other_obj(context); \ + if (!gjs_parse_call_args(context, #method, argv, "o", "other_region", \ + &other_obj)) \ + return false; \ + \ + cairo_region_t* other_region = get_region(context, other_obj); \ + \ + cairo_region_##method(this_region, other_region); \ + argv.rval().setUndefined(); \ + RETURN_STATUS; \ } -#define REGION_DEFINE_RECT_FUNC(method) \ - static bool \ - method##_rectangle_func(JSContext *context, \ - unsigned argc, \ - JS::Value *vp) \ - { \ - PRELUDE; \ - JS::RootedObject rect_obj(context); \ - cairo_rectangle_int_t rect; \ - if (!gjs_parse_call_args(context, #method, argv, "o", \ - "rect", &rect_obj)) \ - return false; \ - \ - if (!fill_rectangle(context, rect_obj, &rect)) \ - return false; \ - \ - cairo_region_##method##_rectangle(this_region, &rect); \ - argv.rval().setUndefined(); \ - RETURN_STATUS; \ +#define REGION_DEFINE_RECT_FUNC(method) \ + GJS_JSAPI_RETURN_CONVENTION \ + static bool method##_rectangle_func(JSContext* context, unsigned argc, \ + JS::Value* vp) { \ + PRELUDE; \ + JS::RootedObject rect_obj(context); \ + if (!gjs_parse_call_args(context, #method, argv, "o", "rect", \ + &rect_obj)) \ + return false; \ + \ + cairo_rectangle_int_t rect; \ + if (!fill_rectangle(context, rect_obj, &rect)) \ + return false; \ + \ + cairo_region_##method##_rectangle(this_region, &rect); \ + argv.rval().setUndefined(); \ + RETURN_STATUS; \ } REGION_DEFINE_REGION_FUNC(union) @@ -118,29 +122,31 @@ REGION_DEFINE_RECT_FUNC(subtract) REGION_DEFINE_RECT_FUNC(intersect) REGION_DEFINE_RECT_FUNC(xor) +GJS_JSAPI_RETURN_CONVENTION static bool fill_rectangle(JSContext *context, JS::HandleObject obj, cairo_rectangle_int_t *rect) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedValue val(context); - if (!gjs_object_get_property(context, obj, GJS_STRING_X, &val)) + if (!JS_GetPropertyById(context, obj, atoms.x(), &val)) return false; if (!JS::ToInt32(context, val, &rect->x)) return false; - if (!gjs_object_get_property(context, obj, GJS_STRING_Y, &val)) + if (!JS_GetPropertyById(context, obj, atoms.y(), &val)) return false; if (!JS::ToInt32(context, val, &rect->y)) return false; - if (!gjs_object_get_property(context, obj, GJS_STRING_WIDTH, &val)) + if (!JS_GetPropertyById(context, obj, atoms.width(), &val)) return false; if (!JS::ToInt32(context, val, &rect->width)) return false; - if (!gjs_object_get_property(context, obj, GJS_STRING_HEIGHT, &val)) + if (!JS_GetPropertyById(context, obj, atoms.height(), &val)) return false; if (!JS::ToInt32(context, val, &rect->height)) return false; @@ -148,28 +154,37 @@ fill_rectangle(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static JSObject * make_rectangle(JSContext *context, cairo_rectangle_int_t *rect) { + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); JS::RootedObject rect_obj(context, JS_NewPlainObject(context)); + if (!rect_obj) + return nullptr; JS::RootedValue val(context); val = JS::Int32Value(rect->x); - JS_SetProperty(context, rect_obj, "x", val); + if (!JS_SetPropertyById(context, rect_obj, atoms.x(), val)) + return nullptr; val = JS::Int32Value(rect->y); - JS_SetProperty(context, rect_obj, "y", val); + if (!JS_SetPropertyById(context, rect_obj, atoms.y(), val)) + return nullptr; val = JS::Int32Value(rect->width); - JS_SetProperty(context, rect_obj, "width", val); + if (!JS_SetPropertyById(context, rect_obj, atoms.width(), val)) + return nullptr; val = JS::Int32Value(rect->height); - JS_SetProperty(context, rect_obj, "height", val); + if (!JS_SetPropertyById(context, rect_obj, atoms.height(), val)) + return nullptr; return rect_obj; } +GJS_JSAPI_RETURN_CONVENTION static bool num_rectangles_func(JSContext *context, unsigned argc, @@ -186,6 +201,7 @@ num_rectangles_func(JSContext *context, RETURN_STATUS; } +GJS_JSAPI_RETURN_CONVENTION static bool get_rectangle_func(JSContext *context, unsigned argc, @@ -207,43 +223,33 @@ get_rectangle_func(JSContext *context, RETURN_STATUS; } +// clang-format off JSPropertySpec gjs_cairo_region_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Region", JSPROP_READONLY), + JS_PS_END}; +// clang-format on JSFunctionSpec gjs_cairo_region_proto_funcs[] = { - JS_FS("union", union_func, 0, 0), - JS_FS("subtract", subtract_func, 0, 0), - JS_FS("intersect", intersect_func, 0, 0), - JS_FS("xor", xor_func, 0, 0), - - JS_FS("unionRectangle", union_rectangle_func, 0, 0), - JS_FS("subtractRectangle", subtract_rectangle_func, 0, 0), - JS_FS("intersectRectangle", intersect_rectangle_func, 0, 0), - JS_FS("xorRectangle", xor_rectangle_func, 0, 0), - - JS_FS("numRectangles", num_rectangles_func, 0, 0), - JS_FS("getRectangle", get_rectangle_func, 0, 0), - JS_FS_END -}; + JS_FN("union", union_func, 0, 0), + JS_FN("subtract", subtract_func, 0, 0), + JS_FN("intersect", intersect_func, 0, 0), + JS_FN("xor", xor_func, 0, 0), -JSFunctionSpec gjs_cairo_region_static_funcs[] = { JS_FS_END }; - -static void -_gjs_cairo_region_construct_internal(JSContext *context, - JS::HandleObject obj, - cairo_region_t *region) -{ - GjsCairoRegion *priv; + JS_FN("unionRectangle", union_rectangle_func, 0, 0), + JS_FN("subtractRectangle", subtract_rectangle_func, 0, 0), + JS_FN("intersectRectangle", intersect_rectangle_func, 0, 0), + JS_FN("xorRectangle", xor_rectangle_func, 0, 0), - priv = g_slice_new0(GjsCairoRegion); + JS_FN("numRectangles", num_rectangles_func, 0, 0), + JS_FN("getRectangle", get_rectangle_func, 0, 0), + JS_FS_END}; - g_assert(priv_from_js(context, obj) == NULL); - JS_SetPrivate(obj, priv); +JSFunctionSpec gjs_cairo_region_static_funcs[] = { JS_FS_END }; - priv->context = context; - priv->object = obj; - priv->region = cairo_region_reference(region); +static void _gjs_cairo_region_construct_internal(JSObject* obj, + cairo_region_t* region) { + g_assert(!JS_GetPrivate(obj)); + JS_SetPrivate(obj, cairo_region_reference(region)); } GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_region) @@ -258,7 +264,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_region) region = cairo_region_create(); - _gjs_cairo_region_construct_internal(context, object, region); + _gjs_cairo_region_construct_internal(object, region); cairo_region_destroy(region); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_region); @@ -266,19 +272,14 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_region) return true; } -static void -gjs_cairo_region_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GjsCairoRegion *priv; - priv = (GjsCairoRegion*) JS_GetPrivate(obj); - if (priv == NULL) - return; - - cairo_region_destroy(priv->region); - g_slice_free(GjsCairoRegion, priv); +static void gjs_cairo_region_finalize(JSFreeOp*, JSObject* obj) { + using AutoCairoRegion = + GjsAutoPointer; + AutoCairoRegion region = static_cast(JS_GetPrivate(obj)); + JS_SetPrivate(obj, nullptr); } +GJS_JSAPI_RETURN_CONVENTION static JSObject * gjs_cairo_region_from_region(JSContext *context, cairo_region_t *region) @@ -287,22 +288,29 @@ gjs_cairo_region_from_region(JSContext *context, JS::RootedObject object(context, JS_NewObjectWithGivenProto(context, &gjs_cairo_region_class, proto)); if (!object) - return NULL; + return nullptr; - _gjs_cairo_region_construct_internal(context, object, region); + _gjs_cairo_region_construct_internal(object, region); return object; } -static bool -region_to_g_argument(JSContext *context, - JS::Value value, - const char *arg_name, - GjsArgumentType argument_type, - GITransfer transfer, - bool may_be_null, - GArgument *arg) -{ +[[nodiscard]] static bool region_to_g_argument( + JSContext* context, JS::Value value, const char* arg_name, + GjsArgumentType argument_type, GITransfer transfer, bool may_be_null, + GIArgument* arg) { + if (value.isNull()) { + if (!may_be_null) { + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, argument_type); + gjs_throw(context, "%s may not be null", display_name.get()); + return false; + } + + gjs_arg_unset(arg); + return true; + } + JS::RootedObject obj(context, &value.toObject()); cairo_region_t *region; @@ -312,18 +320,18 @@ region_to_g_argument(JSContext *context, if (transfer == GI_TRANSFER_EVERYTHING) cairo_region_destroy(region); - arg->v_pointer = region; + gjs_arg_set(arg, region); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool region_from_g_argument(JSContext *context, JS::MutableHandleValue value_p, GIArgument *arg) { - JSObject *obj; - - obj = gjs_cairo_region_from_region(context, (cairo_region_t*)arg->v_pointer); + JSObject* obj = gjs_cairo_region_from_region( + context, gjs_arg_get(arg)); if (!obj) return false; @@ -331,12 +339,10 @@ region_from_g_argument(JSContext *context, return true; } -static bool -region_release_argument(JSContext *context, - GITransfer transfer, - GArgument *arg) -{ - cairo_region_destroy((cairo_region_t*)arg->v_pointer); +static bool region_release_argument(JSContext*, GITransfer transfer, + GIArgument* arg) { + if (transfer != GI_TRANSFER_NOTHING) + cairo_region_destroy(gjs_arg_get(arg)); return true; } @@ -346,8 +352,6 @@ static GjsForeignInfo foreign_info = { region_release_argument }; -void -gjs_cairo_region_init(JSContext *context) -{ +void gjs_cairo_region_init(void) { gjs_struct_foreign_register("cairo", "Region", &foreign_info); } diff --git a/modules/cairo-solid-pattern.cpp b/modules/cairo-solid-pattern.cpp index e02afb3..b4b2849 100644 --- a/modules/cairo-solid-pattern.cpp +++ b/modules/cairo-solid-pattern.cpp @@ -22,13 +22,24 @@ #include +#include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_NewObjectWithGivenProto + #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_solid_pattern_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_solid_pattern_get_proto(JSContext*); GJS_DEFINE_PROTO_ABSTRACT_WITH_PARENT("SolidPattern", cairo_solid_pattern, cairo_pattern, @@ -41,10 +52,13 @@ gjs_cairo_solid_pattern_finalize(JSFreeOp *fop, gjs_cairo_pattern_finalize_pattern(fop, obj); } +// clang-format off JSPropertySpec gjs_cairo_solid_pattern_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "SolidPattern", JSPROP_READONLY), + JS_PS_END}; +// clang-format on +GJS_JSAPI_RETURN_CONVENTION static bool createRGB_func(JSContext *context, unsigned argc, @@ -73,6 +87,7 @@ createRGB_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool createRGBA_func(JSContext *context, unsigned argc, @@ -103,10 +118,9 @@ createRGBA_func(JSContext *context, } JSFunctionSpec gjs_cairo_solid_pattern_proto_funcs[] = { - JS_FS("createRGB", createRGB_func, 0, 0), - JS_FS("createRGBA", createRGBA_func, 0, 0), - JS_FS_END -}; + JS_FN("createRGB", createRGB_func, 0, 0), + JS_FN("createRGBA", createRGBA_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_cairo_solid_pattern_static_funcs[] = { JS_FS_END }; @@ -114,9 +128,10 @@ JSObject * gjs_cairo_solid_pattern_from_pattern(JSContext *context, cairo_pattern_t *pattern) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(pattern != NULL, NULL); - g_return_val_if_fail(cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_SOLID, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(pattern, nullptr); + g_return_val_if_fail( + cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_SOLID, nullptr); JS::RootedObject proto(context, gjs_cairo_solid_pattern_get_proto(context)); JS::RootedObject object(context, @@ -124,10 +139,10 @@ gjs_cairo_solid_pattern_from_pattern(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create solid pattern"); - return NULL; + return nullptr; } - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); return object; } diff --git a/modules/cairo-surface-pattern.cpp b/modules/cairo-surface-pattern.cpp index 917e7e6..2aa7773 100644 --- a/modules/cairo-surface-pattern.cpp +++ b/modules/cairo-surface-pattern.cpp @@ -22,13 +22,24 @@ #include +#include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include // for JS_NewObjectWithGivenProto + #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include "cairo-private.h" +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" -static JSObject *gjs_cairo_surface_pattern_get_proto(JSContext *); +[[nodiscard]] static JSObject* gjs_cairo_surface_pattern_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("SurfacePattern", cairo_surface_pattern, cairo_pattern, JSCLASS_BACKGROUND_FINALIZE) @@ -36,7 +47,6 @@ GJS_DEFINE_PROTO_WITH_PARENT("SurfacePattern", cairo_surface_pattern, GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_surface_pattern) { GJS_NATIVE_CONSTRUCTOR_VARIABLES(cairo_surface_pattern) - cairo_surface_t *surface; cairo_pattern_t *pattern; GJS_NATIVE_CONSTRUCTOR_PRELUDE(cairo_surface_pattern); @@ -46,18 +56,17 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_surface_pattern) "surface", &surface_wrapper)) return false; - surface = gjs_cairo_surface_get_surface(context, surface_wrapper); - if (!surface) { - gjs_throw(context, "first argument to SurfacePattern() should be a surface"); + cairo_surface_t* surface = + gjs_cairo_surface_get_surface(context, surface_wrapper); + if (!surface) return false; - } pattern = cairo_pattern_create_for_surface(surface); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) return false; - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); cairo_pattern_destroy(pattern); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_surface_pattern); @@ -74,10 +83,10 @@ gjs_cairo_surface_pattern_finalize(JSFreeOp *fop, } JSPropertySpec gjs_cairo_surface_pattern_proto_props[] = { - JS_PS_END -}; - + JS_STRING_SYM_PS(toStringTag, "SurfacePattern", JSPROP_READONLY), + JS_PS_END}; +GJS_JSAPI_RETURN_CONVENTION static bool setExtend_func(JSContext *context, unsigned argc, @@ -85,13 +94,15 @@ setExtend_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, argv, obj); cairo_extend_t extend; - cairo_pattern_t *pattern; if (!gjs_parse_call_args(context, "setExtend", argv, "i", "extend", &extend)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + cairo_pattern_set_extend(pattern, extend); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -101,6 +112,7 @@ setExtend_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getExtend_func(JSContext *context, unsigned argc, @@ -108,14 +120,16 @@ getExtend_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, rec, obj); cairo_extend_t extend; - cairo_pattern_t *pattern; if (argc > 0) { gjs_throw(context, "SurfacePattern.getExtend() requires no arguments"); return false; } - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + extend = cairo_pattern_get_extend(pattern); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -126,6 +140,7 @@ getExtend_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool setFilter_func(JSContext *context, unsigned argc, @@ -133,13 +148,15 @@ setFilter_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, argv, obj); cairo_filter_t filter; - cairo_pattern_t *pattern; if (!gjs_parse_call_args(context, "setFilter", argv, "i", "filter", &filter)) return false; - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + cairo_pattern_set_filter(pattern, filter); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -149,6 +166,7 @@ setFilter_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getFilter_func(JSContext *context, unsigned argc, @@ -156,14 +174,16 @@ getFilter_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, rec, obj); cairo_filter_t filter; - cairo_pattern_t *pattern; if (argc > 0) { gjs_throw(context, "SurfacePattern.getFilter() requires no arguments"); return false; } - pattern = gjs_cairo_pattern_get_pattern(context, obj); + cairo_pattern_t* pattern = gjs_cairo_pattern_get_pattern(context, obj); + if (!pattern) + return false; + filter = cairo_pattern_get_filter(pattern); if (!gjs_cairo_check_status(context, cairo_pattern_status(pattern), "pattern")) @@ -175,12 +195,11 @@ getFilter_func(JSContext *context, } JSFunctionSpec gjs_cairo_surface_pattern_proto_funcs[] = { - JS_FS("setExtend", setExtend_func, 0, 0), - JS_FS("getExtend", getExtend_func, 0, 0), - JS_FS("setFilter", setFilter_func, 0, 0), - JS_FS("getFilter", getFilter_func, 0, 0), - JS_FS_END -}; + JS_FN("setExtend", setExtend_func, 0, 0), + JS_FN("getExtend", getExtend_func, 0, 0), + JS_FN("setFilter", setFilter_func, 0, 0), + JS_FN("getFilter", getFilter_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_cairo_surface_pattern_static_funcs[] = { JS_FS_END }; @@ -188,9 +207,10 @@ JSObject * gjs_cairo_surface_pattern_from_pattern(JSContext *context, cairo_pattern_t *pattern) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(pattern != NULL, NULL); - g_return_val_if_fail(cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_SURFACE, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(pattern, nullptr); + g_return_val_if_fail( + cairo_pattern_get_type(pattern) == CAIRO_PATTERN_TYPE_SURFACE, nullptr); JS::RootedObject proto(context, gjs_cairo_surface_pattern_get_proto(context)); @@ -199,10 +219,10 @@ gjs_cairo_surface_pattern_from_pattern(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create surface pattern"); - return NULL; + return nullptr; } - gjs_cairo_pattern_construct(context, object, pattern); + gjs_cairo_pattern_construct(object, pattern); return object; } diff --git a/modules/cairo-surface.cpp b/modules/cairo-surface.cpp index 749adba..f2a7a1f 100644 --- a/modules/cairo-surface.cpp +++ b/modules/cairo-surface.cpp @@ -22,44 +22,49 @@ #include +#include +#include +#include +#include + +#include +#include +#include // for JSPROP_READONLY +#include +#include +#include +#include +#include // for JS_GetPrivate, JS_GetClass, ... + +#include "gi/arg-inl.h" +#include "gi/arg.h" #include "gi/foreign.h" #include "cjs/jsapi-class.h" #include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" -#include -#include -#include "cairo-private.h" - -typedef struct { - void *dummy; - JSContext *context; - JSObject *object; - cairo_surface_t *surface; -} GjsCairoSurface; +#include "cjs/jsapi-util.h" +#include "cjs/macros.h" +#include "modules/cairo-private.h" GJS_DEFINE_PROTO_ABSTRACT_WITH_GTYPE("Surface", cairo_surface, CAIRO_GOBJECT_TYPE_SURFACE, JSCLASS_BACKGROUND_FINALIZE) -GJS_DEFINE_PRIV_FROM_JS(GjsCairoSurface, gjs_cairo_surface_class) -static void -gjs_cairo_surface_finalize(JSFreeOp *fop, - JSObject *obj) -{ - GjsCairoSurface *priv; - priv = (GjsCairoSurface*) JS_GetPrivate(obj); - if (priv == NULL) - return; - cairo_surface_destroy(priv->surface); - g_slice_free(GjsCairoSurface, priv); +static void gjs_cairo_surface_finalize(JSFreeOp*, JSObject* obj) { + using AutoSurface = + GjsAutoPointer; + AutoSurface surface = static_cast(JS_GetPrivate(obj)); + JS_SetPrivate(obj, nullptr); } /* Properties */ +// clang-format off JSPropertySpec gjs_cairo_surface_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "Surface", JSPROP_READONLY), + JS_PS_END}; +// clang-format on /* Methods */ +GJS_JSAPI_RETURN_CONVENTION static bool writeToPNG_func(JSContext *context, unsigned argc, @@ -67,13 +72,12 @@ writeToPNG_func(JSContext *context, { GJS_GET_THIS(context, argc, vp, argv, obj); GjsAutoChar filename; - cairo_surface_t *surface; if (!gjs_parse_call_args(context, "writeToPNG", argv, "F", "filename", &filename)) return false; - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); if (!surface) return false; @@ -85,13 +89,13 @@ writeToPNG_func(JSContext *context, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool getType_func(JSContext *context, unsigned argc, JS::Value *vp) { GJS_GET_THIS(context, argc, vp, rec, obj); - cairo_surface_t *surface; cairo_surface_type_t type; if (argc > 1) { @@ -99,7 +103,10 @@ getType_func(JSContext *context, return false; } - surface = gjs_cairo_surface_get_surface(context, obj); + cairo_surface_t* surface = gjs_cairo_surface_get_surface(context, obj); + if (!surface) + return false; + type = cairo_surface_get_type(surface); if (!gjs_cairo_check_status(context, cairo_surface_status(surface), "surface")) @@ -113,7 +120,7 @@ JSFunctionSpec gjs_cairo_surface_proto_funcs[] = { // flush // getContent // getFontOptions - JS_FS("getType", getType_func, 0, 0), + JS_FN("getType", getType_func, 0, 0), // markDirty // markDirtyRectangle // setDeviceOffset @@ -123,9 +130,8 @@ JSFunctionSpec gjs_cairo_surface_proto_funcs[] = { // copyPage // showPage // hasShowTextGlyphs - JS_FS("writeToPNG", writeToPNG_func, 0, 0), - JS_FS_END -}; + JS_FN("writeToPNG", writeToPNG_func, 0, 0), + JS_FS_END}; JSFunctionSpec gjs_cairo_surface_static_funcs[] = { JS_FS_END }; @@ -133,7 +139,6 @@ JSFunctionSpec gjs_cairo_surface_static_funcs[] = { JS_FS_END }; /** * gjs_cairo_surface_construct: - * @context: the context * @object: object to construct * @surface: cairo_surface to attach to the object * @@ -142,25 +147,12 @@ JSFunctionSpec gjs_cairo_surface_static_funcs[] = { JS_FS_END }; * * This is mainly used for subclasses where object is already created. */ -void -gjs_cairo_surface_construct(JSContext *context, - JS::HandleObject object, - cairo_surface_t *surface) -{ - GjsCairoSurface *priv; - - g_return_if_fail(context != NULL); - g_return_if_fail(object != nullptr); - g_return_if_fail(surface != NULL); - - priv = g_slice_new0(GjsCairoSurface); - - g_assert(priv_from_js(context, object) == NULL); - JS_SetPrivate(object, priv); +void gjs_cairo_surface_construct(JSObject* object, cairo_surface_t* surface) { + g_return_if_fail(object); + g_return_if_fail(surface); - priv->context = context; - priv->object = object; - priv->surface = cairo_surface_reference(surface); + g_assert(!JS_GetPrivate(object)); + JS_SetPrivate(object, cairo_surface_reference(surface)); } /** @@ -176,8 +168,8 @@ void gjs_cairo_surface_finalize_surface(JSFreeOp *fop, JSObject *object) { - g_return_if_fail(fop != NULL); - g_return_if_fail(object != NULL); + g_return_if_fail(fop); + g_return_if_fail(object); gjs_cairo_surface_finalize(fop, object); } @@ -195,8 +187,8 @@ JSObject * gjs_cairo_surface_from_surface(JSContext *context, cairo_surface_t *surface) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(surface != NULL, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(surface, nullptr); cairo_surface_type_t type = cairo_surface_get_type(surface); if (type == CAIRO_SURFACE_TYPE_IMAGE) @@ -213,68 +205,84 @@ gjs_cairo_surface_from_surface(JSContext *context, JS_NewObjectWithGivenProto(context, &gjs_cairo_surface_class, proto)); if (!object) { gjs_throw(context, "failed to create surface"); - return NULL; + return nullptr; } - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); return object; } /** * gjs_cairo_surface_get_surface: - * @context: the context - * @object: surface wrapper - * - * Returns: the surface attaches to the wrapper. + * @cx: the context + * @surface_wrapper: surface wrapper * + * Returns: the surface attached to the wrapper. */ -cairo_surface_t * -gjs_cairo_surface_get_surface(JSContext *context, - JSObject *object) -{ - GjsCairoSurface *priv; - - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(object != NULL, NULL); +cairo_surface_t* gjs_cairo_surface_get_surface( + JSContext* cx, JS::HandleObject surface_wrapper) { + g_return_val_if_fail(cx, nullptr); + g_return_val_if_fail(surface_wrapper, nullptr); + + JS::RootedObject proto(cx, gjs_cairo_surface_get_proto(cx)); + + bool is_surface_subclass = false; + if (!gjs_object_in_prototype_chain(cx, proto, surface_wrapper, + &is_surface_subclass)) + return nullptr; + if (!is_surface_subclass) { + gjs_throw(cx, "Expected Cairo.Surface but got %s", + JS_GetClass(surface_wrapper)->name); + return nullptr; + } - priv = (GjsCairoSurface*) JS_GetPrivate(object); - if (priv == NULL) - return NULL; - return priv->surface; + return static_cast(JS_GetPrivate(surface_wrapper)); } -static bool -surface_to_g_argument(JSContext *context, - JS::Value value, - const char *arg_name, - GjsArgumentType argument_type, - GITransfer transfer, - bool may_be_null, - GArgument *arg) -{ - JSObject *obj; - cairo_surface_t *s; +[[nodiscard]] static bool surface_to_g_argument( + JSContext* context, JS::Value value, const char* arg_name, + GjsArgumentType argument_type, GITransfer transfer, bool may_be_null, + GIArgument* arg) { + if (value.isNull()) { + if (!may_be_null) { + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, argument_type); + gjs_throw(context, "%s may not be null", display_name.get()); + return false; + } + + gjs_arg_unset(arg); + return true; + } - obj = &value.toObject(); - s = gjs_cairo_surface_get_surface(context, obj); + if (!value.isObject()) { + GjsAutoChar display_name = + gjs_argument_display_name(arg_name, argument_type); + gjs_throw(context, "%s is not a Cairo.Surface", display_name.get()); + return false; + } + + JS::RootedObject surface_wrapper(context, &value.toObject()); + cairo_surface_t* s = + gjs_cairo_surface_get_surface(context, surface_wrapper); if (!s) return false; if (transfer == GI_TRANSFER_EVERYTHING) cairo_surface_destroy(s); - arg->v_pointer = s; + gjs_arg_set(arg, s); return true; } +GJS_JSAPI_RETURN_CONVENTION static bool surface_from_g_argument(JSContext *context, JS::MutableHandleValue value_p, GIArgument *arg) { - JSObject *obj; - - obj = gjs_cairo_surface_from_surface(context, (cairo_surface_t*)arg->v_pointer); + JSObject* obj = gjs_cairo_surface_from_surface( + context, gjs_arg_get(arg)); if (!obj) return false; @@ -282,12 +290,10 @@ surface_from_g_argument(JSContext *context, return true; } -static bool -surface_release_argument(JSContext *context, - GITransfer transfer, - GArgument *arg) -{ - cairo_surface_destroy((cairo_surface_t*)arg->v_pointer); +static bool surface_release_argument(JSContext*, GITransfer transfer, + GIArgument* arg) { + if (transfer != GI_TRANSFER_NOTHING) + cairo_surface_destroy(gjs_arg_get(arg)); return true; } @@ -297,8 +303,6 @@ static GjsForeignInfo foreign_info = { surface_release_argument }; -void -gjs_cairo_surface_init(JSContext *context) -{ +void gjs_cairo_surface_init(void) { gjs_struct_foreign_register("cairo", "Surface", &foreign_info); } diff --git a/modules/cairo-svg-surface.cpp b/modules/cairo-svg-surface.cpp index 8140ed6..64d3009 100644 --- a/modules/cairo-svg-surface.cpp +++ b/modules/cairo-svg-surface.cpp @@ -22,16 +22,29 @@ #include -#include "cjs/jsapi-class.h" -#include "cjs/jsapi-util-args.h" -#include "cjs/jsapi-wrapper.h" +#include // for CAIRO_HAS_SVG_SURFACE #include -#include "cairo-private.h" + +#include + +#include "cjs/jsapi-util.h" #if CAIRO_HAS_SVG_SURFACE -#include +# include +# include + +# include +# include // for JSPROP_READONLY +# include +# include +# include // for JS_NewObjectWithGivenProto -static JSObject *gjs_cairo_svg_surface_get_proto(JSContext *); +# include "cjs/jsapi-class.h" +# include "cjs/jsapi-util-args.h" +# include "cjs/macros.h" +# include "modules/cairo-private.h" + +[[nodiscard]] static JSObject* gjs_cairo_svg_surface_get_proto(JSContext*); GJS_DEFINE_PROTO_WITH_PARENT("SVGSurface", cairo_svg_surface, cairo_surface, JSCLASS_BACKGROUND_FINALIZE) @@ -57,7 +70,7 @@ GJS_NATIVE_CONSTRUCTOR_DECLARE(cairo_svg_surface) "surface")) return false; - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); cairo_surface_destroy(surface); GJS_NATIVE_CONSTRUCTOR_FINISH(cairo_svg_surface); @@ -72,9 +85,11 @@ gjs_cairo_svg_surface_finalize(JSFreeOp *fop, gjs_cairo_surface_finalize_surface(fop, obj); } +// clang-format off JSPropertySpec gjs_cairo_svg_surface_proto_props[] = { - JS_PS_END -}; + JS_STRING_SYM_PS(toStringTag, "SVGSurface", JSPROP_READONLY), + JS_PS_END}; +// clang-format on JSFunctionSpec gjs_cairo_svg_surface_proto_funcs[] = { JS_FS_END @@ -86,9 +101,10 @@ JSObject * gjs_cairo_svg_surface_from_surface(JSContext *context, cairo_surface_t *surface) { - g_return_val_if_fail(context != NULL, NULL); - g_return_val_if_fail(surface != NULL, NULL); - g_return_val_if_fail(cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_SVG, NULL); + g_return_val_if_fail(context, nullptr); + g_return_val_if_fail(surface, nullptr); + g_return_val_if_fail( + cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_SVG, nullptr); JS::RootedObject proto(context, gjs_cairo_svg_surface_get_proto(context)); JS::RootedObject object(context, @@ -96,10 +112,10 @@ gjs_cairo_svg_surface_from_surface(JSContext *context, proto)); if (!object) { gjs_throw(context, "failed to create svg surface"); - return NULL; + return nullptr; } - gjs_cairo_surface_construct(context, object, surface); + gjs_cairo_surface_construct(object, surface); return object; } @@ -111,6 +127,6 @@ gjs_cairo_svg_surface_from_surface(JSContext *context, gjs_throw(context, "could not create SVG surface, recompile cairo and gjs with " "SVG support."); - return NULL; + return nullptr; } #endif /* CAIRO_HAS_SVG_SURFACE */ diff --git a/modules/cairo.cpp b/modules/cairo.cpp index 9ff7010..e44f80a 100644 --- a/modules/cairo.cpp +++ b/modules/cairo.cpp @@ -22,12 +22,26 @@ #include +#include // for CAIRO_HAS_PDF_SURFACE, CAIRO_HAS_PS_SURFA... +#include + +#include +#include +#include // for JS_NewPlainObject + #include "cjs/jsapi-util.h" -#include "cjs/jsapi-wrapper.h" -#include "cairo-private.h" +#include "modules/cairo-private.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} #ifdef CAIRO_HAS_XLIB_SURFACE -#include "cairo-xlib.h" +# include class XLibConstructor { public: @@ -64,15 +78,15 @@ gjs_js_define_cairo_stuff(JSContext *context, if (!gjs_cairo_region_define_proto(context, module, &proto)) return false; - gjs_cairo_region_init(context); + gjs_cairo_region_init(); if (!gjs_cairo_context_define_proto(context, module, &proto)) return false; - gjs_cairo_context_init(context); + gjs_cairo_context_init(); if (!gjs_cairo_surface_define_proto(context, module, &proto)) return false; - gjs_cairo_surface_init(context); + gjs_cairo_surface_init(); return gjs_cairo_image_surface_define_proto(context, module, &proto) && diff --git a/modules/console.cpp b/modules/console.cpp index 56d0df8..fa00fcf 100644 --- a/modules/console.cpp +++ b/modules/console.cpp @@ -38,104 +38,40 @@ * * ***** END LICENSE BLOCK ***** */ -#include "config.h" - -#include -#include +#include // for HAVE_READLINE_READLINE_H #ifdef HAVE_READLINE_READLINE_H -#include -#include -#include +# include // include before readline/readline.h + +# include +# include #endif #include -#include - -#include "console.h" -#include "cjs/context.h" +#include // for g_fprintf + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // for JS_IsExceptionPending, Exce... + +#include "cjs/atoms.h" #include "cjs/context-private.h" -#include "cjs/jsapi-wrapper.h" - -static void -gjs_console_print_error(JSErrorReport *report) -{ - /* Code modified from SpiderMonkey js/src/jscntxt.cpp, js::PrintError() */ - - g_assert(report); - - char *prefix = nullptr; - if (report->filename) - prefix = g_strdup_printf("%s:", report->filename); - if (report->lineno) { - char *tmp = prefix; - prefix = g_strdup_printf("%s%u:%u ", tmp ? tmp : "", report->lineno, - report->column); - g_free(tmp); - } - if (JSREPORT_IS_WARNING(report->flags)) { - char *tmp = prefix; - prefix = g_strdup_printf("%s%swarning: ", - tmp ? tmp : "", - JSREPORT_IS_STRICT(report->flags) ? "strict " : ""); - g_free(tmp); - } - - const char *message = report->message().c_str(); - - /* embedded newlines -- argh! */ - const char *ctmp; - while ((ctmp = strchr(message, '\n')) != 0) { - ctmp++; - if (prefix) - fputs(prefix, stderr); - fwrite(message, 1, ctmp - message, stderr); - message = ctmp; - } +#include "cjs/jsapi-util.h" +#include "modules/console.h" - /* If there were no filename or lineno, the prefix might be empty */ - if (prefix) - fputs(prefix, stderr); - fputs(message, stderr); - - if (const char16_t* linebuf = report->linebuf()) { - size_t n = report->linebufLength(); - - fputs(":\n", stderr); - if (prefix) - fputs(prefix, stderr); - - for (size_t i = 0; i < n; i++) - fputc(static_cast(linebuf[i]), stderr); - - // linebuf usually ends with a newline. If not, add one here. - if (n == 0 || linebuf[n - 1] != '\n') - fputc('\n', stderr); - - if (prefix) - fputs(prefix, stderr); - - n = report->tokenOffset(); - for (size_t i = 0, j = 0; i < n; i++) { - if (linebuf[i] == '\t') { - for (size_t k = (j + 8) & ~7; j < k; j++) - fputc('.', stderr); - continue; - } - fputc('.', stderr); - j++; - } - fputc('^', stderr); - } - fputc('\n', stderr); - fflush(stderr); - g_free(prefix); +namespace mozilla { +union Utf8Unit; } -static void -gjs_console_warning_reporter(JSContext *cx, JSErrorReport *report) -{ - gjs_console_print_error(report); +static void gjs_console_warning_reporter(JSContext* cx, JSErrorReport* report) { + JS::PrintError(cx, stderr, report, /* reportWarnings = */ true); } /* Based on js::shell::AutoReportException from SpiderMonkey. */ @@ -150,27 +86,23 @@ public: return; /* Get exception object before printing and clearing exception. */ - JS::RootedValue v_exn(m_cx); - (void) JS_GetPendingException(m_cx, &v_exn); - - JS::RootedObject exn(m_cx, &v_exn.toObject()); - JSErrorReport *report = JS_ErrorFromException(m_cx, exn); - if (report) { - g_assert(!JSREPORT_IS_WARNING(report->flags)); - gjs_console_print_error(report); - } else { - JS::RootedString message(m_cx, JS::ToString(m_cx, v_exn)); - if (!message) { - g_printerr("(could not convert thrown exception to string)\n"); - } else { - GjsAutoJSChar message_utf8 = JS_EncodeStringToUTF8(m_cx, message); - g_printerr("%s\n", message_utf8.get()); - } + JS::ExceptionStack exnStack(m_cx); + JS::ErrorReportBuilder report(m_cx); + if (!JS::StealPendingExceptionStack(m_cx, &exnStack) || + !report.init(m_cx, exnStack, + JS::ErrorReportBuilder::NoSideEffects)) { + g_printerr("(Unable to print exception)\n"); + JS_ClearPendingException(m_cx); + return; } - JS::RootedObject stack(m_cx, ExceptionStackOrNull(exn)); - if (stack) { - GjsAutoChar stack_str = gjs_format_stack_trace(m_cx, stack); + g_assert(!report.report()->isWarning()); + + JS::PrintError(m_cx, stderr, report, /* reportWarnings = */ false); + + if (exnStack.stack()) { + GjsAutoChar stack_str = + gjs_format_stack_trace(m_cx, exnStack.stack()); if (!stack_str) g_printerr("(Unable to print stack trace)\n"); else @@ -181,10 +113,9 @@ public: } }; +[[nodiscard]] static bool gjs_console_readline(char** bufp, + const char* prompt) { #ifdef HAVE_READLINE_READLINE_H -static bool -gjs_console_readline(JSContext *cx, char **bufp, FILE *file, const char *prompt) -{ char *line; line = readline(prompt); if (!line) @@ -192,51 +123,44 @@ gjs_console_readline(JSContext *cx, char **bufp, FILE *file, const char *prompt) if (line[0] != '\0') add_history(line); *bufp = line; - return true; -} -#else -static bool -gjs_console_readline(JSContext *cx, char **bufp, FILE *file, const char *prompt) -{ +#else // !HAVE_READLINE_READLINE_H char line[256]; fprintf(stdout, "%s", prompt); fflush(stdout); - if (!fgets(line, sizeof line, file)) + if (!fgets(line, sizeof line, stdin)) return false; *bufp = g_strdup(line); +#endif // !HAVE_READLINE_READLINE_H return true; } -#endif /* Return value of false indicates an uncatchable exception, rather than any * exception. (This is because the exception should be auto-printed around the * invocation of this function.) */ -static bool -gjs_console_eval_and_print(JSContext *cx, - const char *bytes, - size_t length, - int lineno) -{ +[[nodiscard]] static bool gjs_console_eval_and_print(JSContext* cx, + const char* bytes, + size_t length, + int lineno) { + JS::SourceText source; + if (!source.init(cx, bytes, length, JS::SourceOwnership::Borrowed)) + return false; + JS::CompileOptions options(cx); - options.setUTF8(true) - .setFileAndLine("typein", lineno); + options.setFileAndLine("typein", lineno); JS::RootedValue result(cx); - if (!JS::Evaluate(cx, options, bytes, length, &result)) { + if (!JS::Evaluate(cx, options, source, &result)) { if (!JS_IsExceptionPending(cx)) return false; } - gjs_schedule_gc_if_needed(cx); + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx); + gjs->schedule_gc_if_needed(); if (result.isUndefined()) return true; - JS::RootedString str(cx, JS::ToString(cx, result)); - if (!str) - return true; - char *display_str; display_str = gjs_value_debug_string(cx, result); if (display_str) { @@ -246,6 +170,7 @@ gjs_console_eval_and_print(JSContext *cx, return true; } +GJS_JSAPI_RETURN_CONVENTION static bool gjs_console_interact(JSContext *context, unsigned argc, @@ -258,7 +183,6 @@ gjs_console_interact(JSContext *context, char *temp_buf = NULL; int lineno; int startline; - FILE *file = stdin; JS::SetWarningReporter(context, gjs_console_warning_reporter); @@ -274,16 +198,16 @@ gjs_console_interact(JSContext *context, startline = lineno; buffer = g_string_new(""); do { - if (!gjs_console_readline(context, &temp_buf, file, - startline == lineno ? "cjs> " : ".... ")) { + if (!gjs_console_readline( + &temp_buf, startline == lineno ? "gjs> " : ".... ")) { eof = true; break; } g_string_append(buffer, temp_buf); g_free(temp_buf); lineno++; - } while (!JS_BufferIsCompilableUnit(context, global, - buffer->str, buffer->len)); + } while (!JS_Utf8BufferIsCompilableUnit(context, global, buffer->str, + buffer->len)); bool ok; { @@ -293,8 +217,8 @@ gjs_console_interact(JSContext *context, } g_string_free(buffer, true); - auto gjs_context = static_cast(JS_GetContextPrivate(context)); - ok = _gjs_context_run_jobs(gjs_context) && ok; + GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); + ok = gjs->run_jobs_fallible() && ok; if (!ok) { /* If this was an uncatchable exception, throw another uncatchable @@ -308,9 +232,6 @@ gjs_console_interact(JSContext *context, g_fprintf(stdout, "\n"); - if (file != stdin) - fclose(file); - argv.rval().setUndefined(); return true; } @@ -320,6 +241,8 @@ gjs_define_console_stuff(JSContext *context, JS::MutableHandleObject module) { module.set(JS_NewPlainObject(context)); - return JS_DefineFunction(context, module, "interact", gjs_console_interact, - 1, GJS_MODULE_PROP_FLAGS); + const GjsAtoms& atoms = GjsContextPrivate::atoms(context); + return JS_DefineFunctionById(context, module, atoms.interact(), + gjs_console_interact, 1, + GJS_MODULE_PROP_FLAGS); } diff --git a/modules/console.h b/modules/console.h index 9e44fc5..4ca1707 100644 --- a/modules/console.h +++ b/modules/console.h @@ -21,18 +21,17 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_CONSOLE_H__ -#define __GJS_CONSOLE_H__ +#ifndef MODULES_CONSOLE_H_ +#define MODULES_CONSOLE_H_ #include -#include -#include "cjs/jsapi-util.h" -G_BEGIN_DECLS +#include +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION bool gjs_define_console_stuff(JSContext *context, JS::MutableHandleObject module); -G_END_DECLS - -#endif /* __GJS_CONSOLE_H__ */ +#endif // MODULES_CONSOLE_H_ diff --git a/modules/cairo.js b/modules/core/_cairo.js similarity index 56% rename from modules/cairo.js rename to modules/core/_cairo.js index bc5ad57..f02656d 100644 --- a/modules/cairo.js +++ b/modules/core/_cairo.js @@ -18,130 +18,128 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -const Lang = imports.lang; +/* exported Antialias, Content, Extend, FillRule, Filter, FontSlant, FontWeight, +Format, LineCap, LineJoin, Operator, PatternType, SurfaceType */ var Antialias = { DEFAULT: 0, NONE: 1, GRAY: 2, - SUBPIXEL: 3 + SUBPIXEL: 3, }; var Content = { - COLOR : 0x1000, - ALPHA : 0x2000, - COLOR_ALPHA : 0x3000 + COLOR: 0x1000, + ALPHA: 0x2000, + COLOR_ALPHA: 0x3000, }; var Extend = { - NONE : 0, - REPEAT : 1, - REFLECT : 2, - PAD : 3 + NONE: 0, + REPEAT: 1, + REFLECT: 2, + PAD: 3, }; var FillRule = { WINDING: 0, - EVEN_ODD: 1 + EVEN_ODD: 1, }; var Filter = { - FAST : 0, - GOOD : 1, - BEST : 2, - NEAREST : 3, - BILINEAR : 4, - GAUSSIAN : 5 + FAST: 0, + GOOD: 1, + BEST: 2, + NEAREST: 3, + BILINEAR: 4, + GAUSSIAN: 5, }; var FontSlant = { NORMAL: 0, ITALIC: 1, - OBLIQUE: 2 + OBLIQUE: 2, }; var FontWeight = { - NORMAL : 0, - BOLD : 1 + NORMAL: 0, + BOLD: 1, }; var Format = { - ARGB32 : 0, - RGB24 : 1, - A8 : 2, - A1 : 3, + ARGB32: 0, + RGB24: 1, + A8: 2, + A1: 3, // The value of 4 is reserved by a deprecated enum value - RGB16_565: 5 + RGB16_565: 5, }; var LineCap = { BUTT: 0, ROUND: 1, - SQUASH: 2 + SQUASH: 2, }; var LineJoin = { MITER: 0, ROUND: 1, - BEVEL: 2 + BEVEL: 2, }; var Operator = { CLEAR: 0, SOURCE: 1, OVER: 2, - IN : 3, - OUT : 4, - ATOP : 5, - DEST : 6, - DEST_OVER : 7, - DEST_IN : 8, - DEST_OUT : 9, - DEST_ATOP : 10, - XOR : 11, - ADD : 12, - SATURATE : 13, - MULTIPLY : 14, - SCREEN : 15, - OVERLAY : 16, - DARKEN : 17, - LIGHTEN : 18, - COLOR_DODGE : 19, - COLOR_BURN : 20, - HARD_LIGHT : 21, - SOFT_LIGHT : 22, - DIFFERENCE : 23, - EXCLUSION : 24, - HSL_HUE : 25, - HSL_SATURATION : 26, - HSL_COLOR : 27, - HSL_LUMINOSITY : 28 + IN: 3, + OUT: 4, + ATOP: 5, + DEST: 6, + DEST_OVER: 7, + DEST_IN: 8, + DEST_OUT: 9, + DEST_ATOP: 10, + XOR: 11, + ADD: 12, + SATURATE: 13, + MULTIPLY: 14, + SCREEN: 15, + OVERLAY: 16, + DARKEN: 17, + LIGHTEN: 18, + COLOR_DODGE: 19, + COLOR_BURN: 20, + HARD_LIGHT: 21, + SOFT_LIGHT: 22, + DIFFERENCE: 23, + EXCLUSION: 24, + HSL_HUE: 25, + HSL_SATURATION: 26, + HSL_COLOR: 27, + HSL_LUMINOSITY: 28, }; var PatternType = { - SOLID : 0, - SURFACE : 1, - LINEAR : 2, - RADIAL : 3 + SOLID: 0, + SURFACE: 1, + LINEAR: 2, + RADIAL: 3, }; var SurfaceType = { - IMAGE : 0, - PDF : 1, - PS : 2, - XLIB : 3, - XCB : 4, - GLITZ : 5, - QUARTZ : 6, - WIN32 : 7, - BEOS : 8, - DIRECTFB : 9, - SVG : 10, - OS2 : 11, - WIN32_PRINTING : 12, - QUARTZ_IMAGE : 13 + IMAGE: 0, + PDF: 1, + PS: 2, + XLIB: 3, + XCB: 4, + GLITZ: 5, + QUARTZ: 6, + WIN32: 7, + BEOS: 8, + DIRECTFB: 9, + SVG: 10, + OS2: 11, + WIN32_PRINTING: 12, + QUARTZ_IMAGE: 13, }; -// Merge stuff defined in native code -Lang.copyProperties(imports.cairoNative, this); - diff --git a/modules/core/_common.js b/modules/core/_common.js new file mode 100644 index 0000000..73edb05 --- /dev/null +++ b/modules/core/_common.js @@ -0,0 +1,107 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +// Copyright 2020 Philip Chimento +// +// 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. + +/* exported _checkAccessors */ + +// This is a helper module in which to put code that is common between the +// legacy GObject.Class system and the new GObject.registerClass system. + +function _generateAccessors(pspec, propdesc, GObject) { + const {name, flags} = pspec; + const readable = flags & GObject.ParamFlags.READABLE; + const writable = flags & GObject.ParamFlags.WRITABLE; + + if (!propdesc) { + propdesc = { + configurable: true, + enumerable: true, + }; + } + + if (readable && writable) { + if (!propdesc.get && !propdesc.set) { + const privateName = Symbol(`__autogeneratedAccessor__${name}`); + const defaultValue = pspec.get_default_value(); + propdesc.get = function () { + if (!(privateName in this)) + this[privateName] = defaultValue; + return this[privateName]; + }; + propdesc.set = function (value) { + if (value !== this[privateName]) { + this[privateName] = value; + this.notify(name); + } + }; + } else if (!propdesc.get) { + propdesc.get = function () { + throw new Error(`setter defined without getter for property ${name}`); + }; + } else if (!propdesc.set) { + propdesc.set = function () { + throw new Error(`getter defined without setter for property ${name}`); + }; + } + } else if (readable && !propdesc.get) { + propdesc.get = function () { + throw new Error(`missing getter for read-only property ${name}`); + }; + } else if (writable && !propdesc.set) { + propdesc.set = function () { + throw new Error(`missing setter for write-only property ${name}`); + }; + } + + return propdesc; +} + +function _checkAccessors(proto, pspec, GObject) { + const {name, flags} = pspec; + + const underscoreName = name.replace(/-/g, '_'); + const camelName = name.replace(/-([a-z])/g, match => match[1].toUpperCase()); + let propdesc = Object.getOwnPropertyDescriptor(proto, name); + let dashPropdesc = propdesc, underscorePropdesc, camelPropdesc; + const nameIsCompound = name.includes('-'); + if (nameIsCompound) { + underscorePropdesc = Object.getOwnPropertyDescriptor(proto, underscoreName); + camelPropdesc = Object.getOwnPropertyDescriptor(proto, camelName); + if (!propdesc) + propdesc = underscorePropdesc; + if (!propdesc) + propdesc = camelPropdesc; + } + + const readable = flags & GObject.ParamFlags.READABLE; + const writable = flags & GObject.ParamFlags.WRITABLE; + if (!propdesc || (readable && !propdesc.get) || (writable && !propdesc.set)) + propdesc = _generateAccessors(pspec, propdesc, GObject); + + if (!dashPropdesc) + Object.defineProperty(proto, name, propdesc); + if (nameIsCompound) { + if (!underscorePropdesc) + Object.defineProperty(proto, underscoreName, propdesc); + if (!camelPropdesc) + Object.defineProperty(proto, camelName, propdesc); + } +} diff --git a/modules/core/_format.js b/modules/core/_format.js new file mode 100644 index 0000000..0b25b82 --- /dev/null +++ b/modules/core/_format.js @@ -0,0 +1,67 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +/* exported vprintf */ + +const CjsPrivate = imports.gi.CjsPrivate; + +function vprintf(string, args) { + let i = 0; + let usePos = false; + return string.replace(/%(?:([1-9][0-9]*)\$)?(I+)?([0-9]+)?(?:\.([0-9]+))?(.)/g, function (str, posGroup, flagsGroup, widthGroup, precisionGroup, genericGroup) { + if (precisionGroup !== '' && precisionGroup !== undefined && + genericGroup !== 'f') + throw new Error("Precision can only be specified for 'f'"); + + let hasAlternativeIntFlag = flagsGroup && + flagsGroup.indexOf('I') !== -1; + if (hasAlternativeIntFlag && genericGroup !== 'd') + throw new Error("Alternative output digits can only be specfied for 'd'"); + + let pos = parseInt(posGroup, 10) || 0; + if (!usePos && i === 0) + usePos = pos > 0; + if (usePos && pos === 0 || !usePos && pos > 0) + throw new Error('Numbered and unnumbered conversion specifications cannot be mixed'); + + let fillChar = widthGroup && widthGroup[0] === '0' ? '0' : ' '; + let width = parseInt(widthGroup, 10) || 0; + + function fillWidth(s, c, w) { + let fill = c.repeat(w); + return fill.substr(s.length) + s; + } + + function getArg() { + return usePos ? args[pos - 1] : args[i++]; + } + + let s = ''; + switch (genericGroup) { + case '%': + return '%'; + case 's': + s = String(getArg()); + break; + case 'd': { + let intV = parseInt(getArg()); + if (hasAlternativeIntFlag) + s = CjsPrivate.format_int_alternative_output(intV); + else + s = intV.toString(); + break; + } + case 'x': + s = parseInt(getArg()).toString(16); + break; + case 'f': + if (precisionGroup === '' || precisionGroup === undefined) + s = parseFloat(getArg()).toString(); + else + s = parseFloat(getArg()).toFixed(parseInt(precisionGroup)); + break; + default: + throw new Error(`Unsupported conversion character %${genericGroup}`); + } + return fillWidth(s, fillChar, width); + }); +} diff --git a/modules/gettext.js b/modules/core/_gettext.js similarity index 70% rename from modules/gettext.js rename to modules/core/_gettext.js index e68b166..f67e1f3 100644 --- a/modules/gettext.js +++ b/modules/core/_gettext.js @@ -18,6 +18,9 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. +/* exported bindtextdomain, dcgettext, dgettext, dngettext, domain, dpgettext, +gettext, LocaleCategory, ngettext, pgettext, setlocale, textdomain */ + /** * This module provides a convenience layer for the "gettext" family of functions, * relying on GLib for the actual implementation. @@ -35,65 +38,63 @@ const GLib = imports.gi.GLib; const CjsPrivate = imports.gi.CjsPrivate; -const LocaleCategory = CjsPrivate.LocaleCategory; +var LocaleCategory = CjsPrivate.LocaleCategory; function setlocale(category, locale) { return CjsPrivate.setlocale(category, locale); } -function textdomain(domain) { - return CjsPrivate.textdomain(domain); +function textdomain(dom) { + return CjsPrivate.textdomain(dom); } -function bindtextdomain(domain, location) { - return CjsPrivate.bindtextdomain(domain, location); +function bindtextdomain(dom, location) { + return CjsPrivate.bindtextdomain(dom, location); } function gettext(msgid) { return GLib.dgettext(null, msgid); } -function dgettext(domain, msgid) { - return GLib.dgettext(domain, msgid); +function dgettext(dom, msgid) { + return GLib.dgettext(dom, msgid); } -function dcgettext(domain, msgid, category) { - return GLib.dcgettext(domain, msgid, category); +function dcgettext(dom, msgid, category) { + return GLib.dcgettext(dom, msgid, category); } function ngettext(msgid1, msgid2, n) { return GLib.dngettext(null, msgid1, msgid2, n); } -function dngettext(domain, msgid1, msgid2, n) { - return GLib.dngettext(domain, msgid1, msgid2, n); +function dngettext(dom, msgid1, msgid2, n) { + return GLib.dngettext(dom, msgid1, msgid2, n); } // FIXME: missing dcngettext ? function pgettext(context, msgid) { return GLib.dpgettext2(null, context, msgid); } -function dpgettext(domain, context, msgid) { - return GLib.dpgettext2(domain, context, msgid); +function dpgettext(dom, context, msgid) { + return GLib.dpgettext2(dom, context, msgid); } /** * Create an object with bindings for gettext, ngettext, * and pgettext bound to a particular translation domain. * - * @param domainName Translation domain string - * @returns: an object with gettext bindings - * @type: function + * @param {string} domainName Translation domain string + * @returns {object} an object with gettext bindings */ -var domain = function(domainName) { +function domain(domainName) { return { - gettext: function(msgid) { + gettext(msgid) { return GLib.dgettext(domainName, msgid); }, - ngettext: function(msgid1, msgid2, n) { + ngettext(msgid1, msgid2, n) { return GLib.dngettext(domainName, msgid1, msgid2, n); }, - pgettext: function(context, msgid) { + pgettext(context, msgid) { return GLib.dpgettext2(domainName, context, msgid); - } + }, }; -}; - +} diff --git a/modules/signals.js b/modules/core/_signals.js similarity index 67% rename from modules/signals.js rename to modules/core/_signals.js index 4f4e240..bd133cf 100644 --- a/modules/signals.js +++ b/modules/core/_signals.js @@ -20,19 +20,19 @@ * IN THE SOFTWARE. */ +/* exported addSignalMethods */ + // A couple principals of this simple signal system: // 1) should look just like our GObject signal binding // 2) memory and safety matter more than speed of connect/disconnect/emit // 3) the expectation is that a given object will have a very small number of // connections, but they may be to different signal names -const Lang = imports.lang; - function _connect(name, callback) { // be paranoid about callback arg since we'd start to throw from emit() // if it was messed up - if (typeof(callback) != 'function') - throw new Error("When connecting signal must give a callback that is a function"); + if (typeof callback !== 'function') + throw new Error('When connecting signal must give a callback that is a function'); // we instantiate the "signal machinery" only on-demand if anything // gets connected. @@ -47,11 +47,12 @@ function _connect(name, callback) { // this makes it O(n) in total connections to emit, but I think // it's right to optimize for low memory and reentrancy-safety // rather than speed - this._signalConnections.push({ 'id' : id, - 'name' : name, - 'callback' : callback, - 'disconnected' : false - }); + this._signalConnections.push({ + id, + name, + callback, + 'disconnected': false, + }); return id; } @@ -61,9 +62,9 @@ function _disconnect(id) { let length = this._signalConnections.length; for (i = 0; i < length; ++i) { let connection = this._signalConnections[i]; - if (connection.id == id) { + if (connection.id === id) { if (connection.disconnected) - throw new Error("Signal handler id " + id + " already disconnected"); + throw new Error(`Signal handler id ${id} already disconnected`); // set a flag to deal with removal during emission connection.disconnected = true; @@ -73,20 +74,18 @@ function _disconnect(id) { } } } - throw new Error("No signal connection " + id + " found"); + throw new Error(`No signal connection ${id} found`); } function _signalHandlerIsConnected(id) { - if (! '_signalConnections' in this) + if (!('_signalConnections' in this)) return false; - for (let connection of this._signalConnections) { - if (connection.id == id) { - if (connection.disconnected) - return false; - else - return true; - } + const {length} = this._signalConnections; + for (let i = 0; i < length; ++i) { + const connection = this._signalConnections[i]; + if (connection.id === id) + return !connection.disconnected; } return false; @@ -94,13 +93,12 @@ function _signalHandlerIsConnected(id) { function _disconnectAll() { if ('_signalConnections' in this) { - while (this._signalConnections.length > 0) { + while (this._signalConnections.length > 0) _disconnect.call(this, this._signalConnections[0].id); - } } } -function _emit(name /* , arg1, arg2 */) { +function _emit(name, ...args) { // may not be any signal handlers at all, if not then return if (!('_signalConnections' in this)) return; @@ -114,9 +112,8 @@ function _emit(name /* , arg1, arg2 */) { let length = this._signalConnections.length; for (i = 0; i < length; ++i) { let connection = this._signalConnections[i]; - if (connection.name == name) { + if (connection.name === name) handlers.push(connection); - } } // create arg array which is emitter + everything passed in except @@ -125,13 +122,7 @@ function _emit(name /* , arg1, arg2 */) { // which does pass it in. Also if we pass in the emitter here, // people don't create closures with the emitter in them, // which would be a cycle. - - let arg_array = [ this ]; - // arguments[0] should be signal name so skip it - length = arguments.length; - for (i = 1; i < length; ++i) { - arg_array.push(arguments[i]); - } + let argArray = [this, ...args]; length = handlers.length; for (i = 0; i < length; ++i) { @@ -139,44 +130,33 @@ function _emit(name /* , arg1, arg2 */) { if (!connection.disconnected) { try { // since we pass "null" for this, the global object will be used. - let ret = connection.callback.apply(null, arg_array); + let ret = connection.callback.apply(null, argArray); // if the callback returns true, we don't call the next // signal handlers - if (ret === true) { + if (ret === true) break; - } - } catch(e) { + } catch (e) { // just log any exceptions so that callbacks can't disrupt // signal emission - logError(e, "Exception in callback for signal: "+name); + logError(e, `Exception in callback for signal: ${name}`); } } } } function _addSignalMethod(proto, functionName, func) { - if (proto[functionName] && proto[functionName] != func) { - log("WARNING: addSignalMethods is replacing existing " + - proto + " " + functionName + " method"); - } + if (proto[functionName] && proto[functionName] !== func) + log(`WARNING: addSignalMethods is replacing existing ${proto} ${functionName} method`); proto[functionName] = func; } function addSignalMethods(proto) { - _addSignalMethod(proto, "connect", _connect); - _addSignalMethod(proto, "disconnect", _disconnect); - _addSignalMethod(proto, "emit", _emit); - _addSignalMethod(proto, "signalHandlerIsConnected", _signalHandlerIsConnected) + _addSignalMethod(proto, 'connect', _connect); + _addSignalMethod(proto, 'disconnect', _disconnect); + _addSignalMethod(proto, 'emit', _emit); + _addSignalMethod(proto, 'signalHandlerIsConnected', _signalHandlerIsConnected); // this one is not in GObject, but useful - _addSignalMethod(proto, "disconnectAll", _disconnectAll); + _addSignalMethod(proto, 'disconnectAll', _disconnectAll); } - -var WithSignals = new Lang.Interface({ - Name: 'WithSignals', - connect: _connect, - disconnect: _disconnect, - emit: _emit, - disconnectAll: _disconnectAll, -}); diff --git a/modules/core/overrides/.eslintrc.yml b/modules/core/overrides/.eslintrc.yml new file mode 100644 index 0000000..189476b --- /dev/null +++ b/modules/core/overrides/.eslintrc.yml @@ -0,0 +1,5 @@ +--- +rules: + no-unused-vars: + - error + - varsIgnorePattern: ^_init$ diff --git a/modules/core/overrides/GLib.js b/modules/core/overrides/GLib.js new file mode 100644 index 0000000..b6073d5 --- /dev/null +++ b/modules/core/overrides/GLib.js @@ -0,0 +1,463 @@ +// Copyright 2011 Giovanni Campagna +// +// 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. + +const ByteArray = imports.byteArray; + +let GLib; + +const SIMPLE_TYPES = ['b', 'y', 'n', 'q', 'i', 'u', 'x', 't', 'h', 'd', 's', 'o', 'g']; + +function _readSingleType(signature, forceSimple) { + let char = signature.shift(); + let isSimple = false; + + if (!SIMPLE_TYPES.includes(char)) { + if (forceSimple) + throw new TypeError('Invalid GVariant signature (a simple type was expected)'); + } else { + isSimple = true; + } + + if (char === 'm' || char === 'a') + return [char].concat(_readSingleType(signature, false)); + if (char === '{') { + let key = _readSingleType(signature, true); + let val = _readSingleType(signature, false); + let close = signature.shift(); + if (close !== '}') + throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}"'); + return [char].concat(key, val, close); + } + if (char === '(') { + let res = [char]; + while (true) { + if (signature.length === 0) + throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")'); + let next = signature[0]; + if (next === ')') { + signature.shift(); + return res.concat(next); + } + let el = _readSingleType(signature); + res = res.concat(el); + } + } + + // Valid types are simple types, arrays, maybes, tuples, dictionary entries and variants + if (!isSimple && char !== 'v') + throw new TypeError(`Invalid GVariant signature (${char} is not a valid type)`); + + return [char]; +} + +function _makeBytes(byteArray) { + if (byteArray instanceof Uint8Array || byteArray instanceof ByteArray.ByteArray) + return ByteArray.toGBytes(byteArray); + else + return new GLib.Bytes(byteArray); +} + +function _packVariant(signature, value) { + if (signature.length === 0) + throw new TypeError('GVariant signature cannot be empty'); + + let char = signature.shift(); + switch (char) { + case 'b': + return GLib.Variant.new_boolean(value); + case 'y': + return GLib.Variant.new_byte(value); + case 'n': + return GLib.Variant.new_int16(value); + case 'q': + return GLib.Variant.new_uint16(value); + case 'i': + return GLib.Variant.new_int32(value); + case 'u': + return GLib.Variant.new_uint32(value); + case 'x': + return GLib.Variant.new_int64(value); + case 't': + return GLib.Variant.new_uint64(value); + case 'h': + return GLib.Variant.new_handle(value); + case 'd': + return GLib.Variant.new_double(value); + case 's': + return GLib.Variant.new_string(value); + case 'o': + return GLib.Variant.new_object_path(value); + case 'g': + return GLib.Variant.new_signature(value); + case 'v': + return GLib.Variant.new_variant(value); + case 'm': + if (value !== null) { + return GLib.Variant.new_maybe(null, _packVariant(signature, value)); + } else { + return GLib.Variant.new_maybe(new GLib.VariantType( + _readSingleType(signature, false).join('')), null); + } + case 'a': { + let arrayType = _readSingleType(signature, false); + if (arrayType[0] === 's') { + // special case for array of strings + return GLib.Variant.new_strv(value); + } + if (arrayType[0] === 'y') { + // special case for array of bytes + let bytes; + if (typeof value === 'string') { + let byteArray = ByteArray.fromString(value); + if (byteArray[byteArray.length - 1] !== 0) + byteArray = Uint8Array.of(...byteArray, 0); + bytes = ByteArray.toGBytes(byteArray); + } else { + bytes = _makeBytes(value); + } + return GLib.Variant.new_from_bytes(new GLib.VariantType('ay'), + bytes, true); + } + + let arrayValue = []; + if (arrayType[0] === '{') { + // special case for dictionaries + for (let key in value) { + let copy = [].concat(arrayType); + let child = _packVariant(copy, [key, value[key]]); + arrayValue.push(child); + } + } else { + for (let i = 0; i < value.length; i++) { + let copy = [].concat(arrayType); + let child = _packVariant(copy, value[i]); + arrayValue.push(child); + } + } + return GLib.Variant.new_array(new GLib.VariantType(arrayType.join('')), arrayValue); + } + + case '(': { + let children = []; + for (let i = 0; i < value.length; i++) { + let next = signature[0]; + if (next === ')') + break; + children.push(_packVariant(signature, value[i])); + } + + if (signature[0] !== ')') + throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")'); + signature.shift(); + return GLib.Variant.new_tuple(children); + } + case '{': { + let key = _packVariant(signature, value[0]); + let child = _packVariant(signature, value[1]); + + if (signature[0] !== '}') + throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}")'); + signature.shift(); + + return GLib.Variant.new_dict_entry(key, child); + } + default: + throw new TypeError(`Invalid GVariant signature (unexpected character ${char})`); + } +} + +function _unpackVariant(variant, deep, recursive = false) { + switch (String.fromCharCode(variant.classify())) { + case 'b': + return variant.get_boolean(); + case 'y': + return variant.get_byte(); + case 'n': + return variant.get_int16(); + case 'q': + return variant.get_uint16(); + case 'i': + return variant.get_int32(); + case 'u': + return variant.get_uint32(); + case 'x': + return variant.get_int64(); + case 't': + return variant.get_uint64(); + case 'h': + return variant.get_handle(); + case 'd': + return variant.get_double(); + case 'o': + case 'g': + case 's': + // g_variant_get_string has length as out argument + return variant.get_string()[0]; + case 'v': { + const ret = variant.get_variant(); + if (deep && recursive && ret instanceof GLib.Variant) + return _unpackVariant(ret, deep, recursive); + return ret; + } + case 'm': { + let val = variant.get_maybe(); + if (deep && val) + return _unpackVariant(val, deep, recursive); + else + return val; + } + case 'a': + if (variant.is_of_type(new GLib.VariantType('a{?*}'))) { + // special case containers + let ret = { }; + let nElements = variant.n_children(); + for (let i = 0; i < nElements; i++) { + // always unpack the dictionary entry, and always unpack + // the key (or it cannot be added as a key) + let val = _unpackVariant(variant.get_child_value(i), deep, + recursive); + let key; + if (!deep) + key = _unpackVariant(val[0], true); + else + key = val[0]; + ret[key] = val[1]; + } + return ret; + } + if (variant.is_of_type(new GLib.VariantType('ay'))) { + // special case byte arrays + return variant.get_data_as_bytes().toArray(); + } + + // fall through + case '(': + case '{': { + let ret = []; + let nElements = variant.n_children(); + for (let i = 0; i < nElements; i++) { + let val = variant.get_child_value(i); + if (deep) + ret.push(_unpackVariant(val, deep, recursive)); + else + ret.push(val); + } + return ret; + } + } + + throw new Error('Assertion failure: this code should not be reached'); +} + +function _notIntrospectableError(funcName, replacement) { + return new Error(`${funcName} is not introspectable. Use ${replacement} instead.`); +} + +function _warnNotIntrospectable(funcName, replacement) { + logError(_notIntrospectableError(funcName, replacement)); +} + +function _escapeCharacterSetChars(char) { + if ('-^]\\'.includes(char)) + return `\\${char}`; + return char; +} + +function _init() { + // this is imports.gi.GLib + + GLib = this; + + // small HACK: we add a matches() method to standard Errors so that + // you can do "if (e.matches(Ns.FooError, Ns.FooError.SOME_CODE))" + // without checking instanceof + Error.prototype.matches = function () { + return false; + }; + + this.Variant._new_internal = function (sig, value) { + let signature = Array.prototype.slice.call(sig); + + let variant = _packVariant(signature, value); + if (signature.length !== 0) + throw new TypeError('Invalid GVariant signature (more than one single complete type)'); + + return variant; + }; + + // Deprecate version of new GLib.Variant() + this.Variant.new = function (sig, value) { + return new GLib.Variant(sig, value); + }; + this.Variant.prototype.unpack = function () { + return _unpackVariant(this, false); + }; + this.Variant.prototype.deepUnpack = function () { + return _unpackVariant(this, true); + }; + // backwards compatibility alias + this.Variant.prototype.deep_unpack = this.Variant.prototype.deepUnpack; + + // Note: discards type information, if the variant contains any 'v' types + this.Variant.prototype.recursiveUnpack = function () { + return _unpackVariant(this, true, true); + }; + + this.Variant.prototype.toString = function () { + return `[object variant of type "${this.get_type_string()}"]`; + }; + + this.Bytes.prototype.toArray = function () { + return imports.byteArray.fromGBytes(this); + }; + + this.log_structured = function (logDomain, logLevel, stringFields) { + let fields = {}; + for (let key in stringFields) + fields[key] = new GLib.Variant('s', stringFields[key]); + + GLib.log_variant(logDomain, logLevel, new GLib.Variant('a{sv}', fields)); + }; + + this.VariantDict.prototype.lookup = function (key, variantType = null, deep = false) { + if (typeof variantType === 'string') + variantType = new GLib.VariantType(variantType); + + const variant = this.lookup_value(key, variantType); + if (variant === null) + return null; + return _unpackVariant(variant, deep); + }; + + // Prevent user code from calling GLib string manipulation functions that + // return the same string that was passed in. These can't be annotated + // properly, and will mostly crash. + // Here we provide approximate implementations of the functions so that if + // they had happened to work in the past, they will continue working, but + // log a stack trace and a suggestion of what to use instead. + // Exceptions are thrown instead for GLib.stpcpy() of which the return value + // is useless anyway and GLib.ascii_formatd() which is too complicated to + // implement here. + + this.stpcpy = function () { + throw _notIntrospectableError('GLib.stpcpy()', 'the + operator'); + }; + + this.strstr_len = function (haystack, len, needle) { + _warnNotIntrospectable('GLib.strstr_len()', 'String.indexOf()'); + let searchString = haystack; + if (len !== -1) + searchString = searchString.slice(0, len); + const index = searchString.indexOf(needle); + if (index === -1) + return null; + return haystack.slice(index); + }; + + this.strrstr = function (haystack, needle) { + _warnNotIntrospectable('GLib.strrstr()', 'String.lastIndexOf()'); + const index = haystack.lastIndexOf(needle); + if (index === -1) + return null; + return haystack.slice(index); + }; + + this.strrstr_len = function (haystack, len, needle) { + _warnNotIntrospectable('GLib.strrstr_len()', 'String.lastIndexOf()'); + let searchString = haystack; + if (len !== -1) + searchString = searchString.slice(0, len); + const index = searchString.lastIndexOf(needle); + if (index === -1) + return null; + return haystack.slice(index); + }; + + this.strup = function (string) { + _warnNotIntrospectable('GLib.strup()', + 'String.toUpperCase() or GLib.ascii_strup()'); + return string.toUpperCase(); + }; + + this.strdown = function (string) { + _warnNotIntrospectable('GLib.strdown()', + 'String.toLowerCase() or GLib.ascii_strdown()'); + return string.toLowerCase(); + }; + + this.strreverse = function (string) { + _warnNotIntrospectable('GLib.strreverse()', + 'Array.reverse() and String.join()'); + return [...string].reverse().join(''); + }; + + this.ascii_dtostr = function (unused, len, number) { + _warnNotIntrospectable('GLib.ascii_dtostr()', 'JS string conversion'); + return `${number}`.slice(0, len); + }; + + this.ascii_formatd = function () { + throw _notIntrospectableError('GLib.ascii_formatd()', + 'Number.toExponential() and string interpolation'); + }; + + this.strchug = function (string) { + _warnNotIntrospectable('GLib.strchug()', 'String.trimStart()'); + return string.trimStart(); + }; + + this.strchomp = function (string) { + _warnNotIntrospectable('GLib.strchomp()', 'String.trimEnd()'); + return string.trimEnd(); + }; + + // g_strstrip() is a macro and therefore doesn't even appear in the GIR + // file, but we may as well include it here since it's trivial + this.strstrip = function (string) { + _warnNotIntrospectable('GLib.strstrip()', 'String.trim()'); + return string.trim(); + }; + + this.strdelimit = function (string, delimiters, newDelimiter) { + _warnNotIntrospectable('GLib.strdelimit()', 'String.replace()'); + + if (delimiters === null) + delimiters = GLib.STR_DELIMITERS; + if (typeof newDelimiter === 'number') + newDelimiter = String.fromCharCode(newDelimiter); + + const delimiterChars = delimiters.split(''); + const escapedDelimiterChars = delimiterChars.map(_escapeCharacterSetChars); + const delimiterRegex = new RegExp(`[${escapedDelimiterChars.join('')}]`, 'g'); + return string.replace(delimiterRegex, newDelimiter); + }; + + this.strcanon = function (string, validChars, substitutor) { + _warnNotIntrospectable('GLib.strcanon()', 'String.replace()'); + + if (typeof substitutor === 'number') + substitutor = String.fromCharCode(substitutor); + + const validArray = validChars.split(''); + const escapedValidArray = validArray.map(_escapeCharacterSetChars); + const invalidRegex = new RegExp(`[^${escapedValidArray.join('')}]`, 'g'); + return string.replace(invalidRegex, substitutor); + }; +} diff --git a/modules/core/overrides/GObject.js b/modules/core/overrides/GObject.js new file mode 100644 index 0000000..9d851d2 --- /dev/null +++ b/modules/core/overrides/GObject.js @@ -0,0 +1,752 @@ +/* exported _init, interfaces, properties, registerClass, requires, signals */ +// Copyright 2011 Jasper St. Pierre +// Copyright 2017 Philip Chimento , +// +// 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. + +const Gi = imports._gi; +const CjsPrivate = imports.gi.CjsPrivate; +const {_checkAccessors} = imports._common; +const Legacy = imports._legacy; + +let GObject; + +var GTypeName = Symbol('GType name'); +var GTypeFlags = Symbol('GType flags'); +var interfaces = Symbol('GObject interfaces'); +var properties = Symbol('GObject properties'); +var requires = Symbol('GObject interface requires'); +var signals = Symbol('GObject signals'); + +// These four will be aliased to GTK +var _gtkChildren = Symbol('GTK widget template children'); +var _gtkCssName = Symbol('GTK widget CSS name'); +var _gtkInternalChildren = Symbol('GTK widget template internal children'); +var _gtkTemplate = Symbol('GTK widget template'); + +function registerClass(...args) { + let klass = args[0]; + if (args.length === 2) { + // The two-argument form is the convenient syntax without ESnext + // decorators and class data properties. The first argument is an + // object with meta info such as properties and signals. The second + // argument is the class expression for the class itself. + // + // var MyClass = GObject.registerClass({ + // Properties: { ... }, + // Signals: { ... }, + // }, class MyClass extends GObject.Object { + // _init() { ... } + // }); + // + // When decorators and class data properties become part of the JS + // standard, this function can be used directly as a decorator. + let metaInfo = args[0]; + klass = args[1]; + if ('GTypeName' in metaInfo) + klass[GTypeName] = metaInfo.GTypeName; + if ('GTypeFlags' in metaInfo) + klass[GTypeFlags] = metaInfo.GTypeFlags; + if ('Implements' in metaInfo) + klass[interfaces] = metaInfo.Implements; + if ('Properties' in metaInfo) + klass[properties] = metaInfo.Properties; + if ('Signals' in metaInfo) + klass[signals] = metaInfo.Signals; + if ('Requires' in metaInfo) + klass[requires] = metaInfo.Requires; + if ('CssName' in metaInfo) + klass[_gtkCssName] = metaInfo.CssName; + if ('Template' in metaInfo) + klass[_gtkTemplate] = metaInfo.Template; + if ('Children' in metaInfo) + klass[_gtkChildren] = metaInfo.Children; + if ('InternalChildren' in metaInfo) + klass[_gtkInternalChildren] = metaInfo.InternalChildren; + } + + if (!(klass.prototype instanceof GObject.Object) && + !(klass.prototype instanceof GObject.Interface)) { + throw new TypeError('GObject.registerClass() used with invalid base ' + + `class (is ${Object.getPrototypeOf(klass).name})`); + } + + // Find the "least derived" class with a _classInit static function; there + // definitely is one, since this class must inherit from GObject + let initclass = klass; + while (typeof initclass._classInit === 'undefined') + initclass = Object.getPrototypeOf(initclass.prototype).constructor; + return initclass._classInit(klass); +} + +// Some common functions between GObject.Class and GObject.Interface + +function _createSignals(gtype, sigs) { + for (let signalName in sigs) { + let obj = sigs[signalName]; + let flags = obj.flags !== undefined ? obj.flags : GObject.SignalFlags.RUN_FIRST; + let accumulator = obj.accumulator !== undefined ? obj.accumulator : GObject.AccumulatorType.NONE; + let rtype = obj.return_type !== undefined ? obj.return_type : GObject.TYPE_NONE; + let paramtypes = obj.param_types !== undefined ? obj.param_types : []; + + try { + obj.signal_id = Gi.signal_new(gtype, signalName, flags, accumulator, rtype, paramtypes); + } catch (e) { + throw new TypeError(`Invalid signal ${signalName}: ${e.message}`); + } + } +} + +function _getCallerBasename() { + const stackLines = new Error().stack.trim().split('\n'); + const lineRegex = new RegExp(/@(.+:\/\/)?(.*\/)?(.+)\.js:\d+(:[\d]+)?$/); + let thisFile = null; + let thisDir = null; + + for (let line of stackLines) { + let match = line.match(lineRegex); + if (match) { + let scriptDir = match[2]; + let scriptBasename = match[3]; + + if (!thisFile) { + thisDir = scriptDir; + thisFile = scriptBasename; + continue; + } + + if (scriptDir === thisDir && scriptBasename === thisFile) + continue; + + if (scriptDir && scriptDir.startsWith('/org/gnome/gjs/')) + continue; + + let basename = scriptBasename; + if (scriptDir) { + scriptDir = scriptDir.replace(/^\/|\/$/g, ''); + basename = `${scriptDir.split('/').reverse()[0]}_${basename}`; + } + return basename; + } + } + + return null; +} + +function _createGTypeName(klass) { + const sanitizeGType = s => s.replace(/[^a-z0-9+_-]/gi, '_'); + + if (klass.hasOwnProperty(GTypeName)) { + let sanitized = sanitizeGType(klass[GTypeName]); + if (sanitized !== klass[GTypeName]) { + logError(new RangeError(`Provided GType name '${klass[GTypeName]}' ` + + `is not valid; automatically sanitized to '${sanitized}'`)); + } + return sanitized; + } + + let gtypeClassName = klass.name; + if (GObject.gtypeNameBasedOnJSPath) { + let callerBasename = _getCallerBasename(); + if (callerBasename) + gtypeClassName = `${callerBasename}_${gtypeClassName}`; + } + + return sanitizeGType(`Gjs_${gtypeClassName}`); +} + +function _propertiesAsArray(klass) { + let propertiesArray = []; + if (klass.hasOwnProperty(properties)) { + for (let prop in klass[properties]) + propertiesArray.push(klass[properties][prop]); + } + return propertiesArray; +} + +function _copyAllDescriptors(target, source, filter) { + Object.getOwnPropertyNames(source) + .filter(key => !['prototype', 'constructor'].concat(filter).includes(key)) + .concat(Object.getOwnPropertySymbols(source)) + .forEach(key => { + let descriptor = Object.getOwnPropertyDescriptor(source, key); + Object.defineProperty(target, key, descriptor); + }); +} + +function _interfacePresent(required, klass) { + if (!klass[interfaces]) + return false; + if (klass[interfaces].includes(required)) + return true; // implemented here + // Might be implemented on a parent class + return _interfacePresent(required, Object.getPrototypeOf(klass)); +} + +function _checkInterface(iface, proto) { + // Checks for specific interfaces + + // Default vfunc_async_init() will run vfunc_init() in a thread and crash. + // Change error message when https://gitlab.gnome.org/GNOME/gjs/issues/72 + // has been solved. + if (iface.$gtype.name === 'GAsyncInitable' && + !Object.getOwnPropertyNames(proto).includes('vfunc_init_async')) + throw new Error("It's not currently possible to implement Gio.AsyncInitable."); + + // Check that proto implements all of this interface's required interfaces. + // "proto" refers to the object's prototype (which implements the interface) + // whereas "iface.prototype" is the interface's prototype (which may still + // contain unimplemented methods.) + if (typeof iface[requires] === 'undefined') + return; + + let unfulfilledReqs = iface[requires].filter(required => { + // Either the interface is not present or it is not listed before the + // interface that requires it or the class does not inherit it. This is + // so that required interfaces don't copy over properties from other + // interfaces that require them. + let ifaces = proto.constructor[interfaces]; + return (!_interfacePresent(required, proto.constructor) || + ifaces.indexOf(required) > ifaces.indexOf(iface)) && + !(proto instanceof required); + }).map(required => + // required.name will be present on JS classes, but on introspected + // GObjects it will be the C name. The alternative is just so that + // we print something if there is garbage in Requires. + required.name || required); + if (unfulfilledReqs.length > 0) { + throw new Error('The following interfaces must be implemented before ' + + `${iface.name}: ${unfulfilledReqs.join(', ')}`); + } +} + +function _init() { + + GObject = this; + + function _makeDummyClass(obj, name, upperName, gtypeName, actual) { + let gtype = GObject.type_from_name(gtypeName); + obj[`TYPE_${upperName}`] = gtype; + obj[name] = function (v) { + return new actual(v); + }; + obj[name].$gtype = gtype; + } + + GObject.gtypeNameBasedOnJSPath = false; + + _makeDummyClass(GObject, 'VoidType', 'NONE', 'void', function () {}); + _makeDummyClass(GObject, 'Char', 'CHAR', 'gchar', Number); + _makeDummyClass(GObject, 'UChar', 'UCHAR', 'guchar', Number); + _makeDummyClass(GObject, 'Unichar', 'UNICHAR', 'gint', String); + + GObject.TYPE_BOOLEAN = GObject.type_from_name('gboolean'); + GObject.Boolean = Boolean; + Boolean.$gtype = GObject.TYPE_BOOLEAN; + + _makeDummyClass(GObject, 'Int', 'INT', 'gint', Number); + _makeDummyClass(GObject, 'UInt', 'UINT', 'guint', Number); + _makeDummyClass(GObject, 'Long', 'LONG', 'glong', Number); + _makeDummyClass(GObject, 'ULong', 'ULONG', 'gulong', Number); + _makeDummyClass(GObject, 'Int64', 'INT64', 'gint64', Number); + _makeDummyClass(GObject, 'UInt64', 'UINT64', 'guint64', Number); + + GObject.TYPE_ENUM = GObject.type_from_name('GEnum'); + GObject.TYPE_FLAGS = GObject.type_from_name('GFlags'); + + _makeDummyClass(GObject, 'Float', 'FLOAT', 'gfloat', Number); + GObject.TYPE_DOUBLE = GObject.type_from_name('gdouble'); + GObject.Double = Number; + Number.$gtype = GObject.TYPE_DOUBLE; + + GObject.TYPE_STRING = GObject.type_from_name('gchararray'); + GObject.String = String; + String.$gtype = GObject.TYPE_STRING; + + GObject.TYPE_POINTER = GObject.type_from_name('gpointer'); + GObject.TYPE_BOXED = GObject.type_from_name('GBoxed'); + GObject.TYPE_PARAM = GObject.type_from_name('GParam'); + GObject.TYPE_INTERFACE = GObject.type_from_name('GInterface'); + GObject.TYPE_OBJECT = GObject.type_from_name('GObject'); + GObject.TYPE_VARIANT = GObject.type_from_name('GVariant'); + + _makeDummyClass(GObject, 'Type', 'GTYPE', 'GType', GObject.type_from_name); + + GObject.ParamSpec.char = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_char(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.uchar = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_uchar(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.int = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_int(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.uint = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_uint(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.long = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_long(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.ulong = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_ulong(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.int64 = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_int64(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.uint64 = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_uint64(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.float = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_float(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.boolean = function (name, nick, blurb, flags, defaultValue) { + return GObject.param_spec_boolean(name, nick, blurb, defaultValue, flags); + }; + + GObject.ParamSpec.flags = function (name, nick, blurb, flags, flagsType, defaultValue) { + return GObject.param_spec_flags(name, nick, blurb, flagsType, defaultValue, flags); + }; + + GObject.ParamSpec.enum = function (name, nick, blurb, flags, enumType, defaultValue) { + return GObject.param_spec_enum(name, nick, blurb, enumType, defaultValue, flags); + }; + + GObject.ParamSpec.double = function (name, nick, blurb, flags, minimum, maximum, defaultValue) { + return GObject.param_spec_double(name, nick, blurb, minimum, maximum, defaultValue, flags); + }; + + GObject.ParamSpec.string = function (name, nick, blurb, flags, defaultValue) { + return GObject.param_spec_string(name, nick, blurb, defaultValue, flags); + }; + + GObject.ParamSpec.boxed = function (name, nick, blurb, flags, boxedType) { + return GObject.param_spec_boxed(name, nick, blurb, boxedType, flags); + }; + + GObject.ParamSpec.object = function (name, nick, blurb, flags, objectType) { + return GObject.param_spec_object(name, nick, blurb, objectType, flags); + }; + + GObject.ParamSpec.param = function (name, nick, blurb, flags, paramType) { + return GObject.param_spec_param(name, nick, blurb, paramType, flags); + }; + + GObject.ParamSpec.override = Gi.override_property; + + Object.defineProperties(GObject.ParamSpec.prototype, { + 'name': { + configurable: false, + enumerable: false, + get() { + return this.get_name(); + }, + }, + '_nick': { + configurable: false, + enumerable: false, + get() { + return this.get_nick(); + }, + }, + 'nick': { + configurable: false, + enumerable: false, + get() { + return this.get_nick(); + }, + }, + '_blurb': { + configurable: false, + enumerable: false, + get() { + return this.get_blurb(); + }, + }, + 'blurb': { + configurable: false, + enumerable: false, + get() { + return this.get_blurb(); + }, + }, + 'default_value': { + configurable: false, + enumerable: false, + get() { + return this.get_default_value(); + }, + }, + 'flags': { + configurable: false, + enumerable: false, + get() { + return CjsPrivate.param_spec_get_flags(this); + }, + }, + 'value_type': { + configurable: false, + enumerable: false, + get() { + return CjsPrivate.param_spec_get_value_type(this); + }, + }, + 'owner_type': { + configurable: false, + enumerable: false, + get() { + return CjsPrivate.param_spec_get_owner_type(this); + }, + }, + }); + + let {GObjectMeta, GObjectInterface} = Legacy.defineGObjectLegacyObjects(GObject); + GObject.Class = GObjectMeta; + GObject.Interface = GObjectInterface; + GObject.Object.prototype.__metaclass__ = GObject.Class; + + // For compatibility with Lang.Class... we need a _construct + // or the Lang.Class constructor will fail. + GObject.Object.prototype._construct = function (...args) { + this._init(...args); + return this; + }; + + GObject.registerClass = registerClass; + + GObject.Object._classInit = function (klass) { + let gtypename = _createGTypeName(klass); + let gflags = klass.hasOwnProperty(GTypeFlags) ? klass[GTypeFlags] : 0; + let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? klass[interfaces] : []; + let propertiesArray = _propertiesAsArray(klass); + let parent = Object.getPrototypeOf(klass); + let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : []; + + propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject)); + + let newClass = Gi.register_type(parent.prototype, gtypename, gflags, + gobjectInterfaces, propertiesArray); + Object.setPrototypeOf(newClass, parent); + + _createSignals(newClass.$gtype, gobjectSignals); + + _copyAllDescriptors(newClass, klass); + gobjectInterfaces.forEach(iface => + _copyAllDescriptors(newClass.prototype, iface.prototype, + ['toString'])); + _copyAllDescriptors(newClass.prototype, klass.prototype); + + Object.getOwnPropertyNames(newClass.prototype) + .filter(name => name.startsWith('vfunc_') || name.startsWith('on_')) + .forEach(name => { + let descr = Object.getOwnPropertyDescriptor(newClass.prototype, name); + if (typeof descr.value !== 'function') + return; + + let func = newClass.prototype[name]; + + if (name.startsWith('vfunc_')) { + newClass.prototype[Gi.hook_up_vfunc_symbol](name.slice(6), func); + } else if (name.startsWith('on_')) { + let id = GObject.signal_lookup(name.slice(3).replace('_', '-'), + newClass.$gtype); + if (id !== 0) { + GObject.signal_override_class_closure(id, newClass.$gtype, function (...argArray) { + let emitter = argArray.shift(); + + return func.apply(emitter, argArray); + }); + } + } + }); + + gobjectInterfaces.forEach(iface => + _checkInterface(iface, newClass.prototype)); + + // For backwards compatibility only. Use instanceof instead. + newClass.implements = function (iface) { + if (iface.$gtype) + return GObject.type_is_a(newClass.$gtype, iface.$gtype); + return false; + }; + + return newClass; + }; + + GObject.Interface._classInit = function (klass) { + let gtypename = _createGTypeName(klass); + let gobjectInterfaces = klass.hasOwnProperty(requires) ? klass[requires] : []; + let props = _propertiesAsArray(klass); + let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : []; + + let newInterface = Gi.register_interface(gtypename, gobjectInterfaces, + props); + + _createSignals(newInterface.$gtype, gobjectSignals); + + _copyAllDescriptors(newInterface, klass); + + Object.getOwnPropertyNames(klass.prototype) + .filter(key => key !== 'constructor') + .concat(Object.getOwnPropertySymbols(klass.prototype)) + .forEach(key => { + let descr = Object.getOwnPropertyDescriptor(klass.prototype, key); + + // Create wrappers on the interface object so that generics work (e.g. + // SomeInterface.some_function(this, blah) instead of + // SomeInterface.prototype.some_function.call(this, blah) + if (typeof descr.value === 'function') { + let interfaceProto = klass.prototype; // capture in closure + newInterface[key] = function (thisObj, ...args) { + return interfaceProto[key].call(thisObj, ...args); + }; + } + + Object.defineProperty(newInterface.prototype, key, descr); + }); + + return newInterface; + }; + + /** + * Use this to signify a function that must be overridden in an + * implementation of the interface. + */ + GObject.NotImplementedError = class NotImplementedError extends Error { + get name() { + return 'NotImplementedError'; + } + }; + + // These will be copied in the Gtk overrides + // Use __X__ syntax to indicate these variables should not be used publicly. + + GObject.__gtkCssName__ = _gtkCssName; + GObject.__gtkTemplate__ = _gtkTemplate; + GObject.__gtkChildren__ = _gtkChildren; + GObject.__gtkInternalChildren__ = _gtkInternalChildren; + + // Expose GObject static properties for ES6 classes + + GObject.GTypeName = GTypeName; + GObject.requires = requires; + GObject.interfaces = interfaces; + GObject.properties = properties; + GObject.signals = signals; + + // Replacement for non-introspectable g_object_set() + GObject.Object.prototype.set = function (params) { + Object.assign(this, params); + }; + + // fake enum for signal accumulators, keep in sync with gi/object.c + GObject.AccumulatorType = { + NONE: 0, + FIRST_WINS: 1, + TRUE_HANDLED: 2, + }; + + GObject.Object.prototype.disconnect = function (id) { + return GObject.signal_handler_disconnect(this, id); + }; + GObject.Object.prototype.block_signal_handler = function (id) { + return GObject.signal_handler_block(this, id); + }; + GObject.Object.prototype.unblock_signal_handler = function (id) { + return GObject.signal_handler_unblock(this, id); + }; + GObject.Object.prototype.stop_emission_by_name = function (detailedName) { + return GObject.signal_stop_emission_by_name(this, detailedName); + }; + + // A simple workaround if you have a class with .connect, .disconnect or .emit + // methods (such as Gio.Socket.connect or NMClient.Device.disconnect) + // The original g_signal_* functions are not introspectable anyway, because + // we need our own handling of signal argument marshalling + GObject.signal_connect = function (object, name, handler) { + return GObject.Object.prototype.connect.call(object, name, handler); + }; + GObject.signal_connect_after = function (object, name, handler) { + return GObject.Object.prototype.connect_after.call(object, name, handler); + }; + GObject.signal_emit_by_name = function (object, ...nameAndArgs) { + return GObject.Object.prototype.emit.apply(object, nameAndArgs); + }; + + // Replacements for signal_handler_find() and similar functions, which can't + // work normally since we connect private closures + GObject._real_signal_handler_find = GObject.signal_handler_find; + GObject._real_signal_handlers_block_matched = GObject.signal_handlers_block_matched; + GObject._real_signal_handlers_unblock_matched = GObject.signal_handlers_unblock_matched; + GObject._real_signal_handlers_disconnect_matched = GObject.signal_handlers_disconnect_matched; + + /** + * Finds the first signal handler that matches certain selection criteria. + * The criteria are passed as properties of a match object. + * The match object has to be non-empty for successful matches. + * If no handler was found, a falsy value is returned. + * @function + * @param {GObject.Object} instance - the instance owning the signal handler + * to be found. + * @param {Object} match - a properties object indicating whether to match + * by signal ID, detail, or callback function. + * @param {string} [match.signalId] - signal the handler has to be connected + * to. + * @param {string} [match.detail] - signal detail the handler has to be + * connected to. + * @param {Function} [match.func] - the callback function the handler will + * invoke. + * @returns {number|BigInt|Object|null} A valid non-0 signal handler ID for + * a successful match. + */ + GObject.signal_handler_find = function (instance, match) { + // For backwards compatibility + if (arguments.length === 7) + // eslint-disable-next-line prefer-rest-params + return GObject._real_signal_handler_find(...arguments); + return instance[Gi.signal_find_symbol](match); + }; + /** + * Blocks all handlers on an instance that match certain selection criteria. + * The criteria are passed as properties of a match object. + * The match object has to have at least `func` for successful matches. + * If no handlers were found, 0 is returned, the number of blocked handlers + * otherwise. + * @function + * @param {GObject.Object} instance - the instance owning the signal handler + * to be found. + * @param {Object} match - a properties object indicating whether to match + * by signal ID, detail, or callback function. + * @param {string} [match.signalId] - signal the handler has to be connected + * to. + * @param {string} [match.detail] - signal detail the handler has to be + * connected to. + * @param {Function} match.func - the callback function the handler will + * invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_block_matched = function (instance, match) { + // For backwards compatibility + if (arguments.length === 7) + // eslint-disable-next-line prefer-rest-params + return GObject._real_signal_handlers_block_matched(...arguments); + return instance[Gi.signals_block_symbol](match); + }; + /** + * Unblocks all handlers on an instance that match certain selection + * criteria. + * The criteria are passed as properties of a match object. + * The match object has to have at least `func` for successful matches. + * If no handlers were found, 0 is returned, the number of unblocked + * handlers otherwise. + * The match criteria should not apply to any handlers that are not + * currently blocked. + * @function + * @param {GObject.Object} instance - the instance owning the signal handler + * to be found. + * @param {Object} match - a properties object indicating whether to match + * by signal ID, detail, or callback function. + * @param {string} [match.signalId] - signal the handler has to be connected + * to. + * @param {string} [match.detail] - signal detail the handler has to be + * connected to. + * @param {Function} match.func - the callback function the handler will + * invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_unblock_matched = function (instance, match) { + // For backwards compatibility + if (arguments.length === 7) + // eslint-disable-next-line prefer-rest-params + return GObject._real_signal_handlers_unblock_matched(...arguments); + return instance[Gi.signals_unblock_symbol](match); + }; + /** + * Disconnects all handlers on an instance that match certain selection + * criteria. + * The criteria are passed as properties of a match object. + * The match object has to have at least `func` for successful matches. + * If no handlers were found, 0 is returned, the number of disconnected + * handlers otherwise. + * @function + * @param {GObject.Object} instance - the instance owning the signal handler + * to be found. + * @param {Object} match - a properties object indicating whether to match + * by signal ID, detail, or callback function. + * @param {string} [match.signalId] - signal the handler has to be connected + * to. + * @param {string} [match.detail] - signal detail the handler has to be + * connected to. + * @param {Function} match.func - the callback function the handler will + * invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_disconnect_matched = function (instance, match) { + // For backwards compatibility + if (arguments.length === 7) + // eslint-disable-next-line prefer-rest-params + return GObject._real_signal_handlers_disconnect_matched(...arguments); + return instance[Gi.signals_disconnect_symbol](match); + }; + + // Also match the macros used in C APIs, even though they're not introspected + + /** + * Blocks all handlers on an instance that match `func`. + * @function + * @param {GObject.Object} instance - the instance to block handlers from. + * @param {Function} func - the callback function the handler will invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_block_by_func = function (instance, func) { + return instance[Gi.signals_block_symbol]({func}); + }; + /** + * Unblocks all handlers on an instance that match `func`. + * @function + * @param {GObject.Object} instance - the instance to unblock handlers from. + * @param {Function} func - the callback function the handler will invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_unblock_by_func = function (instance, func) { + return instance[Gi.signals_unblock_symbol]({func}); + }; + /** + * Disconnects all handlers on an instance that match `func`. + * @function + * @param {GObject.Object} instance - the instance to remove handlers from. + * @param {Function} func - the callback function the handler will invoke. + * @returns {number} The number of handlers that matched. + */ + GObject.signal_handlers_disconnect_by_func = function (instance, func) { + return instance[Gi.signals_disconnect_symbol]({func}); + }; + GObject.signal_handlers_disconnect_by_data = function () { + throw new Error('GObject.signal_handlers_disconnect_by_data() is not \ +introspectable. Use GObject.signal_handlers_disconnect_by_func() instead.'); + }; +} diff --git a/modules/core/overrides/Gio.js b/modules/core/overrides/Gio.js new file mode 100644 index 0000000..c26b4c4 --- /dev/null +++ b/modules/core/overrides/Gio.js @@ -0,0 +1,624 @@ +// Copyright 2011 Giovanni Campagna +// +// 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. + +var GLib = imports.gi.GLib; +var CjsPrivate = imports.gi.CjsPrivate; +var Signals = imports.signals; +var Gio; + +// Ensures that a Gio.UnixFDList being passed into or out of a DBus method with +// a parameter type that includes 'h' somewhere, actually has entries in it for +// each of the indices being passed as an 'h' parameter. +function _validateFDVariant(variant, fdList) { + switch (String.fromCharCode(variant.classify())) { + case 'b': + case 'y': + case 'n': + case 'q': + case 'i': + case 'u': + case 'x': + case 't': + case 'd': + case 'o': + case 'g': + case 's': + return; + case 'h': { + const val = variant.get_handle(); + const numFds = fdList.get_length(); + if (val >= numFds) { + throw new Error(`handle ${val} is out of range of Gio.UnixFDList ` + + `containing ${numFds} FDs`); + } + return; + } + case 'v': + _validateFDVariant(variant.get_variant(), fdList); + return; + case 'm': { + let val = variant.get_maybe(); + if (val) + _validateFDVariant(val, fdList); + return; + } + case 'a': + case '(': + case '{': { + let nElements = variant.n_children(); + for (let ix = 0; ix < nElements; ix++) + _validateFDVariant(variant.get_child_value(ix), fdList); + return; + } + } + + throw new Error('Assertion failure: this code should not be reached'); +} + +function _proxyInvoker(methodName, sync, inSignature, argArray) { + var replyFunc; + var flags = 0; + var cancellable = null; + let fdList = null; + + /* Convert argArray to a *real* array */ + argArray = Array.prototype.slice.call(argArray); + + /* The default replyFunc only logs the responses */ + replyFunc = _logReply; + + var signatureLength = inSignature.length; + var minNumberArgs = signatureLength; + var maxNumberArgs = signatureLength + 4; + + if (argArray.length < minNumberArgs) { + throw new Error(`Not enough arguments passed for method: ${ + methodName}. Expected ${minNumberArgs}, got ${argArray.length}`); + } else if (argArray.length > maxNumberArgs) { + throw new Error(`Too many arguments passed for method ${methodName}. ` + + `Maximum is ${maxNumberArgs} including one callback, ` + + 'Gio.Cancellable, Gio.UnixFDList, and/or flags'); + } + + while (argArray.length > signatureLength) { + var argNum = argArray.length - 1; + var arg = argArray.pop(); + if (typeof arg === 'function' && !sync) { + replyFunc = arg; + } else if (typeof arg === 'number') { + flags = arg; + } else if (arg instanceof Gio.Cancellable) { + cancellable = arg; + } else if (arg instanceof Gio.UnixFDList) { + fdList = arg; + } else { + throw new Error(`Argument ${argNum} of method ${methodName} is ` + + `${typeof arg}. It should be a callback, flags, ` + + 'Gio.UnixFDList, or a Gio.Cancellable'); + } + } + + const inTypeString = `(${inSignature.join('')})`; + const inVariant = new GLib.Variant(inTypeString, argArray); + if (inTypeString.includes('h')) { + if (!fdList) { + throw new Error(`Method ${methodName} with input type containing ` + + '\'h\' must have a Gio.UnixFDList as an argument'); + } + _validateFDVariant(inVariant, fdList); + } + + var asyncCallback = (proxy, result) => { + try { + const [outVariant, outFdList] = + proxy.call_with_unix_fd_list_finish(result); + replyFunc(outVariant.deepUnpack(), null, outFdList); + } catch (e) { + replyFunc([], e, null); + } + }; + + if (sync) { + const [outVariant, outFdList] = this.call_with_unix_fd_list_sync( + methodName, inVariant, flags, -1, fdList, cancellable); + if (fdList) + return [outVariant.deepUnpack(), outFdList]; + return outVariant.deepUnpack(); + } + + return this.call_with_unix_fd_list(methodName, inVariant, flags, -1, fdList, + cancellable, asyncCallback); +} + +function _logReply(result, exc) { + if (exc !== null) + log(`Ignored exception from dbus method: ${exc}`); +} + +function _makeProxyMethod(method, sync) { + var i; + var name = method.name; + var inArgs = method.in_args; + var inSignature = []; + for (i = 0; i < inArgs.length; i++) + inSignature.push(inArgs[i].signature); + + return function (...args) { + return _proxyInvoker.call(this, name, sync, inSignature, args); + }; +} + +function _convertToNativeSignal(proxy, senderName, signalName, parameters) { + Signals._emit.call(proxy, signalName, senderName, parameters.deepUnpack()); +} + +function _propertyGetter(name) { + let value = this.get_cached_property(name); + return value ? value.deepUnpack() : null; +} + +function _propertySetter(name, signature, value) { + let variant = new GLib.Variant(signature, value); + this.set_cached_property(name, variant); + + this.call('org.freedesktop.DBus.Properties.Set', + new GLib.Variant('(ssv)', [this.g_interface_name, name, variant]), + Gio.DBusCallFlags.NONE, -1, null, + (proxy, result) => { + try { + this.call_finish(result); + } catch (e) { + log(`Could not set property ${name} on remote object ${ + this.g_object_path}: ${e.message}`); + } + }); +} + +function _addDBusConvenience() { + let info = this.g_interface_info; + if (!info) + return; + + if (info.signals.length > 0) + this.connect('g-signal', _convertToNativeSignal); + + let i, methods = info.methods; + for (i = 0; i < methods.length; i++) { + var method = methods[i]; + this[`${method.name}Remote`] = _makeProxyMethod(methods[i], false); + this[`${method.name}Sync`] = _makeProxyMethod(methods[i], true); + } + + let properties = info.properties; + for (i = 0; i < properties.length; i++) { + let name = properties[i].name; + let signature = properties[i].signature; + let flags = properties[i].flags; + let getter = () => { + throw new Error(`Property ${name} is not readable`); + }; + let setter = () => { + throw new Error(`Property ${name} is not writable`); + }; + + if (flags & Gio.DBusPropertyInfoFlags.READABLE) + getter = _propertyGetter.bind(this, name); + + if (flags & Gio.DBusPropertyInfoFlags.WRITABLE) + setter = _propertySetter.bind(this, name, signature); + + Object.defineProperty(this, name, { + get: getter, + set: setter, + configurable: false, + enumerable: true, + }); + } +} + +function _makeProxyWrapper(interfaceXml) { + var info = _newInterfaceInfo(interfaceXml); + var iname = info.name; + return function (bus, name, object, asyncCallback, cancellable, + flags = Gio.DBusProxyFlags.NONE) { + var obj = new Gio.DBusProxy({ + g_connection: bus, + g_interface_name: iname, + g_interface_info: info, + g_name: name, + g_flags: flags, + g_object_path: object, + }); + + if (!cancellable) + cancellable = null; + if (asyncCallback) { + obj.init_async(GLib.PRIORITY_DEFAULT, cancellable, (initable, result) => { + let caughtErrorWhenInitting = null; + try { + initable.init_finish(result); + } catch (e) { + caughtErrorWhenInitting = e; + } + + if (caughtErrorWhenInitting === null) + asyncCallback(initable, null); + else + asyncCallback(null, caughtErrorWhenInitting); + }); + } else { + obj.init(cancellable); + } + return obj; + }; +} + + +function _newNodeInfo(constructor, value) { + if (typeof value === 'string') + return constructor(value); + throw TypeError(`Invalid type ${Object.prototype.toString.call(value)}`); +} + +function _newInterfaceInfo(value) { + var nodeInfo = Gio.DBusNodeInfo.new_for_xml(value); + return nodeInfo.interfaces[0]; +} + +function _injectToMethod(klass, method, addition) { + var previous = klass[method]; + + klass[method] = function (...args) { + addition.apply(this, args); + return previous.apply(this, args); + }; +} + +function _injectToStaticMethod(klass, method, addition) { + var previous = klass[method]; + + klass[method] = function (...parameters) { + let obj = previous.apply(this, parameters); + addition.apply(obj, parameters); + return obj; + }; +} + +function _wrapFunction(klass, method, addition) { + var previous = klass[method]; + + klass[method] = function (...args) { + args.unshift(previous); + return addition.apply(this, args); + }; +} + +function _makeOutSignature(args) { + var ret = '('; + for (var i = 0; i < args.length; i++) + ret += args[i].signature; + + return `${ret})`; +} + +function _handleMethodCall(info, impl, methodName, parameters, invocation) { + // prefer a sync version if available + if (this[methodName]) { + let retval; + try { + const fdList = invocation.get_message().get_unix_fd_list(); + retval = this[methodName](...parameters.deepUnpack(), fdList); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + let name = e.name; + if (!name.includes('.')) { + // likely to be a normal JS error + name = `org.gnome.gjs.JSError.${name}`; + } + logError(e, `Exception in method call: ${methodName}`); + invocation.return_dbus_error(name, e.message); + } + return; + } + if (retval === undefined) { + // undefined (no return value) is the empty tuple + retval = new GLib.Variant('()', []); + } + try { + let outFdList = null; + if (!(retval instanceof GLib.Variant)) { + // attempt packing according to out signature + let methodInfo = info.lookup_method(methodName); + let outArgs = methodInfo.out_args; + let outSignature = _makeOutSignature(outArgs); + if (outSignature.includes('h') && + retval[retval.length - 1] instanceof Gio.UnixFDList) { + outFdList = retval.pop(); + } else if (outArgs.length === 1) { + // if one arg, we don't require the handler wrapping it + // into an Array + retval = [retval]; + } + retval = new GLib.Variant(outSignature, retval); + } + invocation.return_value_with_unix_fd_list(retval, outFdList); + } catch (e) { + // if we don't do this, the other side will never see a reply + invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type'); + } + } else if (this[`${methodName}Async`]) { + const fdList = invocation.get_message().get_unix_fd_list(); + this[`${methodName}Async`](parameters.deepUnpack(), invocation, fdList); + } else { + log(`Missing handler for DBus method ${methodName}`); + invocation.return_gerror(new Gio.DBusError({ + code: Gio.DBusError.UNKNOWN_METHOD, + message: `Method ${methodName} is not implemented`, + })); + } +} + +function _handlePropertyGet(info, impl, propertyName) { + let propInfo = info.lookup_property(propertyName); + let jsval = this[propertyName]; + if (jsval !== undefined) + return new GLib.Variant(propInfo.signature, jsval); + else + return null; +} + +function _handlePropertySet(info, impl, propertyName, newValue) { + this[propertyName] = newValue.deepUnpack(); +} + +function _wrapJSObject(interfaceInfo, jsObj) { + var info; + if (interfaceInfo instanceof Gio.DBusInterfaceInfo) + info = interfaceInfo; + else + info = Gio.DBusInterfaceInfo.new_for_xml(interfaceInfo); + info.cache_build(); + + var impl = new CjsPrivate.DBusImplementation({g_interface_info: info}); + impl.connect('handle-method-call', function (self, methodName, parameters, invocation) { + return _handleMethodCall.call(jsObj, info, self, methodName, parameters, invocation); + }); + impl.connect('handle-property-get', function (self, propertyName) { + return _handlePropertyGet.call(jsObj, info, self, propertyName); + }); + impl.connect('handle-property-set', function (self, propertyName, value) { + return _handlePropertySet.call(jsObj, info, self, propertyName, value); + }); + + return impl; +} + +function* _listModelIterator() { + let _index = 0; + const _len = this.get_n_items(); + while (_index < _len) + yield this.get_item(_index++); +} + +function _promisify(proto, asyncFunc, finishFunc) { + if (proto[`_original_${asyncFunc}`] !== undefined) + return; + proto[`_original_${asyncFunc}`] = proto[asyncFunc]; + proto[asyncFunc] = function (...args) { + if (!args.every(arg => typeof arg !== 'function')) + return this[`_original_${asyncFunc}`](...args); + return new Promise((resolve, reject) => { + const callStack = new Error().stack.split('\n').filter(line => !line.match(/promisify/)).join('\n'); + this[`_original_${asyncFunc}`](...args, function (source, res) { + try { + const result = source !== null && source[finishFunc] !== undefined + ? source[finishFunc](res) + : proto[finishFunc](res); + if (Array.isArray(result) && result.length > 1 && result[0] === true) + result.shift(); + resolve(result); + } catch (error) { + if (error.stack) + error.stack += `### Promise created here: ###\n${callStack}`; + else + error.stack = callStack; + reject(error); + } + }); + }); + }; +} + +function _init() { + Gio = this; + + Gio.DBus = { + get session() { + return Gio.bus_get_sync(Gio.BusType.SESSION, null); + }, + get system() { + return Gio.bus_get_sync(Gio.BusType.SYSTEM, null); + }, + + // Namespace some functions + get: Gio.bus_get, + get_finish: Gio.bus_get_finish, + get_sync: Gio.bus_get_sync, + + own_name: Gio.bus_own_name, + own_name_on_connection: Gio.bus_own_name_on_connection, + unown_name: Gio.bus_unown_name, + + watch_name: Gio.bus_watch_name, + watch_name_on_connection: Gio.bus_watch_name_on_connection, + unwatch_name: Gio.bus_unwatch_name, + }; + + Gio.DBusConnection.prototype.watch_name = function (name, flags, appeared, vanished) { + return Gio.bus_watch_name_on_connection(this, name, flags, appeared, vanished); + }; + Gio.DBusConnection.prototype.unwatch_name = function (id) { + return Gio.bus_unwatch_name(id); + }; + Gio.DBusConnection.prototype.own_name = function (name, flags, acquired, lost) { + return Gio.bus_own_name_on_connection(this, name, flags, acquired, lost); + }; + Gio.DBusConnection.prototype.unown_name = function (id) { + return Gio.bus_unown_name(id); + }; + + _injectToMethod(Gio.DBusProxy.prototype, 'init', _addDBusConvenience); + _injectToMethod(Gio.DBusProxy.prototype, 'init_async', _addDBusConvenience); + _injectToStaticMethod(Gio.DBusProxy, 'new_sync', _addDBusConvenience); + _injectToStaticMethod(Gio.DBusProxy, 'new_finish', _addDBusConvenience); + _injectToStaticMethod(Gio.DBusProxy, 'new_for_bus_sync', _addDBusConvenience); + _injectToStaticMethod(Gio.DBusProxy, 'new_for_bus_finish', _addDBusConvenience); + Gio.DBusProxy.prototype.connectSignal = Signals._connect; + Gio.DBusProxy.prototype.disconnectSignal = Signals._disconnect; + + Gio.DBusProxy.makeProxyWrapper = _makeProxyWrapper; + + // Some helpers + _wrapFunction(Gio.DBusNodeInfo, 'new_for_xml', _newNodeInfo); + Gio.DBusInterfaceInfo.new_for_xml = _newInterfaceInfo; + + Gio.DBusExportedObject = CjsPrivate.DBusImplementation; + Gio.DBusExportedObject.wrapJSObject = _wrapJSObject; + + // ListStore + Gio.ListStore.prototype[Symbol.iterator] = _listModelIterator; + + // Promisify + Gio._promisify = _promisify; + + // Temporary Gio.File.prototype fix + Gio._LocalFilePrototype = Gio.File.new_for_path('').constructor.prototype; + + // Override Gio.Settings and Gio.SettingsSchema - the C API asserts if + // trying to access a nonexistent schema or key, which is not handy for + // shell-extension writers + + Gio.SettingsSchema.prototype._realGetKey = Gio.SettingsSchema.prototype.get_key; + Gio.SettingsSchema.prototype.get_key = function (key) { + if (!this.has_key(key)) + throw new Error(`GSettings key ${key} not found in schema ${this.get_id()}`); + return this._realGetKey(key); + }; + + Gio.Settings.prototype._realMethods = Object.assign({}, Gio.Settings.prototype); + + function createCheckedMethod(method, checkMethod = '_checkKey') { + return function (id, ...args) { + this[checkMethod](id); + return this._realMethods[method].call(this, id, ...args); + }; + } + + Object.assign(Gio.Settings.prototype, { + _realInit: Gio.Settings.prototype._init, // add manually, not enumerable + _init(props = {}) { + // 'schema' is a deprecated alias for schema_id + const schemaIdProp = ['schema', 'schema-id', 'schema_id', + 'schemaId'].find(prop => prop in props); + const settingsSchemaProp = ['settings-schema', 'settings_schema', + 'settingsSchema'].find(prop => prop in props); + if (!schemaIdProp && !settingsSchemaProp) { + throw new Error('One of property \'schema-id\' or ' + + '\'settings-schema\' are required for Gio.Settings'); + } + + const source = Gio.SettingsSchemaSource.get_default(); + const settingsSchema = settingsSchemaProp + ? props[settingsSchemaProp] + : source.lookup(props[schemaIdProp], true); + + if (!settingsSchema) + throw new Error(`GSettings schema ${props[schemaIdProp]} not found`); + + const settingsSchemaPath = settingsSchema.get_path(); + if (props['path'] === undefined && !settingsSchemaPath) { + throw new Error('Attempting to create schema ' + + `'${settingsSchema.get_id()}' without a path`); + } + + if (props['path'] !== undefined && settingsSchemaPath && + props['path'] !== settingsSchemaPath) { + throw new Error(`GSettings created for path '${props['path']}'` + + `, but schema specifies '${settingsSchemaPath}'`); + } + + return this._realInit(props); + }, + + _checkKey(key) { + // Avoid using has_key(); checking a JS array is faster than calling + // through G-I. + if (!this._keys) + this._keys = this.settings_schema.list_keys(); + + if (!this._keys.includes(key)) + throw new Error(`GSettings key ${key} not found in schema ${this.schema_id}`); + }, + + _checkChild(name) { + if (!this._children) + this._children = this.list_children(); + + if (!this._children.includes(name)) + throw new Error(`Child ${name} not found in GSettings schema ${this.schema_id}`); + }, + + get_boolean: createCheckedMethod('get_boolean'), + set_boolean: createCheckedMethod('set_boolean'), + get_double: createCheckedMethod('get_double'), + set_double: createCheckedMethod('set_double'), + get_enum: createCheckedMethod('get_enum'), + set_enum: createCheckedMethod('set_enum'), + get_flags: createCheckedMethod('get_flags'), + set_flags: createCheckedMethod('set_flags'), + get_int: createCheckedMethod('get_int'), + set_int: createCheckedMethod('set_int'), + get_int64: createCheckedMethod('get_int64'), + set_int64: createCheckedMethod('set_int64'), + get_string: createCheckedMethod('get_string'), + set_string: createCheckedMethod('set_string'), + get_strv: createCheckedMethod('get_strv'), + set_strv: createCheckedMethod('set_strv'), + get_uint: createCheckedMethod('get_uint'), + set_uint: createCheckedMethod('set_uint'), + get_uint64: createCheckedMethod('get_uint64'), + set_uint64: createCheckedMethod('set_uint64'), + get_value: createCheckedMethod('get_value'), + set_value: createCheckedMethod('set_value'), + + bind: createCheckedMethod('bind'), + bind_writable: createCheckedMethod('bind_writable'), + create_action: createCheckedMethod('create_action'), + get_default_value: createCheckedMethod('get_default_value'), + get_user_value: createCheckedMethod('get_user_value'), + is_writable: createCheckedMethod('is_writable'), + reset: createCheckedMethod('reset'), + + get_child: createCheckedMethod('get_child', '_checkChild'), + }); +} diff --git a/modules/core/overrides/Gtk.js b/modules/core/overrides/Gtk.js new file mode 100644 index 0000000..12fb10a --- /dev/null +++ b/modules/core/overrides/Gtk.js @@ -0,0 +1,164 @@ +// application/javascript;version=1.8 +// Copyright 2013 Giovanni Campagna +// +// 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. + +const Legacy = imports._legacy; +const {Gio, CjsPrivate, GObject} = imports.gi; + +let Gtk; +let BuilderScope; + +function _init() { + + Gtk = this; + + Gtk.children = GObject.__gtkChildren__; + Gtk.cssName = GObject.__gtkCssName__; + Gtk.internalChildren = GObject.__gtkInternalChildren__; + Gtk.template = GObject.__gtkTemplate__; + + let {GtkWidgetClass} = Legacy.defineGtkLegacyObjects(GObject, Gtk); + Gtk.Widget.prototype.__metaclass__ = GtkWidgetClass; + + if (Gtk.Container && Gtk.Container.prototype.child_set_property) { + Gtk.Container.prototype.child_set_property = function (child, property, value) { + CjsPrivate.gtk_container_child_set_property(this, child, property, value); + }; + } + + Gtk.Widget.prototype._init = function (params) { + if (this.constructor[Gtk.template]) { + if (BuilderScope) { + Gtk.Widget.set_template_scope.call(this.constructor, + new BuilderScope(this)); + } else { + Gtk.Widget.set_connect_func.call(this.constructor, + (builder, obj, signalName, handlerName, connectObj, flags) => { + const swapped = flags & GObject.ConnectFlags.SWAPPED; + const closure = _createClosure( + builder, this, handlerName, swapped, connectObj); + + if (flags & GObject.ConnectFlags.AFTER) + obj.connect_after(signalName, closure); + else + obj.connect(signalName, closure); + }); + } + } + + GObject.Object.prototype._init.call(this, params); + + if (this.constructor[Gtk.template]) { + let children = this.constructor[Gtk.children] || []; + for (let child of children) { + this[child.replace(/-/g, '_')] = + this.get_template_child(this.constructor, child); + } + + let internalChildren = this.constructor[Gtk.internalChildren] || []; + for (let child of internalChildren) { + this[`_${child.replace(/-/g, '_')}`] = + this.get_template_child(this.constructor, child); + } + } + }; + + Gtk.Widget._classInit = function (klass) { + let template = klass[Gtk.template]; + let cssName = klass[Gtk.cssName]; + let children = klass[Gtk.children]; + let internalChildren = klass[Gtk.internalChildren]; + + if (template) { + klass.prototype._instance_init = function () { + this.init_template(); + }; + } + + klass = GObject.Object._classInit(klass); + + if (cssName) + Gtk.Widget.set_css_name.call(klass, cssName); + + if (template) { + if (typeof template === 'string') { + if (template.startsWith('resource:///')) { + Gtk.Widget.set_template_from_resource.call(klass, + template.slice(11)); + } else if (template.startsWith('file:///')) { + let file = Gio.File.new_for_uri(template); + let [, contents] = file.load_contents(null); + Gtk.Widget.set_template.call(klass, contents); + } + } else { + Gtk.Widget.set_template.call(klass, template); + } + } + + if (children) { + children.forEach(child => + Gtk.Widget.bind_template_child_full.call(klass, child, false, 0)); + } + + if (internalChildren) { + internalChildren.forEach(child => + Gtk.Widget.bind_template_child_full.call(klass, child, true, 0)); + } + + return klass; + }; + + if (Gtk.Widget.prototype.get_first_child) { + Gtk.Widget.prototype[Symbol.iterator] = function* () { + for (let c = this.get_first_child(); c; c = c.get_next_sibling()) + yield c; + }; + } + + if (Gtk.BuilderScope) { + BuilderScope = GObject.registerClass({ + Implements: [Gtk.BuilderScope], + }, class extends GObject.Object { + _init(thisArg) { + super._init(); + this._this = thisArg; + } + + vfunc_create_closure(builder, handlerName, flags, connectObject) { + const swapped = flags & Gtk.BuilderClosureFlags.SWAPPED; + return _createClosure( + builder, this._this, handlerName, swapped, connectObject); + } + }); + } +} + +function _createClosure(builder, thisArg, handlerName, swapped, connectObject) { + connectObject = connectObject || thisArg; + + if (swapped) { + throw new Error('Unsupported template signal flag "swapped"'); + } else if (typeof thisArg[handlerName] === 'undefined') { + throw new Error(`A handler called ${handlerName} was not ` + + `defined on ${thisArg}`); + } + + return thisArg[handlerName].bind(connectObject); +} diff --git a/modules/overrides/cairo.js b/modules/core/overrides/cairo.js similarity index 100% rename from modules/overrides/cairo.js rename to modules/core/overrides/cairo.js diff --git a/modules/format.js b/modules/format.js deleted file mode 100644 index 995c519..0000000 --- a/modules/format.js +++ /dev/null @@ -1,87 +0,0 @@ -// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - -const CjsPrivate = imports.gi.CjsPrivate; - -function vprintf(str, args) { - let i = 0; - let usePos = false; - return str.replace(/%(?:([1-9][0-9]*)\$)?(I+)?([0-9]+)?(?:\.([0-9]+))?(.)/g, function (str, posGroup, flagsGroup, widthGroup, precisionGroup, genericGroup) { - if (precisionGroup !== '' && precisionGroup !== undefined && - genericGroup != 'f') - throw new Error("Precision can only be specified for 'f'"); - - let hasAlternativeIntFlag = (flagsGroup && - flagsGroup.indexOf('I') != -1); - if (hasAlternativeIntFlag && genericGroup != 'd') - throw new Error("Alternative output digits can only be specfied for 'd'"); - - let pos = parseInt(posGroup, 10) || 0; - if (usePos == false && i == 0) - usePos = pos > 0; - if (usePos && pos == 0 || !usePos && pos > 0) - throw new Error("Numbered and unnumbered conversion specifications cannot be mixed"); - - let fillChar = (widthGroup && widthGroup[0] == '0') ? '0' : ' '; - let width = parseInt(widthGroup, 10) || 0; - - function fillWidth(s, c, w) { - let fill = ''; - for (let i = 0; i < w; i++) - fill += c; - return fill.substr(s.length) + s; - } - - function getArg() { - return usePos ? args[pos - 1] : args[i++]; - } - - let s = ''; - switch (genericGroup) { - case '%': - return '%'; - case 's': - s = String(getArg()); - break; - case 'd': - let intV = parseInt(getArg()); - if (hasAlternativeIntFlag) - s = CjsPrivate.format_int_alternative_output(intV); - else - s = intV.toString(); - break; - case 'x': - s = parseInt(getArg()).toString(16); - break; - case 'f': - if (precisionGroup === '' || precisionGroup === undefined) - s = parseFloat(getArg()).toString(); - else - s = parseFloat(getArg()).toFixed(parseInt(precisionGroup)); - break; - default: - throw new Error('Unsupported conversion character %' + genericGroup); - } - return fillWidth(s, fillChar, width); - }); -} - -function printf() { - let args = Array.prototype.slice.call(arguments); - let fmt = args.shift(); - print(vprintf(fmt, args)); -} - -/* - * This function is intended to extend the String object and provide - * an String.format API for string formatting. - * It has to be set up using String.prototype.format = Format.format; - * Usage: - * "somestring %s %d".format('hello', 5); - * It supports %s, %d, %x and %f, for %f it also support precisions like - * "%.2f".format(1.526). All specifiers can be prefixed with a minimum - * field width, e.g. "%5s".format("foo"). Unless the width is prefixed - * with '0', the formatted string will be padded with spaces. - */ -function format() { - return vprintf(this, arguments); -} diff --git a/modules/modules.cpp b/modules/modules.cpp index 620f6b5..85232b4 100644 --- a/modules/modules.cpp +++ b/modules/modules.cpp @@ -21,18 +21,18 @@ * IN THE SOFTWARE. */ -#include +#include // for ENABLE_CAIRO #include "cjs/native.h" -#include "modules.h" +#include "modules/console.h" +#include "modules/modules.h" +#include "modules/print.h" +#include "modules/system.h" #ifdef ENABLE_CAIRO -#include "cairo-module.h" +# include "modules/cairo-module.h" #endif -#include "system.h" -#include "console.h" - void gjs_register_static_modules (void) { @@ -41,4 +41,5 @@ gjs_register_static_modules (void) #endif gjs_register_native_module("system", gjs_js_define_system_stuff); gjs_register_native_module("console", gjs_define_console_stuff); + gjs_register_native_module("_print", gjs_define_print_stuff); } diff --git a/modules/modules.gresource.xml b/modules/modules.gresource.xml deleted file mode 100644 index 65d942e..0000000 --- a/modules/modules.gresource.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - modules/_bootstrap/default.js - modules/_bootstrap/coverage.js - - modules/tweener/equations.js - modules/tweener/tweener.js - modules/tweener/tweenList.js - - modules/overrides/cairo.js - modules/overrides/GLib.js - modules/overrides/Gio.js - modules/overrides/GObject.js - modules/overrides/Gtk.js - - modules/cairo.js - modules/gettext.js - modules/lang.js - modules/_legacy.js - modules/mainloop.js - modules/jsUnit.js - modules/signals.js - modules/format.js - modules/package.js - - diff --git a/modules/modules.h b/modules/modules.h index 94f3d15..a244b89 100644 --- a/modules/modules.h +++ b/modules/modules.h @@ -21,17 +21,9 @@ * IN THE SOFTWARE. */ -#ifndef __GJS_MODULES_H__ -#define __GJS_MODULES_H__ - -#include -#include -#include "cjs/jsapi-util.h" - -G_BEGIN_DECLS +#ifndef MODULES_MODULES_H_ +#define MODULES_MODULES_H_ void gjs_register_static_modules (void); -G_END_DECLS - -#endif /* __GJS_CONSOLE_H__ */ +#endif // MODULES_MODULES_H_ diff --git a/modules/overrides/GLib.js b/modules/overrides/GLib.js deleted file mode 100644 index f361bbe..0000000 --- a/modules/overrides/GLib.js +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2011 Giovanni Campagna -// -// 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. - -const ByteArray = imports.byteArray; - -let GLib; - -const SIMPLE_TYPES = ['b', 'y', 'n', 'q', 'i', 'u', 'x', 't', 'h', 'd', 's', 'o', 'g']; - -function _read_single_type(signature, forceSimple) { - let char = signature.shift(); - let isSimple = false; - - if (SIMPLE_TYPES.indexOf(char) == -1) { - if (forceSimple) - throw new TypeError('Invalid GVariant signature (a simple type was expected)'); - } else - isSimple = true; - - if (char == 'm' || char == 'a') - return [char].concat(_read_single_type(signature, false)); - if (char == '{') { - let key = _read_single_type(signature, true); - let val = _read_single_type(signature, false); - let close = signature.shift(); - if (close != '}') - throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}"'); - return [char].concat(key, val, close); - } - if (char == '(') { - let res = [char]; - while (true) { - if (signature.length == 0) - throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")'); - let next = signature[0]; - if (next == ')') { - signature.shift(); - return res.concat(next); - } - let el = _read_single_type(signature); - res = res.concat(el); - } - } - - // Valid types are simple types, arrays, maybes, tuples, dictionary entries and variants - if (!isSimple && char != 'v') - throw new TypeError('Invalid GVariant signature (' + char + ' is not a valid type)'); - - return [char]; -} - -function _makeBytes(byteArray) { - if (byteArray instanceof ByteArray.ByteArray) - return byteArray.toGBytes(); - else - return new GLib.Bytes(byteArray); -} - -function _pack_variant(signature, value) { - if (signature.length == 0) - throw new TypeError('GVariant signature cannot be empty'); - - let char = signature.shift(); - switch (char) { - case 'b': - return GLib.Variant.new_boolean(value); - case 'y': - return GLib.Variant.new_byte(value); - case 'n': - return GLib.Variant.new_int16(value); - case 'q': - return GLib.Variant.new_uint16(value); - case 'i': - return GLib.Variant.new_int32(value); - case 'u': - return GLib.Variant.new_uint32(value); - case 'x': - return GLib.Variant.new_int64(value); - case 't': - return GLib.Variant.new_uint64(value); - case 'h': - return GLib.Variant.new_handle(value); - case 'd': - return GLib.Variant.new_double(value); - case 's': - return GLib.Variant.new_string(value); - case 'o': - return GLib.Variant.new_object_path(value); - case 'g': - return GLib.Variant.new_signature(value); - case 'v': - return GLib.Variant.new_variant(value); - case 'm': - if (value != null) - return GLib.Variant.new_maybe(null, _pack_variant(signature, value)); - else - return GLib.Variant.new_maybe(new GLib.VariantType(_read_single_type(signature, false).join('')), null); - case 'a': - let arrayType = _read_single_type(signature, false); - if (arrayType[0] == 's') { - // special case for array of strings - return GLib.Variant.new_strv(value); - } - if (arrayType[0] == 'y') { - // special case for array of bytes - return GLib.Variant.new_from_bytes(new GLib.VariantType('ay'), - _makeBytes(value), true); - } - - let arrayValue = []; - if (arrayType[0] == '{') { - // special case for dictionaries - for (let key in value) { - let copy = [].concat(arrayType); - let child = _pack_variant(copy, [key, value[key]]); - arrayValue.push(child); - } - } else { - for (let i = 0; i < value.length; i++) { - let copy = [].concat(arrayType); - let child = _pack_variant(copy, value[i]); - arrayValue.push(child); - } - } - return GLib.Variant.new_array(new GLib.VariantType(arrayType.join('')), arrayValue); - - case '(': - let children = [ ]; - for (let i = 0; i < value.length; i++) { - let next = signature[0]; - if (next == ')') - break; - children.push(_pack_variant(signature, value[i])); - } - - if (signature[0] != ')') - throw new TypeError('Invalid GVariant signature for type TUPLE (expected ")")'); - signature.shift(); - return GLib.Variant.new_tuple(children); - case '{': - let key = _pack_variant(signature, value[0]); - let child = _pack_variant(signature, value[1]); - - if (signature[0] != '}') - throw new TypeError('Invalid GVariant signature for type DICT_ENTRY (expected "}")'); - signature.shift(); - - return GLib.Variant.new_dict_entry(key, child); - default: - throw new TypeError('Invalid GVariant signature (unexpected character ' + char + ')'); - } -} - -function _unpack_variant(variant, deep) { - switch (String.fromCharCode(variant.classify())) { - case 'b': - return variant.get_boolean(); - case 'y': - return variant.get_byte(); - case 'n': - return variant.get_int16(); - case 'q': - return variant.get_uint16(); - case 'i': - return variant.get_int32(); - case 'u': - return variant.get_uint32(); - case 'x': - return variant.get_int64(); - case 't': - return variant.get_uint64(); - case 'h': - return variant.get_handle(); - case 'd': - return variant.get_double(); - case 'o': - case 'g': - case 's': - // g_variant_get_string has length as out argument - return variant.get_string()[0]; - case 'v': - return variant.get_variant(); - case 'm': - let val = variant.get_maybe(); - if (deep && val) - return _unpack_variant(val, deep); - else - return val; - case 'a': - if (variant.is_of_type(new GLib.VariantType('a{?*}'))) { - // special case containers - let ret = { }; - let nElements = variant.n_children(); - for (let i = 0; i < nElements; i++) { - // always unpack the dictionary entry, and always unpack - // the key (or it cannot be added as a key) - let val = _unpack_variant(variant.get_child_value(i), deep); - let key; - if (!deep) - key = _unpack_variant(val[0], true); - else - key = val[0]; - ret[key] = val[1]; - } - return ret; - } - if (variant.is_of_type(new GLib.VariantType('ay'))) { - // special case byte arrays - return variant.get_data_as_bytes().toArray(); - } - - // fall through - case '(': - case '{': - let ret = [ ]; - let nElements = variant.n_children(); - for (let i = 0; i < nElements; i++) { - let val = variant.get_child_value(i); - if (deep) - ret.push(_unpack_variant(val, deep)); - else - ret.push(val); - } - return ret; - } - - throw new Error('Assertion failure: this code should not be reached'); -} - -function _init() { - // this is imports.gi.GLib - - GLib = this; - - // small HACK: we add a matches() method to standard Errors so that - // you can do "catch(e if e.matches(Ns.FooError, Ns.FooError.SOME_CODE))" - // without checking instanceof - Error.prototype.matches = function() { return false; }; - - this.Variant._new_internal = function(sig, value) { - let signature = Array.prototype.slice.call(sig); - - let variant = _pack_variant(signature, value); - if (signature.length != 0) - throw new TypeError('Invalid GVariant signature (more than one single complete type)'); - - return variant; - }; - - // Deprecate version of new GLib.Variant() - this.Variant.new = function(sig, value) { - return new GLib.Variant(sig, value); - }; - this.Variant.prototype.unpack = function() { - return _unpack_variant(this, false); - }; - this.Variant.prototype.deep_unpack = function() { - return _unpack_variant(this, true); - }; - this.Variant.prototype.toString = function() { - return '[object variant of type "' + this.get_type_string() + '"]'; - }; - - this.Bytes.prototype.toArray = function() { - return imports.byteArray.fromGBytes(this); - }; - - this.log_structured = function(logDomain, logLevel, stringFields) { - let fields = {}; - for (let key in stringFields) { - fields[key] = new GLib.Variant('s', stringFields[key]); - } - - GLib.log_variant(logDomain, logLevel, new GLib.Variant('a{sv}', fields)); - }; - -} diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js deleted file mode 100644 index 0e8aaf8..0000000 --- a/modules/overrides/GObject.js +++ /dev/null @@ -1,470 +0,0 @@ -/* exported _init, interfaces, properties, registerClass, requires, signals */ -// Copyright 2011 Jasper St. Pierre -// Copyright 2017 Philip Chimento , -// -// 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. - -const Gi = imports._gi; -const CjsPrivate = imports.gi.CjsPrivate; -const Legacy = imports._legacy; - -let GObject; - -var GTypeName = Symbol('GType name'); -var interfaces = Symbol('GObject interfaces'); -var properties = Symbol('GObject properties'); -var requires = Symbol('GObject interface requires'); -var signals = Symbol('GObject signals'); - -// These four will be aliased to GTK -var _children = Symbol('GTK widget template children'); -var _cssName = Symbol('GTK widget CSS name'); -var _internalChildren = Symbol('GTK widget template internal children'); -var _template = Symbol('GTK widget template'); - -function registerClass(klass) { - if (arguments.length == 2) { - // The two-argument form is the convenient syntax without ESnext - // decorators and class data properties. The first argument is an - // object with meta info such as properties and signals. The second - // argument is the class expression for the class itself. - // - // var MyClass = GObject.registerClass({ - // Properties: { ... }, - // Signals: { ... }, - // }, class MyClass extends GObject.Object { - // _init() { ... } - // }); - // - // When decorators and class data properties become part of the JS - // standard, this function can be used directly as a decorator. - let metaInfo = arguments[0]; - klass = arguments[1]; - if ('GTypeName' in metaInfo) - klass[GTypeName] = metaInfo.GTypeName; - if ('Implements' in metaInfo) - klass[interfaces] = metaInfo.Implements; - if ('Properties' in metaInfo) - klass[properties] = metaInfo.Properties; - if ('Signals' in metaInfo) - klass[signals] = metaInfo.Signals; - if ('Requires' in metaInfo) - klass[requires] = metaInfo.Requires; - if ('CssName' in metaInfo) - klass[_cssName] = metaInfo.CssName; - if ('Template' in metaInfo) - klass[_template] = metaInfo.Template; - if ('Children' in metaInfo) - klass[_children] = metaInfo.Children; - if ('InternalChildren' in metaInfo) - klass[_internalChildren] = metaInfo.InternalChildren; - } - - if (!(klass.prototype instanceof GObject.Object) && - !(klass.prototype instanceof GObject.Interface)) - throw new TypeError('GObject.registerClass() used with invalid base ' + - `class (is ${Object.getPrototypeOf(klass).name})`); - - // Find the "least derived" class with a _classInit static function; there - // definitely is one, since this class must inherit from GObject - let initclass = klass; - while (typeof initclass._classInit === 'undefined') - initclass = Object.getPrototypeOf(initclass.prototype).constructor; - return initclass._classInit(klass); -} - -// Some common functions between GObject.Class and GObject.Interface - -function _createSignals(gtype, signals) { - for (let signalName in signals) { - let obj = signals[signalName]; - let flags = (obj.flags !== undefined) ? obj.flags : GObject.SignalFlags.RUN_FIRST; - let accumulator = (obj.accumulator !== undefined) ? obj.accumulator : GObject.AccumulatorType.NONE; - let rtype = (obj.return_type !== undefined) ? obj.return_type : GObject.TYPE_NONE; - let paramtypes = (obj.param_types !== undefined) ? obj.param_types : []; - - try { - obj.signal_id = Gi.signal_new(gtype, signalName, flags, accumulator, rtype, paramtypes); - } catch (e) { - throw new TypeError('Invalid signal ' + signalName + ': ' + e.message); - } - } -} - -function _createGTypeName(klass) { - if (klass.hasOwnProperty(GTypeName)) - return klass[GTypeName]; - return `Gjs_${klass.name}`; -} - -function _propertiesAsArray(klass) { - let propertiesArray = []; - if (klass.hasOwnProperty(properties)) { - for (let prop in klass[properties]) { - propertiesArray.push(klass[properties][prop]); - } - } - return propertiesArray; -} - -function _copyAllDescriptors(target, source) { - Object.getOwnPropertyNames(source) - .filter(key => !['prototype', 'constructor'].includes(key)) - .concat(Object.getOwnPropertySymbols(source)) - .forEach(key => { - let descriptor = Object.getOwnPropertyDescriptor(source, key); - Object.defineProperty(target, key, descriptor); - }); -} - -function _interfacePresent(required, klass) { - if (!klass[interfaces]) - return false; - if (klass[interfaces].includes(required)) - return true; // implemented here - // Might be implemented on a parent class - return _interfacePresent(required, Object.getPrototypeOf(klass)); -} - -function _checkInterface(iface, proto) { - // Check that proto implements all of this interface's required interfaces. - // "proto" refers to the object's prototype (which implements the interface) - // whereas "iface.prototype" is the interface's prototype (which may still - // contain unimplemented methods.) - if (typeof iface[requires] === 'undefined') - return; - - let unfulfilledReqs = iface[requires].filter(required => { - // Either the interface is not present or it is not listed before the - // interface that requires it or the class does not inherit it. This is - // so that required interfaces don't copy over properties from other - // interfaces that require them. - let ifaces = proto.constructor[interfaces]; - return ((!_interfacePresent(required, proto.constructor) || - ifaces.indexOf(required) > ifaces.indexOf(iface)) && - !(proto instanceof required)); - }).map(required => - // required.name will be present on JS classes, but on introspected - // GObjects it will be the C name. The alternative is just so that - // we print something if there is garbage in Requires. - required.name || required); - if (unfulfilledReqs.length > 0) { - throw new Error('The following interfaces must be implemented before ' + - `${iface.name}: ${unfulfilledReqs.join(', ')}`); - } -} - -function _init() { - - GObject = this; - - function _makeDummyClass(obj, name, upperName, gtypeName, actual) { - let gtype = GObject.type_from_name(gtypeName); - obj['TYPE_' + upperName] = gtype; - obj[name] = function(v) { return new actual(v); }; - obj[name].$gtype = gtype; - } - - _makeDummyClass(this, 'VoidType', 'NONE', 'void', function() {}); - _makeDummyClass(this, 'Char', 'CHAR', 'gchar', Number); - _makeDummyClass(this, 'UChar', 'UCHAR', 'guchar', Number); - _makeDummyClass(this, 'Unichar', 'UNICHAR', 'gint', String); - - this.TYPE_BOOLEAN = GObject.type_from_name('gboolean'); - this.Boolean = Boolean; - Boolean.$gtype = this.TYPE_BOOLEAN; - - _makeDummyClass(this, 'Int', 'INT', 'gint', Number); - _makeDummyClass(this, 'UInt', 'UINT', 'guint', Number); - _makeDummyClass(this, 'Long', 'LONG', 'glong', Number); - _makeDummyClass(this, 'ULong', 'ULONG', 'gulong', Number); - _makeDummyClass(this, 'Int64', 'INT64', 'gint64', Number); - _makeDummyClass(this, 'UInt64', 'UINT64', 'guint64', Number); - - this.TYPE_ENUM = GObject.type_from_name('GEnum'); - this.TYPE_FLAGS = GObject.type_from_name('GFlags'); - - _makeDummyClass(this, 'Float', 'FLOAT', 'gfloat', Number); - this.TYPE_DOUBLE = GObject.type_from_name('gdouble'); - this.Double = Number; - Number.$gtype = this.TYPE_DOUBLE; - - this.TYPE_STRING = GObject.type_from_name('gchararray'); - this.String = String; - String.$gtype = this.TYPE_STRING; - - this.TYPE_POINTER = GObject.type_from_name('gpointer'); - this.TYPE_BOXED = GObject.type_from_name('GBoxed'); - this.TYPE_PARAM = GObject.type_from_name('GParam'); - this.TYPE_INTERFACE = GObject.type_from_name('GInterface'); - this.TYPE_OBJECT = GObject.type_from_name('GObject'); - this.TYPE_VARIANT = GObject.type_from_name('GVariant'); - - _makeDummyClass(this, 'Type', 'GTYPE', 'GType', GObject.type_from_name); - - this.ParamSpec.char = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_char(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.uchar = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_uchar(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.int = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_int(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.uint = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_uint(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.long = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_long(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.ulong = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_ulong(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.int64 = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_int64(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.uint64 = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_uint64(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.float = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_float(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.boolean = function(name, nick, blurb, flags, default_value) { - return GObject.param_spec_boolean(name, nick, blurb, default_value, flags); - }; - - this.ParamSpec.flags = function(name, nick, blurb, flags, flags_type, default_value) { - return GObject.param_spec_flags(name, nick, blurb, flags_type, default_value, flags); - }; - - this.ParamSpec.enum = function(name, nick, blurb, flags, enum_type, default_value) { - return GObject.param_spec_enum(name, nick, blurb, enum_type, default_value, flags); - }; - - this.ParamSpec.double = function(name, nick, blurb, flags, minimum, maximum, default_value) { - return GObject.param_spec_double(name, nick, blurb, minimum, maximum, default_value, flags); - }; - - this.ParamSpec.string = function(name, nick, blurb, flags, default_value) { - return GObject.param_spec_string(name, nick, blurb, default_value, flags); - }; - - this.ParamSpec.boxed = function(name, nick, blurb, flags, boxed_type) { - return GObject.param_spec_boxed(name, nick, blurb, boxed_type, flags); - }; - - this.ParamSpec.object = function(name, nick, blurb, flags, object_type) { - return GObject.param_spec_object(name, nick, blurb, object_type, flags); - }; - - this.ParamSpec.param = function(name, nick, blurb, flags, param_type) { - return GObject.param_spec_param(name, nick, blurb, param_type, flags); - }; - - this.ParamSpec.override = Gi.override_property; - - Object.defineProperties(this.ParamSpec.prototype, { - 'name': { configurable: false, - enumerable: false, - get: function() { return this.get_name() } }, - '_nick': { configurable: false, - enumerable: false, - get: function() { return this.get_nick() } }, - 'nick': { configurable: false, - enumerable: false, - get: function() { return this.get_nick() } }, - '_blurb': { configurable: false, - enumerable: false, - get: function() { return this.get_blurb() } }, - 'blurb': { configurable: false, - enumerable: false, - get: function() { return this.get_blurb() } }, - 'default_value': { configurable: false, - enumerable: false, - get: function() { return this.get_default_value() } }, - 'flags': { configurable: false, - enumerable: false, - get: function() { return CjsPrivate.param_spec_get_flags(this) } }, - 'value_type': { configurable: false, - enumerable: false, - get: function() { return CjsPrivate.param_spec_get_value_type(this) } }, - 'owner_type': { configurable: false, - enumerable: false, - get: function() { return CjsPrivate.param_spec_get_owner_type(this) } }, - }); - - let {GObjectMeta, GObjectInterface} = Legacy.defineGObjectLegacyObjects(GObject); - this.Class = GObjectMeta; - this.Interface = GObjectInterface; - this.Object.prototype.__metaclass__ = this.Class; - - // For compatibility with Lang.Class... we need a _construct - // or the Lang.Class constructor will fail. - this.Object.prototype._construct = function() { - this._init.apply(this, arguments); - return this; - }; - - GObject.registerClass = registerClass; - - GObject.Object._classInit = function(klass) { - let gtypename = _createGTypeName(klass); - let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? - klass[interfaces] : []; - let propertiesArray = _propertiesAsArray(klass); - let parent = Object.getPrototypeOf(klass); - let gobjectSignals = klass.hasOwnProperty(signals) ? - klass[signals] : []; - - let newClass = Gi.register_type(parent.prototype, gtypename, - gobjectInterfaces, propertiesArray); - Object.setPrototypeOf(newClass, parent); - - _createSignals(newClass.$gtype, gobjectSignals); - - _copyAllDescriptors(newClass, klass); - gobjectInterfaces.forEach(iface => - _copyAllDescriptors(newClass.prototype, iface.prototype)); - _copyAllDescriptors(newClass.prototype, klass.prototype); - - Object.getOwnPropertyNames(newClass.prototype) - .filter(name => name.startsWith('vfunc_') || name.startsWith('on_')) - .forEach(name => { - let descr = Object.getOwnPropertyDescriptor(newClass.prototype, name); - if (typeof descr.value !== 'function') - return; - - let func = newClass.prototype[name]; - - if (name.startsWith('vfunc_')) { - Gi.hook_up_vfunc(newClass.prototype, name.slice(6), func); - } else if (name.startsWith('on_')) { - let id = GObject.signal_lookup(name.slice(3).replace('_', '-'), - newClass.$gtype); - if (id !== 0) { - GObject.signal_override_class_closure(id, newClass.$gtype, function() { - let argArray = Array.from(arguments); - let emitter = argArray.shift(); - - return func.apply(emitter, argArray); - }); - } - } - }); - - gobjectInterfaces.forEach(iface => - _checkInterface(iface, newClass.prototype)); - - // For backwards compatibility only. Use instanceof instead. - newClass.implements = function(iface) { - if (iface.$gtype) - return GObject.type_is_a(newClass.$gtype, iface.$gtype); - return false; - }; - - return newClass; - }; - - GObject.Interface._classInit = function(klass) { - let gtypename = _createGTypeName(klass); - let gobjectInterfaces = klass.hasOwnProperty(requires) ? - klass[requires] : []; - let properties = _propertiesAsArray(klass); - let gobjectSignals = klass.hasOwnProperty(signals) ? - klass[signals] : []; - - let newInterface = Gi.register_interface(gtypename, gobjectInterfaces, - properties); - - _createSignals(newInterface.$gtype, gobjectSignals); - - _copyAllDescriptors(newInterface, klass); - - Object.getOwnPropertyNames(klass.prototype) - .filter(key => key !== 'constructor') - .concat(Object.getOwnPropertySymbols(klass.prototype)) - .forEach(key => { - let descr = Object.getOwnPropertyDescriptor(klass.prototype, key); - - // Create wrappers on the interface object so that generics work (e.g. - // SomeInterface.some_function(this, blah) instead of - // SomeInterface.prototype.some_function.call(this, blah) - if (typeof descr.value === 'function') { - let interfaceProto = klass.prototype; // capture in closure - newInterface[key] = function () { - return interfaceProto[key].call.apply(interfaceProto[key], - arguments); - }; - } - - Object.defineProperty(newInterface.prototype, key, descr); - }); - - return newInterface; - }; - - /** - * Use this to signify a function that must be overridden in an - * implementation of the interface. - */ - GObject.NotImplementedError = class NotImplementedError extends Error { - get name() { return 'NotImplementedError'; } - }; - - GObject._cssName = _cssName; - GObject._template = _template; - GObject._children = _children; - GObject._internalChildren = _internalChildren; - - // fake enum for signal accumulators, keep in sync with gi/object.c - this.AccumulatorType = { - NONE: 0, - FIRST_WINS: 1, - TRUE_HANDLED: 2 - }; - - this.Object.prototype.disconnect = function(id) { - return GObject.signal_handler_disconnect(this, id); - }; - - // A simple workaround if you have a class with .connect, .disconnect or .emit - // methods (such as Gio.Socket.connect or NMClient.Device.disconnect) - // The original g_signal_* functions are not introspectable anyway, because - // we need our own handling of signal argument marshalling - this.signal_connect = function(object, name, handler) { - return GObject.Object.prototype.connect.call(object, name, handler); - }; - this.signal_connect_after = function(object, name, handler) { - return GObject.Object.prototype.connect_after.call(object, name, handler); - }; - this.signal_emit_by_name = function(object, ...nameAndArgs) { - return GObject.Object.prototype.emit.apply(object, nameAndArgs); - }; -} - - diff --git a/modules/overrides/Gio.js b/modules/overrides/Gio.js deleted file mode 100644 index 5a76ace..0000000 --- a/modules/overrides/Gio.js +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright 2011 Giovanni Campagna -// -// 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. - -var GLib = imports.gi.GLib; -var CjsPrivate = imports.gi.CjsPrivate; -var Lang = imports.lang; -var Signals = imports.signals; -var Gio; - -function _proxyInvoker(methodName, sync, inSignature, arg_array) { - var replyFunc; - var flags = 0; - var cancellable = null; - - /* Convert arg_array to a *real* array */ - arg_array = Array.prototype.slice.call(arg_array); - - /* The default replyFunc only logs the responses */ - replyFunc = _logReply; - - var signatureLength = inSignature.length; - var minNumberArgs = signatureLength; - var maxNumberArgs = signatureLength + 3; - - if (arg_array.length < minNumberArgs) { - throw new Error("Not enough arguments passed for method: " + methodName + - ". Expected " + minNumberArgs + ", got " + arg_array.length); - } else if (arg_array.length > maxNumberArgs) { - throw new Error("Too many arguments passed for method: " + methodName + - ". Maximum is " + maxNumberArgs + - " + one callback and/or flags"); - } - - while (arg_array.length > signatureLength) { - var argNum = arg_array.length - 1; - var arg = arg_array.pop(); - if (typeof(arg) == "function" && !sync) { - replyFunc = arg; - } else if (typeof(arg) == "number") { - flags = arg; - } else if (arg instanceof Gio.Cancellable) { - cancellable = arg; - } else { - throw new Error("Argument " + argNum + " of method " + methodName + - " is " + typeof(arg) + ". It should be a callback, flags or a Gio.Cancellable"); - } - } - - var inVariant = new GLib.Variant('(' + inSignature.join('') + ')', arg_array); - - var asyncCallback = function (proxy, result) { - var outVariant = null, succeeded = false; - try { - outVariant = proxy.call_finish(result); - succeeded = true; - } catch (e) { - replyFunc([], e); - } - - if (succeeded) - replyFunc(outVariant.deep_unpack(), null); - }; - - if (sync) { - return this.call_sync(methodName, - inVariant, - flags, - -1, - cancellable).deep_unpack(); - } else { - return this.call(methodName, - inVariant, - flags, - -1, - cancellable, - asyncCallback); - } -} - -function _logReply(result, exc) { - if (exc != null) { - log("Ignored exception from dbus method: " + exc.toString()); - } -} - -function _makeProxyMethod(method, sync) { - var i; - var name = method.name; - var inArgs = method.in_args; - var inSignature = [ ]; - for (i = 0; i < inArgs.length; i++) - inSignature.push(inArgs[i].signature); - - return function() { - return _proxyInvoker.call(this, name, sync, inSignature, arguments); - }; -} - -function _convertToNativeSignal(proxy, sender_name, signal_name, parameters) { - Signals._emit.call(proxy, signal_name, sender_name, parameters.deep_unpack()); -} - -function _propertyGetter(name) { - let value = this.get_cached_property(name); - return value ? value.deep_unpack() : null; -} - -function _propertySetter(value, name, signature) { - let variant = new GLib.Variant(signature, value); - this.set_cached_property(name, variant); - - this.call('org.freedesktop.DBus.Properties.Set', - new GLib.Variant('(ssv)', - [this.g_interface_name, - name, variant]), - Gio.DBusCallFlags.NONE, -1, null, - Lang.bind(this, function(proxy, result) { - try { - this.call_finish(result); - } catch(e) { - log('Could not set property ' + name + ' on remote object ' + - this.g_object_path + ': ' + e.message); - } - })); -} - -function _addDBusConvenience() { - let info = this.g_interface_info; - if (!info) - return; - - if (info.signals.length > 0) - this.connect('g-signal', _convertToNativeSignal); - - let i, methods = info.methods; - for (i = 0; i < methods.length; i++) { - var method = methods[i]; - this[method.name + 'Remote'] = _makeProxyMethod(methods[i], false); - this[method.name + 'Sync'] = _makeProxyMethod(methods[i], true); - } - - let properties = info.properties; - for (i = 0; i < properties.length; i++) { - let name = properties[i].name; - let signature = properties[i].signature; - Object.defineProperty(this, name, { get: Lang.bind(this, _propertyGetter, name), - set: Lang.bind(this, _propertySetter, name, signature), - configurable: true, - enumerable: true }); - } -} - -function _makeProxyWrapper(interfaceXml) { - var info = _newInterfaceInfo(interfaceXml); - var iname = info.name; - return function(bus, name, object, asyncCallback, cancellable) { - var obj = new Gio.DBusProxy({ g_connection: bus, - g_interface_name: iname, - g_interface_info: info, - g_name: name, - g_object_path: object }); - if (!cancellable) - cancellable = null; - if (asyncCallback) - obj.init_async(GLib.PRIORITY_DEFAULT, cancellable, function(initable, result) { - let caughtErrorWhenInitting = null; - try { - initable.init_finish(result); - } catch(e) { - caughtErrorWhenInitting = e; - } - - if (caughtErrorWhenInitting === null) { - asyncCallback(initable, null); - } else { - asyncCallback(null, caughtErrorWhenInitting); - } - }); - else - obj.init(cancellable); - return obj; - }; -} - - -function _newNodeInfo(constructor, value) { - if (typeof value == 'string') - return constructor(value); - throw TypeError(`Invalid type ${Object.prototype.toString.call(value)}`); -} - -function _newInterfaceInfo(value) { - var nodeInfo = Gio.DBusNodeInfo.new_for_xml(value); - return nodeInfo.interfaces[0]; -} - -function _injectToMethod(klass, method, addition) { - var previous = klass[method]; - - klass[method] = function() { - addition.apply(this, arguments); - return previous.apply(this, arguments); - }; -} - -function _injectToStaticMethod(klass, method, addition) { - var previous = klass[method]; - - klass[method] = function(...parameters) { - let obj = previous.apply(this, parameters); - addition.apply(obj, parameters); - return obj; - }; -} - -function _wrapFunction(klass, method, addition) { - var previous = klass[method]; - - klass[method] = function() { - var args = Array.prototype.slice.call(arguments); - args.unshift(previous); - return addition.apply(this, args); - }; -} - -function _makeOutSignature(args) { - var ret = '('; - for (var i = 0; i < args.length; i++) - ret += args[i].signature; - - return ret + ')'; -} - -function _handleMethodCall(info, impl, method_name, parameters, invocation) { - // prefer a sync version if available - if (this[method_name]) { - let retval; - try { - retval = this[method_name].apply(this, parameters.deep_unpack()); - } catch (e) { - if (e instanceof GLib.Error) { - invocation.return_gerror(e); - } else { - let name = e.name; - if (name.indexOf('.') == -1) { - // likely to be a normal JS error - name = 'org.gnome.gjs.JSError.' + name; - } - logError(e, "Exception in method call: " + method_name); - invocation.return_dbus_error(name, e.message); - } - return; - } - if (retval === undefined) { - // undefined (no return value) is the empty tuple - retval = new GLib.Variant('()', []); - } - try { - if (!(retval instanceof GLib.Variant)) { - // attempt packing according to out signature - let methodInfo = info.lookup_method(method_name); - let outArgs = methodInfo.out_args; - let outSignature = _makeOutSignature(outArgs); - if (outArgs.length == 1) { - // if one arg, we don't require the handler wrapping it - // into an Array - retval = [retval]; - } - retval = new GLib.Variant(outSignature, retval); - } - invocation.return_value(retval); - } catch(e) { - // if we don't do this, the other side will never see a reply - invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', - "Service implementation returned an incorrect value type"); - } - } else if (this[method_name + 'Async']) { - this[method_name + 'Async'](parameters.deep_unpack(), invocation); - } else { - log('Missing handler for DBus method ' + method_name); - invocation.return_gerror(new Gio.DBusError({ code: Gio.DBusError.UNKNOWN_METHOD, - message: 'Method ' + method_name + ' is not implemented' })); - } -} - -function _handlePropertyGet(info, impl, property_name) { - let propInfo = info.lookup_property(property_name); - let jsval = this[property_name]; - if (jsval != undefined) - return new GLib.Variant(propInfo.signature, jsval); - else - return null; -} - -function _handlePropertySet(info, impl, property_name, new_value) { - this[property_name] = new_value.deep_unpack(); -} - -function _wrapJSObject(interfaceInfo, jsObj) { - var info; - if (interfaceInfo instanceof Gio.DBusInterfaceInfo) - info = interfaceInfo; - else - info = Gio.DBusInterfaceInfo.new_for_xml(interfaceInfo); - info.cache_build(); - - var impl = new CjsPrivate.DBusImplementation({ g_interface_info: info }); - impl.connect('handle-method-call', function(impl, method_name, parameters, invocation) { - return _handleMethodCall.call(jsObj, info, impl, method_name, parameters, invocation); - }); - impl.connect('handle-property-get', function(impl, property_name) { - return _handlePropertyGet.call(jsObj, info, impl, property_name); - }); - impl.connect('handle-property-set', function(impl, property_name, value) { - return _handlePropertySet.call(jsObj, info, impl, property_name, value); - }); - - return impl; -} - -function* _listModelIterator() { - let _index = 0; - const _len = this.get_n_items(); - while (_index < _len) { - yield this.get_item(_index++); - } -} - -function _init() { - Gio = this; - - Gio.DBus = { - get session() { - return Gio.bus_get_sync(Gio.BusType.SESSION, null); - }, - get system() { - return Gio.bus_get_sync(Gio.BusType.SYSTEM, null); - }, - - // Namespace some functions - get: Gio.bus_get, - get_finish: Gio.bus_get_finish, - get_sync: Gio.bus_get_sync, - - own_name: Gio.bus_own_name, - own_name_on_connection: Gio.bus_own_name_on_connection, - unown_name: Gio.bus_unown_name, - - watch_name: Gio.bus_watch_name, - watch_name_on_connection: Gio.bus_watch_name_on_connection, - unwatch_name: Gio.bus_unwatch_name - }; - - Gio.DBusConnection.prototype.watch_name = function(name, flags, appeared, vanished) { - return Gio.bus_watch_name_on_connection(this, name, flags, appeared, vanished); - }; - Gio.DBusConnection.prototype.unwatch_name = function(id) { - return Gio.bus_unwatch_name(id); - }; - Gio.DBusConnection.prototype.own_name = function(name, flags, acquired, lost) { - return Gio.bus_own_name_on_connection(this, name, flags, acquired, lost); - }; - Gio.DBusConnection.prototype.unown_name = function(id) { - return Gio.bus_unown_name(id); - }; - - _injectToMethod(Gio.DBusProxy.prototype, 'init', _addDBusConvenience); - _injectToMethod(Gio.DBusProxy.prototype, 'init_async', _addDBusConvenience); - _injectToStaticMethod(Gio.DBusProxy, 'new_sync', _addDBusConvenience); - _injectToStaticMethod(Gio.DBusProxy, 'new_finish', _addDBusConvenience); - _injectToStaticMethod(Gio.DBusProxy, 'new_for_bus_sync', _addDBusConvenience); - _injectToStaticMethod(Gio.DBusProxy, 'new_for_bus_finish', _addDBusConvenience); - Gio.DBusProxy.prototype.connectSignal = Signals._connect; - Gio.DBusProxy.prototype.disconnectSignal = Signals._disconnect; - - Gio.DBusProxy.makeProxyWrapper = _makeProxyWrapper; - - // Some helpers - _wrapFunction(Gio.DBusNodeInfo, 'new_for_xml', _newNodeInfo); - Gio.DBusInterfaceInfo.new_for_xml = _newInterfaceInfo; - - Gio.DBusExportedObject = CjsPrivate.DBusImplementation; - Gio.DBusExportedObject.wrapJSObject = _wrapJSObject; - - // ListStore - Gio.ListStore.prototype[Symbol.iterator] = _listModelIterator; -} diff --git a/modules/overrides/Gtk.js b/modules/overrides/Gtk.js deleted file mode 100644 index a88db2e..0000000 --- a/modules/overrides/Gtk.js +++ /dev/null @@ -1,117 +0,0 @@ -// application/javascript;version=1.8 -// Copyright 2013 Giovanni Campagna -// -// 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. - -const Legacy = imports._legacy; -const GObject = imports.gi.GObject; - -var CjsPrivate = imports.gi.CjsPrivate; - -let Gtk; - -function _init() { - - Gtk = this; - - Gtk.children = GObject._children; - Gtk.cssName = GObject._cssName; - Gtk.internalChildren = GObject._internalChildren; - Gtk.template = GObject._template; - - let {GtkWidgetClass} = Legacy.defineGtkLegacyObjects(GObject, Gtk); - Gtk.Widget.prototype.__metaclass__ = GtkWidgetClass; - - if (CjsPrivate.gtk_container_child_set_property) { - Gtk.Container.prototype.child_set_property = function(child, property, value) { - CjsPrivate.gtk_container_child_set_property(this, child, property, value); - }; - } - - Gtk.Widget.prototype._init = function(params) { - if (this.constructor[Gtk.template]) { - Gtk.Widget.set_connect_func.call(this.constructor, (builder, obj, signalName, handlerName, connectObj, flags) => { - if (connectObj !== null) { - throw new Error('Unsupported template signal attribute "object"'); - } else if (flags & GObject.ConnectFlags.SWAPPED) { - throw new Error('Unsupported template signal flag "swapped"'); - } else if (flags & GObject.ConnectFlags.AFTER) { - obj.connect_after(signalName, this[handlerName].bind(this)); - } else { - obj.connect(signalName, this[handlerName].bind(this)); - } - }); - } - - GObject.Object.prototype._init.call(this, params); - - if (this.constructor[Gtk.template]) { - let children = this.constructor[Gtk.children] || []; - for (let child of children) { - this[child.replace(/-/g, '_')] = - this.get_template_child(this.constructor, child); - } - - let internalChildren = this.constructor[Gtk.internalChildren] || []; - for (let child of internalChildren) { - this['_' + child.replace(/-/g, '_')] = - this.get_template_child(this.constructor, child); - } - } - }; - - Gtk.Widget._classInit = function(klass) { - let template = klass[Gtk.template]; - let cssName = klass[Gtk.cssName]; - let children = klass[Gtk.children]; - let internalChildren = klass[Gtk.internalChildren]; - - if (template) { - klass.prototype._instance_init = function() { - this.init_template(); - }; - } - - klass = GObject.Object._classInit(klass); - - if (cssName) - Gtk.Widget.set_css_name.call(klass, cssName); - - if (template) { - if (typeof template === 'string' && - template.startsWith('resource:///')) - Gtk.Widget.set_template_from_resource.call(klass, - template.slice(11)); - else - Gtk.Widget.set_template.call(klass, template); - } - - if (children) { - children.forEach(child => - Gtk.Widget.bind_template_child_full.call(klass, child, false, 0)); - } - - if (internalChildren) { - internalChildren.forEach(child => - Gtk.Widget.bind_template_child_full.call(klass, child, true, 0)); - } - - return klass; - }; -} diff --git a/modules/print.cpp b/modules/print.cpp new file mode 100644 index 0000000..8356f60 --- /dev/null +++ b/modules/print.cpp @@ -0,0 +1,166 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later + * + * Copyright (c) 2008 litl, LLC + * Copyright (c) 2009 Red Hat, Inc. + */ + +#include + +#include + +#include +#include // for JS_EncodeStringToUTF8 +#include +#include // for JS_FN, JSFunctionSpec, JS_FS_END +#include +#include +#include // for UniqueChars +#include + +#include "cjs/jsapi-util.h" +#include "modules/print.h" + +// Avoid static_assert in MSVC builds +namespace JS { +template struct GCPolicy; + +template <> +struct GCPolicy : public IgnoreGCPolicy {}; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_log(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + + if (argc != 1) { + gjs_throw(cx, "Must pass a single argument to log()"); + return false; + } + + /* JS::ToString might throw, in which case we will only log that the value + * could not be converted to string */ + JS::AutoSaveExceptionState exc_state(cx); + JS::RootedString jstr(cx, JS::ToString(cx, argv[0])); + exc_state.restore(); + + if (!jstr) { + g_message("JS LOG: "); + return true; + } + + JS::UniqueChars s(JS_EncodeStringToUTF8(cx, jstr)); + if (!s) + return false; + + g_message("JS LOG: %s", s.get()); + + argv.rval().setUndefined(); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_log_error(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + + if ((argc != 1 && argc != 2) || !argv[0].isObject()) { + gjs_throw( + cx, + "Must pass an exception and optionally a message to logError()"); + return false; + } + + JS::RootedString jstr(cx); + + if (argc == 2) { + /* JS::ToString might throw, in which case we will only log that the + * value could not be converted to string */ + JS::AutoSaveExceptionState exc_state(cx); + jstr = JS::ToString(cx, argv[1]); + exc_state.restore(); + } + + gjs_log_exception_full(cx, argv[0], jstr, G_LOG_LEVEL_WARNING); + + argv.rval().setUndefined(); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_print_parse_args(JSContext* cx, const JS::CallArgs& argv, + GjsAutoChar* buffer) { + GString* str = g_string_new(""); + for (unsigned n = 0; n < argv.length(); ++n) { + /* JS::ToString might throw, in which case we will only log that the + * value could not be converted to string */ + JS::AutoSaveExceptionState exc_state(cx); + JS::RootedString jstr(cx, JS::ToString(cx, argv[n])); + exc_state.restore(); + + if (jstr) { + JS::UniqueChars s(JS_EncodeStringToUTF8(cx, jstr)); + if (!s) { + g_string_free(str, true); + return false; + } + + g_string_append(str, s.get()); + if (n < (argv.length() - 1)) + g_string_append_c(str, ' '); + } else { + *buffer = g_string_free(str, true); + if (!*buffer) + *buffer = g_strdup(""); + return true; + } + } + *buffer = g_string_free(str, false); + + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_print(JSContext* context, unsigned argc, JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + + GjsAutoChar buffer; + if (!gjs_print_parse_args(context, argv, &buffer)) + return false; + + g_print("%s\n", buffer.get()); + + argv.rval().setUndefined(); + return true; +} + +GJS_JSAPI_RETURN_CONVENTION +static bool gjs_printerr(JSContext* context, unsigned argc, JS::Value* vp) { + JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); + + GjsAutoChar buffer; + if (!gjs_print_parse_args(context, argv, &buffer)) + return false; + + g_printerr("%s\n", buffer.get()); + + argv.rval().setUndefined(); + return true; +} + +// clang-format off +static constexpr JSFunctionSpec funcs[] = { + JS_FN("log", gjs_log, 1, GJS_MODULE_PROP_FLAGS), + JS_FN("logError", gjs_log_error, 2, GJS_MODULE_PROP_FLAGS), + JS_FN("print", gjs_print, 0, GJS_MODULE_PROP_FLAGS), + JS_FN("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS), + JS_FS_END}; +// clang-format on + +bool gjs_define_print_stuff(JSContext* context, + JS::MutableHandleObject module) { + module.set(JS_NewPlainObject(context)); + if (!module) + return false; + return JS_DefineFunctions(context, module, funcs); +} diff --git a/modules/print.h b/modules/print.h new file mode 100644 index 0000000..05b8682 --- /dev/null +++ b/modules/print.h @@ -0,0 +1,20 @@ +/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* + * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later + * + * Copyright (c) 2020 Evan Welsh + */ + +#ifndef MODULES_PRINT_H_ +#define MODULES_PRINT_H_ + +#include + +#include + +#include "cjs/macros.h" + +GJS_JSAPI_RETURN_CONVENTION +bool gjs_define_print_stuff(JSContext* context, JS::MutableHandleObject module); + +#endif // MODULES_PRINT_H_ diff --git a/modules/script/_bootstrap/.eslintrc.yml b/modules/script/_bootstrap/.eslintrc.yml new file mode 100644 index 0000000..9cad1d1 --- /dev/null +++ b/modules/script/_bootstrap/.eslintrc.yml @@ -0,0 +1,7 @@ +--- +extends: "../../../.eslintrc.yml" +globals: + log: "off" + logError: "off" + print: "off" + printerr: "off" diff --git a/modules/_bootstrap/coverage.js b/modules/script/_bootstrap/coverage.js similarity index 76% rename from modules/_bootstrap/coverage.js rename to modules/script/_bootstrap/coverage.js index b299870..7cc58a6 100644 --- a/modules/_bootstrap/coverage.js +++ b/modules/script/_bootstrap/coverage.js @@ -1,6 +1,6 @@ -(function(exports) { +(function (exports) { 'use strict'; exports.debugger = new Debugger(exports.debuggee); exports.debugger.collectCoverageInfo = true; -})(window); +})(globalThis); diff --git a/modules/script/_bootstrap/debugger.js b/modules/script/_bootstrap/debugger.js new file mode 100644 index 0000000..a48d397 --- /dev/null +++ b/modules/script/_bootstrap/debugger.js @@ -0,0 +1,858 @@ +/* global debuggee, quit, loadNative, readline, uneval */ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * This is a simple command-line debugger for GJS programs. It is based on + * jorendb, which is a toy debugger for shell-js programs included in the + * SpiderMonkey source. + * + * To run it: gjs -d path/to/file.js + * Execution will stop at debugger statements, and you'll get a prompt before + * the first frame is executed. + */ + +const {print, logError} = loadNative('_print'); + +// Debugger state. +var focusedFrame = null; +var topFrame = null; +var debuggeeValues = {}; +var nextDebuggeeValueIndex = 1; +var lastExc = null; +var options = {pretty: true}; +var breakpoints = [undefined]; // Breakpoint numbers start at 1 + +// Cleanup functions to run when we next re-enter the repl. +var replCleanups = []; + +// Convert a debuggee value v to a string. +function dvToString(v) { + if (typeof v === 'undefined') + return 'undefined'; // uneval(undefined) === '(void 0)', confusing + if (v === null) + return 'null'; // typeof null === 'object', so avoid that case + return typeof v !== 'object' || v === null ? uneval(v) : `[object ${v.class}]`; +} + +function summarizeObject(dv) { + const obj = {}; + for (var name of dv.getOwnPropertyNames()) { + var v = dv.getOwnPropertyDescriptor(name).value; + if (v instanceof Debugger.Object) + v = '(...)'; + obj[name] = v; + } + return obj; +} + +function debuggeeValueToString(dv, style = {pretty: options.pretty}) { + const dvrepr = dvToString(dv); + if (!style.pretty || dv === null || typeof dv !== 'object') + return [dvrepr, undefined]; + + if (['Error', 'GIRespositoryNamespace', 'GObject_Object'].includes(dv.class)) { + const errval = debuggeeGlobalWrapper.executeInGlobalWithBindings( + 'v.toString()', {v: dv}); + return [dvrepr, errval['return']]; + } + + if (style.brief) + return [dvrepr, JSON.stringify(summarizeObject(dv), null, 4)]; + + const str = debuggeeGlobalWrapper.executeInGlobalWithBindings( + 'JSON.stringify(v, null, 4)', {v: dv}); + if ('throw' in str) { + if (style.noerror) + return [dvrepr, undefined]; + + const substyle = {}; + Object.assign(substyle, style); + substyle.noerror = true; + return [dvrepr, debuggeeValueToString(str.throw, substyle)]; + } + + return [dvrepr, str['return']]; +} + +function showDebuggeeValue(dv, style = {pretty: options.pretty}) { + const i = nextDebuggeeValueIndex++; + debuggeeValues[`$${i}`] = dv; + const [brief, full] = debuggeeValueToString(dv, style); + print(`$${i} = ${brief}`); + if (full !== undefined) + print(full); +} + +Object.defineProperty(Debugger.Frame.prototype, 'num', { + configurable: true, + enumerable: false, + get() { + let i = 0; + let f; + for (f = topFrame; f && f !== this; f = f.older) + i++; + return f === null ? undefined : i; + }, +}); + +Debugger.Frame.prototype.describeFrame = function () { + if (this.type === 'call') { + return `${this.callee.name || ''}(${ + this.arguments.map(dvToString).join(', ')})`; + } else if (this.type === 'global') { + return 'toplevel'; + } else { + return `${this.type} code`; + } +}; + +Debugger.Frame.prototype.describePosition = function () { + if (this.script) + return this.script.describeOffset(this.offset); + return null; +}; + +Debugger.Frame.prototype.describeFull = function () { + const fr = this.describeFrame(); + const pos = this.describePosition(); + if (pos) + return `${fr} at ${pos}`; + return fr; +}; + +Object.defineProperty(Debugger.Frame.prototype, 'line', { + configurable: true, + enumerable: false, + get() { + if (this.script) + return this.script.getOffsetLocation(this.offset).lineNumber; + else + return null; + }, +}); + +Debugger.Script.prototype.describeOffset = function describeOffset(offset) { + const {lineNumber, columnNumber} = this.getOffsetLocation(offset); + const url = this.url || ''; + return `${url}:${lineNumber}:${columnNumber}`; +}; + +function showFrame(f, n) { + if (f === undefined || f === null) { + f = focusedFrame; + if (f === null) { + print('No stack.'); + return; + } + } + if (n === undefined) { + n = f.num; + if (n === undefined) + throw new Error('Internal error: frame not on stack'); + } + + print(`#${n.toString().padEnd(4)} ${f.describeFull()}`); +} + +function saveExcursion(fn) { + const tf = topFrame, ff = focusedFrame; + try { + return fn(); + } finally { + topFrame = tf; + focusedFrame = ff; + } +} + +// Accept debugger commands starting with '#' so that scripting the debugger +// can be annotated +function commentCommand(comment) { + void comment; +} + +// Evaluate an expression in the Debugger global - used for debugging the +// debugger +function evalCommand(expr) { + eval(expr); +} + +function quitCommand() { + dbg.removeAllDebuggees(); + quit(0); +} +quitCommand.summary = 'Quit the debugger'; +quitCommand.helpText = `USAGE + quit`; + +function backtraceCommand() { + if (topFrame === null) + print('No stack.'); + for (var i = 0, f = topFrame; f; i++, f = f.older) + showFrame(f, i); +} +backtraceCommand.summary = 'Print backtrace of all stack frames'; +backtraceCommand.helpText = `USAGE + bt`; + +function setCommand(rest) { + var space = rest.indexOf(' '); + if (space === -1) { + print('Invalid set