diff --git a/.gitignore b/.gitignore index 7d9644a2cbee6..eeb8d65260d82 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,6 @@ test/runner.bat tools/file_packager.bat tools/webidl_binder.bat -bootstrap.exe em++.exe emcc.exe em-config.exe diff --git a/AUTHORS b/AUTHORS index aa0e91eebc7ef..ab0f876b503fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -603,3 +603,4 @@ a license to everyone to use it as detailed in LICENSE.) * Sean Morris * Mitchell Wills (copyright owned by Google, Inc.) * Han Jiang +* Abdelrahman Teima diff --git a/ChangeLog.md b/ChangeLog.md index 2e1d037f5970f..99d84f7ffb075 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -18,11 +18,168 @@ to browse the changes between the tags. See docs/process.md for more on how version tagging works. -5.0.6 (in development) +6.0.3 (in development) ---------------------- +- Added support for compiling FMA intrinsics. All 32 FMA intrinsics are + supported, with 256-bit variants emulated via two 128-bit operations. Pass + ``-msimd128 -mfma`` to enable. With ``-mrelaxed-simd -mfma``, Wasm relaxed + SIMD FMA is used. (#27183) +- New `AUTO_INIT` setting to opt an instance ES module (`MODULARIZE=instance` or + `WASM_ESM_INTEGRATION`) into self-initialization via top-level await on import, + rather than exporting a default `init` function. Since there is no + init/moduleArg, module-level configuration is unavailable: + `INCOMING_MODULE_JS_API` is disabled and passing a non-empty one is an error. +- The async `poll()`/`select()` implementation was refactored onto a per-inode + readiness wait-queue. As part of this, the (undocumented) `stream_ops.poll` + FS-backend handler signature changed from `poll(stream, timeout)` to + `poll(stream)` returning the current readiness mask; out-of-tree custom FS + backends with a `poll` handler must update. (#27226) +- compiler-rt and libunwind were updated to LLVM 22.1.8. (#27245, #27246) +- `-fcoverage-mapping` is currently broken due to a mismatch between the version + of LLVM used and the imported version of compiler-rt. We hope to fix this + in the next release. (#27261) +- The default value for `GROWABLE_ARRAYBUFFERS` was reverted to `0` since we + found issues with Web API compatibility. (#27260) + +6.0.2 - 07/01/26 +---------------- +- The `GROWABLE_ARRAYBUFFERS` setting now supports both `=1` (auto-detect and + use the feature) and `=2` (unconditionally use the feature, avoiding the + overhead in multi-threaded builds). It now defaults to `=1`, meaning the + feature will be used when available. Note that this only affects programs + that are built with `ALLOW_MEMORY_GROWTH`, which is not enabled by default. + (#27096, #27212) +- New `-sNODERAWSOCKETS` setting that backs the POSIX sockets API with real TCP + (`node:net`) and UDP (`node:dgram`) sockets on Node.js, with no `ws`, proxy + process, or pthreads required. Supports incoming and outgoing TCP, UDP, IPv6, + and `-pthread` with `PROXY_TO_PTHREAD`. Uses the public node APIs where + available, falling back to `tcp_wrap`/`udp_wrap` on older Node.js. (#27080) +- The following symbols are no longer included in `INCOMING_MODULE_JS_API` + by default: + - GL_MAX_TEXTURE_IMAGE_UNITS + - SDL_canPlayWithWebAudio + - SDL_numSimultaneouslyQueuedBuffers + - freePreloadedMediaOnUse + - preinitializedWebGLContext + - keyboardListeningElement + - doNotCaptureKeyboard + - extraStackTrace + - preloadPlugins + - postMainLoop + - preMainLoop + - forcedAspectRatio + - mainScriptUrlOrBlob + - onFullScreen + - INITIAL_MEMORY + - wasmMemory + - wasmBinary + Anybody using these will see a clear error in their debug builds signaling + that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. + +6.0.1 - 06/22/26 +---------------- +- The ability to redirect JS compiler stderr using `EMCC_STDERR_FILE` was + removed. These days you can use `EMCC_DEBUG` and/or `EMCC_DEBUG_SAVE` to + preserve all the intermediate JS compiler files. (#27101) +- The installed versions of the compiler-rt library now follow the upstream + naming convetion of `libclang_rt..a`. (#27089) +- Dynamic linking now explicitly requires asynchronous Wasm compilation. The + process of loading side modules at startup currently depends on this. (#27086) +- New experimental `-sCROSS_ORIGIN_STORAGE` linker flag integrating the + proposed [Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) + as a progressive enhancement for Wasm loading on the web target. See + `docs/compiling/CrossOriginStorage.rst` for details. (#27066) +- The `-sUSE_PTHREADS` and `-sMEMORY64` flags have been deprecated in favor of the + more standard `-pthread` and `-m64` (or `--target=wasm64`) flags. (#27025) +- Adds wasm-bindgen support. When `-sWASM_BINDGEN` is set, Emscripten will call + out to `wasm-bindgen` in the users's path and integrate the wasm-bindgen JS + with the normal Emscripten JS. Some wasm-bindgen features may not yet be fully + supported. (#23493) +- Fixed `getentropy`/`random_get` spuriously failing under Node.js and the + shell environment for small requests. (#27122) +- The startup process for the generated program now makes use of `async` / + `await` under more circumstances (specifically when using `setStatus`, or + run dependencies). This means that errors during startup (or during the + `main()` function) will more often show up as unhandled promise rejections + (`onunhandledreject`) rather than synchronous errors (`onerror`). (#27121) + +6.0.0 - 06/04/26 +---------------- +- On Windows, Emscripten now ships `.exe` tool launchers, rather + than `.bat` and/or `.ps1`. This means that any scripts that explicitly + reference, e.g. `emcc.bat`, will need to be updated to just `emcc` (or + `emcc.exe`). For the time being you can still get the old `.bat` files by + running `tools/maint/create_entry_points.py --bat-files`. (#24858) +- When performing a streaming Fetch operation, the max chunk size of downloaded + bytes that is handed over to the Wasm side from JS is now capped to maximum + of 8 megabytes. This ensures that a streaming Fetch stays streaming, rather + than transferring the whole (potentially large) file as one huge chunk, which + might not fit in the WebAssembly memory. (#26898) +- The minimum versions of browser engines supported by emscripten's generated + code were bumped, allowing us to remove our internal support for transpilation + via babel: + MIN_CHROME_VERSION: 74 -> 85 + MIN_FIREFOX_VERSION: 68 -> 79 + MIN_SAFARI_VERSION: 12.2 -> 14.1 + This allows us to assume that features such as mutable-globals and sign-ext + are universally available. Disabling these is no longer possible in + emscripten. If you still need to support extremely old browsers, you can + manually transpile the output of emscripten (e.g. using babel for JS and + binaryen for wasm). (#26677) +- musl libc updated from v1.2.5 to v1.2.6. (#26860) +- libpng port updated from 1.6.55 to 1.6.58. (#26592, #26983) +- The `-m64` compiler flag is now honored, and works as an alias for + `-sMEMORY64` and/or `--target=wasm64`. (#26765) +- The autopersistence feature in IDBFS mount now supports registering a global + callback `IDBFS.onAutoPersistStateChanged = active => {}`, which will be + notified of all IDBFS sync start and end events. (#26895) +- google-closure-compiler was updated to 20260429.0.0. (#26869) + Closure compiler now provides a native macOS arm64 binary for Apple Silicon, + in addition to having native binaries for Win-x64, Linux-x64 and Linux-ARM64. + For other platforms for which Closure compiler does not ship a native binary, + e.g. Intel x64 Macs and Windows-on-ARM, downloading Java SE Development Kit + 21.0.11 from https://www.oracle.com/europe/java/technologies/downloads/#java21 + is required in order to use Emscripten's Closure Compiler integration. +- The `FAKE_DYLIBS` setting is now disabled by default. This means that + `-shared` will produce real dynamic libraries by default (`-sSIDE_MODULE` is + implied). Also, if you include real dynamic libraries in your link command + emscripten will now automatically produce a dynamically linked program + (`-sMAIN_MODULE=2` is implied). (#25930) +- The `PThread.runningWorkers` field was removed from the `PThread` object. + If you have JS code that was depending on this you can transition to using the + `PThread.pthreads` object. (#26998) +- The POSIX `pause()` function will now return 0 rather than EINTR. (#27044) + +5.0.7 - 04/30/26 +---------------- +- mimalloc was updated to 3.3.1. (#26696) +- The `WASM_JS_TYPES` setting was removed, as the corresponsing propsal was + pushed back to phase 1. (#26739) +- The `-sDETERMINISTIC` setting was removed. This setting just injected + `src/deterministic.js` as a `--pre-js`. For now, this file remains part of + emscripten so folks can sill use it manually if needed. If you are a user of + this feature please let us know otherwise this may be deleted in a future + release. (#26648) +- The emscripten_futex_wait internals were improved to avoid unnecessary thread + wakeups. As part of this change emscripten_futex_wait is now documented to + explicitly allow for returning EINTR when the wait is interrupted by an async + operation. All callers of emscripten_futex_wait are advised to use a loop + to handle these types of spurious wakeups / interruptions. (#26659, #26735) +- Attempting to use PTHREAD_PROCESS_SHARED when creating pthread primitives such + as locks and condvars will now fail with ENOTSUP. (#26743) +- When building code with both Wasm Workers and pthreads (hybrid mode) it is now + possible to call most of the core pthread APIs (e.g. lock, condvar, etc) from + a Wasm Worker. This mode increases the memory used by each Wasm Worker by + ~500 bytes (in the same way that declaring ~500 bytes of TLS data would). + (#26757) +- The filesystem opteration that create new files now honor the global umask, + which defaults for 0o222 and can be updated by calling `umask()`. (#50739) + +5.0.6 - 04/14/26 +---------------- - The minimum version of node supported by the generated code was bumped from v12.22.0 to v18.3.0. (#26604) -- The DETERMINISIC settings was marked as deprecated (#26653) +- The DETERMINISTIC settings was marked as deprecated (#26653) - Some musl-internal headers are no longer installed into the sysroot include directory. In particular, `syscall_arch.h` no longer exists, but can be replaced with `emscripten/syscalls.h`. (#26658) @@ -117,10 +274,9 @@ See docs/process.md for more on how version tagging works. inconsistent with JS and was not supported in browser devtools. We plan to provide this information using Scopes encoding later. (#26149) - compiler-rt, libcxx, libcxxabi, libunwind, and llvm-libc were updated to LLVM - 21.1.8. (#26036, #26045, #26058, and #26151) + 21.1.8. (#26036, #26045, #26058, #26151) - Calling pthread_create in a single-threaded build will now return ENOTSUP rather then EAGAIN. (#26105) -- compiler-rt and libunwind were updated to LLVM 21.1.8. (#26036 and #26045) - A new `-sEXECUTABLE` setting was added which adds a #! line to the resulting JavaScript and makes it executable. This setting defaults to true when the output filename has no extension, or ends in `.out` (e.g. `a.out`) (#26085) @@ -365,7 +521,7 @@ See docs/process.md for more on how version tagging works. example, when you run `pkg-config --list-all` or `pkg-config --cflags `. Bare in mind that the correct PKG_CONFIG_PATH needs to be set for this to work. One way to do this is to run `emmake pkg-config`. (#24426) -- libcxx, libcxxabi, and compiler-rt were updated to LLVM 20.1.4. (#24346 and +- libcxx, libcxxabi, and compiler-rt were updated to LLVM 20.1.4. (#24346, #24357) - Emscripten will not longer generate trampoline functions for Wasm exports prior to the module being instantiated. Storing a reference to a Wasm export @@ -552,7 +708,7 @@ See docs/process.md for more on how version tagging works. new proposal by default yet. This option replaces the existing `-sWASM_EXNREF`, whose meaning was the opposite. - compiler-rt, libcxx, libcxxabi, and libunwind were updated to LLVM 19.1.6. - (#22937, #22994, and #23294) + (#22937, #22994, #23294) - The default Safari version targeted by Emscripten has been raised from 14.1 to 15.0 (the `MIN_SAFARI_VERSION` setting) (#23312). This has several effects: - The Wasm nontrapping-fptoint feature is enabled by default. Clang will @@ -801,7 +957,7 @@ See docs/process.md for more on how version tagging works. 3.1.57 - 04/10/24 ----------------- - libcxx, libcxxabi, libunwind, and compiler-rt were updated to LLVM 18.1.2. - (#21607, #21638, and #21663) + (#21607, #21638, #21663) - musl libc updated from v1.2.4 to v1.2.5. (#21598) - In `MODULARIZE` mode we no longer export the module ready promise as `ready`. This was previously exposed on the Module for historical reasons even though @@ -882,8 +1038,7 @@ See docs/process.md for more on how version tagging works. community and supported on a "best effort" basis. See `tools/ports/contrib/README.md` for details.A first contrib port is available via `--use-port=contrib.glfw3`: an emscripten port of glfw written - in C++ with many features like support for multiple windows. (#21244 and - #21276) + in C++ with many features like support for multiple windows. (#21244, #21276) - Added concept of external ports which live outside emscripten and are loaded on demand using the syntax `--use-port=/path/to/my_port.py` (#21316) - `embuilder` can now build ports with options as well as external ports using @@ -991,7 +1146,7 @@ See docs/process.md for more on how version tagging works. For those that would rather perform transpilation separately outside of emscripten you can use the `-sPOLYFILL=0` setting. (#20700) - libcxx, libcxxabi, libunwind, and compiler-rt were updated to LLVM 17.0.4. - (#20705, #20707, and #20708) + (#20705, #20707, #20708) - Remove `BENCHMARK` setting. That has not been used by the benchmark suite for some time now (at least not by default), and is much less useful these days given lazy compilation in VMs (which makes it impossible to truly benchmark @@ -1270,7 +1425,7 @@ See docs/process.md for more on how version tagging works. - Added new linker option `-sEXCEPTION_STACK_TRACES` which will display a stack trace when an uncaught exception occurs. This defaults to true when `ASSERTIONS` is enabled. This option is mainly for the users who want only - exceptions' stack traces without turning `ASSERTIONS` on. (#18642 and #18535) + exceptions' stack traces without turning `ASSERTIONS` on. (#18642, #18535) - `SUPPORT_LONGJMP`'s default value now depends on the exception mode. If Wasm EH (`-fwasm-exceptions`) is used, it defaults to `wasm`, and if Emscripten EH (`-sDISABLE_EXCEPTION_CATCHING=0`) is used or no exception support is used, it @@ -1419,7 +1574,7 @@ See docs/process.md for more on how version tagging works. 3.1.24 - 10/11/22 ----------------- - In Wasm exception mode (`-fwasm-exceptions`), when `ASSERTIONS` is enabled, - uncaught exceptions will display stack traces and what() message. (#17979 and + uncaught exceptions will display stack traces and what() message. (#17979, #18003) - It is now possible to specify indirect dependencies on JS library functions directly in C/C++ source code. For example, in the case of a EM_JS or EM_ASM @@ -1728,7 +1883,7 @@ See docs/process.md for more on how version tagging works. `-sDISABLE_EXCEPTION_CATCHING=0`). When using Wasm EH with Wasm SjLj, there is one restriction that you cannot directly call `setjmp` within a `catch` clause. (Calling another function that calls `setjmp` is fine.) - (#14976 and #16072) + (#14976, #16072) 3.1.2 - 01/20/2022 ------------------ @@ -3735,7 +3890,7 @@ v1.36.6: 8/8/2016 - Fixed inconsistencies in fullscreen API signatures (#4310, #4318, #4379) - Changed the behavior of Emscripten WebGL createContext() to not forcibly set CSS style on created canvases, but let page customize the style themselves - (#3406, #4194 and #4350, #4355) + (#3406, #4194, #4350, #4355) - Adjusted the reported GL_VERSION field to adapt to the OpenGL ES specifications (#4345) - Added support for GLES3 GL_MAJOR/MINOR_VERSION fields. (#4368) diff --git a/Makefile b/Makefile index 8065e8a8cf9c9..bf66dcae258fa 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,11 @@ VERSION = $(shell cat emscripten-version.txt | sed s/\"//g) -DESTDIR ?= ../emscripten-$(VERSION) +DESTDIR ?= out/emscripten-$(VERSION) DISTFILE = emscripten-$(VERSION).tar.bz2 +no_default: + @echo 'Error: You must specify an explicit make target (e.g. `dist` or `install`)' + @exit 1 + dist: $(DISTFILE) install: diff --git a/README.md b/README.md index ee8857de1a53e..656e778a1d936 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ GitHub CI status: [![CircleCI](https://circleci.com/gh/emscripten-core/emscripte Chromium builder status: [emscripten-releases](https://ci.chromium.org/p/emscripten-releases) -Overview --------- + +# Overview Emscripten compiles C and C++ to [WebAssembly](https://webassembly.org/) using [LLVM](https://en.wikipedia.org/wiki/LLVM) and @@ -26,8 +26,58 @@ While Emscripten mostly focuses on compiling C and C++ using compilers (for example, Rust has Emscripten integration, with the `wasm32-unknown-emscripten` target). -License -------- + +# Getting Started + +For detailed instructions and tutorials, visit the [Emscripten Website](https://emscripten.org). + +## Installation + +There are two primary ways to install Emscripten: + +1. **Using the Emscripten SDK (emsdk) (Recommended)** + The easiest way to get started is by using the Emscripten SDK. Follow the instructions on the [downloads page](https://emscripten.org/docs/getting_started/downloads.html) to install it. + +2. **From a Git Checkout (Manual Installation)** + If you have cloned the repository from Git, you can install the dependencies manually and then run the bootstrap script: + ```bash + ./bootstrap.py + ``` + For more details, see the [developer guide](https://emscripten.org/docs/contributing/developers_guide.html). + +## Using the compiler + +Run `emcc` like you would `gcc` or `clang`: + +```bash +$ emcc hello.c -o hello.js +$ node hello.js +Hello, world! +``` + +Emscripten will compile your code into a WebAssembly module along with a +JavaScript file that can load and run it. You can then run the resulting +JavaScript in your browser or under [Node.js](https://nodejs.org/) (or +[Deno](https://deno.com/) or [Bun](https://bun.sh/)). + +Emscripten can also generate a sample HTML page that then loads the JavaScript: + +```bash +$ emcc hello.c -o hello.html +``` + +You can then serve the generated `hello.html` using the `emrun` tool, or a +web server of your choosing. + + +# Contributing + +For information on how to contribute to the project, see +[CONTRIBUTING.md](CONTRIBUTING.md) and the [Contributing section on the +website](https://emscripten.org/docs/contributing/contributing.html). + + +# License Emscripten is available under 2 licenses, the MIT license and the University of Illinois/NCSA Open Source License. diff --git a/bootstrap b/bootstrap deleted file mode 100755 index 7e507c13ba9c1..0000000000000 --- a/bootstrap +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -# Copyright 2020 The Emscripten Authors. All rights reserved. -# Emscripten is available under two separate licenses, the MIT license and the -# University of Illinois/NCSA Open Source License. Both these licenses can be -# found in the LICENSE file. -# -# Entry point for running python scripts on UNIX systems. -# -# Automatically generated by `create_entry_points.py`; DO NOT EDIT. -# -# To make modifications to this file, edit `tools/run_python.sh` and then run -# `tools/maint/create_entry_points.py` - -# python -E does not ignore _PYTHON_SYSCONFIGDATA_NAME, an internal of cpython -# used in cross compilation via setup.py, so we unset it explicitly here. -unset _PYTHON_SYSCONFIGDATA_NAME - -_EM_PY=$EMSDK_PYTHON - -if [ -z "$_EM_PY" ]; then - _EM_PY=$(command -v python3 2> /dev/null) -fi - -if [ -z "$_EM_PY" ]; then - _EM_PY=$(command -v python 2> /dev/null) -fi - -if [ -z "$_EM_PY" ]; then - echo 'unable to find python in $PATH' - exit 1 -fi - -exec "$_EM_PY" -E "$0.py" "$@" diff --git a/bootstrap.bat b/bootstrap.bat deleted file mode 100644 index e9080623b77f0..0000000000000 --- a/bootstrap.bat +++ /dev/null @@ -1,86 +0,0 @@ -:: Entry point for running python scripts on windows systems. -:: -:: Automatically generated by `create_entry_points.py`; DO NOT EDIT. -:: -:: To make modifications to this file, edit `tools\run_python.bat` and then run -:: `tools\maint\create_entry_points.py` - -:: N.b. In Windows .bat scripts, the ':' character cannot appear inside any if () blocks, -:: or there will be a parsing error. - -:: All env. vars specified in this file are to be local only to this script. -@setlocal -:: -E will not ignore _PYTHON_SYSCONFIGDATA_NAME an internal -:: of cpython used in cross compilation via setup.py. -@set _PYTHON_SYSCONFIGDATA_NAME= -@set _EM_PY=%EMSDK_PYTHON% -@if "%_EM_PY%"=="" ( - set _EM_PY=python -) - -:: Work around Windows bug https://github.com/microsoft/terminal/issues/15212 : If this -:: script is invoked via enclosing the invocation in quotes via PATH lookup, then %~f0 and -:: %~dp0 expansions will not work. -:: So first try if %~dp0 might work, and if not, manually look up this script from PATH. -@if exist "%~f0" ( - set "MYDIR=%~dp0" - goto FOUND_MYDIR -) -@for %%I in (%~n0.bat) do ( - @if exist %%~$PATH:I ( - set MYDIR=%%~dp$PATH:I - ) else ( - echo Fatal Error! Due to a Windows bug, we are unable to locate the path to %~n0.bat. - echo To help this issue, try removing unnecessary quotes in the invocation of emcc, - echo or add Emscripten directory to PATH. - echo See github.com/microsoft/terminal/issues/15212 and - echo github.com/emscripten-core/emscripten/issues/19207 for more details. - ) -) -:FOUND_MYDIR - -:: Python Windows bug https://bugs.python.org/issue34780: If this script was invoked via a -:: shared stdin handle from the parent process, and that parent process stdin handle is in -:: a certain state, running python.exe might hang here. To work around this, if -:: EM_WORKAROUND_PYTHON_BUG_34780 is defined, invoke python with '< NUL' stdin to avoid -:: sharing the parent's stdin handle to it, avoiding the hang. - -:: On Windows 7, the compiler batch scripts are observed to exit with a non-zero errorlevel, -:: even when the python executable above did succeed and quit with errorlevel 0 above. -:: On Windows 8 and newer, this issue has not been observed. It is possible that this -:: issue is related to the above python bug, but this has not been conclusively confirmed, -:: so using a separate env. var EM_WORKAROUND_WIN7_BAD_ERRORLEVEL_BUG to enable the known -:: workaround this issue, which is to explicitly quit the calling process with the previous -:: errorlevel from the above command. - -:: Also must use goto to jump to the command dispatch, since we cannot invoke emcc from -:: inside a if() block, because if a cmdline param would contain a char '(' or ')', that -:: would throw off the parsing of the cmdline arg. -@if "%EM_WORKAROUND_PYTHON_BUG_34780%"=="" ( - @if "%EM_WORKAROUND_WIN7_BAD_ERRORLEVEL_BUG%"=="" ( - goto NORMAL - ) else ( - goto NORMAL_EXIT - ) -) else ( - @if "%EM_WORKAROUND_WIN7_BAD_ERRORLEVEL_BUG%"=="" ( - goto MUTE_STDIN - ) else ( - goto MUTE_STDIN_EXIT - ) -) - -:NORMAL_EXIT -@"%_EM_PY%" -E "%MYDIR%%~n0.py" %* -@exit %ERRORLEVEL% - -:MUTE_STDIN -@"%_EM_PY%" -E "%MYDIR%%~n0.py" %* < NUL -@exit /b %ERRORLEVEL% - -:MUTE_STDIN_EXIT -@"%_EM_PY%" -E "%MYDIR%%~n0.py" %* < NUL -@exit %ERRORLEVEL% - -:NORMAL -@"%_EM_PY%" -E "%MYDIR%%~n0.py" %* diff --git a/bootstrap.py b/bootstrap.py index 059415053c5ed..7ed6823571fd8 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -23,24 +23,6 @@ # created. Bootstrap.py needs to be run before an .emscripten config exists. from tools import utils -actions = [ - ('npm packages', [ - 'package.json', - 'package-lock.json', - ], ['npm', 'ci']), - ('create entry points', [ - 'tools/maint/create_entry_points.py', - 'tools/maint/run_python.bat', - 'tools/maint/run_python.sh', - 'tools/maint/run_python.ps1', - ], [sys.executable, 'tools/maint/create_entry_points.py']), - ('git submodules', [ - 'test/third_party/posixtestsuite/', - 'test/third_party/googletest', - 'test/third_party/wasi-test-suite', - ], ['git', 'submodule', 'update', '--init']), -] - def get_stamp_file(action_name): return os.path.join(STAMP_DIR, action_name.replace(' ', '_') + '.stamp') @@ -60,47 +42,113 @@ def check_deps(name, deps): def check(): for name, deps, _ in actions: if not check_deps(name, deps): - utils.exit_with_error(f'emscripten setup is not complete ("{name}" is out-of-date). Run `bootstrap` to update') + utils.exit_with_error(f'emscripten setup is not complete ("{name}" is out-of-date). Run `bootstrap.py` to update') + + +def ask_yes_no(question): + while True: + try: + reply = input(f"{question} (y/n): ").strip().lower() + except EOFError: + return False + + if reply in {'y', 'yes'}: + return True + if reply in {'n', 'no'}: + return False + + print("Invalid input. Please enter 'y' or 'n'.") + + +def maybe_install_hooks(): + if os.path.exists('.git/hooks/pre-push'): + print('git hooks already installed; skipping') + return + if os.environ.get('CI') or not sys.stdin.isatty(): + # Do nothing when running in CI or non-interactive shell + return + if ask_yes_no('Install emscripten git hooks (see tools/maint/git-hooks)?'): + install_hooks() + + +def install_hooks(): + if not os.path.exists(utils.path_from_root('.git')): + print('--install-git-hooks requires a git checkout') + return 1 + + dst = utils.path_from_root('.git/hooks') + if not os.path.exists(dst): + os.mkdir(dst) + + for src in ('tools/maint/git-hooks/post-checkout', 'tools/maint/git-hooks/pre-push'): + shutil.copy(utils.path_from_root(src), dst) + return 0 + + +def run_cmd(cmd): + orig_exe = cmd[0] + if not os.path.isabs(orig_exe): + cmd[0] = shutil.which(orig_exe) + if not cmd[0]: + utils.exit_with_error(f'command not found: {orig_exe}') + print(' -> %s' % ' '.join(cmd)) + subprocess.run(cmd, check=True, text=True, encoding='utf-8', cwd=utils.path_from_root()) + + +actions = [ + ('npm packages', [ + 'package.json', + 'package-lock.json', + ], ['npm', 'ci']), + ('create entry points', [ + 'tools/maint/create_entry_points.py', + 'tools/pylauncher/pylauncher.exe', + 'tools/maint/run_python.bat', + 'tools/maint/run_python.sh', + 'tools/maint/run_python.ps1', + ], [sys.executable, 'tools/maint/create_entry_points.py']), + ('git submodules', [ + 'test/third_party/posixtestsuite/', + 'test/third_party/googletest', + 'test/third_party/wasi-test-suite', + ], ['git', 'submodule', 'update', '--init']), + ('install hooks', [ + 'tools/maint/git-hooks/post-checkout', + 'tools/maint/git-hooks/pre-push', + ], maybe_install_hooks), +] def main(args): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-v', '--verbose', action='store_true', help='verbose', default=False) + parser.add_argument('-f', '--force', action='store_true', help='force all actions to run', default=False) parser.add_argument('-n', '--dry-run', action='store_true', help='dry run', default=False) parser.add_argument('-i', '--install-git-hooks', action='store_true', help='install emscripten git hooks', default=False) args = parser.parse_args() if args.install_git_hooks: - if not os.path.exists(utils.path_from_root('.git')): - print('--install-git-hooks requires git checkout') - return 1 - - dst = utils.path_from_root('.git/hooks') - if not os.path.exists(dst): - os.mkdir(dst) - - src = utils.path_from_root('tools/maint/post-checkout') - for src in ('tools/maint/post-checkout', 'tools/maint/pre-push'): - shutil.copy(utils.path_from_root(src), dst) - return 0 - - for name, deps, cmd in actions: - if check_deps(name, deps): - print('Up-to-date: %s' % name) - continue - print('Out-of-date: %s' % name) - stamp_file = get_stamp_file(name) + return install_hooks() + + for name, deps, action in actions: + if not args.force: + if check_deps(name, deps): + print('Up-to-date: %s' % name) + continue + print('Out-of-date: %s' % name) if args.dry_run: - print(' (skipping: dry run) -> %s' % ' '.join(cmd)) + if type(action) == list: + action_str = ' '.join(action) + else: + action_str = action.__name__ + print(f' (skipping: dry run) -> {action_str}') continue - orig_exe = cmd[0] - if not os.path.isabs(orig_exe): - cmd[0] = shutil.which(orig_exe) - if not cmd[0]: - utils.exit_with_error(f'command not found: {orig_exe}') - print(' -> %s' % ' '.join(cmd)) - subprocess.run(cmd, check=True, text=True, encoding='utf-8', cwd=utils.path_from_root()) + if type(action) == list: + run_cmd(action) + else: + action() utils.safe_ensure_dirs(STAMP_DIR) + stamp_file = get_stamp_file(name) utils.write_file(stamp_file, 'Timestamp file created by bootstrap.py') return 0 diff --git a/cmake/Modules/Platform/Emscripten.cmake b/cmake/Modules/Platform/Emscripten.cmake index 26800a03ae765..087dab4894da4 100644 --- a/cmake/Modules/Platform/Emscripten.cmake +++ b/cmake/Modules/Platform/Emscripten.cmake @@ -18,7 +18,14 @@ set(CMAKE_SYSTEM_NAME Emscripten) set(CMAKE_SYSTEM_VERSION 1) set(CMAKE_CROSSCOMPILING TRUE) -set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) + +# Certain cmake versions are not compatible with dynnamic linking due to +# https://gitlab.kitware.com/cmake/cmake/-/work_items/27240 +if (("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "4.2.0" AND "${CMAKE_VERSION}" VERSION_LESS "4.2.6") OR + ("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "4.3.0" AND "${CMAKE_VERSION}" VERSION_LESS "4.3.3")) + message(WARNING "This version of cmake (${CMAKE_VERSION}) does not support emscripten shared libraries. Use cmake < 4.2.0 or cmake > 4.3.3 if you need shared library support") + set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) +endif() # Advertise Emscripten as a 32-bit platform (as opposed to # CMAKE_SYSTEM_PROCESSOR=x86_64 for 64-bit platform), since some projects (e.g. @@ -160,6 +167,7 @@ if (EMSCRIPTEN_FORCE_COMPILERS) set(CMAKE_CXX_COMPILER_ID Clang) set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT GNU) set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT 98) + set(CMAKE_CXX_STANDARD_LIBRARY libc++) set(CMAKE_C_PLATFORM_ID "emscripten") set(CMAKE_CXX_PLATFORM_ID "emscripten") @@ -219,7 +227,7 @@ endif() list(APPEND CMAKE_FIND_ROOT_PATH "${EMSCRIPTEN_SYSROOT}") list(APPEND CMAKE_SYSTEM_PREFIX_PATH /) -if (${CMAKE_C_FLAGS} MATCHES "MEMORY64") +if (${CMAKE_C_FLAGS} MATCHES "MEMORY64|-m64|--target=wasm64") set(CMAKE_LIBRARY_ARCHITECTURE "wasm64-emscripten") set(CMAKE_SIZEOF_VOID_P 8) set(CMAKE_C_SIZEOF_DATA_PTR 8) @@ -373,3 +381,45 @@ endif() # complain about unused CMake variable. if (CMAKE_CROSSCOMPILING_EMULATOR) endif() + +# C++23 stl modules (Ref: https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.7.html#import-std-support) +# Opt-in via CMAKE_CXX_MODULE_STD (Ref: https://cmake.org/cmake/help/latest/variable/CMAKE_CXX_MODULE_STD.html) +if(CMAKE_CXX_MODULE_STD AND CMAKE_CXX_COMPILER_LOADED AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.30 AND NOT CMAKE_CXX_STDLIB_MODULES_JSON) + # include guard and auxiliary for _cmake_cxx_import_std + set(CMAKE_CXX_STDLIB_MODULES_JSON "${EMSCRIPTEN_SYSROOT}/lib/${CMAKE_LIBRARY_ARCHITECTURE}/${CMAKE_CXX_STANDARD_LIBRARY}.modules.json") + + # Ref: https://cmake.org/cmake/help/latest/variable/CMAKE_CXX_COMPILER_IMPORT_STD.html + unset(CMAKE_CXX_COMPILER_IMPORT_STD) + foreach(COMPILE_FEATURE IN LISTS CMAKE_CXX_COMPILE_FEATURES) + if(COMPILE_FEATURE MATCHES [[cxx_std_([0-9]+)]] AND CMAKE_MATCH_1 GREATER_EQUAL 23 AND CMAKE_MATCH_1 LESS 98) + list(APPEND CMAKE_CXX_COMPILER_IMPORT_STD ${CMAKE_MATCH_1}) + endif() + endforeach() + + include(Compiler/Clang-CXX-CXXImportStd) + if(COMMAND _cmake_cxx_find_modules_json) + # CMake >= 4.3 + _cmake_cxx_find_modules_json() + if(CMAKE_CXX_COMPILER_IMPORT_STD_ERROR_MESSAGE) + message(WARNING "${CMAKE_CXX_COMPILER_IMPORT_STD_ERROR_MESSAGE}") + unset(CMAKE_CXX_COMPILER_IMPORT_STD) + endif() + elseif(COMMAND _cmake_cxx_import_std) + # CMake 3.30 ... 4.3 + foreach(STD_VERSION IN LISTS CMAKE_CXX_COMPILER_IMPORT_STD) + _cmake_cxx_import_std(${STD_VERSION} eval_code) + cmake_language(EVAL CODE "${eval_code}") + if(CMAKE_CXX${STD_VERSION}_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE) + message(WARNING "${CMAKE_CXX${STD_VERSION}_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE}") + list(REMOVE_ITEM CMAKE_CXX_COMPILER_IMPORT_STD ${STD_VERSION}) + endif() + endforeach() + endif() +endif() + +set_property(SOURCE + "${EMSCRIPTEN_SYSROOT}/share/libc++/v1/std.cppm" + "${EMSCRIPTEN_SYSROOT}/share/libc++/v1/std.compat.cppm" + PROPERTY + COMPILE_FLAGS -Wno-reserved-module-identifier +) diff --git a/em-config.py b/em-config.py index cd50c1a2e8e8b..5409323fff6d1 100755 --- a/em-config.py +++ b/em-config.py @@ -4,8 +4,10 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""This is a helper tool which is designed to make it possible -for other apps to read emscripten's configuration variables +"""Display emscripten configure file settings. + +Helper script which is designed to make it possible for +other apps to read emscripten's configuration variables in a unified way. Usage: em-config VAR_NAME diff --git a/emar.py b/emar.py index 8f57837b34fa7..593dc547509d0 100755 --- a/emar.py +++ b/emar.py @@ -4,11 +4,10 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""Wrapper script around `llvm-ar`. -""" +"""Wrapper script around `llvm-ar`.""" import sys from tools import shared -shared.exec_process([shared.LLVM_AR] + sys.argv[1:]) +shared.exec_process([shared.LLVM_AR, *sys.argv[1:]]) diff --git a/embuilder.py b/embuilder.py index 6232fc5dabc7d..19749dc658d62 100755 --- a/embuilder.py +++ b/embuilder.py @@ -26,11 +26,11 @@ # Minimal subset of targets used by CI systems to build enough to be useful MINIMAL_TASKS = [ - 'libcompiler_rt', - 'libcompiler_rt-mt', - 'libcompiler_rt-legacysjlj', - 'libcompiler_rt-wasmsjlj', - 'libcompiler_rt-ww', + 'libclang_rt.builtins', + 'libclang_rt.builtins-mt', + 'libclang_rt.builtins-legacysjlj', + 'libclang_rt.builtins-wasmsjlj', + 'libclang_rt.builtins-ww', 'libc', 'libc-debug', 'libc-mt-debug', @@ -81,6 +81,7 @@ 'libGL-emu-getprocaddr', 'libGL-emu-webgl2-ofb-getprocaddr', 'libGL-webgl2-ofb-getprocaddr', + 'libGL-webgl2-ofb-full_es3-getprocaddr', 'libGL-ww-getprocaddr', 'libhtml5', 'libsockets', @@ -91,16 +92,21 @@ 'libstandalonewasm-nocatch', 'crt1', 'crt1_proxy_main', - 'crtbegin', + 'crtbegin-mt', 'libunwind-legacyexcept', 'libunwind-wasmexcept', 'libnoexit', 'bullet', + 'libstb_image', + 'libwasmfs_no_fs', + 'libwasmfs-debug', + 'libwasm_workers-debug', ] # Additional tasks on top of MINIMAL_TASKS that are necessary for PIC testing on # CI (which has slightly more tests than other modes that want to use MINIMAL) -MINIMAL_PIC_TASKS = MINIMAL_TASKS + [ +MINIMAL_PIC_TASKS = [ + *MINIMAL_TASKS, 'libc-mt', 'libc_optz-mt', 'libc_optz-mt-debug', @@ -117,15 +123,11 @@ 'libGL-mt-emu-webgl2-getprocaddr', 'libGL-mt-emu-webgl2-ofb-getprocaddr', 'libsockets_proxy', - 'crtbegin', - 'libsanitizer_common_rt', - 'libubsan_rt', - 'libwasm_workers-debug', + 'libclang_rt.sanitizer_common', + 'libclang_rt.ubsan', 'libfetch', 'libfetch-mt', 'libwasmfs', - 'libwasmfs-debug', - 'libwasmfs_no_fs', 'giflib', 'sdl2', 'sdl2_gfx', diff --git a/emcc.py b/emcc.py index 6814682d1266b..4134a489bda3d 100644 --- a/emcc.py +++ b/emcc.py @@ -4,7 +4,8 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""emcc - compiler helper script +"""\ +emcc - compiler helper script ============================= emcc is a drop-in replacement for a compiler like gcc or clang. @@ -18,7 +19,7 @@ (by default /tmp/emscripten_temp). "2" will save additional emcc-* steps, that would normally not be separately produced (so this slows down compilation). -""" +""" # noqa: D205, D400, D415 import logging import os @@ -28,6 +29,7 @@ import tarfile from dataclasses import dataclass from enum import Enum, auto, unique +from subprocess import PIPE # This assert needs to happen early, before any too-recent python syntax is used. # In particular it needs to happen before we import any python file that uses the @@ -84,6 +86,19 @@ '--threadprofiler', '--use-preload-plugins', } +PASSTHROUGH_FLAGS = { + '-print-resource-dir', + '--print-resource-dir', + '-dumpmachine', + '-print-target-triple', + '--print-target-triple', +} + +PASSTHROUGH_PREFIXES = { + '-print-prog-name', + '--print-prog-name', +} + @unique class Mode(Enum): @@ -104,6 +119,7 @@ class LinkFlag: A list of these is returned by separate_linker_flags. """ + value: str is_file: int @@ -170,6 +186,15 @@ def make_relative(filename): reproduce_file.add(rsp_name, os.path.join(root, 'response.txt')) +def get_clang_resource_dir(args): + resource_dir = [a for a in args if a.startswith(('-resource-dir=', '--resource-dir='))] + if resource_dir: + return resource_dir[-1].split('=')[1] + else: + output = utils.run_process([shared.CLANG_CC, '-print-resource-dir'], stdout=PIPE).stdout + return output.strip() + + @ToolchainProfiler.profile() def main(args): if shared.run_via_emxx: @@ -185,7 +210,7 @@ def main(args): if len(args) == 2 and args[1] == '-v': # autoconf likes to see 'GNU' in the output to enable shared object support print(cmdline.version_string(), file=sys.stderr) - return shared.check_call([clang, '-v'] + compile.get_target_flags(), check=False).returncode + return shared.check_call([clang, '-v', *compile.get_target_flags()], check=False).returncode # Additional compiler flags that we treat as if they were passed to us on the # commandline @@ -230,6 +255,12 @@ def main(args): if not shared.SKIP_SUBPROCS: shared.check_sanity() + # For internal consistency, ensure we don't attempt to read or write any link time + # settings until we reach the linking phase. + settings.limit_settings(COMPILE_TIME_SETTINGS) + + phase_setup(state) + # Begin early-exit flag handling. if '--version' in args: @@ -245,34 +276,36 @@ def main(args): print(utils.EMSCRIPTEN_VERSION) return 0 - if '-dumpmachine' in args or '-print-target-triple' in args or '--print-target-triple' in args: - print(shared.get_llvm_target()) - return 0 - + # Sadly we cannot rely on PASSTHROUGH_FLAGS for -print-search-dirs or -print-libgcc-file-name + # because there is no way to tell clang today about our custom library paths. + # TODO: Teach clang about emscripten's library layout so we can remove this code. if '-print-search-dirs' in args or '--print-search-dirs' in args: print(f'programs: ={config.LLVM_ROOT}') - print(f'libraries: ={cache.get_lib_dir(absolute=True)}') + resource_dir = get_clang_resource_dir(args) + libdir = cache.get_lib_dir(absolute=True) + print(f'libraries: ={resource_dir}{os.pathsep}{libdir}') return 0 if '-print-libgcc-file-name' in args or '--print-libgcc-file-name' in args: settings.limit_settings(None) - compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'] - print(compiler_rt.get_path(absolute=True)) + clang_rt = system_libs.Library.get_usable_variations()['libclang_rt.builtins'] + print(clang_rt.get_path(absolute=True)) return 0 print_file_name = [a for a in args if a.startswith(('-print-file-name=', '--print-file-name='))] if print_file_name: libname = print_file_name[-1].split('=')[1] + resource_dir = get_clang_resource_dir(args) system_libpath = cache.get_lib_dir(absolute=True) - fullpath = os.path.join(system_libpath, libname) - if os.path.isfile(fullpath): - print(fullpath) + for dirname in (resource_dir, system_libpath): + fullpath = os.path.join(dirname, libname) + if os.path.isfile(fullpath): + print(fullpath) + break else: print(libname) return 0 - # End early-exit flag handling - if 'EMMAKEN_NO_SDK' in os.environ: exit_with_error('EMMAKEN_NO_SDK is no longer supported. The standard -nostdlib and -nostdinc flags should be used instead') @@ -286,14 +319,10 @@ def main(args): if 'EMCC_REPRODUCE' in os.environ: options.reproduce = os.environ['EMCC_REPRODUCE'] - # For internal consistency, ensure we don't attempt to read or write any link time - # settings until we reach the linking phase. - settings.limit_settings(COMPILE_TIME_SETTINGS) - - phase_setup(state) - - if '-print-resource-dir' in args or any(a.startswith('--print-prog-name') for a in args): - shared.exec_process([clang] + compile.get_cflags(tuple(args)) + args) + if any(a in PASSTHROUGH_FLAGS for a in args) or any(a.startswith(p) for p in PASSTHROUGH_PREFIXES for a in args): + # For several -print-xxx-name flags we just defer to clang rather than + # trying to re-implement the logic. + shared.exec_process([clang, *compile.get_cflags(tuple(args)), *newargs]) assert False, 'exec_process should not return' if '--cflags' in args: @@ -303,6 +332,8 @@ def main(args): print(shlex.join(cflags)) return 0 + # End early-exit flag handling + if options.reproduce: create_reproduce_file(options.reproduce, args) @@ -312,7 +343,7 @@ def main(args): linker_args = separate_linker_flags(newargs)[1] linker_args = [f.value for f in linker_args] # Delay import of link.py to avoid processing this file when only compiling - from tools import link # noqa: PLC0415 + from tools import link link.run_post_link(options.input_files[0], options, linker_args) return 0 @@ -335,7 +366,6 @@ def separate_linker_flags(newargs): - Linker flags include input files and are returned a list of LinkFlag objects. - Compiler flags are those to be passed to `clang -c`. """ - compiler_args = [] linker_args = [] @@ -369,7 +399,7 @@ def get_next_arg(): add_link_arg(flag) elif arg == '-Xlinker': add_link_arg(get_next_arg()) - elif arg == '-s' or arg.startswith(('-l', '-L', '--js-library=', '-z', '-u')): + elif arg in {'-s', '-Bstatic', '-Bdynamic'} or arg.startswith(('-l', '-L', '--js-library=', '-z', '-u')): add_link_arg(arg) elif not arg.startswith('-o') and arg not in {'-nostdlib', '-nostartfiles', '-nolibc', '-nodefaultlibs', '-s'}: # All other flags are for the compiler @@ -382,9 +412,7 @@ def get_next_arg(): @ToolchainProfiler.profile_block('setup') def phase_setup(state): - """Second phase: configure and setup the compiler based on the specified settings and arguments. - """ - + """Second phase: configure and setup the compiler based on the specified settings and arguments.""" has_header_inputs = any(get_file_suffix(f) in HEADER_EXTENSIONS for f in options.input_files) if options.post_link: @@ -543,7 +571,7 @@ def compile_source_file(input_file): cmd = get_clang_command() if ext == '.pcm': cmd = [c for c in cmd if not c.startswith('-fprebuilt-module-path=')] - cmd += compile_args + ['-c', input_file, '-o', output_file] + cmd += [*compile_args, '-c', input_file, '-o', output_file] if options.requested_debug == '-gsplit-dwarf': # When running in COMPILE_AND_LINK mode we compile objects to a temporary location # but we want the `.dwo` file to be generated in the current working directory, @@ -579,7 +607,7 @@ def compile_source_file(input_file): # Default to assuming the inputs are object files and pass them to the linker pass - return [f.value for f in linker_args] + return linker_args if __name__ == '__main__': diff --git a/emconfigure.py b/emconfigure.py index 83b35cf132fbf..beb8e4fee3b51 100755 --- a/emconfigure.py +++ b/emconfigure.py @@ -4,9 +4,12 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""This is a helper script. It runs ./configure (or cmake, -etc.) for you, setting the environment variables to use -emcc and so forth. Usage: +"""Helper for running ./configure. + +This script runs ./configure (or cmake, etc.) for you, +setting the environment variables to use emcc and so forth. + +Usage: emconfigure ./configure [FLAGS] diff --git a/emmake.py b/emmake.py index 776b7e87e9da0..10808f2bc6f5d 100755 --- a/emmake.py +++ b/emmake.py @@ -4,8 +4,10 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""This is a helper script. It runs make for you, setting -the environment variables to use emcc and so forth. Usage: +"""Helper script for running make. + +This script runs make with correct environment +variables to use emcc and so forth. Usage: emmake make [FLAGS] diff --git a/emranlib.py b/emranlib.py index 3d1659d05de8e..fc1367886a52a 100755 --- a/emranlib.py +++ b/emranlib.py @@ -4,7 +4,7 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""emranlib - ranlib helper script +"""emranlib - ranlib helper script. This script acts as a frontend replacement for ranlib, and simply invokes llvm-ranlib internally. @@ -14,4 +14,4 @@ from tools import shared -shared.exec_process([shared.LLVM_RANLIB] + sys.argv[1:]) +shared.exec_process([shared.LLVM_RANLIB, *sys.argv[1:]]) diff --git a/emscan-deps.py b/emscan-deps.py index b9d79863313be..8c49771dbbad8 100755 --- a/emscan-deps.py +++ b/emscan-deps.py @@ -4,7 +4,7 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""emscan-deps - clang-scan-deps helper script +"""emscan-deps - clang-scan-deps helper script. This script acts as a frontend replacement for clang-scan-deps. """ @@ -21,4 +21,4 @@ # Add any clang flags that emcc would add. newargs += compile.get_cflags(tuple(argv)) -shared.exec_process([shared.CLANG_SCAN_DEPS] + newargs) +shared.exec_process([shared.CLANG_SCAN_DEPS, *newargs]) diff --git a/emscons.py b/emscons.py index 643f4a0863ab1..c3b035dc68a31 100755 --- a/emscons.py +++ b/emscons.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -"""Wrapping the scons invocation, EMSCRIPTEN_TOOL_PATH is set in the process -environment, and can be used to locate the emscripten SCons Tool. +"""Wrapper for the scons invocation. -Example: +EMSCRIPTEN_TOOL_PATH is set in the process environment, and can be used to +locate the emscripten SCons Tool. +Example: # Load emscripten Tool my_env = Environment(tools=['emscripten'], toolpath=[os.environ['EMSCRIPTEN_TOOL_PATH']]) + """ import os diff --git a/emscripten-version.txt b/emscripten-version.txt index aff2882b0fa03..2c8d92745c326 100644 --- a/emscripten-version.txt +++ b/emscripten-version.txt @@ -1 +1 @@ -5.0.6-git +6.0.3-git diff --git a/emscripten.proj b/emscripten.proj index a050581bbb304..81977bb0e6d33 100644 --- a/emscripten.proj +++ b/emscripten.proj @@ -1,27 +1,36 @@ - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/emsize.py b/emsize.py index 935c237c9d224..5144161f6e773 100755 --- a/emsize.py +++ b/emsize.py @@ -4,7 +4,7 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -"""Size helper script +"""Size helper script. This script acts as a frontend replacement for `size` that supports combining JS and wasm output from emscripten. diff --git a/emstrip.py b/emstrip.py index 3744e843d5f11..7df6bf33412d7 100755 --- a/emstrip.py +++ b/emstrip.py @@ -6,9 +6,9 @@ """Wrapper script around `llvm-strip`. - It also supports taking a JS file as an argument and running 'llvm-strip' on - the corresponding Wasm file. This is convenient for some build systems that - expect to strip the output of a compile. +It also supports taking a JS file as an argument and running 'llvm-strip' on +the corresponding Wasm file. This is convenient for some build systems that +expect to strip the output of a compile. """ import os @@ -29,7 +29,7 @@ def run(): continue new_args.append(arg) - shared.exec_process([llvm_strip] + new_args) + shared.exec_process([llvm_strip, *new_args]) if __name__ == '__main__': diff --git a/html/shell.html b/html/shell.html index f7c65bf38c8f3..8e5f42bcc6cf3 100644 --- a/html/shell.html +++ b/html/shell.html @@ -96,7 +96,7 @@ } }; setStatus('Downloading...'); - window.onerror = (event) => { + window.onerror = window.onunhandledrejection = () => { // TODO: do not warn on ok events like simulating an infinite loop or exitStatus setStatus('Exception thrown, see JavaScript console'); spinnerElement.style.display = 'none'; diff --git a/html/shell_minimal.html b/html/shell_minimal.html index 91f81b8179c2d..6a07b3433ce7e 100644 --- a/html/shell_minimal.html +++ b/html/shell_minimal.html @@ -134,7 +134,7 @@ } }; setStatus('Downloading...'); - window.onerror = () => { + window.onerror = window.onunhandledrejection = () => { setStatus('Exception thrown, see JavaScript console'); spinnerElement.style.display = 'none'; setStatus = (text) => { diff --git a/media/Emscripten_logo_full.svg b/media/Emscripten_logo_full.svg deleted file mode 100644 index c5ce9506f394c..0000000000000 --- a/media/Emscripten_logo_full.svg +++ /dev/null @@ -1,1328 +0,0 @@ - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/package-lock.json b/package-lock.json index b6a5ddf966d48..7b6c57d9b9195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,11 +5,9 @@ "packages": { "": { "dependencies": { - "@babel/cli": "^7.28.6", - "@babel/core": "^7.29.0", - "@babel/preset-env": "^7.29.0", - "acorn": "^8.15.0", - "google-closure-compiler": "20240317.0.0", + "acorn": "^8.17.0", + "acorn-import-phases": "^1.0.4", + "google-closure-compiler": "20260629.0.0", "html-minifier-terser": "7.2.0" } }, @@ -1527,7 +1525,18 @@ "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", "integrity": "sha1-Mj1y3SUQPQxPvc6J2t9XSnh7H5s=", "license": "MIT", - "optional": true + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } }, "node_modules/acorn": { "version": "8.16.0", @@ -1615,6 +1624,19 @@ "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.24", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", @@ -1844,7 +1866,7 @@ "integrity": "sha1-B5LraC37wyWZm7K4T93duhEKxzw=", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=20" } }, "node_modules/concat-map": { @@ -1932,6 +1954,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escalade/-/escalade-3.2.0.tgz", @@ -2045,22 +2074,23 @@ "integrity": "sha1-KzWylgR9aCTBh2d3yIwuWsYFD7k=", "license": "Apache-2.0", "dependencies": { - "chalk": "4.x", - "google-closure-compiler-java": "^20240317.0.0", - "minimist": "1.x", - "vinyl": "2.x", + "chalk": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 <5.6.1 || ^5.6.2 >5.6.1", + "google-closure-compiler-java": "^20260629.0.0", + "minimist": "^1.0.0", + "vinyl": "^3.0.1", "vinyl-sourcemaps-apply": "^0.2.0" }, "bin": { "google-closure-compiler": "cli.js" }, "engines": { - "node": ">=10" + "node": ">=18" }, "optionalDependencies": { - "google-closure-compiler-linux": "^20240317.0.0", - "google-closure-compiler-osx": "^20240317.0.0", - "google-closure-compiler-windows": "^20240317.0.0" + "google-closure-compiler-linux": "^20260629.0.0", + "google-closure-compiler-linux-arm64": "^20260629.0.0", + "google-closure-compiler-macos": "^20260629.0.0", + "google-closure-compiler-windows": "^20260629.0.0" } }, "node_modules/google-closure-compiler-java": { @@ -2088,8 +2118,6 @@ "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/google-closure-compiler-osx/-/google-closure-compiler-osx-20240317.0.0.tgz", "integrity": "sha1-wXC3CaepWRlnvH8hP+oglsIZn/Y=", "cpu": [ - "x32", - "x64", "arm64" ], "license": "Apache-2.0", @@ -2343,6 +2371,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ms/-/ms-2.1.3.tgz", @@ -2555,7 +2644,7 @@ "integrity": "sha1-LW2ZbQShWFXZZ0Q2Md1fd4JbAWo=", "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/resolve": { @@ -2628,7 +2717,9 @@ "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" } }, "node_modules/supports-color": { @@ -2780,15 +2871,13 @@ "integrity": "sha1-I8+4u6tezjgDqiwKHrKK98u6GXQ=", "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/vinyl-sourcemaps-apply": { @@ -2822,4 +2911,4 @@ "license": "ISC" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 96c577cf8033a..2af92bb0ab613 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,9 @@ { "private": true, "dependencies": { - "@babel/cli": "^7.28.6", - "@babel/core": "^7.29.0", - "@babel/preset-env": "^7.29.0", - "acorn": "^8.15.0", - "google-closure-compiler": "20240317.0.0", + "acorn": "^8.17.0", + "acorn-import-phases": "^1.0.4", + "google-closure-compiler": "20260629.0.0", "html-minifier-terser": "7.2.0" }, "scripts": { @@ -13,4 +11,4 @@ "fmt": "prettier --write src/*.mjs tools/*.mjs --ignore-path src/pthread_esm_startup.mjs", "check": "prettier --check src/*.mjs tools/*.mjs --ignore-path src/pthread_esm_startup.mjs" } -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 263f46c9e5587..cf36dec16ebf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,5 @@ [project] +name = "emscripten" requires-python = ">=3.10" [tool.ruff] @@ -22,6 +23,7 @@ lint.select = [ "ARG", "ASYNC", "B", + "D", "C4", "C90", "COM", @@ -33,10 +35,22 @@ lint.select = [ "PL", "UP", "W", + "RUF", "YTT", ] lint.external = [ "D" ] lint.ignore = [ + "D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/ + "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ + "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ + "D103", # https://docs.astral.sh/ruff/rules/undocumented-public-function/ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ + "D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/ + "D106", # https://docs.astral.sh/ruff/rules/undocumented-public-nested-class/ + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ + "D203", # https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class/ + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ + "D401", # https://docs.astral.sh/ruff/rules/non-imperative-mood/ "B011", # See https://github.com/PyCQA/flake8-bugbear/issues/66 "B023", "B026", @@ -64,6 +78,11 @@ lint.ignore = [ "PLW0603", "PLW1510", "PLW2901", + "RUF012", # https://docs.astral.sh/ruff/rules/mutable-class-default/ + "RUF015", # https://docs.astral.sh/ruff/rules/unnecessary-iterable-allocation-for-first-element/ + "RUF027", # https://docs.astral.sh/ruff/rules/missing-f-string-syntax/ + "RUF039", # https://docs.astral.sh/ruff/rules/unraw-re-pattern/ + "RUF067", # https://docs.astral.sh/ruff/rules/non-empty-init-module/ "UP030", # TODO "UP031", # TODO "UP032", # TODO @@ -72,6 +91,7 @@ lint.per-file-ignores."tools/ports/*.py" = [ "ARG001", "ARG005" ] lint.per-file-ignores."test/other/ports/*.py" = [ "ARG001" ] lint.per-file-ignores."test/parallel_testsuite.py" = [ "ARG002" ] lint.per-file-ignores."test/test_benchmark.py" = [ "ARG002" ] +lint.per-file-ignores."test/*.py" = [ "RUF005" ] lint.mccabe.max-complexity = 51 # Recommended: 10 lint.pylint.allow-magic-value-types = [ "bytes", @@ -98,8 +118,9 @@ mypy_path = "third_party/,third_party/ply,third_party/websockify" files = [ "." ] exclude = ''' (?x)( -cache | -third_party | +out/ | +cache/ | +third_party/ | conf\.py | emrun\.py | site/source/_themes/ | diff --git a/requirements-dev.txt b/requirements-dev.txt index 9cb5413753980..8c362e7347a7a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,10 +13,14 @@ unittest-xml-reporting==3.2.0 deadcode==2.3.1 vulture==2.14 -# This version is mentioned in `site/source/docs/site/about.rst`. -# Please keep them in sync. -sphinx==7.1.2 +# This version is mentioned in `site/source/docs/site/about.rst`. Please keep +# them in sync. We cannot currently update to v8 or v9 due to our python 3.10 +# dependency. +# TODO(sbc): Update sphinx version once we update our min python requirements. +sphinx==7.4.7 +sphinx-design==0.6.1 sphinxcontrib-jquery==4.0 +shibuya==2026.5.19 # Needed by test/test_sockets.py websockify==0.12.0 diff --git a/src/Fetch.js b/src/Fetch.js index 5d0fd81c38af5..91520d6248b18 100644 --- a/src/Fetch.js +++ b/src/Fetch.js @@ -81,7 +81,7 @@ class FetchXHR { // Handle Basic Authentication if user/password are provided. // This creates a base64-encoded string and sets the Authorization header. if (user) { - const credentials = btoa(`${user}:${password || ''}`); + const credentials = btoa(`${user}:${password ?? ''}`); this._headers['Authorization'] = `Basic ${credentials}`; } @@ -536,7 +536,7 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { if (!fetchAttrSynchronous) xhr.timeout = timeoutMsecs; // XHR timeout field is only accessible in async XHRs, and must be set after .open() but before .send(). xhr.url_ = url_; // Save the url for debugging purposes (and for comparing to the responseURL that server side advertised) #if ASSERTIONS && !FETCH_STREAMING - assert(!fetchAttrStreamData, 'Streaming is only supported when FETCH_STREAMING is enabled.'); + assert(!fetchAttrStreamData, 'streaming is only supported when FETCH_STREAMING is enabled'); #endif xhr.responseType = 'arraybuffer'; @@ -568,7 +568,7 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { dbg(`fetch: id=${id}`); #endif {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.id, 'id', 'u32') }}}; - var data = (dataPtr && dataLength) ? HEAPU8.slice(dataPtr, dataPtr + dataLength) : null; + var data = (dataPtr && dataLength) ? HEAPU8.subarray(dataPtr, dataPtr + dataLength) : null; // TODO: Support specifying custom headers to the request. // Share the code to save the response, as we need to do so both on success @@ -593,7 +593,7 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen); writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0); - var len = xhr.response ? xhr.response.byteLength : 0; + var len = xhr.response?.byteLength ?? 0; if (len) { // If the final XHR.onload handler receives the bytedata to compute total length, report that, // otherwise don't write anything out here, which will retain the latest byte size reported in @@ -660,31 +660,48 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { if (!Fetch.xhrs.has(id)) { return; } - var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response) ? xhr.response.byteLength : 0; - var ptr = 0; - if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) { + var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData) ? xhr.response?.byteLength ?? 0 : 0; + + // Specifies the maximum chunk size that a streaming fetch will transfer from + // JS over to WebAssembly side. Used to cap a streaming fetch to avoid + // overallocating WebAssembly memory needlessly. + var FETCH_STREAMING_MAX_CHUNK_SIZE = 8*1024*1024; + + for (var bytePos = 0; bytePos < ptrLen || !ptrLen;) { + var sz = Math.min(ptrLen - bytePos, FETCH_STREAMING_MAX_CHUNK_SIZE); + + var ptr = 0; + if (sz > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) { + // Even though we are doing a streaming fetch (i.e. in small chunks), Safari may call onprogress with a huge + // chunk size. This will be a problem for Wasm applications that intend to use streaming fetch to process + // an input file in small chunks (to avoid blowing up the WebAssembly heap size). Therefore apply a max + // chunk size ceiling to the received chunks, and transfer the data over to WebAssembly using max sized chunks. + #if FETCH_DEBUG - dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`); + dbg(`fetch: allocating ${sz} bytes in Emscripten heap for xhr data`); #endif #if ASSERTIONS - assert(onprogress, 'When doing a streaming fetch, you should have an onprogress handler registered to receive the chunks!'); + assert(onprogress, 'streaming fetch requires an onprogress handler'); #endif - // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is - // freed when emscripten_fetch_close() is called. - ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen); - HEAPU8.set(new Uint8Array(/** @type{Array} */(xhr.response)), ptr); + // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is + // freed when emscripten_fetch_close() is called. + ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, sz); + HEAPU8.set(new Uint8Array(/** @type{Array} */(xhr.response), bytePos, sz), ptr); + } + {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} + writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, sz); + writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen + bytePos); + writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total); + {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}} + var status = xhr.status; + // If loading files from a source that does not give HTTP status code, assume success if we get data bytes + if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200; + {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}} + if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); + onprogress(fetch, e); + bytePos += sz; + if (!ptrLen) break; } - {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} - writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen); - writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen); - writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total); - {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}} - var status = xhr.status; - // If loading files from a source that does not give HTTP status code, assume success if we get data bytes - if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200; - {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}} - if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); - onprogress(fetch, e); }; xhr.onreadystatechange = (e) => { // check if xhr was aborted by user and don't try to call back @@ -827,7 +844,7 @@ function startFetch(fetch, successcb, errorcb, progresscb, readystatechangecb) { // TODO(?): Here we perform a clone of the data, because storing shared typed arrays to IndexedDB does not seem to be allowed. var ptr = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestData, '*') }}}; var size = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestDataSize, '*') }}}; - fetchCacheData(Fetch.dbInstance, fetch, HEAPU8.slice(ptr, ptr + size), reportSuccess, reportError); + fetchCacheData(Fetch.dbInstance, fetch, HEAPU8.subarray(ptr, ptr + size), reportSuccess, reportError); } else if (requestMethod === 'EM_IDB_DELETE') { fetchDeleteCachedData(Fetch.dbInstance, fetch, reportSuccess, reportError); } else if (!fetchAttrReplace) { diff --git a/src/audio_worklet.js b/src/audio_worklet.js index 8114b2e8a4fae..8eb686e15a440 100644 --- a/src/audio_worklet.js +++ b/src/audio_worklet.js @@ -12,10 +12,6 @@ // the node constructor's "processorOptions" field, we can share the necessary // bootstrap information from the main thread to the AudioWorkletGlobalScope. -#if MINIMAL_RUNTIME -var instantiatePromise; -#endif - if (ENVIRONMENT_IS_AUDIO_WORKLET) { #if AUDIO_WORKLET_SUPPORT_AUDIO_PARAMS @@ -100,9 +96,8 @@ function createWasmAudioWorkletProcessor() { process(inputList, outputList) { #endif -#if ALLOW_MEMORY_GROWTH +#if ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS != 2 // Recreate the output views if the heap has changed - // TODO: add support for GROWABLE_ARRAYBUFFERS if (HEAPF32.buffer != this.outputViews[0].buffer) { this.createOutputViews(); } diff --git a/src/build_as_worker.js b/src/build_as_worker.js index 51d6bd5053cd0..654206cef4414 100644 --- a/src/build_as_worker.js +++ b/src/build_as_worker.js @@ -1,3 +1,4 @@ +#preprocess var workerResponded = false, workerCallbackId = -1; (() => { @@ -43,9 +44,9 @@ var workerResponded = false, workerCallbackId = -1; workerResponded = false; workerCallbackId = msg.data['callbackId']; if (data) { - func(buffer, data.length); + func({{{ to64('buffer') }}}, data.length); } else { - func(0, 0); + func({{{ to64('0') }}}, 0); } } })(); diff --git a/src/closure-externs/closure-externs.js b/src/closure-externs/closure-externs.js index 774f46f847460..424ca9ebd4166 100644 --- a/src/closure-externs/closure-externs.js +++ b/src/closure-externs/closure-externs.js @@ -11,22 +11,11 @@ * The closure_compiler() method in tools/shared.py refers to this file when calling closure. */ -// Special placeholder for `import.meta` and `await import`. -var EMSCRIPTEN$IMPORT$META; -var EMSCRIPTEN$AWAIT$IMPORT; -var EMSCRIPTEN$AWAIT; - // Don't minify createRequire var createRequire; // Closure externs used by library_sockfs.js -/** - * Backported from latest closure... - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript - */ -Document.prototype.currentScript; - /** * Don't minify Math.* */ @@ -56,34 +45,11 @@ Math.max = function() {}; Math.clz32 = function() {}; Math.trunc = function() {}; -/** - * Atomics - */ - -var Atomics = {}; -Atomics.compareExchange = function() {}; -Atomics.exchange = function() {}; -Atomics.wait = function() {}; -/** - * @param {number=} maxWaitMilliseconds - * @suppress {duplicate, checkTypes} - */ -Atomics.waitAsync = function(i32a, index, value, maxWaitMilliseconds) {}; -Atomics.notify = function() {}; -Atomics.load = function() {}; -Atomics.store = function() {}; - /** * @const * @suppress {duplicate, checkTypes} */ var WebAssembly = {}; -/** - * @constructor - * @param {Object} globalDescriptor - * @param {*=} value - */ -WebAssembly.Global = function(globalDescriptor, value) {}; /** * @param {!WebAssembly.Tag} tag * @param {number} index @@ -102,20 +68,10 @@ WebAssembly.Exception.stack; * Note: Closure compiler does not support function overloading, omit this overload for now. * {function(!WebAssembly.Module, Object=):!Promise} */ -/** @dict */ -WebAssembly.Instance.prototype.exports; -/** - * @type {!ArrayBuffer} - */ -WebAssembly.Memory.prototype.buffer; /** * @returns {ArrayBuffer} */ WebAssembly.Memory.prototype.toResizableBuffer = function() {}; -/** - * @type {number} - */ -WebAssembly.Table.prototype.length; /** * @param {!Function} func * @returns {Function} @@ -227,20 +183,6 @@ var devicePixelRatio; */ var id; -/** - * Used in MODULARIZE mode as the name of the incoming module argument. - * This is generated outside of the code we pass to closure so from closure's - * POV this is "extern". - */ -var moduleArg; - -/** - * Used in MODULARIZE mode. - * We need to access this after the code we pass to closure so from closure's - * POV this is "extern". - */ -var moduleRtn; - /** * This was removed from upstream closure compiler in * https://github.com/google/closure-compiler/commit/f83322c1b. @@ -258,13 +200,15 @@ var moduleRtn; Navigator.prototype.webkitGetUserMedia = function( constraints, successCallback, errorCallback) {}; -/** - * A symbol from the explicit resource management proposal that isn't yet part of Closure. - * @type {symbol} - */ -Symbol.dispose; - // Common between node-externs and v8-externs var os = {}; AudioWorkletProcessor.parameterDescriptors; + +var scheduler = {}; + +/** @type {boolean} */ +ArrayBuffer.prototype.resizable; + +/** @type {boolean} */ +SharedArrayBuffer.prototype.growable; diff --git a/src/closure-externs/modularize-externs.js b/src/closure-externs/modularize-externs.js index b0fec4a9dc4b0..4fddfdb23b8ef 100644 --- a/src/closure-externs/modularize-externs.js +++ b/src/closure-externs/modularize-externs.js @@ -1,8 +1,22 @@ -// In MODULARIZE mode the JS code may be executed later, after `document.currentScript` is gone, so we store -// it to `_scriptName` outside the wrapper function. Therefore, it cannot be minified. -// In EXPORT_ES6 mode we use `import.meta.url` and for Node.js CommonJS builds we use `__filename`. - /** + * In MODULARIZE mode the JS code may be executed later, after `document.currentScript` is gone, so + * we store it to `_scriptName` outside the wrapper function. Therefore, it cannot be minified. + * In EXPORT_ES6 mode we use `import.meta.url` and for Node.js CommonJS builds we use `__filename`. * @suppress {duplicate, undefinedVars} */ var _scriptName; + +/** + * Used in MODULARIZE mode as the name of the incoming module argument. + * This is generated outside of the code we pass to closure so from closure's + * POV this is "extern". + */ +var moduleArg; + +/** + * @suppress {duplicate, undefinedVars} + */ +var Module; + +// Special placeholder for `await`. +var EMSCRIPTEN$AWAIT; diff --git a/src/cpuprofiler.js b/src/cpuprofiler.js index a4d36dee5470c..ceaa119f344ab 100644 --- a/src/cpuprofiler.js +++ b/src/cpuprofiler.js @@ -742,13 +742,13 @@ setInterval = (fn, delay) => { // Hook into setTimeout to be able to capture the time spent executing them. emscriptenCpuProfiler.createSection(3, 'setTimeout', emscriptenCpuProfiler.colorSetTimeoutSection, /*traceable=*/true); var realSetTimeout = setTimeout; -setTimeout = (fn, delay) => { - function wrappedSetTimeout() { +setTimeout = (fn, delay, ...args) => { + function wrappedSetTimeout(...args) { emscriptenCpuProfiler.enterSection(3); - fn(); + fn(...args); emscriptenCpuProfiler.endSection(3); }; - return realSetTimeout(wrappedSetTimeout, delay); + return realSetTimeout(wrappedSetTimeout, delay, ...args); } // Backwards compatibility with previously compiled code. Don't call this anymore! diff --git a/src/deterministic.js b/src/deterministic.js index 9c4b3ca5bd74f..4eef50459d111 100644 --- a/src/deterministic.js +++ b/src/deterministic.js @@ -20,7 +20,6 @@ Date.now = deterministicNow; // Note: this approach does not work on certain versions of Node.js // Specifically it seems like its not possible to override performance.now on // node v16 through v18. -// See getPerformanceNow in parseTools.mjs for how we deal with this. if (globalThis.performance) performance.now = deterministicNow; // for consistency between different builds than between runs of the same build diff --git a/src/emrun_postjs.js b/src/emrun_postjs.js index b2ede002e796f..9f43926aeeb77 100644 --- a/src/emrun_postjs.js +++ b/src/emrun_postjs.js @@ -7,7 +7,16 @@ * emcc is run with `--emrun` */ -if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIRONMENT_IS_PTHREAD)) { +// POSTs the given binary data represented as a (typed) array data back to the +// emrun-based web server. +// To use from C code, call e.g: +// EM_ASM({emrun_file_dump("file.dat", HEAPU8.subarray($0, $0 + $1));}, my_data_pointer, my_data_pointer_byte_length); +// Note: this functions does nothing by default but gets redefined below +// in `emrun_register_handlers` when emrun is active, along with `out` and +// `err`. +var emrun_file_dump = (filename, data) => {}; + +if (globalThis.window && globalThis.document && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIRONMENT_IS_PTHREAD)) { var emrun_register_handlers = () => { // When C code exit()s, we may still have remaining stdout and stderr // messages in flight. In that case, we can't close the browser until all @@ -29,7 +38,7 @@ if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIR window.close(); } catch(e) {} }; - var post = (msg) => { + var post = (url, msg) => { var http = new XMLHttpRequest(); ++emrun_num_post_messages_in_flight; http.onreadystatechange = () => { @@ -39,7 +48,7 @@ if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIR } } } - http.open("POST", "stdio.html", true); + http.open("POST", url, true); http.send(msg); }; // If the address contains localhost, or we are running the page from port @@ -57,19 +66,26 @@ if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIR } }); out = (text) => { - post('^out^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(text)); + post('stdio.html', '^out^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(text)); prevPrint(text); }; err = (text) => { - post('^err^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(text)); + post('stdio.html', '^err^'+(emrun_http_sequence_number++)+'^'+encodeURIComponent(text)); prevErr(text); }; + emrun_file_dump = (filename, data) => { + out(`Dumping out file "${filename}" with ${data.length} bytes of data.`); + if (ArrayBuffer.isView(data) && typeof SharedArrayBuffer !== "undefined" && data.buffer instanceof SharedArrayBuffer) { + data = new data.constructor(data); // Make a clone of the typed array of the same type, since http.send() does not allow SharedArrayBuffer backing. + } + post("stdio.html?file=" + filename, data); + }; // Notify emrun web server that this browser has successfully launched the // page. Note that we may need to wait for the server to be ready. var tryToSendPageload = () => { try { - post('^pageload^'); + post('stdio.html', '^pageload^'); } catch (e) { setTimeout(tryToSendPageload, 50); } @@ -78,18 +94,5 @@ if (globalThis.window && (typeof ENVIRONMENT_IS_PTHREAD == 'undefined' || !ENVIR } }; - // POSTs the given binary data represented as a (typed) array data back to the - // emrun-based web server. - // To use from C code, call e.g: - // EM_ASM({emrun_file_dump("file.dat", HEAPU8.subarray($0, $0 + $1));}, my_data_pointer, my_data_pointer_byte_length); - var emrun_file_dump = (filename, data) => { - var http = new XMLHttpRequest(); - out(`Dumping out file "${filename}" with ${data.length} bytes of data.`); - http.open("POST", "stdio.html?file=" + filename, true); - http.send(data); // XXX this does not work in workers, for some odd reason (issue #2681) - }; - - if (globalThis.document) { - emrun_register_handlers(); - } + emrun_register_handlers(); } diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 036eccfec5e38..e23a9deb66d9b 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -136,7 +136,7 @@ function getTransitiveDeps(symbol) { while (toVisit.length) { const sym = toVisit.pop(); if (!seen.has(sym)) { - let directDeps = LibraryManager.library[sym + '__deps'] || []; + let directDeps = LibraryManager.library[sym + '__deps'] ?? []; directDeps = directDeps.filter((d) => typeof d === 'string'); for (const dep of directDeps) { if (!transitiveDeps.has(dep)) { @@ -228,7 +228,7 @@ function checkDependencies(symbol, snippet, deps, postset) { continue; } const mangled = mangleCSymbolName(dep); - if (!snippet.includes(mangled) && (!postset || !postset.includes(mangled))) { + if (!snippet.includes(mangled) && !postset?.includes(mangled)) { error(`${symbol}: unused dependency: ${dep}`); } } @@ -397,6 +397,12 @@ export async function runJSify(outputFile, symbolsOnly) { } async function writeOutput(str) { + // Unmangle previously mangled `import.meta` references. + // See also: `mangleUnsupportedSyntax` in parseTools.mjs. + if (EXPORT_ES6) { + str = str.replaceAll('EMSCRIPTEN$IMPORT$META', 'import.meta'); + } + await outputHandle.write(str + '\n'); } @@ -497,7 +503,7 @@ function(${args}) { proxyMode = PROXY_SYNC; } } - const rtnType = sig && sig.length ? sig[0] : null; + const rtnType = sig?.[0]; const proxyFunc = MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread'; deps.push('$' + proxyFunc); @@ -515,7 +521,7 @@ ${body} snippet, (args, body) => ` function(${args}) { - assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!"); + assert(!ENVIRONMENT_IS_WASM_WORKER, "attempt to call proxied function '${mangled}' from a Wasm Worker (where proxying is not possible)"); ${body} }\n`, ); @@ -567,11 +573,7 @@ function(${args}) { } addedLibraryItems[symbol] = true; - if (!(symbol + '__deps' in LibraryManager.library)) { - LibraryManager.library[symbol + '__deps'] = []; - } - - const deps = LibraryManager.library[symbol + '__deps']; + const deps = LibraryManager.library[symbol + '__deps'] ??= []; let sig = LibraryManager.library[symbol + '__sig']; if (!WASM_BIGINT && sig && sig[0] == 'j') { // Without WASM_BIGINT functions that return i64 depend on setTempRet0 @@ -702,7 +704,7 @@ function(${args}) { // signatures are relevant and they differ between and alais and // it's target) we need to construct a forwarding function from // one to the other. - const isSigRelevant = MAIN_MODULE || MEMORY64 || CAN_ADDRESS_2GB || (sig && sig.includes('j')); + const isSigRelevant = MAIN_MODULE || MEMORY64 || CAN_ADDRESS_2GB || sig?.includes('j'); const targetSig = LibraryManager.library[aliasTarget + '__sig']; if (isSigRelevant && sig && targetSig && sig != targetSig) { debugLog(`${symbol}: Alias target (${aliasTarget}) has different signature (${sig} vs ${targetSig})`) @@ -872,7 +874,7 @@ function(${args}) { orderedPostSets[j] = temp; i--; limit--; - assert(limit > 0, 'Could not sort postsets!'); + assert(limit > 0, 'could not sort postsets'); break; } } @@ -888,7 +890,7 @@ function(${args}) { writeOutput('// Begin JS library code\n'); for (const item of libraryItems.concat(postSets)) { - writeOutput(indentify(item || '', 2)); + writeOutput(indentify(item ?? '', 2)); } writeOutput('// End JS library code\n'); diff --git a/src/lib/libaddfunction.js b/src/lib/libaddfunction.js index ea6efd267d3f0..30d07de694277 100644 --- a/src/lib/libaddfunction.js +++ b/src/lib/libaddfunction.js @@ -17,39 +17,6 @@ addToLibrary({ // but we don't care as it's only used in a temporary representation. return [(n % 128) | 128, n >> 7, ...arr]; }, -#if WASM_JS_TYPES - // Converts a signature like 'vii' into a description of the wasm types, like - // { parameters: ['i32', 'i32'], results: [] }. - $sigToWasmTypes__internal: true, - $sigToWasmTypes: (sig) => { -#if ASSERTIONS && !WASM_BIGINT - assert(!sig.includes('j'), 'i64 not permitted in function signatures when WASM_BIGINT is disabled'); -#endif - var typeNames = { - 'i': 'i32', - 'j': 'i64', - 'f': 'f32', - 'd': 'f64', - 'e': 'externref', -#if MEMORY64 - 'p': 'i64', -#else - 'p': 'i32', -#endif - }; - var type = { - parameters: [], - results: sig[0] == 'v' ? [] : [typeNames[sig[0]]] - }; - for (var i = 1; i < sig.length; ++i) { -#if ASSERTIONS - assert(sig[i] in typeNames, 'invalid signature char: ' + sig[i]); -#endif - type.parameters.push(typeNames[sig[i]]); - } - return type; - }, -#endif $wasmTypeCodes__internal: true, // Note: using template literal here instead of plain object // because jsify serializes objects w/o quotes and Closure will then @@ -81,26 +48,14 @@ addToLibrary({ // Wraps a JS function as a wasm function with a given signature. $convertJsFunctionToWasm__deps: [ '$uleb128EncodeWithLen', -#if WASM_JS_TYPES - '$sigToWasmTypes', -#endif '$generateTypePack' ], $convertJsFunctionToWasm: (func, sig) => { #if ASSERTIONS && !WASM_BIGINT assert(!sig.includes('j'), 'i64 not permitted in function signatures when WASM_BIGINT is disabled'); #endif -#if WASM_JS_TYPES - // If the type reflection proposal is available, use the new - // "WebAssembly.Function" constructor. - // Otherwise, construct a minimal wasm module importing the JS function and - // re-exporting it. - if (WebAssembly.Function) { - return new WebAssembly.Function(sigToWasmTypes(sig), func); - } -#endif - - // Rest of the module is static + // TODO: If the type reflection proposal ever makes progress we can use + // it here instead of creatign a new module. var bytes = Uint8Array.of( 0x00, 0x61, 0x73, 0x6d, // magic ("\0asm") 0x01, 0x00, 0x00, 0x00, // version: 1 diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 17ca2c0b011d4..4c54f73208f2c 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -253,8 +253,8 @@ addToLibrary({ whenDone() { #if ASSERTIONS - assert(Asyncify.currData, 'Tried to wait for an async operation when none is in progress.'); - assert(!Asyncify.asyncPromiseHandlers, 'Cannot have multiple async operations in flight at once'); + assert(Asyncify.currData, 'tried to wait for an async operation when none is in progress'); + assert(!Asyncify.asyncPromiseHandlers, 'cannot have multiple async operations in flight at once'); #endif return new Promise((resolve, reject) => { Asyncify.asyncPromiseHandlers = { resolve, reject }; @@ -330,7 +330,7 @@ addToLibrary({ // and other async methods for simple examples of usage. handleSleep(startAsync) { #if ASSERTIONS - assert(Asyncify.state !== Asyncify.State.Disabled, 'Asyncify cannot be done during or after the runtime exits'); + assert(Asyncify.state !== Asyncify.State.Disabled, 'handleSleep called after Asyncify was shut down'); #endif if (ABORT) return; #if ASYNCIFY_DEBUG @@ -361,7 +361,7 @@ addToLibrary({ // as it might break later operations (we can rewind ok now, but if // we unwind again, we would unwind through the extra compiled code // too). - assert(!Asyncify.exportCallStack.length, 'Waking up (starting to rewind) must be done from JS, without compiled code on the stack.'); + assert(!Asyncify.exportCallStack.length, 'waking up (starting to rewind) must be done from JS, without compiled code on the stack'); #endif #if ASYNCIFY_DEBUG dbg(`ASYNCIFY: start rewind ${Asyncify.currData}`); diff --git a/src/lib/libatomic.js b/src/lib/libatomic.js index c30dff83323cd..01b0b5e5f464c 100644 --- a/src/lib/libatomic.js +++ b/src/lib/libatomic.js @@ -47,18 +47,13 @@ addToLibrary({ #if ASSERTIONS && WASM_WORKERS if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.'); #endif - /** - * @param {number=} maxWaitMilliseconds - */ Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => { let val = Atomics.load(i32a, index); if (val != value) return { async: false, value: 'not-equal' }; if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' }; maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity); - let promiseResolve; - let promise = new Promise((resolve) => { promiseResolve = resolve; }); if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10); - __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]); + let promise = new Promise((resolve) => __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, resolve])); return { async: true, value: promise }; }; }`, @@ -73,6 +68,25 @@ addToLibrary({ // Any function using Atomics.waitAsync should depend on this. }, +#if ASYNCIFY + _emscripten_atomic_wait_promise__deps: ['$polyfillWaitAsync', '$atomicWaitStates', '$addPromise'], + _emscripten_atomic_wait_promise: (addr, val, maxWaitMilliseconds) => { + var wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds); + if (wait.async) { + // In the async case return the promise ID. + var chainedPromise = wait.value.then((value) => atomicWaitStates.indexOf(value)); + var id = addPromise(chainedPromise); + return id; + } + // In the synchronous case return the negative result code + return -atomicWaitStates.indexOf(wait.value); + }, +#else + _emscripten_atomic_wait_promise: (addr, val, maxWaitMilliseconds) => { + abort('Please compile your program with async support in order to use asynchronous operations like emscripten_atomic_wait_suspending'); + }, +#endif + $atomicWaitStates__internal: true, $atomicWaitStates: ['ok', 'not-equal', 'timed-out'], $liveAtomicWaitAsyncs: {}, diff --git a/src/lib/libbrowser.js b/src/lib/libbrowser.js index 0b9a043776edb..6996a770c9622 100644 --- a/src/lib/libbrowser.js +++ b/src/lib/libbrowser.js @@ -6,6 +6,10 @@ // Utilities for browser environments var LibraryBrowser = { + $workerHandles__internal: true, + $workerHandles__deps: ['$HandleAllocator'], + $workerHandles: 'new HandleAllocator();', + $Browser__deps: [ '$callUserCallback', '$getFullscreenElement', @@ -24,7 +28,6 @@ var LibraryBrowser = { isFullscreen: false, pointerLock: false, moduleContextCreatedCallbacks: [], - workers: [], preloadedImages: {}, preloadedAudios: {}, @@ -94,7 +97,7 @@ var LibraryBrowser = { var b = new Blob([byteArray], { type: Browser.getMimetype(name) }); var url = URL.createObjectURL(b); // XXX we never revoke this! var audio = new Audio(); - audio.addEventListener('canplaythrough', () => finish(audio), false); // use addEventListener due to chromium bug 124926 + audio.addEventListener('canplaythrough', () => finish(audio)); // use addEventListener due to chromium bug 124926 audio.onerror = (event) => { if (done) return; err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`); @@ -146,16 +149,18 @@ var LibraryBrowser = { // forced aspect ratio can be enabled by defining 'forcedAspectRatio' on Module // Module['forcedAspectRatio'] = 4 / 3; - document.addEventListener('pointerlockchange', pointerLockChange, false); + document.addEventListener('pointerlockchange', pointerLockChange); +#if expectToReceiveOnModule('elementPointerLock') if (Module['elementPointerLock']) { canvas.addEventListener("click", (ev) => { if (!Browser.pointerLock && Browser.getCanvas().requestPointerLock) { Browser.getCanvas().requestPointerLock(); ev.preventDefault(); } - }, false); + }); } +#endif } }, @@ -245,16 +250,18 @@ var LibraryBrowser = { Browser.updateCanvasDimensions(canvas); } } +#if expectToReceiveOnModule('onFullScreen') Module['onFullScreen']?.(Browser.isFullscreen); Module['onFullscreen']?.(Browser.isFullscreen); +#endif } if (!Browser.fullscreenHandlersInstalled) { Browser.fullscreenHandlersInstalled = true; - document.addEventListener('fullscreenchange', fullscreenChange, false); - document.addEventListener('mozfullscreenchange', fullscreenChange, false); - document.addEventListener('webkitfullscreenchange', fullscreenChange, false); - document.addEventListener('MSFullscreenChange', fullscreenChange, false); + document.addEventListener('fullscreenchange', fullscreenChange); + document.addEventListener('mozfullscreenchange', fullscreenChange); + document.addEventListener('webkitfullscreenchange', fullscreenChange); + document.addEventListener('MSFullscreenChange', fullscreenChange); } // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root @@ -397,18 +404,8 @@ var LibraryBrowser = { var canvas = Browser.getCanvas(); var rect = canvas.getBoundingClientRect(); - // Neither .scrollX or .pageXOffset are defined in a spec, but - // we prefer .scrollX because it is currently in a spec draft. - // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) - var scrollX = ((typeof window.scrollX != 'undefined') ? window.scrollX : window.pageXOffset); - var scrollY = ((typeof window.scrollY != 'undefined') ? window.scrollY : window.pageYOffset); -#if ASSERTIONS - // If this assert lands, it's likely because the browser doesn't support scrollX or pageXOffset - // and we have no viable fallback. - assert((typeof scrollX != 'undefined') && (typeof scrollY != 'undefined'), 'Unable to retrieve scroll position, mouse positions likely broken.'); -#endif - var adjustedX = pageX - (scrollX + rect.left); - var adjustedY = pageY - (scrollY + rect.top); + var adjustedX = pageX - (window.scrollX + rect.left); + var adjustedY = pageY - (window.scrollY + rect.top); // the canvas might be CSS-scaled compared to its backbuffer; // SDL-using content will want mouse coordinates in terms @@ -517,6 +514,7 @@ var LibraryBrowser = { } var w = wNative; var h = hNative; +#if expectToReceiveOnModule('forcedAspectRatio') if (Module['forcedAspectRatio'] > 0) { if (w/h < Module['forcedAspectRatio']) { w = Math.round(h * Module['forcedAspectRatio']); @@ -524,6 +522,7 @@ var LibraryBrowser = { h = Math.round(w / Module['forcedAspectRatio']); } } +#endif if ((getFullscreenElement() === canvas.parentNode) && (typeof screen != 'undefined')) { var factor = Math.min(screen.width / w, screen.height / h); w = Math.round(w * factor); @@ -602,7 +601,7 @@ var LibraryBrowser = { FS.createPreloadedFile( '/', name, - {{{ makeHEAPView('U8', 'data', 'data + size') }}}, + HEAPU8.subarray(data, data + size), true, true, () => { {{{ runtimeKeepalivePop() }}} @@ -624,7 +623,7 @@ var LibraryBrowser = { }, // TODO: currently not callable from a pthread, but immediately calls onerror() if not on main thread. - emscripten_async_load_script__deps: ['$UTF8ToString'], + emscripten_async_load_script__deps: ['$UTF8ToString', '$runDependencies', '$resolveRunDependencies'], emscripten_async_load_script: async (url, onload, onerror) => { url = UTF8ToString(url); #if PTHREADS @@ -642,12 +641,7 @@ var LibraryBrowser = { var loadDone = () => { {{{ runtimeKeepalivePop() }}} if (onload) { - var onloadCallback = () => callUserCallback({{{ makeDynCall('v', 'onload') }}}); - if (runDependencies > 0) { - dependenciesFulfilled = onloadCallback; - } else { - onloadCallback(); - } + resolveRunDependencies().then(() => callUserCallback({{{ makeDynCall('v', 'onload') }}})); } } @@ -727,20 +721,21 @@ var LibraryBrowser = { // To avoid creating worker parent->child chains, always proxies to execute on the main thread. emscripten_create_worker__proxy: 'sync', - emscripten_create_worker__deps: ['$UTF8ToString', 'realloc'], + emscripten_create_worker__deps: ['$UTF8ToString', 'realloc', '$workerHandles'], emscripten_create_worker: (url) => { url = UTF8ToString(url); - var id = Browser.workers.length; + var worker = new Worker(url); var info = { - worker: new Worker(url), + worker, callbacks: [], awaited: 0, buffer: 0, }; - info.worker.onmessage = (msg) => { + var id = workerHandles.allocate(info); + worker.onmessage = (msg) => { if (ABORT) return; - var info = Browser.workers[id]; - if (!info) return; // worker was destroyed meanwhile + if (!workerHandles.has(id)) return; // worker was destroyed meanwhile + var info = workerHandles.get(id); var callbackId = msg.data['callbackId']; var callbackInfo = info.callbacks[callbackId]; if (!callbackInfo) return; // no callback or callback removed meanwhile @@ -760,23 +755,23 @@ var LibraryBrowser = { callbackInfo.func(0, 0, callbackInfo.arg); } }; - Browser.workers.push(info); return id; }, - emscripten_destroy_worker__deps: ['free'], + emscripten_destroy_worker__deps: ['free', '$workerHandles'], emscripten_destroy_worker__proxy: 'sync', emscripten_destroy_worker: (id) => { - var info = Browser.workers[id]; + var info = workerHandles.get(id); info.worker.terminate(); _free(info.buffer); - Browser.workers[id] = null; + workerHandles.free(id); }, + emscripten_call_worker__deps: ['$workerHandles'], emscripten_call_worker__proxy: 'sync', emscripten_call_worker: (id, funcName, data, size, callback, arg) => { funcName = UTF8ToString(funcName); - var info = Browser.workers[id]; + var info = workerHandles.get(id); var callbackId = -1; if (callback) { // If we are waiting for a response from the worker we need to keep @@ -794,7 +789,7 @@ var LibraryBrowser = { var transferObject = { 'funcName': funcName, 'callbackId': callbackId, - 'data': data ? new Uint8Array({{{ makeHEAPView('U8', 'data', 'data + size') }}}) : 0 + 'data': data ? HEAPU8.slice(data, data + size) : 0 }; if (data) { info.worker.postMessage(transferObject, [transferObject.data.buffer]); @@ -810,7 +805,7 @@ var LibraryBrowser = { var transferObject = { 'callbackId': workerCallbackId, 'finalResponse': false, - 'data': data ? new Uint8Array({{{ makeHEAPView('U8', 'data', 'data + size') }}}) : 0 + 'data': data ? HEAPU8.slice(data, data + size) : 0 }; if (data) { postMessage(transferObject, [transferObject.data.buffer]); @@ -826,7 +821,7 @@ var LibraryBrowser = { var transferObject = { 'callbackId': workerCallbackId, 'finalResponse': true, - 'data': data ? new Uint8Array({{{ makeHEAPView('U8', 'data', 'data + size') }}}) : 0 + 'data': data ? HEAPU8.slice(data, data + size) : 0 }; if (data) { postMessage(transferObject, [transferObject.data.buffer]); @@ -836,10 +831,11 @@ var LibraryBrowser = { }, #endif + emscripten_get_worker_queue_size__deps: ['$workerHandles'], emscripten_get_worker_queue_size__proxy: 'sync', emscripten_get_worker_queue_size: (id) => { - var info = Browser.workers[id]; - if (!info) return -1; + if (!workerHandles.has(id)) return -1; + var info = workerHandles.get(id); return info.awaited; }, diff --git a/src/lib/libc_preprocessor.js b/src/lib/libc_preprocessor.js index 56259599f6889..494fe68adcfd1 100644 --- a/src/lib/libc_preprocessor.js +++ b/src/lib/libc_preprocessor.js @@ -65,7 +65,7 @@ addToLibrary({ function classifyChar(str, idx) { var cc = str.charCodeAt(idx); #if ASSERTIONS - assert(!(cc > 127), "Only 7-bit ASCII can be used in preprocessor #if/#ifdef/#define statements!"); + assert(!(cc > 127), "only 7-bit ASCII can be used in preprocessor #if/#ifdef/#define statements"); #endif if (cc > 32) { if (cc < 48) return 1; // an operator symbol, any of !"#$%&'()*+,-./ @@ -128,7 +128,12 @@ addToLibrary({ var kind2 = classifyChar(str, j); if (kind2 != 2/*0-9*/ && kind2 != 3/*a-z*/) { var symbol = str.substring(i, j); +#if MIN_FIREFOX_VERSION < 92 + // Firefox only introduced Object.hasOwn() in Firefox 92. + if (defs.hasOwnProperty(symbol)) { +#else if (Object.hasOwn(defs, symbol)) { +#endif var pp = defs[symbol], expanded; if (typeof pp == 'function') { // definition is a function? if (pp.length) { // Expanding a macro? (#define FOO(X) ...) @@ -204,24 +209,25 @@ addToLibrary({ if (operatorAndPriority >= 0) { var left = buildExprTree(tokens.slice(0, i)); var right = buildExprTree(tokens.slice(i+1)); - switch(tokens[i]) { - case '&&': return [function() { return left() && right(); }]; - case '||': return [function() { return left() || right(); }]; - case '==': return [function() { return left() == right(); }]; - case '!=': return [function() { return left() != right(); }]; - case '<' : return [function() { return left() < right(); }]; - case '<=': return [function() { return left() <= right(); }]; - case '>' : return [function() { return left() > right(); }]; - case '>=': return [function() { return left() >= right(); }]; - case '+': return [function() { return left() + right(); }]; - case '-': return [function() { return left() - right(); }]; - case '*': return [function() { return left() * right(); }]; - case '/': return [function() { return Math.floor(left() / right()); }]; - } + var opers = { + '&&': () => left() && right(), + '||': () => left() || right(), + '==': () => left() == right(), + '!=': () => left() != right(), + '<' : () => left() < right(), + '<=': () => left() <= right(), + '>' : () => left() > right(), + '>=': () => left() >= right(), + '+': () => left() + right(), + '-': () => left() - right(), + '*': () => left() * right(), + '/': () => Math.floor(left() / right()) + }; + return [opers[tokens[i]]]; } // else a number: #if ASSERTIONS - assert(tokens[i] !== ')', 'Parsing failure, mismatched parentheses in parsing!' + tokens.toString()); + assert(tokens[i] !== ')', 'parse failure, mismatched parentheses in parsing' + tokens.toString()); assert(operatorAndPriority == -1); #endif var num = Number(tokens[i]); diff --git a/src/lib/libccall.js b/src/lib/libccall.js index bf48d9a912d5c..4d40b8dbf884c 100644 --- a/src/lib/libccall.js +++ b/src/lib/libccall.js @@ -64,7 +64,7 @@ addToLibrary({ var cArgs = []; var stack = 0; #if ASSERTIONS - assert(returnType !== 'array', 'Return type should not be "array".'); + assert(returnType !== 'array', 'return type should not be "array"'); #endif if (args) { for (var i = 0; i < args.length; i++) { diff --git a/src/lib/libcore.js b/src/lib/libcore.js index ee9cfcedef751..e7269f5803da8 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -66,6 +66,7 @@ addToLibrary({ setTempRet0: '$setTempRet0', getTempRet0: '$getTempRet0', + // Assign a name to a given function. This is mostly useful for debugging // purposes in cases where new functions are created at runtime. $createNamedFunction: (name, func) => Object.defineProperty(func, 'name', { value: name }), @@ -154,9 +155,6 @@ addToLibrary({ // if exit() was called explicitly, warn the user if the runtime isn't actually being shut down if (keepRuntimeAlive() && !implicit) { var msg = `program exited (with status: ${status}), but keepRuntimeAlive() is set (counter=${runtimeKeepaliveCounter}) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)`; -#if MODULARIZE - readyPromiseReject?.(msg); -#endif // MODULARIZE err(msg); } #endif // ASSERTIONS @@ -219,7 +217,7 @@ addToLibrary({ try { // round size grow request up to wasm page size (fixed 64KB per spec) wasmMemory.grow({{{ toIndexType('pages') }}}); // .grow() takes a delta compared to the previous size -#if !GROWABLE_ARRAYBUFFERS +#if GROWABLE_ARRAYBUFFERS != 2 updateMemoryViews(); #endif #if MEMORYPROFILER @@ -359,7 +357,7 @@ addToLibrary({ #endif // ALLOW_MEMORY_GROWTH }, -#if !GROWABLE_ARRAYBUFFERS +#if GROWABLE_ARRAYBUFFERS != 2 // Called after wasm grows memory. At that time we need to update the views. // Without this notification, we'd need to check the buffer in JS every time // we return from any wasm, which adds overhead. See @@ -1284,11 +1282,19 @@ addToLibrary({ $timers: {}, + $clearTimers__internal: true, + $clearTimers: () => { + for (var t of Object.values(timers)) { + clearTimeout(t.id); + } + }, + // Helper function for setitimer that registers timers with the eventloop. // Timers always fire on the main thread, either directly from JS (here) or // or when the main thread is busy waiting calling _emscripten_yield. + _setitimer_js__postset: () => addAtExit('clearTimers();'), _setitimer_js__proxy: 'sync', - _setitimer_js__deps: ['$timers', '$callUserCallback', '_emscripten_timeout', 'emscripten_get_now'], + _setitimer_js__deps: ['$timers', '$clearTimers', '$callUserCallback', '_emscripten_timeout', 'emscripten_get_now'], _setitimer_js: (which, timeout_ms) => { #if RUNTIME_DEBUG dbg(`setitimer_js ${which} timeout=${timeout_ms}`); @@ -1355,13 +1361,13 @@ addToLibrary({ emscripten_date_now: () => Date.now(), - emscripten_performance_now: () => {{{ getPerformanceNow() }}}(), + emscripten_performance_now: () => performance.now(), #if PTHREADS && !AUDIO_WORKLET // Pthreads need their clocks synchronized to the execution of the main // thread, so, when using them, make sure to adjust all timings to the // respective time origins. - emscripten_get_now: () => performance.timeOrigin + {{{ getPerformanceNow() }}}(), + emscripten_get_now: () => performance.timeOrigin + performance.now(), #else #if AUDIO_WORKLET // https://github.com/WebAudio/web-audio-api/issues/2413 emscripten_get_now: `; @@ -1369,11 +1375,11 @@ addToLibrary({ // (https://github.com/WebAudio/web-audio-api/issues/2527), so if building // with // Audio Worklets enabled, do a dynamic check for its presence. - if (globalThis.performance && {{{ getPerformanceNow() }}}) { + if (globalThis.performance?.now) { #if PTHREADS - _emscripten_get_now = () => performance.timeOrigin + {{{ getPerformanceNow() }}}(); + _emscripten_get_now = () => performance.timeOrigin + performance.now(); #else - _emscripten_get_now = () => {{{ getPerformanceNow() }}}(); + _emscripten_get_now = () => performance.now(); #endif } else { _emscripten_get_now = Date.now; @@ -1383,7 +1389,7 @@ addToLibrary({ // Modern environment where performance.now() is supported: // N.B. a shorter form "_emscripten_get_now = performance.now;" is // unfortunately not allowed even in current browsers (e.g. FF Nightly 75). - emscripten_get_now: () => {{{ getPerformanceNow() }}}(), + emscripten_get_now: () => performance.now(), #endif #endif @@ -1514,7 +1520,7 @@ addToLibrary({ _emscripten_sanitizer_get_option__deps: ['$stringToNewUTF8', '$UTF8ToString'], _emscripten_sanitizer_get_option__sig: 'pp', - _emscripten_sanitizer_get_option: (name) => stringToNewUTF8(Module[UTF8ToString(name)] || ''), + _emscripten_sanitizer_get_option: (name) => stringToNewUTF8(Module[UTF8ToString(name)] ?? ''), #endif $readEmAsmArgsArray: [], @@ -1717,7 +1723,7 @@ addToLibrary({ return "./this.program"; }, #else - $getExecutableName: () => thisProgram || './this.program', + $getExecutableName: () => thisProgram, #endif // Receives a Web Audio context plus a set of elements to listen for user @@ -1884,7 +1890,7 @@ addToLibrary({ } #if ASSERTIONS && ASYNCIFY != 2 // With JSPI the function stored in the table will be a wrapper. /** @suppress {checkTypes} */ - assert(wasmTable.get({{{ toIndexType('funcPtr') }}}) == func, 'JavaScript-side Wasm function table mirror is out of date!'); + assert(wasmTable.get({{{ toIndexType('funcPtr') }}}) == func, 'table mirror is out of date'); #endif return func; }, @@ -1959,44 +1965,17 @@ addToLibrary({ _emscripten_get_progname__deps: ['$getExecutableName', '$stringToUTF8'], _emscripten_get_progname: (str, len) => stringToUTF8(getExecutableName(), str, len), - emscripten_console_log: (str) => { -#if ASSERTIONS - assert(typeof str == 'number'); -#endif - console.log(UTF8ToString(str)); - }, - - emscripten_console_warn: (str) => { -#if ASSERTIONS - assert(typeof str == 'number'); -#endif - console.warn(UTF8ToString(str)); - }, - - emscripten_console_error: (str) => { -#if ASSERTIONS - assert(typeof str == 'number'); -#endif - console.error(UTF8ToString(str)); - }, - - emscripten_console_trace: (str) => { -#if ASSERTIONS - assert(typeof str == 'number'); -#endif - console.trace(UTF8ToString(str)); - }, + // These single-line arrow functions use curly braces since otherwise closure + // compiler will inject a extra `return` keyword when inlining. + // https://github.com/emscripten-core/emscripten/issues/26922 + emscripten_console_log: (str) => { console.log(UTF8ToString(str)) }, + emscripten_console_warn: (str) => { console.warn(UTF8ToString(str)) }, + emscripten_console_error: (str) => { console.error(UTF8ToString(str)) }, + emscripten_console_trace: (str) => { console.trace(UTF8ToString(str)) }, - emscripten_throw_number: (number) => { - throw number; - }, + emscripten_throw_number: (number) => { throw number; }, - emscripten_throw_string: (str) => { -#if ASSERTIONS - assert(typeof str == 'number'); -#endif - throw UTF8ToString(str); - }, + emscripten_throw_string: (str) => { throw UTF8ToString(str); }, #if !MINIMAL_RUNTIME #if STACK_OVERFLOW_CHECK @@ -2023,7 +2002,7 @@ addToLibrary({ } #endif #if RUNTIME_DEBUG - dbg("handleException: got unexpected exception, calling quit_") + dbg(`handleException: got unexpected exception ${e}, calling quit_`) #endif quit_(1, e); }, @@ -2177,7 +2156,7 @@ addToLibrary({ $alignMemory: (size, alignment) => { #if ASSERTIONS - assert(alignment, "alignment argument is required"); + assert(alignment, 'alignment argument is required'); #endif return Math.ceil(size / alignment) * alignment; }, @@ -2238,7 +2217,7 @@ addToLibrary({ return this.allocated[id] !== undefined; } allocate(handle) { - var id = this.freelist.pop() || this.allocated.length; + var id = this.freelist.pop() ?? this.allocated.length; this.allocated[id] = handle; return id; } @@ -2289,10 +2268,15 @@ addToLibrary({ // it happens right before run - run will be postponed until // the dependencies are met. $runDependencies__internal: true, + $runDependencies__deps: ['$resolveRunDependencies'], $runDependencies: 0, - // overridden to take different actions when all run dependencies are fulfilled - $dependenciesFulfilled__internal: true, - $dependenciesFulfilled: null, + $dependenciesPromise__internal: true, + $dependenciesPromise: null, + $dependenciesPromiseResolve__internal: true, + $dependenciesPromiseResolve: null, + $resolveRunDependencies__internal: true, + $resolveRunDependencies__deps: ['$dependenciesPromise'], + $resolveRunDependencies: async () => dependenciesPromise, #if ASSERTIONS $runDependencyTracking__internal: true, $runDependencyTracking: {}, @@ -2300,13 +2284,16 @@ addToLibrary({ $runDependencyWatcher: null, #endif - $addRunDependency__deps: ['$runDependencies', '$removeRunDependency', + $addRunDependency__deps: ['$runDependencies', '$removeRunDependency', '$dependenciesPromise', '$dependenciesPromiseResolve', #if ASSERTIONS '$runDependencyTracking', '$runDependencyWatcher', #endif ], $addRunDependency: (id) => { + if (!runDependencies) { + dependenciesPromise = new Promise((resolve) => dependenciesPromiseResolve = resolve); + } runDependencies++; #if expectToReceiveOnModule('monitorRunDependencies') @@ -2349,7 +2336,7 @@ addToLibrary({ #endif }, - $removeRunDependency__deps: ['$runDependencies', '$dependenciesFulfilled', + $removeRunDependency__deps: ['$runDependencies', '$dependenciesPromiseResolve', #if ASSERTIONS '$runDependencyTracking', '$runDependencyWatcher', @@ -2370,18 +2357,14 @@ addToLibrary({ assert(runDependencyTracking[id]); delete runDependencyTracking[id]; #endif - if (runDependencies == 0) { + if (!runDependencies) { #if ASSERTIONS if (runDependencyWatcher !== null) { clearInterval(runDependencyWatcher); runDependencyWatcher = null; } #endif - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } + dependenciesPromiseResolve(); } }, #endif @@ -2532,7 +2515,7 @@ function wrapSyscallFunction(x, library, isWasi) { library[x + '__deps'] ??= []; -#if PURE_WASI && !GROWABLE_ARRAYBUFFERS +#if PURE_WASI && GROWABLE_ARRAYBUFFERS != 2 // In PURE_WASI mode we can't assume the wasm binary was built by emscripten // and politely notify us on memory growth. Instead we have to check for // possible memory growth on each syscall. @@ -2608,8 +2591,6 @@ function wrapSyscallFunction(x, library, isWasi) { // instead of synchronously, and marked with // __proxy: 'async' // (but essentially all syscalls do have return values). - if (library[x + '__proxy'] === undefined) { - library[x + '__proxy'] = 'sync'; - } + library[x + '__proxy'] ??= 'sync'; #endif } diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index ab8ba1dd5b293..2e829e84383aa 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -834,7 +834,7 @@ var LibraryDylink = { if (!body.includes(argName)) break; args.push(argName); } - args = args.join(','); + args = args.join(); var func = `(${args}) => { ${body} };`; #if DYLINK_DEBUG dbg('adding new EM_ASM constant at:', ptrToString(start)); @@ -1107,7 +1107,7 @@ var LibraryDylink = { if (dso) { // the library is being loaded or has been loaded already. #if ASSERTIONS - assert(dso.exports !== 'loading', `Attempt to load '${libName}' twice before the first load completed`); + assert(dso.exports !== 'loading', `attempt to load '${libName}' a second time, before the first load completed`); #endif if (!flags.global) { if (localScope) { @@ -1235,7 +1235,7 @@ var LibraryDylink = { }, $loadDylibs__internal: true, - $loadDylibs__deps: ['$loadDynamicLibrary', '$reportUndefinedSymbols', '$addRunDependency', '$removeRunDependency'], + $loadDylibs__deps: ['$loadDynamicLibrary', '$reportUndefinedSymbols'], $loadDylibs: async () => { if (!dynamicLibraries.length) { #if DYLINK_DEBUG @@ -1248,7 +1248,6 @@ var LibraryDylink = { #if DYLINK_DEBUG dbg('loadDylibs:', dynamicLibraries); #endif - addRunDependency('loadDylibs'); // Load binaries asynchronously for (var lib of dynamicLibraries) { @@ -1260,7 +1259,6 @@ var LibraryDylink = { #if DYLINK_DEBUG dbg('loadDylibs done!'); #endif - removeRunDependency('loadDylibs'); }, // void* dlopen(const char* filename, int flags); diff --git a/src/lib/libembind.js b/src/lib/libembind.js index 19080626392f3..465f72126d1b6 100644 --- a/src/lib/libembind.js +++ b/src/lib/libembind.js @@ -694,7 +694,7 @@ var LibraryEmbind = { } #if ASSERTIONS && ASYNCIFY != 2 - assert(!isAsync, 'Async bindings are only supported with JSPI.'); + assert(!isAsync, 'async bindings are only supported with JSPI'); #endif var isClassMethodFunc = (argTypes[1] !== null && classType !== null); @@ -821,7 +821,7 @@ var LibraryEmbind = { ], $embind__requireFunction: (signature, rawFunction, isAsync = false) => { #if ASSERTIONS && ASYNCIFY != 2 - assert(!isAsync, 'Async bindings are only supported with JSPI.'); + assert(!isAsync, 'async bindings are only supported with JSPI'); #endif signature = AsciiToString(signature); diff --git a/src/lib/libembind_gen.js b/src/lib/libembind_gen.js index 9ae86581a23ef..6198845ea22e5 100644 --- a/src/lib/libembind_gen.js +++ b/src/lib/libembind_gen.js @@ -337,15 +337,17 @@ var LibraryEmbind = { out.push(` ${this.name}: {`); const outItems = []; for (const [name, value] of this.items) { + // Quote keys that aren't valid JS identifiers. + const key = /^[a-zA-Z_$][\w$]*$/.test(name) ? name : `'${name}'`; switch (this.valueType) { case 'object': - outItems.push(`${name}: ${this.name}Value<${value}>`); + outItems.push(`${key}: ${this.name}Value<${value}>`); break; case 'number': - outItems.push(`${name}: ${value}`); + outItems.push(`${key}: ${value}`); break; case 'string': - outItems.push(`${name}: '${name}'`); + outItems.push(`${key}: '${name}'`); break; } } diff --git a/src/lib/libembind_shared.js b/src/lib/libembind_shared.js index 87d6d36be8939..e75caea394069 100644 --- a/src/lib/libembind_shared.js +++ b/src/lib/libembind_shared.js @@ -216,8 +216,8 @@ var LibraryEmbindShared = { argsList.push(`arg${i}`) argsListWired.push(`arg${i}Wired`) } - argsList = argsList.join(',') - argsListWired = argsListWired.join(',') + argsList = argsList.join() + argsListWired = argsListWired.join() var invokerFnBody = `return function (${argsList}) {\n`; diff --git a/src/lib/libemval.js b/src/lib/libemval.js index ada9d19e87529..327d50cafda06 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -91,7 +91,7 @@ var LibraryEmVal = { _emval_decref: (handle) => { if (handle > {{{ EMVAL_LAST_RESERVED_HANDLE }}} && 0 === --emval_handles[handle + 1]) { #if ASSERTIONS - assert(emval_handles[handle] !== undefined, `Decref for unallocated handle.`); + assert(emval_handles[handle] !== undefined, `decref for unallocated handle`); #endif var value = emval_handles[handle]; emval_handles[handle] = undefined; @@ -420,6 +420,20 @@ ${functionBody} }, #endif + _emval_is_catchable_cpp_exception_object__deps: [ + '$Emval', +#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS + '$isCppExceptionObject', +#endif + ], + _emval_is_catchable_cpp_exception_object: (object) => { +#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS + return isCppExceptionObject(Emval.toValue(object)); +#else + return false; +#endif + }, + _emval_throw__deps: ['$Emval', #if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS #if !DISABLE_EXCEPTION_CATCHING diff --git a/src/lib/libeventloop.js b/src/lib/libeventloop.js index 8e0a84474edd6..c024e755a101d 100644 --- a/src/lib/libeventloop.js +++ b/src/lib/libeventloop.js @@ -60,24 +60,29 @@ LibraryJSEventLoop = { } else if (globalThis.addEventListener) { var __setImmediate_id_counter = 0; var __setImmediate_queue = []; - var __setImmediate_message_id = "_si"; + var __setImmediate_message_id = '_si'; /** @param {Event} e */ var __setImmediate_cb = (e) => { if (e.data === __setImmediate_message_id) { e.stopPropagation(); - __setImmediate_queue.shift()(); + __setImmediate_queue.shift()?.(); ++__setImmediate_id_counter; } } addEventListener("message", __setImmediate_cb, true); emSetImmediate = (func) => { - postMessage(__setImmediate_message_id, "*"); +#if PTHREADS + if (ENVIRONMENT_IS_WORKER) { + postMessage(__setImmediate_message_id); + } else +#endif + postMessage(__setImmediate_message_id, '*'); return __setImmediate_id_counter + __setImmediate_queue.push(func) - 1; } emClearImmediate = /**@type{function(number=)}*/((id) => { var index = id - __setImmediate_id_counter; // must preserve the order and count of elements in the queue, so replace the pending callback with an empty function - if (index >= 0 && index < __setImmediate_queue.length) __setImmediate_queue[index] = () => {}; + if (index >= 0 && index < __setImmediate_queue.length) __setImmediate_queue[index] = null; }) }`, $emSetImmediate: undefined, @@ -341,17 +346,26 @@ LibraryJSEventLoop = { assert(mode == {{{ cDefs.EM_TIMING_SETIMMEDIATE}}}); #endif if (!MainLoop.setImmediate) { - if (globalThis.setImmediate) { + if (globalThis.scheduler) { + // Some modern browsers implement scheduler.postTask, but not all. +#if RUNTIME_DEBUG + dbg('setImmediate: using scheduler.postTask'); +#endif + MainLoop.setImmediate = scheduler.postTask.bind(scheduler); +#if ENVIRONMENT_MAY_BE_NODE + } else if (globalThis.setImmediate) { MainLoop.setImmediate = setImmediate; +#endif } else { +#if RUNTIME_DEBUG + dbg('setImmediate: using polyfill'); +#endif // Emulate setImmediate. (note: not a complete polyfill, we don't emulate clearImmediate() to keep code size to minimum, since not needed) var setImmediates = []; var emscriptenMainLoopMessageId = 'setimmediate'; /** @param {Event} event */ var MainLoop_setImmediate_messageHandler = (event) => { - // When called in current thread or Worker, the main loop ID is structured slightly different to accommodate for --proxy-to-worker runtime listening to Worker events, - // so check for both cases. - if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) { + if (event.data === emscriptenMainLoopMessageId) { event.stopPropagation(); setImmediates.shift()(); } @@ -360,10 +374,12 @@ LibraryJSEventLoop = { MainLoop.setImmediate = /** @type{function(function(): ?, ...?): number} */((func) => { setImmediates.push(func); if (ENVIRONMENT_IS_WORKER) { - Module['setImmediates'] ??= []; - Module['setImmediates'].push(func); - postMessage({target: emscriptenMainLoopMessageId}); // In --proxy-to-worker, route the message via proxyClient.js - } else postMessage(emscriptenMainLoopMessageId, "*"); // On the main thread, can just send the message to itself. + // The postMessge API in a Worker, sends message to the main + // thread and does not support the `targetOrigin` (*) argument. + postMessage(emscriptenMainLoopMessageId); + } else { + postMessage(emscriptenMainLoopMessageId, '*'); + } }); } } @@ -395,7 +411,7 @@ LibraryJSEventLoop = { */`, $setMainLoop: (iterFunc, fps, simulateInfiniteLoop, arg, noSetTiming) => { #if ASSERTIONS - assert(!MainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.'); + assert(!MainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once') #endif MainLoop.func = iterFunc; MainLoop.arg = arg; diff --git a/src/lib/libexceptions.js b/src/lib/libexceptions.js index b04ebf7d3ad4e..de464f872e5e6 100644 --- a/src/lib/libexceptions.js +++ b/src/lib/libexceptions.js @@ -436,7 +436,7 @@ addCxaCatch = (n) => { args.push(`arg${i}`); sig += 'p'; } - const argString = args.join(','); + const argString = args.join(); LibraryManager.library[`__cxa_find_matching_catch_${n}__sig`] = sig; LibraryManager.library[`__cxa_find_matching_catch_${n}__deps`] = ['$findMatchingCatch']; LibraryManager.library[`__cxa_find_matching_catch_${n}`] = eval(`(${args}) => findMatchingCatch([${argString}])`); diff --git a/src/lib/libfetch.js b/src/lib/libfetch.js index 03dba8681e6be..4a1220879c374 100644 --- a/src/lib/libfetch.js +++ b/src/lib/libfetch.js @@ -8,7 +8,13 @@ var LibraryFetch = { $Fetch__postset: 'Fetch.init();', - $Fetch__deps: ['$HandleAllocator'], + $Fetch__deps: [ + '$HandleAllocator', +#if FETCH_SUPPORT_INDEXEDDB + '$addRunDependency', + '$removeRunDependency', +#endif + ], $Fetch: Fetch, _emscripten_fetch_get_response_headers_length__deps: ['$lengthBytesUTF8'], _emscripten_fetch_get_response_headers_length: fetchGetResponseHeadersLength, diff --git a/src/lib/libfs.js b/src/lib/libfs.js index d148c84f5a2d2..7c2aa82c2d04f 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -134,6 +134,15 @@ FS.staticInit();`; readMode = {{{ cDefs.S_IRUGO }}} | {{{ cDefs.S_IXUGO }}}; writeMode = {{{ cDefs.S_IWUGO }}}; mounted = null; +#if USE_CLOSURE_COMPILER + // Closure (@struct) requires these declared ahead of time. The readiness + // wait-queue is populated lazily, and only on nodes that derive real + // readiness (sockets, pipes, an epoll's own node). + /** @type {Set|null} */ + listeners = null; + /** @type {number} */ + exclTurn = 0; +#endif constructor(parent, name, mode, rdev) { if (!parent) { parent = this; // root node sets parent to itself @@ -164,6 +173,48 @@ FS.staticInit();`; get isDevice() { return FS.isChrdev(this.mode); } + // The per-inode readiness wait-queue. The node carries a Set of listener + // entries {cb}; producers (SOCKFS, PIPEFS) call notifyListeners on a + // readiness transition, and poll()/epoll consume it. It lives on the node + // (not the fd) so dup'd fds share one queue. Only nodes that derive real + // readiness (sockets, pipes, and an epoll's own node) ever use this - + // always-ready types (regular files, ttys) never register or notify. + addListener(cb, exclusive = false) { + var entry = {cb, exclusive}; + var listeners = (this.listeners ??= new Set()); + listeners.add(entry); + return {listeners, entry}; + } + notifyListeners(flags) { + // Iterates the set without copying, which is safe ONLY under a + // load-bearing contract that every internal listener must honour: + // 1. A listener must not run user code synchronously (a poll waiter only + // resolves a Promise; an epoll registration only re-lists + + // re-notifies; the epoll callback only schedules a tick). User code + // runs on a later tick, never inside this loop. + // 2. A listener may delete entries only from ITS OWN waiter, never from + // a sibling node's set that may be mid-iteration. (Deleting an entry + // of the set being iterated here is fine - a Set tolerates removal of + // a not-yet-visited entry mid-iteration; mutating a *different* node's + // set is fine because that set is not being iterated.) + // Violating either gives silently skipped wakeups that are near-impossible + // to reproduce. Any new producer/listener must preserve it. + if (!this.listeners) return; + // Fire every non-exclusive listener. Among EPOLLEXCLUSIVE registrations + // (one fd watched by several epolls) wake only one, rotating round-robin + // per node, to avoid a thundering herd. (Only epoll registrations are ever + // exclusive; poll waiters and a node's own consumers are not.) + var excl; + for (var entry of this.listeners) { + if (entry.exclusive) (excl ||= []).push(entry); + else entry.cb(flags); + } + if (excl) { + var i = (this.exclTurn || 0) % excl.length; + this.exclTurn = i + 1; + excl[i].cb(flags); + } + } }, // @@ -179,8 +230,8 @@ FS.staticInit();`; path = FS.cwd() + '/' + path; } - // limit max consecutive symlinks to 40 (SYMLOOP_MAX). - linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // limit max consecutive symlinks to SYMLOOP_MAX. + linkloop: for (var nlinks = 0; nlinks < {{{ cDefs.SYMLOOP_MAX }}}; nlinks++) { // split the absolute path var parts = path.split('/').filter((p) => !!p); @@ -501,7 +552,14 @@ FS.staticInit();`; var arg = setattr ? stream : node; setattr ??= node.node_ops.setattr; FS.checkOpExists(setattr, {{{ cDefs.EPERM }}}) - setattr(arg, attr); + try { + setattr(arg, attr); + } catch (e) { + if (e instanceof RangeError) { + throw new FS.ErrnoError({{{ cDefs.EFBIG }}}); + } + throw e; + } }, // @@ -1182,6 +1240,11 @@ FS.staticInit();`; throw new FS.ErrnoError({{{ cDefs.EBADF }}}); } if (stream.getdents) stream.getdents = null; // free readdir state + // The fd is going away: wake anything waiting on it (poll/epoll) with + // POLLNVAL so a blocking wait unblocks and an epoll registration is evicted + // on its next derive. Only sockets/pipes/epoll ever carry a wait-queue, so + // for every other stream (incl. nodeless noderawfs stdio) this is a no-op. + stream.node?.notifyListeners({{{ cDefs.POLLNVAL }}}); try { if (stream.stream_ops.close) { stream.stream_ops.close(stream); @@ -1335,8 +1398,8 @@ FS.staticInit();`; return stream.stream_ops.ioctl(stream, cmd, arg); }, readFile(path, opts = {}) { - opts.flags = opts.flags || {{{ cDefs.O_RDONLY }}}; - opts.encoding = opts.encoding || 'binary'; + opts.flags = opts.flags ?? {{{ cDefs.O_RDONLY }}}; + opts.encoding = opts.encoding ?? 'binary'; if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { abort(`Invalid encoding type "${opts.encoding}"`); } @@ -1355,7 +1418,7 @@ FS.staticInit();`; * @param {TypedArray|Array|string} data */ writeFile(path, data, opts = {}) { - opts.flags = opts.flags || {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}}; + opts.flags = opts.flags ?? {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}}; var stream = FS.open(path, opts.flags, opts.mode); data = FS_fileDataToTypedArray(data); FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); @@ -1748,8 +1811,8 @@ FS.staticInit();`; // Function to get a range from the remote URL. var doXHR = (from, to) => { - if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength-1) abort("only " + datalength + " bytes available! programmer error!"); + if (from > to) abort(`invalid range (${from}, ${to}) or no bytes requested!`); + if (to > datalength-1) abort(`only ${datalength} bytes available! programmer error!`); // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. var xhr = new XMLHttpRequest(); @@ -1767,7 +1830,7 @@ FS.staticInit();`; if (xhr.response !== undefined) { return new Uint8Array(/** @type{Array} */(xhr.response || [])); } - return intArrayFromString(xhr.responseText || '', true); + return intArrayFromString(xhr.responseText ?? '', true); }; var lazyArray = this; lazyArray.setDataGetter((chunkNum) => { diff --git a/src/lib/libglemu.js b/src/lib/libglemu.js index 256221838c13f..72cbff0340776 100644 --- a/src/lib/libglemu.js +++ b/src/lib/libglemu.js @@ -857,7 +857,7 @@ var LibraryGLEmulation = { #if !FULL_ES2 $GLImmediate__postset: 'GLImmediate.setupFuncs(); Browser.moduleContextCreatedCallbacks.push(() => GLImmediate.init());', #endif - $GLImmediate__deps: ['$Browser', '$GL', '$GLEmulation'], + $GLImmediate__deps: ['$Browser', '$GL', '$GLEmulation', '$webglBufferSubData'], $GLImmediate: { MapTreeLib: null, spawnMapTreeLib: () => { @@ -2552,7 +2552,7 @@ var LibraryGLEmulation = { GLImmediate.lastArrayBuffer = arrayBuffer; } - GLctx.bufferSubData(GLctx.ARRAY_BUFFER, start, GLImmediate.vertexData.subarray(start >> 2, end >> 2)); + webglBufferSubData(GLctx.ARRAY_BUFFER, start, (end - start) >> 2, start >> 2, GLImmediate.vertexData); } #if GL_UNSAFE_OPTS if (canSkip) return; @@ -2848,7 +2848,8 @@ var LibraryGLEmulation = { // User can override the maximum number of texture units that we emulate. Using fewer texture units increases runtime performance // slightly, so it is advantageous to choose as small value as needed. // Limit to a maximum of 28 to not overflow the state bits used for renderer caching (31 bits = 3 attributes + 28 texture units). - GLImmediate.MAX_TEXTURES = Math.min(Module['GL_MAX_TEXTURE_IMAGE_UNITS'] || GLctx.getParameter(GLctx.MAX_TEXTURE_IMAGE_UNITS), 28); + var maxTextureUnits = {{{ makeModuleReceiveExpr('GL_MAX_TEXTURE_IMAGE_UNITS', 'GLctx.getParameter(GLctx.MAX_TEXTURE_IMAGE_UNITS)') }}}; + GLImmediate.MAX_TEXTURES = Math.min(maxTextureUnits, 28); GLImmediate.TexEnvJIT.init(GLctx, GLImmediate.MAX_TEXTURES); @@ -3046,12 +3047,13 @@ var LibraryGLEmulation = { } if (!GLctx.currentElementArrayBufferBinding) { // If no element array buffer is bound, then indices is a literal pointer to clientside data + var byteSize = numProvidedIndexes << 1; #if ASSERTIONS - assert(numProvidedIndexes << 1 <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (a)'); + assert(byteSize <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (a)'); #endif - var indexBuffer = GL.getTempIndexBuffer(numProvidedIndexes << 1); + var indexBuffer = GL.getTempIndexBuffer(byteSize); GLctx.bindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, indexBuffer); - GLctx.bufferSubData(GLctx.ELEMENT_ARRAY_BUFFER, 0, {{{ makeHEAPView('U16', 'ptr', 'ptr + (numProvidedIndexes << 1)') }}}); + webglBufferSubData(GLctx.ELEMENT_ARRAY_BUFFER, 0, byteSize, ptr); ptr = 0; emulatedElementArrayBuffer = true; } diff --git a/src/lib/libglfw.js b/src/lib/libglfw.js index efc5e96bf7371..95b70ce867426 100644 --- a/src/lib/libglfw.js +++ b/src/lib/libglfw.js @@ -1243,16 +1243,18 @@ var LibraryGLFW = { Browser.updateResizeListeners(); } } +#if expectToReceiveOnModule('onFullScreen') Module['onFullScreen']?.(Browser.isFullscreen); Module['onFullscreen']?.(Browser.isFullscreen); +#endif } if (!Browser.fullscreenHandlersInstalled) { Browser.fullscreenHandlersInstalled = true; - document.addEventListener('fullscreenchange', fullscreenChange, false); - document.addEventListener('mozfullscreenchange', fullscreenChange, false); - document.addEventListener('webkitfullscreenchange', fullscreenChange, false); - document.addEventListener('MSFullscreenChange', fullscreenChange, false); + document.addEventListener('fullscreenchange', fullscreenChange); + document.addEventListener('mozfullscreenchange', fullscreenChange); + document.addEventListener('webkitfullscreenchange', fullscreenChange); + document.addEventListener('MSFullscreenChange', fullscreenChange); } // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root @@ -1283,6 +1285,7 @@ var LibraryGLFW = { } var w = wNative; var h = hNative; +#if expectToReceiveOnModule('forcedAspectRatio') if (Module['forcedAspectRatio'] && Module['forcedAspectRatio'] > 0) { if (w/h < Module['forcedAspectRatio']) { w = Math.round(h * Module['forcedAspectRatio']); @@ -1290,6 +1293,7 @@ var LibraryGLFW = { h = Math.round(w / Module['forcedAspectRatio']); } } +#endif if ((getFullscreenElement() === canvas.parentNode) && (typeof screen != 'undefined')) { var factor = Math.min(screen.width / w, screen.height / h); w = Math.round(w * factor); @@ -1320,18 +1324,8 @@ var LibraryGLFW = { // in the coordinates. const rect = Browser.getCanvas().getBoundingClientRect(); - // Neither .scrollX or .pageXOffset are defined in a spec, but - // we prefer .scrollX because it is currently in a spec draft. - // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) - var scrollX = ((typeof window.scrollX != 'undefined') ? window.scrollX : window.pageXOffset); - var scrollY = ((typeof window.scrollY != 'undefined') ? window.scrollY : window.pageYOffset); -#if ASSERTIONS - // If this assert lands, it's likely because the browser doesn't support scrollX or pageXOffset - // and we have no viable fallback. - assert((typeof scrollX != 'undefined') && (typeof scrollY != 'undefined'), 'Unable to retrieve scroll position, mouse positions likely broken.'); -#endif - var adjustedX = pageX - (scrollX + rect.left); - var adjustedY = pageY - (scrollY + rect.top); + var adjustedX = pageX - (window.scrollX + rect.left); + var adjustedY = pageY - (window.scrollY + rect.top); // getBoundingClientRect() returns dimension affected by CSS, so as a result: // - when CSS scaling is enabled, this will fix the mouse coordinates to match the width/height of the window diff --git a/src/lib/libhtml5.js b/src/lib/libhtml5.js index f8088f2c13b06..79f0a33df37df 100644 --- a/src/lib/libhtml5.js +++ b/src/lib/libhtml5.js @@ -111,16 +111,21 @@ var LibraryHTML5 = { }, canPerformEventHandlerRequests() { + // Browsers that support navigator.userActivation.isActive: https://developer.mozilla.org/en-US/docs/Web/API/UserActivation/isActive +#if MIN_CHROME_VERSION < 72 || MIN_FIREFOX_VERSION < 120 || MIN_SAFARI_VERSION < 160400 if (navigator.userActivation) { // Verify against transient activation status from UserActivation API // whether it is possible to perform a request here without needing to defer. See // https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation // and https://caniuse.com/mdn-api_useractivation - // At the time of writing, Firefox does not support this API: https://bugzil.la/1791079 return navigator.userActivation.isActive; } return JSEvents.inEventHandler && JSEvents.currentEventHandler.allowsDeferredCalls; +#else + // We are targeting modern browsers where navigator.userActivation.isActive is unconditionally supported. + return navigator.userActivation.isActive; +#endif }, runDeferredCalls() { @@ -240,10 +245,9 @@ var LibraryHTML5 = { #endif getNodeNameForTarget(target) { - if (!target) return ''; if (target == window) return '#window'; if (target == screen) return '#screen'; - return target?.nodeName || ''; + return target?.nodeName ?? ''; }, fullscreenEnabled() { @@ -292,10 +296,10 @@ var LibraryHTML5 = { HEAP32[idx + {{{ C_STRUCTS.EmscriptenKeyboardEvent.charCode / 4 }}}] = e.charCode; HEAP32[idx + {{{ C_STRUCTS.EmscriptenKeyboardEvent.keyCode / 4 }}}] = e.keyCode; HEAP32[idx + {{{ C_STRUCTS.EmscriptenKeyboardEvent.which / 4 }}}] = e.which; - stringToUTF8(e.key || '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.key }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); - stringToUTF8(e.code || '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.code }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); - stringToUTF8(e.char || '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.charValue }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); - stringToUTF8(e.locale || '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.locale }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); + stringToUTF8(e.key ?? '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.key }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); + stringToUTF8(e.code ?? '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.code }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); + stringToUTF8(e.char ?? '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.charValue }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); + stringToUTF8(e.locale ?? '', keyEventData + {{{ C_STRUCTS.EmscriptenKeyboardEvent.locale }}}, {{{ cDefs.EM_HTML5_SHORT_STRING_LEN_BYTES }}}); #if PTHREADS if (targetThread) __emscripten_run_callback_on_thread(targetThread, callbackfunc, eventTypeId, keyEventData, eventSize, userData); @@ -579,6 +583,11 @@ var LibraryHTML5 = { emscripten_set_mouseout_callback_on_thread: (target, userData, useCapture, callbackfunc, targetThread) => registerMouseEventCallback(target, userData, useCapture, callbackfunc, {{{ cDefs.EMSCRIPTEN_EVENT_MOUSEOUT }}}, "mouseout", targetThread), + emscripten_set_contextmenu_callback_on_thread__proxy: 'sync', + emscripten_set_contextmenu_callback_on_thread__deps: ['$registerMouseEventCallback'], + emscripten_set_contextmenu_callback_on_thread: (target, userData, useCapture, callbackfunc, targetThread) => + registerMouseEventCallback(target, userData, useCapture, callbackfunc, {{{ cDefs.EMSCRIPTEN_EVENT_CONTEXTMENU }}}, "contextmenu", targetThread), + // HTML5 does not really have a polling API for mouse events, so implement one // manually by returning the data from the most recently received event. This // requires that user has registered at least some no-op function as an event @@ -722,7 +731,7 @@ var LibraryHTML5 = { var focusEventHandlerFunc = (e) => { var nodeName = JSEvents.getNodeNameForTarget(e.target); - var id = e.target.id ? e.target.id : ''; + var id = e.target.id ?? ''; var focusEvent = JSEvents.focusEvent; stringToUTF8(nodeName, focusEvent + {{{ C_STRUCTS.EmscriptenFocusEvent.nodeName }}}, {{{ cDefs.EM_HTML5_LONG_STRING_LEN_BYTES }}}); @@ -822,26 +831,23 @@ var LibraryHTML5 = { _emscripten_get_last_deviceorientation_event: () => JSEvents.deviceOrientationEvent, $fillDeviceMotionEventData: (eventStruct, e, target) => { + var a = e.acceleration; + var ag = e.accelerationIncludingGravity; + var rr = e.rotationRate; var supportedFields = 0; - var a = e['acceleration']; supportedFields |= a && {{{ cDefs.EMSCRIPTEN_DEVICE_MOTION_EVENT_SUPPORTS_ACCELERATION }}}; - var ag = e['accelerationIncludingGravity']; supportedFields |= ag && {{{ cDefs.EMSCRIPTEN_DEVICE_MOTION_EVENT_SUPPORTS_ACCELERATION_INCLUDING_GRAVITY }}}; - var rr = e['rotationRate']; supportedFields |= rr && {{{ cDefs.EMSCRIPTEN_DEVICE_MOTION_EVENT_SUPPORTS_ROTATION_RATE }}}; - a = a || {}; - ag = ag || {}; - rr = rr || {}; {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.supportedFields, 'supportedFields', 'i32') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationX, 'a["x"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationY, 'a["y"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationZ, 'a["z"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityX, 'ag["x"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityY, 'ag["y"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityZ, 'ag["z"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateAlpha, 'rr["alpha"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateBeta, 'rr["beta"]', 'double') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateGamma, 'rr["gamma"]', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationX, 'a?.x', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationY, 'a?.y', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationZ, 'a?.z', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityX, 'ag?.x', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityY, 'ag?.y', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.accelerationIncludingGravityZ, 'ag?.z', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateAlpha, 'rr?.alpha', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateBeta, 'rr?.beta', 'double') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenDeviceMotionEvent.rotationRateGamma, 'rr?.gamma', 'double') }}}; }, $registerDeviceMotionEventCallback__noleakcheck: true, @@ -1023,11 +1029,11 @@ var LibraryHTML5 = { // If transitioning to windowed mode, report info about the element that just was fullscreen. var reportedElement = isFullscreen ? fullscreenElement : JSEvents.previousFullscreenElement; var nodeName = JSEvents.getNodeNameForTarget(reportedElement); - var id = reportedElement?.id || ''; + var id = reportedElement?.id ?? ''; stringToUTF8(nodeName, eventStruct + {{{ C_STRUCTS.EmscriptenFullscreenChangeEvent.nodeName }}}, {{{ cDefs.EM_HTML5_LONG_STRING_LEN_BYTES }}}); stringToUTF8(id, eventStruct + {{{ C_STRUCTS.EmscriptenFullscreenChangeEvent.id }}}, {{{ cDefs.EM_HTML5_LONG_STRING_LEN_BYTES }}}); - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.elementWidth, 'reportedElement ? reportedElement.clientWidth : 0', 'i32') }}}; - {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.elementHeight, 'reportedElement ? reportedElement.clientHeight : 0', 'i32') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.elementWidth, 'reportedElement?.clientWidth ?? 0', 'i32') }}}; + {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.elementHeight, 'reportedElement?.clientHeight ?? 0', 'i32') }}}; {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.screenWidth, 'screen.width', 'i32') }}}; {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenFullscreenChangeEvent.screenHeight, 'screen.height', 'i32') }}}; if (isFullscreen) { @@ -1099,7 +1105,20 @@ var LibraryHTML5 = { return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; }, - $JSEvents_requestFullscreen__deps: ['$JSEvents', '$JSEvents_resizeCanvasForFullscreen'], +#if PTHREADS + $callCanvasResizedCallback__deps: ['_emscripten_run_callback_on_thread'], +#endif + $callCanvasResizedCallback: (strategy) => { + if (strategy.canvasResizedCallback) { +#if PTHREADS + if (strategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(strategy.canvasResizedCallbackTargetThread, strategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, 0, strategy.canvasResizedCallbackUserData); + else +#endif + {{{ makeDynCall('iipp', 'strategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); + } + }, + + $JSEvents_requestFullscreen__deps: ['$JSEvents', '$JSEvents_resizeCanvasForFullscreen', '$callCanvasResizedCallback'], $JSEvents_requestFullscreen: (target, strategy) => { // EMSCRIPTEN_FULLSCREEN_SCALE_DEFAULT + EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_NONE is a mode where no extra logic is performed to the DOM elements. if (strategy.scaleMode != {{{ cDefs.EMSCRIPTEN_FULLSCREEN_SCALE_DEFAULT }}} || strategy.canvasResolutionScaleMode != {{{ cDefs.EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_NONE }}}) { @@ -1117,15 +1136,7 @@ var LibraryHTML5 = { } currentFullscreenStrategy = strategy; - - if (strategy.canvasResizedCallback) { -#if PTHREADS - if (strategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(strategy.canvasResizedCallbackTargetThread, strategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - else -#endif - {{{ makeDynCall('iipp', 'strategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - } - + callCanvasResizedCallback(strategy); return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; }, @@ -1187,7 +1198,7 @@ var LibraryHTML5 = { return restoreOldStyle; }, - $registerRestoreOldStyle__deps: ['$getCanvasElementSize', '$setCanvasElementSize', '$currentFullscreenStrategy'], + $registerRestoreOldStyle__deps: ['$getCanvasElementSize', '$setCanvasElementSize', '$currentFullscreenStrategy', '$callCanvasResizedCallback'], $registerRestoreOldStyle: (canvas) => { var canvasSize = getCanvasElementSize(canvas); var oldWidth = canvasSize[0]; @@ -1243,13 +1254,7 @@ var LibraryHTML5 = { canvas.style.imageRendering = oldImageRendering; if (canvas.GLctxObject) canvas.GLctxObject.GLctx.viewport(0, 0, oldWidth, oldHeight); - if (currentFullscreenStrategy.canvasResizedCallback) { -#if PTHREADS - if (currentFullscreenStrategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(currentFullscreenStrategy.canvasResizedCallbackTargetThread, currentFullscreenStrategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, currentFullscreenStrategy.canvasResizedCallbackUserData); - else -#endif - {{{ makeDynCall('iipp', 'currentFullscreenStrategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, currentFullscreenStrategy.canvasResizedCallbackUserData); - } + callCanvasResizedCallback(currentFullscreenStrategy); } } document.addEventListener('fullscreenchange', restoreOldStyle); @@ -1295,10 +1300,10 @@ var LibraryHTML5 = { element.style.paddingTop = element.style.paddingBottom = topBottom + 'px'; }, - $currentFullscreenStrategy: {}, + $currentFullscreenStrategy: 0, $restoreOldWindowedStyle: null, - $softFullscreenResizeWebGLRenderTarget__deps: ['$setLetterbox', '$currentFullscreenStrategy', '$getCanvasElementSize', '$setCanvasElementSize', '$jstoi_q'], + $softFullscreenResizeWebGLRenderTarget__deps: ['$setLetterbox', '$currentFullscreenStrategy', '$getCanvasElementSize', '$setCanvasElementSize', '$jstoi_q', '$callCanvasResizedCallback'], $softFullscreenResizeWebGLRenderTarget: () => { var dpr = devicePixelRatio; var inHiDPIFullscreenMode = currentFullscreenStrategy.canvasResolutionScaleMode == {{{ cDefs.EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF }}}; @@ -1348,12 +1353,8 @@ var LibraryHTML5 = { setLetterbox(canvas, topMargin, b); } - if (!inCenteredWithoutScalingFullscreenMode && currentFullscreenStrategy.canvasResizedCallback) { -#if PTHREADS - if (currentFullscreenStrategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(currentFullscreenStrategy.canvasResizedCallbackTargetThread, currentFullscreenStrategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, currentFullscreenStrategy.canvasResizedCallbackUserData); - else -#endif - {{{ makeDynCall('iipp', 'currentFullscreenStrategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, currentFullscreenStrategy.canvasResizedCallbackUserData); + if (!inCenteredWithoutScalingFullscreenMode) { + callCanvasResizedCallback(currentFullscreenStrategy); } }, @@ -1426,7 +1427,7 @@ var LibraryHTML5 = { return doRequestFullscreen(target, strategy); }, - emscripten_enter_soft_fullscreen__deps: ['$JSEvents', '$hideEverythingExceptGivenElement', '$restoreOldWindowedStyle', '$restoreHiddenElements', '$currentFullscreenStrategy', '$softFullscreenResizeWebGLRenderTarget', '$JSEvents_resizeCanvasForFullscreen', '$findEventTarget'], + emscripten_enter_soft_fullscreen__deps: ['$JSEvents', '$hideEverythingExceptGivenElement', '$restoreOldWindowedStyle', '$restoreHiddenElements', '$currentFullscreenStrategy', '$softFullscreenResizeWebGLRenderTarget', '$JSEvents_resizeCanvasForFullscreen', '$findEventTarget', '$callCanvasResizedCallback'], emscripten_enter_soft_fullscreen__proxy: 'sync', emscripten_enter_soft_fullscreen: (target, fullscreenStrategy) => { #if !DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR @@ -1436,16 +1437,16 @@ var LibraryHTML5 = { if (!target) return {{{ cDefs.EMSCRIPTEN_RESULT_UNKNOWN_TARGET }}}; var strategy = { - scaleMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.scaleMode, 'i32') }}}, - canvasResolutionScaleMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResolutionScaleMode, 'i32') }}}, - filteringMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.filteringMode, 'i32') }}}, - canvasResizedCallback: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResizedCallback, 'i32') }}}, - canvasResizedCallbackUserData: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResizedCallbackUserData, 'i32') }}}, + scaleMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.scaleMode, 'i32') }}}, + canvasResolutionScaleMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResolutionScaleMode, 'i32') }}}, + filteringMode: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.filteringMode, 'i32') }}}, + canvasResizedCallback: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResizedCallback, 'i32') }}}, + canvasResizedCallbackUserData: {{{ makeGetValue('fullscreenStrategy', C_STRUCTS.EmscriptenFullscreenStrategy.canvasResizedCallbackUserData, 'i32') }}}, #if PTHREADS - canvasResizedCallbackTargetThread: JSEvents.getTargetThreadForEventCallback(), + canvasResizedCallbackTargetThread: JSEvents.getTargetThreadForEventCallback(), #endif - target, - softFullscreen: true + target, + softFullscreen: true }; var restoreOldStyle = JSEvents_resizeCanvasForFullscreen(target, strategy); @@ -1460,13 +1461,7 @@ var LibraryHTML5 = { restoreOldStyle(); restoreHiddenElements(hiddenElements); removeEventListener('resize', softFullscreenResizeWebGLRenderTarget); - if (strategy.canvasResizedCallback) { -#if PTHREADS - if (strategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(strategy.canvasResizedCallbackTargetThread, strategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - else -#endif - {{{ makeDynCall('iipp', 'strategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - } + callCanvasResizedCallback(strategy); currentFullscreenStrategy = 0; } restoreOldWindowedStyle = restoreWindowedState; @@ -1474,14 +1469,7 @@ var LibraryHTML5 = { addEventListener('resize', softFullscreenResizeWebGLRenderTarget); // Inform the caller that the canvas size has changed. - if (strategy.canvasResizedCallback) { -#if PTHREADS - if (strategy.canvasResizedCallbackTargetThread) __emscripten_run_callback_on_thread(strategy.canvasResizedCallbackTargetThread, strategy.canvasResizedCallback, {{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - else -#endif - {{{ makeDynCall('iipp', 'strategy.canvasResizedCallback') }}}({{{ cDefs.EMSCRIPTEN_EVENT_CANVASRESIZED }}}, 0, strategy.canvasResizedCallbackUserData); - } - + callCanvasResizedCallback(strategy); return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; }, @@ -1531,7 +1519,7 @@ var LibraryHTML5 = { /** @suppress{checkTypes} */ {{{ makeSetValue('eventStruct', C_STRUCTS.EmscriptenPointerlockChangeEvent.isActive, 'isPointerlocked', 'i8') }}}; var nodeName = JSEvents.getNodeNameForTarget(pointerLockElement); - var id = pointerLockElement?.id || ''; + var id = pointerLockElement?.id ?? ''; stringToUTF8(nodeName, eventStruct + {{{ C_STRUCTS.EmscriptenPointerlockChangeEvent.nodeName }}}, {{{ cDefs.EM_HTML5_LONG_STRING_LEN_BYTES }}}); stringToUTF8(id, eventStruct + {{{ C_STRUCTS.EmscriptenPointerlockChangeEvent.id }}}, {{{ cDefs.EM_HTML5_LONG_STRING_LEN_BYTES }}}); }, @@ -1596,7 +1584,7 @@ var LibraryHTML5 = { var pointerlockErrorEventHandlerFunc = (e) => { #if PTHREADS - if (targetThread) __emscripten_run_callback_on_thread(targetThread, callbackfunc, eventTypeId, 0, userData); + if (targetThread) __emscripten_run_callback_on_thread(targetThread, callbackfunc, eventTypeId, 0, 0, userData); else #endif if ({{{ makeDynCall('iipp', 'callbackfunc') }}}(eventTypeId, 0, userData)) e.preventDefault(); @@ -2002,7 +1990,7 @@ var LibraryHTML5 = { emscripten_get_num_gamepads__deps: ['$JSEvents'], emscripten_get_num_gamepads: () => { #if ASSERTIONS - assert(JSEvents.lastGamepadState, 'emscripten_get_num_gamepads() can only be called after having first called emscripten_sample_gamepad_data() and that function has returned EMSCRIPTEN_RESULT_SUCCESS!'); + assert(JSEvents.lastGamepadState, 'emscripten_get_num_gamepads() called before emscripten_sample_gamepad_data()'); #endif // N.B. Do not call emscripten_get_num_gamepads() unless having first called emscripten_sample_gamepad_data(), and that has returned EMSCRIPTEN_RESULT_SUCCESS. // Otherwise the following line will throw an exception. @@ -2013,7 +2001,7 @@ var LibraryHTML5 = { emscripten_get_gamepad_status__deps: ['$JSEvents', '$fillGamepadEventData'], emscripten_get_gamepad_status: (index, gamepadState) => { #if ASSERTIONS - assert(JSEvents.lastGamepadState, 'emscripten_get_gamepad_status() can only be called after having first called emscripten_sample_gamepad_data() and that function has returned EMSCRIPTEN_RESULT_SUCCESS!'); + assert(JSEvents.lastGamepadState, 'emscripten_get_gamepad_status() called before emscripten_sample_gamepad_data()'); #endif // INVALID_PARAM is returned on a Gamepad index that never was there. if (index < 0 || index >= JSEvents.lastGamepadState.length) return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}}; diff --git a/src/lib/libhtml5_webgl.js b/src/lib/libhtml5_webgl.js index 8db08e6023078..f174eea714b63 100644 --- a/src/lib/libhtml5_webgl.js +++ b/src/lib/libhtml5_webgl.js @@ -82,6 +82,7 @@ var LibraryHtml5WebGL = { 'preserveDrawingBuffer': !!HEAP8[attributes + {{{ C_STRUCTS.EmscriptenWebGLContextAttributes.preserveDrawingBuffer }}}], 'powerPreference': webglPowerPreferences[powerPreference], 'failIfMajorPerformanceCaveat': !!HEAP8[attributes + {{{ C_STRUCTS.EmscriptenWebGLContextAttributes.failIfMajorPerformanceCaveat }}}], + 'desynchronized': !!HEAP8[attributes + {{{ C_STRUCTS.EmscriptenWebGLContextAttributes.desynchronized }}}], // The following are not predefined WebGL context attributes in the WebGL specification, so the property names can be minified by Closure. majorVersion: HEAP32[attr32 + ({{{ C_STRUCTS.EmscriptenWebGLContextAttributes.majorVersion }}}>>2)], minorVersion: HEAP32[attr32 + ({{{ C_STRUCTS.EmscriptenWebGLContextAttributes.minorVersion }}}>>2)], @@ -318,6 +319,7 @@ var LibraryHtml5WebGL = { var power = t['powerPreference'] && webglPowerPreferences.indexOf(t['powerPreference']); {{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.powerPreference, 'power', 'i32') }}}; {{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.failIfMajorPerformanceCaveat, 't.failIfMajorPerformanceCaveat', 'i8') }}}; + {{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.desynchronized, 't.desynchronized', 'i8') }}}; {{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.majorVersion, 'c.version', 'i32') }}}; {{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.minorVersion, 0, 'i32') }}}; #if GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS @@ -431,7 +433,7 @@ var LibraryHtml5WebGL = { var webGlEventHandlerFunc = (e) => { #if PTHREADS - if (targetThread) __emscripten_run_callback_on_thread(targetThread, callbackfunc, eventTypeId, 0, userData); + if (targetThread) __emscripten_run_callback_on_thread(targetThread, callbackfunc, eventTypeId, 0, 0, userData); else #endif if ({{{ makeDynCall('iiii', 'callbackfunc') }}}(eventTypeId, 0, userData)) e.preventDefault(); @@ -514,14 +516,14 @@ var LibraryHtml5WebGL = { writeGLArray(GLctx.getVertexAttrib(index, param), dst, dstLength, dstType), emscripten_webgl_get_uniform_d__proxy: 'sync_on_current_webgl_context_thread', - emscripten_webgl_get_uniform_d__deps: ['$webglGetUniformLocation'], + emscripten_webgl_get_uniform_d__deps: ['$webglGetProgramUniformLocation'], emscripten_webgl_get_uniform_d: (program, location) => - GLctx.getUniform(GL.programs[program], webglGetUniformLocation(location)), + GLctx.getUniform(GL.programs[program], webglGetProgramUniformLocation(GL.programs[program], location)), emscripten_webgl_get_uniform_v__proxy: 'sync_on_current_webgl_context_thread', - emscripten_webgl_get_uniform_v__deps: ['$writeGLArray', '$webglGetUniformLocation'], + emscripten_webgl_get_uniform_v__deps: ['$writeGLArray', '$webglGetProgramUniformLocation'], emscripten_webgl_get_uniform_v: (program, location, dst, dstLength, dstType) => - writeGLArray(GLctx.getUniform(GL.programs[program], webglGetUniformLocation(location)), dst, dstLength, dstType), + writeGLArray(GLctx.getUniform(GL.programs[program], webglGetProgramUniformLocation(GL.programs[program], location)), dst, dstLength, dstType), emscripten_webgl_get_parameter_v__proxy: 'sync_on_current_webgl_context_thread', emscripten_webgl_get_parameter_v__deps: ['$writeGLArray'], @@ -596,7 +598,7 @@ function handleWebGLProxying(funcs) { funcs[i + '__deps'].push(i + '_main_thread'); delete funcs[i + '__proxy']; const funcArgs = listOfNFunctionArgs(funcs[i]); - const funcArgsString = funcArgs.join(','); + const funcArgsString = funcArgs.join(); const retStatement = sig[0] != 'v' ? 'return' : ''; const contextCheck = proxyContextHandle ? 'GL.contexts[p0]' : 'GLctx'; var funcBody = `${retStatement} ${contextCheck} ? _${i}_calling_thread(${funcArgsString}) : _${i}_main_thread(${funcArgsString});`; diff --git a/src/lib/libidbfs.js b/src/lib/libidbfs.js index 036e52fd9126c..d4ecdbccf928b 100644 --- a/src/lib/libidbfs.js +++ b/src/lib/libidbfs.js @@ -6,10 +6,7 @@ addToLibrary({ $IDBFS__deps: ['$FS', '$MEMFS', '$PATH'], - $IDBFS__postset: () => { - addAtExit('IDBFS.quit();'); - return ''; - }, + $IDBFS__postset: () => addAtExit('IDBFS.quit();'), $IDBFS: { dbs: {}, indexedDB: () => { @@ -21,14 +18,31 @@ addToLibrary({ DB_VERSION: 21, DB_STORE_NAME: 'FILE_DATA', + // When using the autopersistence mechanism, users can set + // IDBFS.onAutoPersistStateChanged callback to receive notification events + // for when persistence operations are in-flight. Use the following syntax: + /* + IDBFS.onAutoPersistStateChanged = autoPersistActive => { + if (autoPersistActive) { + console.log('IDBFS persistence operation has started.'); + } else { + console.log('IDBFS persistence operation has finished.'); + } + }; + */ + // Queues a new VFS -> IDBFS synchronization operation queuePersist: (mount) => { function onPersistComplete() { if (mount.idbPersistState === 'again') startPersist(); // If a new sync request has appeared in between, kick off a new sync - else mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later + else { + mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later + IDBFS.onAutoPersistStateChanged?.(false); + } } function startPersist() { mount.idbPersistState = 'idb'; // Mark that we are currently running a sync operation + IDBFS.onAutoPersistStateChanged?.(true); IDBFS.syncfs(mount, /*populate:*/false, onPersistComplete); } diff --git a/src/lib/liblegacy.js b/src/lib/liblegacy.js index 7077bf8a8d0af..f13a131512587 100644 --- a/src/lib/liblegacy.js +++ b/src/lib/liblegacy.js @@ -121,7 +121,9 @@ legacyFuncs = { $stackTrace__deps: ['$jsStackTrace'], $stackTrace: () => { var js = jsStackTrace(); +#if expectToReceiveOnModule('extraStackTrace') if (Module['extraStackTrace']) js += '\n' + Module['extraStackTrace'](); +#endif return js; }, diff --git a/src/lib/liblittle_endian_heap.js b/src/lib/liblittle_endian_heap.js index 8a2ba67c8af2a..6d0328c5a2e30 100644 --- a/src/lib/liblittle_endian_heap.js +++ b/src/lib/liblittle_endian_heap.js @@ -94,6 +94,7 @@ function LE_HEAP_UPDATE() { const res = order(Atomics.load(heap, offset)); return heap.unsigned ? heap.unsigned(res) : res; }, + $LE_ATOMICS_NOTIFY__docs: '/**@param {number=} count*/', $LE_ATOMICS_NOTIFY: (heap, offset, count) => Atomics.notify(heap, offset, count), $LE_ATOMICS_OR: (heap, offset, value) => { const order = LE_ATOMICS_NATIVE_BYTE_ORDER[heap.BYTES_PER_ELEMENT - 1]; diff --git a/src/lib/liblz4.js b/src/lib/liblz4.js index 8e4a85dc6748b..877b6c6e6bc73 100644 --- a/src/lib/liblz4.js +++ b/src/lib/liblz4.js @@ -6,7 +6,7 @@ #if LZ4 addToLibrary({ - $LZ4__deps: ['$FS', '$preloadPlugins', '$getUniqueRunDependency', '$addRunDependency', '$removeRunDependency'], + $LZ4__deps: ['$FS', '$preloadPlugins'], $LZ4: { DIR_MODE: {{{ cDefs.S_IFDIR | 0o777 }}}, FILE_MODE: {{{ cDefs.S_IFREG | 0o777 }}}, @@ -20,7 +20,7 @@ addToLibrary({ })(); LZ4.CHUNK_SIZE = LZ4.codec.CHUNK_SIZE; }, - loadPackage(pack, preloadPlugin) { + async loadPackage(pack, preloadPlugin) { LZ4.init(); var compressedData = pack['compressedData'] || LZ4.codec.compressPackage(pack['data']); assert(compressedData['cachedIndexes'].length === compressedData['cachedChunks'].length); @@ -52,14 +52,11 @@ addToLibrary({ var fullname = file.filename; for (var plugin of preloadPlugins) { if (plugin['canHandle'](fullname)) { - var dep = getUniqueRunDependency('fp ' + fullname); - addRunDependency(dep); - var finish = () => removeRunDependency(dep); var byteArray = FS.readFile(fullname); #if ASSERTIONS assert(plugin['handle'].constructor.name === 'AsyncFunction', 'Filesystem plugin handlers must be async functions (See #24914)') #endif - plugin['handle'](byteArray, fullname).then(finish).catch(finish); + await plugin['handle'](byteArray, fullname); break; } } diff --git a/src/lib/libopenal.js b/src/lib/libopenal.js index 6142a67b41a62..41e0106d97979 100644 --- a/src/lib/libopenal.js +++ b/src/lib/libopenal.js @@ -253,8 +253,8 @@ var LibraryOpenAL = { src.bufOffset = 0.0; } else { var delta = (currentTime - src.bufStartTime) * src.playbackRate; - var loopStart = buf.audioBuf._loopStart || 0.0; - var loopEnd = buf.audioBuf._loopEnd || buf.audioBuf.duration; + var loopStart = buf.audioBuf._loopStart ?? 0.0; + var loopEnd = buf.audioBuf._loopEnd ?? buf.audioBuf.duration; if (loopEnd <= loopStart) { loopEnd = buf.audioBuf.duration; } @@ -969,8 +969,8 @@ var LibraryOpenAL = { return [0, 0]; } return [ - (buf.audioBuf._loopStart || 0.0) * buf.frequency, - (buf.audioBuf._loopEnd || buf.length) * buf.frequency + (buf.audioBuf._loopStart ?? 0.0) * buf.frequency, + (buf.audioBuf._loopEnd ?? buf.length) * buf.frequency ]; default: #if OPENAL_DEBUG diff --git a/src/lib/libpipefs.js b/src/lib/libpipefs.js index 7de0c8817f5f2..20529338ff72e 100644 --- a/src/lib/libpipefs.js +++ b/src/lib/libpipefs.js @@ -21,23 +21,6 @@ addToLibrary({ // able to read from the read end after write end is closed. refcnt : 2, timestamp: new Date(), -#if PTHREADS || ASYNCIFY - readableHandlers: [], - registerReadableHandler: (callback) => { - callback.registerCleanupFunc(() => { - const i = pipe.readableHandlers.indexOf(callback); - if (i !== -1) pipe.readableHandlers.splice(i, 1); - }); - pipe.readableHandlers.push(callback); - }, - notifyReadableHandlers: () => { - while (pipe.readableHandlers.length > 0) { - const cb = pipe.readableHandlers.shift(); - if (cb) cb({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); - } - pipe.readableHandlers = []; - } -#endif }; pipe.buckets.push({ @@ -53,6 +36,8 @@ addToLibrary({ rNode.pipe = pipe; wNode.pipe = pipe; + // The read end's node carries the poll wait-queue; writes wake it. + pipe.readNode = rNode; var readableStream = FS.createStream({ path: rName, @@ -97,7 +82,7 @@ addToLibrary({ blocks: 0, }; }, - poll(stream, timeout, notifyCallback) { + poll(stream) { var pipe = stream.node.pipe; if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_WRONLY }}}) { @@ -108,16 +93,21 @@ addToLibrary({ return ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); } } - -#if PTHREADS || ASYNCIFY - if (notifyCallback) pipe.registerReadableHandler(notifyCallback); -#endif return 0; }, dup(stream) { stream.node.pipe.refcnt++; }, - ioctl(stream, request, varargs) { + ioctl(stream, request, argp) { + if (request == {{{ cDefs.FIONREAD }}}) { + var pipe = stream.node.pipe; + var currentLength = 0; + for (var bucket of pipe.buckets) { + currentLength += bucket.offset - bucket.roffset; + } + {{{ makeSetValue('argp', 0, 'currentLength', 'i32') }}}; + return 0; + } return {{{ cDefs.EINVAL }}}; }, fsync(stream) { @@ -224,9 +214,7 @@ addToLibrary({ if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; -#if PTHREADS || ASYNCIFY - pipe.notifyReadableHandlers(); -#endif + pipe.readNode.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); return dataLen; } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); @@ -258,9 +246,7 @@ addToLibrary({ newBucket.buffer.set(data); } -#if PTHREADS || ASYNCIFY - pipe.notifyReadableHandlers(); -#endif + pipe.readNode.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}); return dataLen; }, close(stream) { diff --git a/src/lib/libpromise.js b/src/lib/libpromise.js index 0ea127ce84c48..35a5700c2925c 100644 --- a/src/lib/libpromise.js +++ b/src/lib/libpromise.js @@ -6,7 +6,7 @@ addToLibrary({ $promiseMap__deps: ['$HandleAllocator'], - $promiseMap: "new HandleAllocator();", + $promiseMap: 'new HandleAllocator();', $getPromise__deps: ['$promiseMap'], $getPromise: (id) => promiseMap.get(id).promise, @@ -25,6 +25,9 @@ addToLibrary({ return promiseInfo; }, + $addPromise__deps: ['$promiseMap'], + $addPromise: (promise) => promiseMap.allocate({promise}), + $idsToPromises__deps: ['$getPromise'], $idsToPromises: (idBuf, size) => { var promises = []; @@ -70,7 +73,7 @@ addToLibrary({ return; } #if ASSERTIONS - abort("unexpected promise callback result " + result); + abort(`unexpected promise callback result ${result}`); #endif }, @@ -81,6 +84,7 @@ addToLibrary({ '$stackRestore', '$stackSave'], $makePromiseCallback: (callback, userData) => { + if (!callback) return; return (value) => { #if RUNTIME_DEBUG dbg(`emscripten promise callback: ${value}`); @@ -128,12 +132,12 @@ addToLibrary({ throw resultVal; } #if ASSERTIONS - abort("unexpected promise callback result " + result); + abort(`unexpected promise callback result ${result}`); #endif }; }, - emscripten_promise_then__deps: ['$promiseMap', + emscripten_promise_then__deps: ['$addPromise', '$getPromise', '$makePromiseCallback'], emscripten_promise_then: (id, onFulfilled, onRejected, userData) => { @@ -142,33 +146,30 @@ addToLibrary({ #endif {{{ runtimeKeepalivePush() }}}; var promise = getPromise(id); - var newId = promiseMap.allocate({ - promise: promise.then(makePromiseCallback(onFulfilled, userData), - makePromiseCallback(onRejected, userData)) - }); + var chainedPromise = promise.then(makePromiseCallback(onFulfilled, userData), + makePromiseCallback(onRejected, userData)); + var newId = addPromise(chainedPromise); #if RUNTIME_DEBUG dbg(`emscripten_promise_then: -> ${newId}`); #endif return newId; }, - emscripten_promise_all__deps: ['$promiseMap', '$idsToPromises'], + emscripten_promise_all__deps: ['$addPromise', '$idsToPromises'], emscripten_promise_all: (idBuf, resultBuf, size) => { var promises = idsToPromises(idBuf, size); #if RUNTIME_DEBUG dbg(`emscripten_promise_all: ${promises}`); #endif - var id = promiseMap.allocate({ - promise: Promise.all(promises).then((results) => { - if (resultBuf) { - for (var i = 0; i < size; i++) { - var result = results[i]; - {{{ makeSetValue('resultBuf', `i*${POINTER_SIZE}`, 'result', '*') }}}; - } + var id = addPromise(Promise.all(promises).then((results) => { + if (resultBuf) { + for (var i = 0; i < size; i++) { + var result = results[i]; + {{{ makeSetValue('resultBuf', `i*${POINTER_SIZE}`, 'result', '*') }}}; } - return resultBuf; - }) - }); + } + return resultBuf; + })); #if RUNTIME_DEBUG dbg(`create: ${id}`); #endif @@ -185,84 +186,71 @@ addToLibrary({ {{{ makeSetValue('ptr', C_STRUCTS.em_settled_result_t.value, 'value', '*') }}}; }, - emscripten_promise_all_settled__deps: ['$promiseMap', '$idsToPromises', '$setPromiseResult'], + emscripten_promise_all_settled__deps: ['$addPromise', '$idsToPromises', '$setPromiseResult'], emscripten_promise_all_settled: (idBuf, resultBuf, size) => { var promises = idsToPromises(idBuf, size); #if RUNTIME_DEBUG dbg(`emscripten_promise_all_settled: ${promises}`); #endif - var id = promiseMap.allocate({ - promise: Promise.allSettled(promises).then((results) => { - if (resultBuf) { - var offset = resultBuf; - for (var i = 0; i < size; i++, offset += {{{ C_STRUCTS.em_settled_result_t.__size__ }}}) { - if (results[i].status === 'fulfilled') { - setPromiseResult(offset, true, results[i].value); - } else { - setPromiseResult(offset, false, results[i].reason); - } + var id = addPromise(Promise.allSettled(promises).then((results) => { + if (resultBuf) { + var offset = resultBuf; + for (var i = 0; i < size; i++, offset += {{{ C_STRUCTS.em_settled_result_t.__size__ }}}) { + if (results[i].status === 'fulfilled') { + setPromiseResult(offset, true, results[i].value); + } else { + setPromiseResult(offset, false, results[i].reason); } } - return resultBuf; - }) - }); + } + return resultBuf; + })); #if RUNTIME_DEBUG dbg(`create: ${id}`); #endif return id; }, - emscripten_promise_any__deps: [ - '$promiseMap', '$idsToPromises', -#if !SUPPORTS_PROMISE_ANY && !INCLUDE_FULL_LIBRARY - () => error("emscripten_promise_any used, but Promise.any is not supported by the current runtime configuration (run with EMCC_DEBUG=1 in the env for more details)"), -#endif - ], + emscripten_promise_any__deps: ['$addPromise', '$idsToPromises'], emscripten_promise_any: (idBuf, errorBuf, size) => { var promises = idsToPromises(idBuf, size); #if RUNTIME_DEBUG dbg(`emscripten_promise_any: ${promises}`); #endif #if ASSERTIONS - assert(typeof Promise.any != 'undefined', "Promise.any does not exist"); + assert(typeof Promise.any != 'undefined', 'Promise.any does not exist'); #endif - var id = promiseMap.allocate({ - promise: Promise.any(promises).catch((err) => { - if (errorBuf) { - for (var i = 0; i < size; i++) { - {{{ makeSetValue('errorBuf', `i*${POINTER_SIZE}`, 'err.errors[i]', '*') }}}; - } + var id = addPromise(Promise.any(promises).catch((err) => { + if (errorBuf) { + for (var i = 0; i < size; i++) { + {{{ makeSetValue('errorBuf', `i*${POINTER_SIZE}`, 'err.errors[i]', '*') }}}; } - throw errorBuf; - }) - }); + } + throw errorBuf; + })); #if RUNTIME_DEBUG dbg(`create: ${id}`); #endif return id; }, - emscripten_promise_race__deps: ['$promiseMap', '$idsToPromises'], + emscripten_promise_race__deps: ['$addPromise', '$idsToPromises'], emscripten_promise_race: (idBuf, size) => { var promises = idsToPromises(idBuf, size); #if RUNTIME_DEBUG dbg(`emscripten_promise_race: ${promises}`); #endif - var id = promiseMap.allocate({ - promise: Promise.race(promises) - }); + var id = addPromise(Promise.race(promises)); #if RUNTIME_DEBUG dbg(`create: ${id}`); #endif return id; }, - emscripten_promise_await__async: 'auto', #if ASYNCIFY + emscripten_promise_await__async: 'auto', emscripten_promise_await__deps: ['$getPromise', '$setPromiseResult'], -#endif emscripten_promise_await: (returnValuePtr, id) => { -#if ASYNCIFY #if RUNTIME_DEBUG dbg(`emscripten_promise_await: ${id}`); #endif @@ -270,8 +258,23 @@ addToLibrary({ value => setPromiseResult(returnValuePtr, true, value), error => setPromiseResult(returnValuePtr, false, error) ); + }, + + emscripten_promise_await_unchecked__async: 'auto', + emscripten_promise_await_unchecked__deps: ['$getPromise'], + emscripten_promise_await_unchecked: (id) => { +#if RUNTIME_DEBUG + dbg(`emscripten_promise_await_unchecked: ${id}`); +#endif + return getPromise(id); + }, #else + emscripten_promise_await: (returnValuePtr, id) => { abort('emscripten_promise_await is only available with ASYNCIFY'); -#endif }, + emscripten_promise_await_unchecked: (id) => { + abort('emscripten_promise_await_unchecked is only available with ASYNCIFY'); + return 0; + }, +#endif }); diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 95eb163ca4f36..ac112b6a6dc80 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -22,9 +22,6 @@ #if EVAL_CTORS #error "EVAL_CTORS is not compatible with pthreads yet (passive segments)" #endif -#if EXPORT_ES6 && (MIN_FIREFOX_VERSION < 114 || MIN_CHROME_VERSION < 80 || MIN_SAFARI_VERSION < 150000) -#error "internal error, feature_matrix should not allow this" -#endif {{{ #if MEMORY64 @@ -33,6 +30,17 @@ const MAX_PTR = Number((2n ** 64n) - 1n); const MAX_PTR = (2 ** 32) - 1 #endif +// Message IDs used when communicating with workers via postMessage. +const CMD_LOAD = 1; +const CMD_RUN = 2; +const CMD_LOADED = 3; +const CMD_CHECK_MAILBOX = 4; +const CMD_SPAWN_THREAD = 5; +const CMD_CLEANUP_THREAD = 6; +const CMD_MARK_AS_FINISHED = 7; +const CMD_UNCAUGHT_EXN = 8; +const CMD_CALL_HANDLER = 9; + #if WASM_ESM_INTEGRATION const pthreadWorkerScript = TARGET_BASENAME + '.pthread.mjs'; #else @@ -86,6 +94,9 @@ var LibraryPThread = { '$spawnThread', '_emscripten_thread_free_data', 'exit', + 'pthread_self', + '__set_thread_state', + '$waitAsyncPolyfilled', #if PTHREADS_DEBUG || ASSERTIONS '$ptrToString', #endif @@ -97,13 +108,17 @@ var LibraryPThread = { // terminated, but is returned to this pool as an optimization so that // starting the next thread is faster. unusedWorkers: [], - // Contains all Workers that are currently hosting an active pthread. - runningWorkers: [], tlsInitFunctions: [], // Maps pthread_t pointers to the workers on which they are running. For // the reverse mapping, each worker has a `pthread_ptr` when its running a // pthread. pthreads: {}, +#if MAIN_MODULE + outstandingPromises: {}, + // Finished threads are threads that have finished running but we are not yet + // joined. + finishedThreads: new Set(), +#endif #if ASSERTIONS nextWorkerID: 1, #endif @@ -119,8 +134,7 @@ var LibraryPThread = { while (pthreadPoolSize--) { PThread.allocateUnusedWorker(); } -#endif -#if !MINIMAL_RUNTIME && PTHREAD_POOL_SIZE +#if !MINIMAL_RUNTIME // MINIMAL_RUNTIME takes care of calling loadWasmModuleToAllWorkers // in postamble_minimal.js addOnPreRun(async () => { @@ -131,13 +145,8 @@ var LibraryPThread = { removeRunDependency('loading-workers'); #endif // PTHREAD_POOL_DELAY_LOAD }); -#endif // !MINIMAL_RUNTIME && PTHREAD_POOL_SIZE -#if MAIN_MODULE - PThread.outstandingPromises = {}; - // Finished threads are threads that have finished running but we are not yet - // joined. - PThread.finishedThreads = new Set(); -#endif +#endif // !MINIMAL_RUNTIME +#endif // PTHREAD_POOL_SIZE }, #if PTHREADS_PROFILING @@ -169,7 +178,7 @@ var LibraryPThread = { terminateAllThreads: () => { #if ASSERTIONS - assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! terminateAllThreads() can only ever be called from main application thread!'); + assert(!ENVIRONMENT_IS_PTHREAD, 'terminateAllThreads() should only be called from the main thread'); #endif #if PTHREADS_DEBUG dbg('terminateAllThreads'); @@ -180,16 +189,31 @@ var LibraryPThread = { // pthreads will continue to be executing after `worker.terminate` has // returned. For this reason, we don't call `returnWorkerToPool` here or // free the underlying pthread data structures. - for (var worker of PThread.runningWorkers) { + for (var worker of Object.values(PThread.pthreads)) { terminateWorker(worker); } for (var worker of PThread.unusedWorkers) { terminateWorker(worker); } PThread.unusedWorkers = []; - PThread.runningWorkers = []; PThread.pthreads = {}; }, + + terminateRuntime: () => { +#if ASSERTIONS + assert(!ENVIRONMENT_IS_PTHREAD, 'terminateRuntime() should only be called from the main thread'); +#endif + PThread.terminateAllThreads(); + var pthread_ptr = _pthread_self(); + ___set_thread_state(0, 0, 0, 1); + if (!waitAsyncPolyfilled) { + // Break the waitAsync loop. Note that checkMailbox will not + // re-register since the `___set_thread_state` above causes _pthread_self + // to return 0. + Atomics.notify(HEAP32, {{{ getHeapOffset('pthread_ptr', 'i32') }}}); + } + }, + returnWorkerToPool: (worker) => { // We don't want to run main thread queued calls here, since we are doing // some operations that leave the worker queue in an invalid state until @@ -202,7 +226,6 @@ var LibraryPThread = { // Note: worker is intentionally not terminated so the pool can // dynamically grow. PThread.unusedWorkers.push(worker); - PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker), 1); // Not a running Worker anymore // Detach the worker from the pthread object, and return it to the // worker pool as an unused worker. @@ -248,67 +271,80 @@ var LibraryPThread = { // ready to host pthreads. loadWasmModuleToWorker: (worker) => new Promise((onFinishedLoading) => { worker.onmessage = (e) => { - var d = e['data']; + var d = e.data; var cmd = d.cmd; #if PTHREADS_DEBUG dbg(`main thread: received message '${cmd}' from worker. ${d}`); #endif // If this message is intended to a recipient that is not the main - // thread, forward it to the target thread. - if (d.targetThread && d.targetThread != _pthread_self()) { + // thread, forward it to the target thread. This is currently only + // used by `CMD_CHECK_MAILBOX`. + if (d.targetThread) { +#if ASSERTIONS + // pthreads should not be relaying messages to themselves. + assert(d.targetThread != _pthread_self()); +#endif var targetWorker = PThread.pthreads[d.targetThread]; - if (targetWorker) { - targetWorker.postMessage(d, d.transferList); - } else { - err(`Internal error! Worker sent a message "${cmd}" to target pthread ${d.targetThread}, but that thread no longer exists!`); - } +#if ASSERTIONS + if (!targetWorker) err(`worker sent message (${cmd}) to pthread (${d.targetThread}) that no longer exists`); +#endif + targetWorker?.postMessage(d); return; } - if (cmd === 'checkMailbox') { - checkMailbox(); - } else if (cmd === 'spawnThread') { - spawnThread(d); - } else if (cmd === 'cleanupThread') { - // cleanupThread needs to be run via callUserCallback since it calls - // back into user code to free thread data. Without this it's possible - // the unwind or ExitStatus exception could escape here. - callUserCallback(() => cleanupThread(d.thread)); -#if MAIN_MODULE - } else if (cmd === 'markAsFinished') { - markAsFinished(d.thread); -#endif - } else if (cmd === 'loaded') { - worker.loaded = true; -#if ENVIRONMENT_MAY_BE_NODE && PTHREAD_POOL_SIZE - // Check that this worker doesn't have an associated pthread. - if (ENVIRONMENT_IS_NODE && !worker.pthread_ptr) { - // Once worker is loaded & idle, mark it as weakly referenced, - // so that mere existence of a Worker in the pool does not prevent - // Node.js from exiting the app. - worker.unref(); - } -#endif - onFinishedLoading(worker); - } else if (d.target === 'setimmediate') { + if (d === 'setimmediate' || d === '_si') { // Worker wants to postMessage() to itself to implement setImmediate() // emulation. worker.postMessage(d); + return; + } + + switch (cmd) { + case {{{ CMD_CHECK_MAILBOX }}}: + checkMailbox(); + break; + case {{{ CMD_SPAWN_THREAD }}}: + spawnThread(d); + break; + case {{{ CMD_CLEANUP_THREAD }}}: + // cleanupThread needs to be run via callUserCallback since it calls + // back into user code to free thread data. Without this it's possible + // the unwind or ExitStatus exception could escape here. + callUserCallback(() => cleanupThread(d.thread)); + break; +#if MAIN_MODULE + case {{{ CMD_MARK_AS_FINISHED }}}: + markAsFinished(d.thread); + break; +#endif + case {{{ CMD_LOADED }}}: #if ENVIRONMENT_MAY_BE_NODE - } else if (cmd === 'uncaughtException') { - // Message handler for Node.js specific out-of-order behavior: - // https://github.com/nodejs/node/issues/59617 - // A pthread sent an uncaught exception event. Re-raise it on the main thread. - worker.onerror(d.error); -#endif - } else if (cmd === 'callHandler') { - Module[d.handler](...d.args); - } else if (cmd) { - // The received message looks like something that should be handled by this message - // handler, (since there is a e.data.cmd field present), but is not one of the - // recognized commands: - err(`worker sent an unknown command ${cmd}`); + if (ENVIRONMENT_IS_NODE && !worker.strongref) { + // Once worker is loaded & idle, mark it as weakly referenced, + // so that mere existence of a Worker in the pool does not prevent + // Node.js from exiting the app. + worker.unref(); + } +#endif + onFinishedLoading(worker); + break; +#if ENVIRONMENT_MAY_BE_NODE + case {{{ CMD_UNCAUGHT_EXN }}}: + // Message handler for Node.js specific out-of-order behavior: + // https://github.com/nodejs/node/issues/59617 + // A pthread sent an uncaught exception event. Re-raise it on the main thread. + worker.onerror(d.error); + break; +#endif + case {{{ CMD_CALL_HANDLER }}}: + Module[d.handler](...d.args); + break; + default: + // The received message looks like something that should be handled by this message + // handler, (since there is a e.data.cmd field present), but is not one of the + // recognized commands: + if (cmd) err(`worker sent an unknown command ${cmd}`); } }; @@ -338,9 +374,9 @@ var LibraryPThread = { #endif #if ASSERTIONS - assert(wasmMemory instanceof WebAssembly.Memory, 'WebAssembly memory should have been loaded by now!'); + assert(wasmMemory instanceof WebAssembly.Memory, 'wasmMemory should have been loaded by now'); #if !WASM_ESM_INTEGRATION - assert(wasmModule instanceof WebAssembly.Module, 'WebAssembly Module should have been loaded by now!'); + assert(wasmModule instanceof WebAssembly.Module, 'wasmModule should have been loaded by now'); #endif #endif @@ -381,7 +417,7 @@ var LibraryPThread = { // Ask the new worker to load up the Emscripten-compiled page. This is a heavy operation. worker.postMessage({ - cmd: 'load', + cmd: {{{ CMD_LOAD }}}, handlers: handlers, #if WASM2JS // the polyfill WebAssembly.Memory instance has function properties, @@ -404,7 +440,7 @@ var LibraryPThread = { sharedModules, #endif #if ASSERTIONS - 'workerID': worker.workerID, + workerID: worker.workerID, #endif }); }), @@ -513,6 +549,7 @@ var LibraryPThread = { worker.workerID = PThread.nextWorkerID++; #endif PThread.unusedWorkers.push(worker); + return worker; }, getNewWorker() { @@ -541,8 +578,8 @@ var LibraryPThread = { #endif #endif // PTHREAD_POOL_SIZE_STRICT #if PTHREAD_POOL_SIZE_STRICT < 2 || ENVIRONMENT_MAY_BE_NODE - PThread.allocateUnusedWorker(); - PThread.loadWasmModuleToWorker(PThread.unusedWorkers[0]); + var newWorker = PThread.allocateUnusedWorker(); + PThread.loadWasmModuleToWorker(newWorker); #endif } return PThread.unusedWorkers.pop(); @@ -561,7 +598,7 @@ var LibraryPThread = { // the onmessage handlers if the message was coming from a valid worker. worker.onmessage = (e) => { #if ASSERTIONS - var cmd = e['data'].cmd; + var cmd = e.data.cmd; err(`received "${cmd}" command from terminated worker: ${worker.workerID}`); #endif }; @@ -577,7 +614,7 @@ var LibraryPThread = { dbg(`_emscripten_thread_cleanup: ${ptrToString(thread)}`) #endif if (!ENVIRONMENT_IS_PTHREAD) cleanupThread(thread); - else postMessage({ cmd: 'cleanupThread', thread }); + else postMessage({ cmd: {{{ CMD_CLEANUP_THREAD }}}, thread }); }, _emscripten_thread_set_strongref: (thread) => { @@ -588,7 +625,12 @@ var LibraryPThread = { // back to the main thread. #if ENVIRONMENT_MAY_BE_NODE if (ENVIRONMENT_IS_NODE) { - PThread.pthreads[thread].ref(); + var worker = PThread.pthreads[thread]; + worker.ref(); + // Also, record that we called strongref, in case this function is called + // bafore the 'loaded' callback from the thread (where we would normally + // `unref` it. + worker.strongref = 1; } #endif }, @@ -598,8 +640,8 @@ var LibraryPThread = { dbg(`cleanupThread: ${ptrToString(pthread_ptr)}`) #endif #if ASSERTIONS - assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! cleanupThread() can only ever be called from main application thread!'); - assert(pthread_ptr, 'Internal Error! Null pthread_ptr in cleanupThread!'); + assert(!ENVIRONMENT_IS_PTHREAD, 'cleanupThread() should only be called from the main thread'); + assert(pthread_ptr, 'null pthread_ptr passed to cleanupThread'); #endif var worker = PThread.pthreads[pthread_ptr]; #if MAIN_MODULE @@ -656,8 +698,8 @@ var LibraryPThread = { $spawnThread: (threadParams) => { #if ASSERTIONS - assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! spawnThread() can only ever be called from main application thread!'); - assert(threadParams.pthread_ptr, 'Internal error, no pthread ptr!'); + assert(!ENVIRONMENT_IS_PTHREAD, 'spawnThread() should only be called from the main thread'); + assert(threadParams.pthread_ptr, 'spawnThread called with null pthread ptr'); #endif var worker = PThread.getNewWorker(); @@ -666,17 +708,15 @@ var LibraryPThread = { return {{{ cDefs.EAGAIN }}}; } #if ASSERTIONS - assert(!worker.pthread_ptr, 'Internal error!'); + assert(!worker.pthread_ptr); #endif - PThread.runningWorkers.push(worker); - // Add to pthreads map PThread.pthreads[threadParams.pthread_ptr] = worker; worker.pthread_ptr = threadParams.pthread_ptr; var msg = { - cmd: 'run', + cmd: {{{ CMD_RUN }}}, start_routine: threadParams.startRoutine, arg: threadParams.arg, pthread_ptr: threadParams.pthread_ptr, @@ -686,15 +726,6 @@ var LibraryPThread = { // in this file, and not from the external worker.js. msg.moduleCanvasId = threadParams.moduleCanvasId; msg.offscreenCanvases = threadParams.offscreenCanvases; -#endif -#if ENVIRONMENT_MAY_BE_NODE - if (ENVIRONMENT_IS_NODE) { - // Mark worker as weakly referenced once we start executing a pthread, - // so that its existence does not prevent Node.js from exiting. This - // has no effect if the worker is already weakly referenced (e.g. if - // this worker was previously idle/unused). - worker.unref(); - } #endif // Ask the worker to start executing its pthread entry point function. worker.postMessage(msg, threadParams.transferList); @@ -702,6 +733,14 @@ var LibraryPThread = { }, _emscripten_init_main_thread_js: (tb) => { + var can_block = !ENVIRONMENT_IS_WEB; +#if ENVIRONMENT_MAY_BE_WEB + // Feature detect whether the main thread can block. + try { + Atomics.wait(HEAP32, 0, 0, 0) + can_block = true; + } catch (e) {} +#endif // Pass the thread address to the native code where they are stored in wasm // globals which act as a form of TLS. Global constructors trying // to access this value will read the wrong value, but that is UB anyway. @@ -709,7 +748,7 @@ var LibraryPThread = { tb, /*is_main=*/!ENVIRONMENT_IS_WORKER, /*is_runtime=*/1, - /*can_block=*/!ENVIRONMENT_IS_WEB, + can_block, /*default_stacksize=*/{{{ DEFAULT_PTHREAD_STACK_SIZE }}}, #if PTHREADS_PROFILING /*start_profiling=*/true, @@ -778,7 +817,7 @@ var LibraryPThread = { #endif var offscreenCanvases = {}; // Dictionary of OffscreenCanvas objects we'll transfer to the created thread to own - var moduleCanvasId = Module['canvas']?.id || ''; + var moduleCanvasId = Module['canvas']?.id ?? ''; // Note that transferredCanvasNames might be null (so we cannot do a for-of loop). for (var name of transferredCanvasNames) { name = name.trim(); @@ -892,7 +931,7 @@ var LibraryPThread = { // The prepopulated pool of web workers that can host pthreads is stored // in the main JS thread. Therefore if a pthread is attempting to spawn a // new thread, the thread creation must be deferred to the main JS thread. - threadParams.cmd = 'spawnThread'; + threadParams.cmd = {{{ CMD_SPAWN_THREAD }}}; postMessage(threadParams, transferList); // When we defer thread creation this way, we have no way to detect thread // creation synchronously today, so we have to assume success and return 0. @@ -1165,7 +1204,7 @@ var LibraryPThread = { // running, but remain around waiting to be joined. In this state they // cannot run any more proxied work. if (!ENVIRONMENT_IS_PTHREAD) markAsFinished(thread); - else postMessage({ cmd: 'markAsFinished', thread }); + else postMessage({ cmd: {{{ CMD_MARK_AS_FINISHED }}}, thread }); }, $markAsFinished: (pthread_ptr) => { @@ -1187,7 +1226,7 @@ var LibraryPThread = { dbg("dlsyncThreadsAsync caller=" + ptrToString(caller)); #endif #if ASSERTIONS - assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! dlsyncThreadsAsync() can only ever be called from main thread'); + assert(!ENVIRONMENT_IS_PTHREAD, 'dlsyncThreadsAsync() should only be called from the main thread'); assert(Object.keys(PThread.outstandingPromises).length === 0); #endif @@ -1251,24 +1290,26 @@ var LibraryPThread = { 'pthread_self', '_emscripten_check_mailbox', '_emscripten_thread_mailbox_await'], - $checkMailbox: () => callUserCallback(() => { - // Only check the mailbox if we have a live pthread runtime. We implement - // pthread_self to return 0 if there is no live runtime. - // - // TODO(https://github.com/emscripten-core/emscripten/issues/25076): - // Is this check still needed? `callUserCallback` is supposed to - // ensure the runtime is alive, and if `_pthread_self` is NULL then the - // runtime certainly is *not* alive, so this should be a redundant check. + $checkMailbox: () => { + // checkMailbox can be called after the pthread has shut down. See + // Pthread.terminateRuntime(). + // In this case we return silently without re-registering using waitAsync. + // Perhaps there is a more universal way we can detect runtime has exited. + // TODO(https://github.com/emscripten-core/emscripten/issues/25076) +#if ABORT_ON_WASM_EXCEPTIONS + if (ABORT) return; +#endif var pthread_ptr = _pthread_self(); - if (pthread_ptr) { + if (!pthread_ptr) return; + callUserCallback(() => { // If we are using Atomics.waitAsync as our notification mechanism, wait // for a notification before processing the mailbox to avoid missing any // work that could otherwise arrive after we've finished processing the // mailbox and before we're ready for the next notification. __emscripten_thread_mailbox_await(pthread_ptr); __emscripten_check_mailbox(); - } - }), + }); + }, _emscripten_thread_mailbox_await__deps: ['$checkMailbox', '$waitAsyncPolyfilled'], _emscripten_thread_mailbox_await: (pthread_ptr) => { @@ -1300,7 +1341,7 @@ var LibraryPThread = { if (targetThread == currThreadId) { setTimeout(checkMailbox); } else if (ENVIRONMENT_IS_PTHREAD) { - postMessage({targetThread, cmd: 'checkMailbox'}); + postMessage({targetThread, cmd: {{{ CMD_CHECK_MAILBOX }}}}); } else { var worker = PThread.pthreads[targetThread]; if (!worker) { @@ -1309,7 +1350,7 @@ var LibraryPThread = { #endif return; } - worker.postMessage({cmd: 'checkMailbox'}); + worker.postMessage({cmd: {{{ CMD_CHECK_MAILBOX }}}}); } } }; diff --git a/src/lib/libsdl.js b/src/lib/libsdl.js index b17b1da60e805..f3502287a617c 100644 --- a/src/lib/libsdl.js +++ b/src/lib/libsdl.js @@ -1005,7 +1005,7 @@ var LibrarySDL = { var ly = Browser.lastTouches[touch.identifier].y / canvas.height; var dx = x - lx; var dy = y - ly; - if (touch['deviceID'] === undefined) touch.deviceID = SDL.TOUCH_DEFAULT_ID; + touch.deviceID ??= SDL.TOUCH_DEFAULT_ID; if (dx === 0 && dy === 0 && event.type === 'touchmove') return false; // don't send these if nothing happened {{{ makeSetValue('ptr', C_STRUCTS.SDL_TouchFingerEvent.type, 'SDL.DOMEventToSDLEvent[event.type]', 'i32') }}}; {{{ makeSetValue('ptr', C_STRUCTS.SDL_TouchFingerEvent.timestamp, '_SDL_GetTicks()', 'i32') }}}; @@ -1382,8 +1382,8 @@ var LibrarySDL = { SDL.initFlags = initFlags; // capture all key events. we just keep down and up, but also capture press to prevent default actions - if (!Module['doNotCaptureKeyboard']) { - var keyboardListeningElement = Module['keyboardListeningElement'] || document; + if (!{{{ makeModuleReceiveExpr('doNotCaptureKeyboard', 'false') }}}) { + var keyboardListeningElement = {{{ makeModuleReceiveExpr('keyboardListeningElement', 'document') }}}; keyboardListeningElement.addEventListener("keydown", SDL.receiveEvent); keyboardListeningElement.addEventListener("keyup", SDL.receiveEvent); keyboardListeningElement.addEventListener("keypress", SDL.receiveEvent); @@ -2258,7 +2258,9 @@ var LibrarySDL = { filename = PATH_FS.resolve(filename); raw = Browser.preloadedImages[filename]; if (!raw) { +#if expectToReceiveOnModule('freePreloadedMediaOnUse') if (raw === null) err('Trying to reuse preloaded image, but freePreloadedMediaOnUse is set!'); +#endif #if STB_IMAGE var name = stringToUTF8OnStack(filename); raw = callStbImage('stbi_load', [name]); @@ -2268,9 +2270,12 @@ var LibrarySDL = { warnOnce(`Cannot find preloaded image ${filename}. Consider using STB_IMAGE=1 if you want synchronous image decoding (see settings.js), or package files with --use-preload-plugins`); return 0; #endif - } else if (Module['freePreloadedMediaOnUse']) { + } +#if expectToReceiveOnModule('freePreloadedMediaOnUse') + if (Module['freePreloadedMediaOnUse']) { Browser.preloadedImages[filename] = null; } +#endif } var surf = SDL.makeSurface(raw.width, raw.height, 0, false, 'load:' + filename); @@ -2282,7 +2287,7 @@ var LibrarySDL = { var imageData = surfData.ctx.getImageData(0, 0, surfData.width, surfData.height); if (raw.bpp == 4) { // rgba - imageData.data.set({{{ makeHEAPView('U8', 'raw.data', 'raw.data+raw.size') }}}); + imageData.data.set(HEAPU8.subarray(raw.data, raw.data + raw.size)); } else if (raw.bpp == 3) { // rgb var pixels = raw.size/3; @@ -2437,7 +2442,7 @@ var LibrarySDL = { // To account for jittering in frametimes, always have multiple audio // buffers queued up for the audio output device. // This helps that we won't starve that easily if a frame takes long to complete. - SDL.audio.numSimultaneouslyQueuedBuffers = Module['SDL_numSimultaneouslyQueuedBuffers'] || 5; + SDL.audio.numSimultaneouslyQueuedBuffers = {{{ makeModuleReceiveExpr('SDL_numSimultaneouslyQueuedBuffers', 5) }}}; // Pulls and queues new audio data if appropriate. This function gets // "over-called" in both requestAnimationFrames and setTimeouts to ensure @@ -2746,7 +2751,9 @@ var LibrarySDL = { filename = PATH_FS.resolve(rwops.filename); var raw = Browser.preloadedAudios[filename]; if (!raw) { +#if expectToReceiveOnModule('freePreloadedMediaOnUse') if (raw === null) err('Trying to reuse preloaded audio, but freePreloadedMediaOnUse is set!'); +#endif if (!Module['noAudioDecoding']) warnOnce('Cannot find preloaded audio ' + filename); // see if we can read the file-contents from the in-memory FS @@ -2757,16 +2764,18 @@ var LibrarySDL = { return 0; } } +#if expectToReceiveOnModule('freePreloadedMediaOnUse') if (Module['freePreloadedMediaOnUse']) { Browser.preloadedAudios[filename] = null; } +#endif audio = raw; } else if (rwops.bytes !== undefined) { // For Web Audio context buffer decoding, we must make a clone of the // audio data, but for element, a view to existing data is // sufficient. if (SDL.webAudioAvailable()) { - bytes = HEAPU8.buffer.slice(rwops.bytes, rwops.bytes + rwops.count); + bytes = HEAPU8.slice(rwops.bytes, rwops.bytes + rwops.count); } else { bytes = HEAPU8.subarray(rwops.bytes, rwops.bytes + rwops.count); } @@ -2776,10 +2785,14 @@ var LibrarySDL = { var arrayBuffer = bytes ? bytes.buffer || bytes : bytes; +#if expectToReceiveOnModule('SDL_canPlayWithWebAudio') // To allow user code to work around browser bugs with audio playback on